Commit 0ccfccaa authored by Andreas Traczyk's avatar Andreas Traczyk Committed by Kateryna Kostiuk

messaging: move message cell logic into MessageCell

Change-Id: Ie81715ba0ebb512c467a4486acb4ca9cfc64e351
Reviewed-by: Kateryna Kostiuk's avatarKateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
parent 1fd2328b
......@@ -80,4 +80,222 @@ class MessageCell: UITableViewCell, NibReusable {
}
return false
}
func formatCellTimeLabel(_ item: MessageViewModel) {
// hide for potentially reused cell
self.timeLabel.isHidden = true
self.leftDivider.isHidden = true
self.rightDivider.isHidden = true
if item.timeStringShown == nil {
return
}
// setup the label
self.timeLabel.text = item.timeStringShown
self.timeLabel.textColor = UIColor.ringMsgCellTimeText
self.timeLabel.font = UIFont.boldSystemFont(ofSize: 14.0)
// show the time
self.timeLabel.isHidden = false
self.leftDivider.isHidden = false
self.rightDivider.isHidden = false
}
// swiftlint:disable cyclomatic_complexity
func applyBubbleStyleToCell(_ items: [MessageViewModel]?, cellForRowAt indexPath: IndexPath) {
guard let item = items?[indexPath.row] else {
return
}
let type = item.bubblePosition()
let bubbleColor = type == .received ? UIColor.ringMsgCellReceived : UIColor.ringMsgCellSent
self.setup()
self.messageLabel.enabledTypes = [.url]
self.messageLabel.setTextWithLineSpacing(withText: item.content, withLineSpacing: 2)
self.messageLabel.handleURLTap { url in
let urlString = url.absoluteString
if let prefixedUrl = URL(string: urlString.contains("http") ? urlString : "http://\(urlString)") {
UIApplication.shared.openURL(prefixedUrl)
}
}
self.topCorner.isHidden = true
self.topCorner.backgroundColor = bubbleColor
self.bottomCorner.isHidden = true
self.bottomCorner.backgroundColor = bubbleColor
self.bubbleBottomConstraint.constant = 8
self.bubbleTopConstraint.constant = 8
var adjustedSequencing = item.sequencing
if item.timeStringShown != nil {
self.bubbleTopConstraint.constant = 32
adjustedSequencing = indexPath.row == (items?.count)! - 1 ?
.singleMessage : adjustedSequencing != .singleMessage && adjustedSequencing != .lastOfSequence ?
.firstOfSequence : .singleMessage
}
if indexPath.row + 1 < (items?.count)! {
if items?[indexPath.row + 1].timeStringShown != nil {
switch adjustedSequencing {
case .firstOfSequence:
adjustedSequencing = .singleMessage
case .middleOfSequence:
adjustedSequencing = .lastOfSequence
default: break
}
}
}
item.sequencing = adjustedSequencing
switch item.sequencing {
case .middleOfSequence:
self.topCorner.isHidden = false
self.bottomCorner.isHidden = false
self.bubbleBottomConstraint.constant = 1
self.bubbleTopConstraint.constant = item.timeStringShown != nil ? 32 : 1
case .firstOfSequence:
self.bottomCorner.isHidden = false
self.bubbleBottomConstraint.constant = 1
self.bubbleTopConstraint.constant = item.timeStringShown != nil ? 32 : 8
case .lastOfSequence:
self.topCorner.isHidden = false
self.bubbleTopConstraint.constant = item.timeStringShown != nil ? 32 : 1
default: break
}
}
// swiftlint:enable cyclomatic_complexity
// swiftlint:disable cyclomatic_complexity
func configureFromItem(_ conversationViewModel: ConversationViewModel,
_ items: [MessageViewModel]?,
cellForRowAt indexPath: IndexPath) {
guard let item = items?[indexPath.row] else {
return
}
// hide/show time label
formatCellTimeLabel(item)
if item.bubblePosition() == .generated {
self.bubble.backgroundColor = UIColor.ringMsgCellReceived
self.messageLabel.setTextWithLineSpacing(withText: item.content, withLineSpacing: 2)
// generated messages should always show the time
self.bubbleTopConstraint.constant = 32
return
}
// bubble grouping for cell
applyBubbleStyleToCell(items, cellForRowAt: indexPath)
// special cases where top/bottom margins should be larger
if indexPath.row == 0 {
self.bubbleTopConstraint.constant = 32
} else if items?.count == indexPath.row + 1 {
self.bubbleBottomConstraint.constant = 16
}
if item.bubblePosition() == .sent {
item.status.asObservable()
.observeOn(MainScheduler.instance)
.map { value in value == MessageStatus.sending ? true : false }
.bind(to: self.sendingIndicator.rx.isAnimating)
.disposed(by: self.disposeBag)
item.status.asObservable()
.observeOn(MainScheduler.instance)
.map { value in value == MessageStatus.failure ? false : true }
.bind(to: self.failedStatusLabel.rx.isHidden)
.disposed(by: self.disposeBag)
} else if item.bubblePosition() == .received {
// avatar
guard let fallbackAvatar = self.fallbackAvatar else {
return
}
self.fallbackAvatar.isHidden = true
self.profileImage?.isHidden = true
if item.sequencing == .lastOfSequence || item.sequencing == .singleMessage {
self.profileImage?.isHidden = false
// Set placeholder avatar
fallbackAvatar.text = nil
self.fallbackAvatarImage.isHidden = true
let name = conversationViewModel.userName.value
let scanner = Scanner(string: name.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
fallbackAvatar.isHidden = false
fallbackAvatar.backgroundColor = avatarColors[Int(index)]
if conversationViewModel.conversation.value.recipientRingId != name {
self.fallbackAvatar.text = name.prefixString().capitalized
} else {
self.fallbackAvatarImage.isHidden = true
}
}
// Avatar placeholder color
conversationViewModel.userName.asObservable()
.observeOn(MainScheduler.instance)
.map { name in
let scanner = Scanner(string: name.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
return avatarColors[Int(index)]
}
return defaultAvatarColor
}.subscribe(onNext: { backgroundColor in
self.fallbackAvatar.backgroundColor = backgroundColor
})
.disposed(by: self.disposeBag)
// Avatar placeholder initial
conversationViewModel.userName.asObservable()
.observeOn(MainScheduler.instance)
.filter({ userName in
return userName != conversationViewModel.conversation.value.recipientRingId
})
.map { value in
value.prefixString().capitalized
}
.bind(to: self.fallbackAvatar.rx.text)
.disposed(by: self.disposeBag)
// If only the ringId is known, use fallback avatar image
conversationViewModel.userName.asObservable()
.observeOn(MainScheduler.instance)
.map { userName in
userName != conversationViewModel.conversation.value.recipientRingId
}
.bind(to: self.fallbackAvatarImage.rx.isHidden)
.disposed(by: self.disposeBag)
// Set image if any
if let imageData = conversationViewModel.profileImageData.value {
if let image = UIImage(data: imageData) {
self.profileImage.image = image
self.fallbackAvatar.isHidden = true
}
} else {
self.fallbackAvatar.isHidden = false
self.profileImage.image = nil
}
conversationViewModel.profileImageData.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { data in
if let imageData = data, let image = UIImage(data: imageData) {
self.profileImage.image = image
self.fallbackAvatar.isHidden = true
} else {
self.fallbackAvatar.isHidden = false
self.profileImage.image = nil
}
}).disposed(by: self.disposeBag)
}
}
}
// swiftlint:enable cyclomatic_complexity
}
......@@ -32,7 +32,6 @@ extension UITextField {
}
}
// swiftlint:disable type_body_length
class ConversationViewController: UIViewController, UITextFieldDelegate, StoryboardBased, ViewModelBased {
let log = SwiftyBeaver.self
......@@ -48,8 +47,6 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
var bottomOffset: CGFloat = 0
let scrollOffsetThreshold: CGFloat = 600
fileprivate var fallbackBGColorObservable: Observable<UIColor>!
override func viewDidLoad() {
super.viewDidLoad()
......@@ -194,18 +191,6 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
.disposed(by: self.disposeBag)
}
// UIColor that observes "best Id" prefix
self.fallbackBGColorObservable = viewModel.userName.asObservable()
.observeOn(MainScheduler.instance)
.map { name in
let scanner = Scanner(string: name.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
return avatarColors[Int(index)]
}
return defaultAvatarColor
}
self.tableView.contentInset.bottom = messageAccessoryView.frame.size.height
self.tableView.scrollIndicatorInsets.bottom = messageAccessoryView.frame.size.height
......@@ -391,6 +376,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
return textFieldShouldEndEditing
}
// MARK: - message formatting
func computeSequencing() {
var lastShownTime: Date?
for (index, messageViewModel) in self.messageViewModels!.enumerated() {
......@@ -417,88 +403,38 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
}
}
func getTimeLabelString(forTime time: Date) -> String {
// get the current time
let currentDateTime = Date()
// prepare formatter
let dateFormatter = DateFormatter()
if Calendar.current.compare(currentDateTime, to: time, toGranularity: .year) == .orderedSame {
if Calendar.current.compare(currentDateTime, to: time, toGranularity: .weekOfYear) == .orderedSame {
if Calendar.current.compare(currentDateTime, to: time, toGranularity: .day) == .orderedSame {
// age: [0, received the previous day[
dateFormatter.dateFormat = "h:mma"
} else {
// age: [received the previous day, received 7 days ago[
dateFormatter.dateFormat = "E h:mma"
}
} else {
// age: [received 7 days ago, received the previous year[
dateFormatter.dateFormat = "MMM d, h:mma"
}
} else {
// age: [received the previous year, inf[
dateFormatter.dateFormat = "MMM d, yyyy h:mma"
}
// generate the string containing the message time
return dateFormatter.string(from: time).uppercased()
}
func formatTimeLabel(forCell cell: MessageCell,
withMessageVM messageVM: MessageViewModel) {
// hide for potentially reused cell
cell.timeLabel.isHidden = true
cell.leftDivider.isHidden = true
cell.rightDivider.isHidden = true
if messageVM.timeStringShown == nil {
return
}
// setup the label
cell.timeLabel.text = messageVM.timeStringShown
cell.timeLabel.textColor = UIColor.ringMsgCellTimeText
cell.timeLabel.font = UIFont.boldSystemFont(ofSize: 14.0)
// show the time
cell.timeLabel.isHidden = false
cell.leftDivider.isHidden = false
cell.rightDivider.isHidden = false
}
func getMessageSequencing(forIndex index: Int) -> MessageSequencing {
if let msgViewModel = self.messageViewModels?[index] {
let msgOwner = msgViewModel.bubblePosition()
if let messageItem = self.messageViewModels?[index] {
let msgOwner = messageItem.bubblePosition()
if self.messageViewModels?.count == 1 || index == 0 {
if self.messageViewModels?.count == index + 1 {
return MessageSequencing.singleMessage
}
let nextMsgViewModel = index + 1 <= (self.messageViewModels?.count)!
let nextMessageItem = index + 1 <= (self.messageViewModels?.count)!
? self.messageViewModels?[index + 1] : nil
if nextMsgViewModel != nil {
return msgOwner != nextMsgViewModel?.bubblePosition()
if nextMessageItem != nil {
return msgOwner != nextMessageItem?.bubblePosition()
? MessageSequencing.singleMessage : MessageSequencing.firstOfSequence
}
} else if self.messageViewModels?.count == index + 1 {
let lastMsgViewModel = index - 1 >= 0 && index - 1 < (self.messageViewModels?.count)!
let lastMessageItem = index - 1 >= 0 && index - 1 < (self.messageViewModels?.count)!
? self.messageViewModels?[index - 1] : nil
if lastMsgViewModel != nil {
return msgOwner != lastMsgViewModel?.bubblePosition()
if lastMessageItem != nil {
return msgOwner != lastMessageItem?.bubblePosition()
? MessageSequencing.singleMessage : MessageSequencing.lastOfSequence
}
}
let lastMsgViewModel = index - 1 >= 0 && index - 1 < (self.messageViewModels?.count)!
let lastMessageItem = index - 1 >= 0 && index - 1 < (self.messageViewModels?.count)!
? self.messageViewModels?[index - 1] : nil
let nextMsgViewModel = index + 1 <= (self.messageViewModels?.count)!
let nextMessageItem = index + 1 <= (self.messageViewModels?.count)!
? self.messageViewModels?[index + 1] : nil
var sequencing = MessageSequencing.singleMessage
if (lastMsgViewModel != nil) && (nextMsgViewModel != nil) {
if msgOwner != lastMsgViewModel?.bubblePosition() && msgOwner == nextMsgViewModel?.bubblePosition() {
if (lastMessageItem != nil) && (nextMessageItem != nil) {
if msgOwner != lastMessageItem?.bubblePosition() && msgOwner == nextMessageItem?.bubblePosition() {
sequencing = MessageSequencing.firstOfSequence
} else if msgOwner != nextMsgViewModel?.bubblePosition() && msgOwner == lastMsgViewModel?.bubblePosition() {
} else if msgOwner != nextMessageItem?.bubblePosition() && msgOwner == lastMessageItem?.bubblePosition() {
sequencing = MessageSequencing.lastOfSequence
} else if msgOwner == nextMsgViewModel?.bubblePosition() && msgOwner == lastMsgViewModel?.bubblePosition() {
} else if msgOwner == nextMessageItem?.bubblePosition() && msgOwner == lastMessageItem?.bubblePosition() {
sequencing = MessageSequencing.middleOfSequence
}
}
......@@ -507,180 +443,35 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
return MessageSequencing.unknown
}
// swiftlint:disable cyclomatic_complexity
func applyBubbleStyleToCell(toCell cell: MessageCell,
cellForRowAt indexPath: IndexPath,
withMessageVM messageVM: MessageViewModel) {
let type = messageVM.bubblePosition()
let bubbleColor = type == .received ? UIColor.ringMsgCellReceived : UIColor.ringMsgCellSent
cell.setup()
cell.messageLabel.enabledTypes = [.url]
cell.messageLabel.setTextWithLineSpacing(withText: messageVM.content, withLineSpacing: 2)
cell.messageLabel.handleURLTap { url in
let urlString = url.absoluteString
if let prefixedUrl = URL(string: urlString.contains("http") ? urlString : "http://\(urlString)") {
UIApplication.shared.openURL(prefixedUrl)
}
}
cell.topCorner.isHidden = true
cell.topCorner.backgroundColor = bubbleColor
cell.bottomCorner.isHidden = true
cell.bottomCorner.backgroundColor = bubbleColor
cell.bubbleBottomConstraint.constant = 8
cell.bubbleTopConstraint.constant = 8
var adjustedSequencing = messageVM.sequencing
if messageVM.timeStringShown != nil {
cell.bubbleTopConstraint.constant = 32
adjustedSequencing = indexPath.row == (self.messageViewModels?.count)! - 1 ?
.singleMessage : adjustedSequencing != .singleMessage && adjustedSequencing != .lastOfSequence ?
.firstOfSequence : .singleMessage
}
func getTimeLabelString(forTime time: Date) -> String {
// get the current time
let currentDateTime = Date()
if indexPath.row + 1 < (self.messageViewModels?.count)! {
if self.messageViewModels?[indexPath.row + 1].timeStringShown != nil {
switch adjustedSequencing {
case .firstOfSequence:
adjustedSequencing = .singleMessage
case .middleOfSequence:
adjustedSequencing = .lastOfSequence
default: break
// prepare formatter
let dateFormatter = DateFormatter()
if Calendar.current.compare(currentDateTime, to: time, toGranularity: .year) == .orderedSame {
if Calendar.current.compare(currentDateTime, to: time, toGranularity: .weekOfYear) == .orderedSame {
if Calendar.current.compare(currentDateTime, to: time, toGranularity: .day) == .orderedSame {
// age: [0, received the previous day[
dateFormatter.dateFormat = "h:mma"
} else {
// age: [received the previous day, received 7 days ago[
dateFormatter.dateFormat = "E h:mma"
}
} else {
// age: [received 7 days ago, received the previous year[
dateFormatter.dateFormat = "MMM d, h:mma"
}
} else {
// age: [received the previous year, inf[
dateFormatter.dateFormat = "MMM d, yyyy h:mma"
}
messageVM.sequencing = adjustedSequencing
switch messageVM.sequencing {
case .middleOfSequence:
cell.topCorner.isHidden = false
cell.bottomCorner.isHidden = false
cell.bubbleBottomConstraint.constant = 1
cell.bubbleTopConstraint.constant = messageVM.timeStringShown != nil ? 32 : 1
case .firstOfSequence:
cell.bottomCorner.isHidden = false
cell.bubbleBottomConstraint.constant = 1
cell.bubbleTopConstraint.constant = messageVM.timeStringShown != nil ? 32 : 8
case .lastOfSequence:
cell.topCorner.isHidden = false
cell.bubbleTopConstraint.constant = messageVM.timeStringShown != nil ? 32 : 1
default: break
}
// generate the string containing the message time
return dateFormatter.string(from: time).uppercased()
}
// swiftlint:enable cyclomatic_complexity
// swiftlint:disable cyclomatic_complexity
func formatCell(withCell cell: MessageCell,
cellForRowAt indexPath: IndexPath,
withMessageVM messageVM: MessageViewModel) {
// hide/show time label
formatTimeLabel(forCell: cell, withMessageVM: messageVM)
if messageVM.bubblePosition() == .generated {
cell.bubble.backgroundColor = UIColor.ringMsgCellReceived
cell.messageLabel.setTextWithLineSpacing(withText: messageVM.content, withLineSpacing: 2)
// generated messages should always show the time
cell.bubbleTopConstraint.constant = 32
return
}
// bubble grouping for cell
applyBubbleStyleToCell(toCell: cell, cellForRowAt: indexPath, withMessageVM: messageVM)
// special cases where top/bottom margins should be larger
if indexPath.row == 0 {
cell.bubbleTopConstraint.constant = 32
} else if self.messageViewModels?.count == indexPath.row + 1 {
cell.bubbleBottomConstraint.constant = 16
}
if messageVM.bubblePosition() == .sent {
messageVM.status.asObservable()
.observeOn(MainScheduler.instance)
.map { value in value == MessageStatus.sending ? true : false }
.bind(to: cell.sendingIndicator.rx.isAnimating)
.disposed(by: cell.disposeBag)
messageVM.status.asObservable()
.observeOn(MainScheduler.instance)
.map { value in value == MessageStatus.failure ? false : true }
.bind(to: cell.failedStatusLabel.rx.isHidden)
.disposed(by: cell.disposeBag)
} else if messageVM.bubblePosition() == .received {
// avatar
guard let fallbackAvatar = cell.fallbackAvatar else {
return
}
fallbackAvatar.isHidden = true
cell.profileImage?.isHidden = true
if messageVM.sequencing == .lastOfSequence || messageVM.sequencing == .singleMessage {
cell.profileImage?.isHidden = false
// Set placeholder avatar
fallbackAvatar.text = nil
cell.fallbackAvatarImage.isHidden = true
let name = viewModel.userName.value
let scanner = Scanner(string: name.toMD5HexString().prefixString())
var index: UInt64 = 0
if scanner.scanHexInt64(&index) {
fallbackAvatar.isHidden = false
fallbackAvatar.backgroundColor = avatarColors[Int(index)]
if viewModel.conversation.value.recipientRingId != name {
fallbackAvatar.text = name.prefixString().capitalized
} else {
cell.fallbackAvatarImage.isHidden = true
}
}
// Observe in case of a lookup
self.fallbackBGColorObservable
.subscribe(onNext: { [weak fallbackAvatar] backgroundColor in
fallbackAvatar?.backgroundColor = backgroundColor
})
.disposed(by: cell.disposeBag)
// Avatar placeholder initial
viewModel.userName.asObservable()
.observeOn(MainScheduler.instance)
.filter({ [weak self] userName in
return userName != self?.viewModel.conversation.value.recipientRingId
})
.map { value in value.prefixString().capitalized }
.bind(to: fallbackAvatar.rx.text)
.disposed(by: cell.disposeBag)
viewModel.userName.asObservable()
.observeOn(MainScheduler.instance)
.map { [weak self] userName in userName != self?.viewModel.conversation.value.recipientRingId }
.bind(to: cell.fallbackAvatarImage.rx.isHidden)
.disposed(by: cell.disposeBag)
// Set image if any
cell.profileImage?.image = nil
self.viewModel.profileImageData.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { data in
if let imageData = data {
if let image = UIImage(data: imageData) {
cell.profileImage?.image = image
fallbackAvatar.isHidden = true
}
} else {
cell.profileImage?.image = nil
fallbackAvatar.isHidden = false
}
}).disposed(by: cell.disposeBag)
}