Skip to content
Snippets Groups Projects
Commit 826a5cef authored by Andreas Traczyk's avatar Andreas Traczyk Committed by Kateryna Kostiuk
Browse files

conversations: add message date/time to conversation


- Inserts message reception time above incoming/outgoing
  messages with a minumum granularity of 1 minute.

- Formats date based on age.

- Resequences message bubbles based on prioritized time
  labels.

Change-Id: I8151484b9ef348105642adf535689eca5fe103f2
Reviewed-by: default avatarKateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
parent d350c48e
No related branches found
No related tags found
No related merge requests found
...@@ -53,4 +53,9 @@ extension UIColor { ...@@ -53,4 +53,9 @@ extension UIColor {
blue: 48.0/255.0, blue: 48.0/255.0,
alpha: 1.0) alpha: 1.0)
static let ringMsgCellTimeText = UIColor(colorLiteralRed: 128.0/255.0,
green: 128.0/255.0,
blue: 128.0/255.0,
alpha: 1.0)
} }
...@@ -30,4 +30,7 @@ class MessageCell: UITableViewCell, NibReusable { ...@@ -30,4 +30,7 @@ class MessageCell: UITableViewCell, NibReusable {
@IBOutlet weak var messageLabel: UILabel! @IBOutlet weak var messageLabel: UILabel!
@IBOutlet weak var bottomCorner: UIView! @IBOutlet weak var bottomCorner: UIView!
@IBOutlet weak var topCorner: UIView! @IBOutlet weak var topCorner: UIView!
@IBOutlet weak var timeLabel: UILabel!
@IBOutlet weak var leftDivider: UIView!
@IBOutlet weak var rightDivider: UIView!
} }
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
...@@ -59,6 +60,27 @@ ...@@ -59,6 +60,27 @@
</userDefinedRuntimeAttribute> </userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</view> </view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="zuX-zz-1Qq" userLabel="Left Divider">
<rect key="frame" x="24" y="16" width="138" height="1"/>
<color key="backgroundColor" red="0.94117647058823528" green="0.94117647058823528" blue="0.94117647058823528" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="OBa-HX-Vts"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eza-Ni-w3g" userLabel="Right Divider">
<rect key="frame" x="348.5" y="16" width="137.5" height="1"/>
<color key="backgroundColor" red="0.94117647058823528" green="0.94117647058823528" blue="0.94117647058823528" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="7z5-Mi-Abp"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="11/14/2016 12:34PM" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mhg-uK-iD9" userLabel="Message Time">
<rect key="frame" x="178" y="6" width="154.5" height="21"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" constant="8" id="1QQ-bu-6Bl"/> <constraint firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" constant="8" id="1QQ-bu-6Bl"/>
...@@ -67,10 +89,18 @@ ...@@ -67,10 +89,18 @@
<constraint firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" priority="1" constant="16" id="99Y-bR-Ioq"/> <constraint firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" priority="1" constant="16" id="99Y-bR-Ioq"/>
<constraint firstItem="kZJ-Ay-LTR" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="H2p-sc-9uM" secondAttribute="leading" priority="1" constant="64" id="Eso-cy-OYs"/> <constraint firstItem="kZJ-Ay-LTR" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="H2p-sc-9uM" secondAttribute="leading" priority="1" constant="64" id="Eso-cy-OYs"/>
<constraint firstItem="XcL-CH-BiH" firstAttribute="leading" secondItem="kZJ-Ay-LTR" secondAttribute="leading" id="GaI-yj-QFt"/> <constraint firstItem="XcL-CH-BiH" firstAttribute="leading" secondItem="kZJ-Ay-LTR" secondAttribute="leading" id="GaI-yj-QFt"/>
<constraint firstItem="mhg-uK-iD9" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" constant="-2" id="O3h-2G-pe5"/>
<constraint firstItem="eza-Ni-w3g" firstAttribute="leading" secondItem="mhg-uK-iD9" secondAttribute="trailing" constant="16" id="Pdf-ru-PnO"/>
<constraint firstItem="zuX-zz-1Qq" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" constant="16" id="QoG-Zs-Lv7"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" constant="64" id="TCY-7X-mFs"/> <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" constant="64" id="TCY-7X-mFs"/>
<constraint firstItem="zuX-zz-1Qq" firstAttribute="trailing" secondItem="mhg-uK-iD9" secondAttribute="leading" constant="-16" id="aUU-d6-Dse"/>
<constraint firstItem="mhg-uK-iD9" firstAttribute="centerX" secondItem="H2p-sc-9uM" secondAttribute="centerX" id="gD0-yo-bga"/>
<constraint firstItem="kZJ-Ay-LTR" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="jhd-A8-c1o"/> <constraint firstItem="kZJ-Ay-LTR" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="jhd-A8-c1o"/>
<constraint firstItem="kZJ-Ay-LTR" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="nWe-5k-Qpn"/> <constraint firstItem="kZJ-Ay-LTR" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="nWe-5k-Qpn"/>
<constraint firstItem="eza-Ni-w3g" firstAttribute="centerY" secondItem="mhg-uK-iD9" secondAttribute="centerY" id="vhB-Uv-04a"/>
<constraint firstItem="zuX-zz-1Qq" firstAttribute="centerY" secondItem="mhg-uK-iD9" secondAttribute="centerY" id="xFW-jt-00h"/>
<constraint firstItem="WBd-CS-7Qv" firstAttribute="leading" secondItem="kZJ-Ay-LTR" secondAttribute="leading" id="yBG-sT-w2a"/> <constraint firstItem="WBd-CS-7Qv" firstAttribute="leading" secondItem="kZJ-Ay-LTR" secondAttribute="leading" id="yBG-sT-w2a"/>
<constraint firstAttribute="trailingMargin" secondItem="eza-Ni-w3g" secondAttribute="trailing" constant="16" id="yMp-aN-6PX"/>
</constraints> </constraints>
</tableViewCellContentView> </tableViewCellContentView>
<connections> <connections>
...@@ -78,7 +108,10 @@ ...@@ -78,7 +108,10 @@
<outlet property="bubble" destination="kZJ-Ay-LTR" id="hdG-fG-L69"/> <outlet property="bubble" destination="kZJ-Ay-LTR" id="hdG-fG-L69"/>
<outlet property="bubbleBottomConstraint" destination="1QQ-bu-6Bl" id="a4F-pf-cXL"/> <outlet property="bubbleBottomConstraint" destination="1QQ-bu-6Bl" id="a4F-pf-cXL"/>
<outlet property="bubbleTopConstraint" destination="jhd-A8-c1o" id="40k-2d-6rW"/> <outlet property="bubbleTopConstraint" destination="jhd-A8-c1o" id="40k-2d-6rW"/>
<outlet property="leftDivider" destination="zuX-zz-1Qq" id="9Jc-cV-VTA"/>
<outlet property="messageLabel" destination="lyR-7c-S2k" id="hd3-pz-Pwh"/> <outlet property="messageLabel" destination="lyR-7c-S2k" id="hd3-pz-Pwh"/>
<outlet property="rightDivider" destination="eza-Ni-w3g" id="Jfy-s9-5t7"/>
<outlet property="timeLabel" destination="mhg-uK-iD9" id="x8D-vK-F6G"/>
<outlet property="topCorner" destination="WBd-CS-7Qv" id="GCm-Hv-5Ei"/> <outlet property="topCorner" destination="WBd-CS-7Qv" id="GCm-Hv-5Ei"/>
</connections> </connections>
<point key="canvasLocation" x="-411" y="-132.5"/> <point key="canvasLocation" x="-411" y="-132.5"/>
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
...@@ -59,16 +60,44 @@ ...@@ -59,16 +60,44 @@
</userDefinedRuntimeAttribute> </userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</view> </view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="h8N-aw-5lV" userLabel="Right Divider">
<rect key="frame" x="347.5" y="16" width="138.5" height="1"/>
<color key="backgroundColor" red="0.94117647058823528" green="0.94117647058823528" blue="0.94117647058823528" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="3h1-r8-Nis"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2U4-l3-KET" userLabel="Left Divider">
<rect key="frame" x="24" y="16" width="137" height="1"/>
<color key="backgroundColor" red="0.94117647058823528" green="0.94117647058823528" blue="0.94117647058823528" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="gla-pJ-IsN"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="11/14/2016 12:34PM" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ogn-wv-fZy" userLabel="Message Time">
<rect key="frame" x="177" y="6" width="154.5" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" constant="8" id="1QQ-bu-6Bl" userLabel="Bubble Bottom Constraint"/> <constraint firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" constant="8" id="1QQ-bu-6Bl" userLabel="Bubble Bottom Constraint"/>
<constraint firstItem="h8N-aw-5lV" firstAttribute="leading" secondItem="ogn-wv-fZy" secondAttribute="trailing" constant="16" id="1jW-JR-t5r"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" priority="1" constant="64" id="99Y-bR-Ioq"/> <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" priority="1" constant="64" id="99Y-bR-Ioq"/>
<constraint firstItem="kZJ-Ay-LTR" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" priority="1" constant="16" id="Eso-cy-OYs"/> <constraint firstItem="kZJ-Ay-LTR" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" priority="1" constant="16" id="Eso-cy-OYs"/>
<constraint firstItem="ogn-wv-fZy" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" constant="-2" id="Fxg-Wa-Rb9"/>
<constraint firstItem="2U4-l3-KET" firstAttribute="centerY" secondItem="ogn-wv-fZy" secondAttribute="centerY" id="J6Y-Ti-HDv"/>
<constraint firstItem="EMh-bG-ilg" firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" id="MY3-Aj-94K"/> <constraint firstItem="EMh-bG-ilg" firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" id="MY3-Aj-94K"/>
<constraint firstItem="ogn-wv-fZy" firstAttribute="centerX" secondItem="H2p-sc-9uM" secondAttribute="centerX" id="RaG-SO-xFo"/>
<constraint firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" constant="16" id="TCY-7X-mFs"/> <constraint firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" constant="16" id="TCY-7X-mFs"/>
<constraint firstItem="h8N-aw-5lV" firstAttribute="centerY" secondItem="ogn-wv-fZy" secondAttribute="centerY" id="Xdu-7c-MbP"/>
<constraint firstItem="2U4-l3-KET" firstAttribute="trailing" secondItem="ogn-wv-fZy" secondAttribute="leading" constant="-16" id="Xw6-H2-byY"/>
<constraint firstAttribute="trailingMargin" secondItem="h8N-aw-5lV" secondAttribute="trailing" constant="16" id="iwC-fc-F9c"/>
<constraint firstItem="kZJ-Ay-LTR" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="jhd-A8-c1o"/> <constraint firstItem="kZJ-Ay-LTR" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="jhd-A8-c1o"/>
<constraint firstItem="hdz-AQ-xHI" firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" id="lSl-vu-Wkl"/> <constraint firstItem="hdz-AQ-xHI" firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" id="lSl-vu-Wkl"/>
<constraint firstItem="kZJ-Ay-LTR" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="64" id="nWe-5k-Qpn"/> <constraint firstItem="kZJ-Ay-LTR" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="64" id="nWe-5k-Qpn"/>
<constraint firstItem="2U4-l3-KET" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" constant="16" id="uoy-US-ksI"/>
<constraint firstItem="EMh-bG-ilg" firstAttribute="top" secondItem="kZJ-Ay-LTR" secondAttribute="top" id="zEh-jv-0Ha"/> <constraint firstItem="EMh-bG-ilg" firstAttribute="top" secondItem="kZJ-Ay-LTR" secondAttribute="top" id="zEh-jv-0Ha"/>
<constraint firstItem="hdz-AQ-xHI" firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" id="zWA-Jg-F6Q"/> <constraint firstItem="hdz-AQ-xHI" firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" id="zWA-Jg-F6Q"/>
</constraints> </constraints>
...@@ -78,7 +107,10 @@ ...@@ -78,7 +107,10 @@
<outlet property="bubble" destination="kZJ-Ay-LTR" id="hdG-fG-L69"/> <outlet property="bubble" destination="kZJ-Ay-LTR" id="hdG-fG-L69"/>
<outlet property="bubbleBottomConstraint" destination="1QQ-bu-6Bl" id="woo-UQ-wXK"/> <outlet property="bubbleBottomConstraint" destination="1QQ-bu-6Bl" id="woo-UQ-wXK"/>
<outlet property="bubbleTopConstraint" destination="jhd-A8-c1o" id="cll-eA-OC5"/> <outlet property="bubbleTopConstraint" destination="jhd-A8-c1o" id="cll-eA-OC5"/>
<outlet property="leftDivider" destination="2U4-l3-KET" id="y4j-CT-gez"/>
<outlet property="messageLabel" destination="lyR-7c-S2k" id="hd3-pz-Pwh"/> <outlet property="messageLabel" destination="lyR-7c-S2k" id="hd3-pz-Pwh"/>
<outlet property="rightDivider" destination="h8N-aw-5lV" id="9pc-93-BG6"/>
<outlet property="timeLabel" destination="ogn-wv-fZy" id="7yt-vi-cSp"/>
<outlet property="topCorner" destination="EMh-bG-ilg" id="nHl-hn-BZ1"/> <outlet property="topCorner" destination="EMh-bG-ilg" id="nHl-hn-BZ1"/>
</connections> </connections>
<point key="canvasLocation" x="-411" y="-132.5"/> <point key="canvasLocation" x="-411" y="-132.5"/>
......
...@@ -23,14 +23,6 @@ import RxSwift ...@@ -23,14 +23,6 @@ import RxSwift
import Reusable import Reusable
import SwiftyBeaver import SwiftyBeaver
enum BubbleChaining {
case singleMessage
case firstOfSequence
case lastOfSequence
case middleOfSequence
case error
}
class ConversationViewController: UIViewController, UITextFieldDelegate, StoryboardBased, ViewModelBased { class ConversationViewController: UIViewController, UITextFieldDelegate, StoryboardBased, ViewModelBased {
let log = SwiftyBeaver.self let log = SwiftyBeaver.self
...@@ -136,6 +128,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo ...@@ -136,6 +128,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
//Bind the TableView to the ViewModel //Bind the TableView to the ViewModel
self.viewModel.messages.subscribe(onNext: { [weak self] (messageViewModels) in self.viewModel.messages.subscribe(onNext: { [weak self] (messageViewModels) in
self?.messageViewModels = messageViewModels self?.messageViewModels = messageViewModels
self?.computeSequencing()
self?.tableView.reloadData() self?.tableView.reloadData()
}).disposed(by: self.disposeBag) }).disposed(by: self.disposeBag)
...@@ -203,62 +196,132 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo ...@@ -203,62 +196,132 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
return textFieldShouldEndEditing return textFieldShouldEndEditing
} }
func isFirstMessage(cellForRowAt indexPath: IndexPath) -> Bool { func computeSequencing() {
return indexPath.row == 0 var lastShownTime: Date?
for (index, messageViewModel) in self.messageViewModels!.enumerated() {
// time labels
let time = messageViewModel.receivedDate
if index == 0 {
// always show first message's time
messageViewModel.timeStringShown = getTimeLabelString(forTime: time)
lastShownTime = time
} else {
// only show time for new messages if beyond an arbitrary time frame (1 minute)
// from the previously shown time
let hourComp = Calendar.current.compare(lastShownTime!, to: time, toGranularity: .hour)
let minuteComp = Calendar.current.compare(lastShownTime!, to: time, toGranularity: .minute)
if hourComp == .orderedSame && minuteComp == .orderedSame {
messageViewModel.timeStringShown = nil
} else {
messageViewModel.timeStringShown = getTimeLabelString(forTime: time)
lastShownTime = time
}
}
// sequencing
messageViewModel.sequencing = getMessageSequencing(forIndex: index)
}
}
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[
let day = Calendar.current.component(.day, from: time)
// apply appropriate suffix to day value
let suffix = day == 1 ? "st" : (day == 2 ? "nd" : (day == 3 ? "rd" : "th"))
dateFormatter.dateFormat = "MMM d'\(suffix),' h:mma"
}
} else {
// age: [received the previous year, inf[
dateFormatter.dateFormat = "MMM d'th yyyy,' h:mma"
}
// generate the string containing the message time
return dateFormatter.string(from: time).uppercased()
} }
func isLastMessage(cellForRowAt indexPath: IndexPath) -> Bool { func formatTimeLabel(forCell cell: MessageCell,
return self.messageViewModels?.count == indexPath.row + 1 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
} }
func getBubbleChaining(cellForRowAt indexPath: IndexPath) -> BubbleChaining { // setup the label
if let msgViewModel = self.messageViewModels?[indexPath.row] { 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() let msgOwner = msgViewModel.bubblePosition()
if self.messageViewModels?.count == 1 || indexPath.row == 0 { if self.messageViewModels?.count == 1 || index == 0 {
if self.messageViewModels?.count == indexPath.row + 1 { if self.messageViewModels?.count == index + 1 {
return BubbleChaining.singleMessage return MessageSequencing.singleMessage
} }
let nextMsgViewModel = indexPath.row + 1 <= (self.messageViewModels?.count)! let nextMsgViewModel = index + 1 <= (self.messageViewModels?.count)!
? self.messageViewModels?[indexPath.row + 1] : nil ? self.messageViewModels?[index + 1] : nil
if nextMsgViewModel != nil { if nextMsgViewModel != nil {
return msgOwner != nextMsgViewModel?.bubblePosition() return msgOwner != nextMsgViewModel?.bubblePosition()
? BubbleChaining.singleMessage : BubbleChaining.firstOfSequence ? MessageSequencing.singleMessage : MessageSequencing.firstOfSequence
} }
} else if self.messageViewModels?.count == indexPath.row + 1 { } else if self.messageViewModels?.count == index + 1 {
let lastMsgViewModel = indexPath.row - 1 >= 0 && indexPath.row - 1 < (self.messageViewModels?.count)! let lastMsgViewModel = index - 1 >= 0 && index - 1 < (self.messageViewModels?.count)!
? self.messageViewModels?[indexPath.row - 1] : nil ? self.messageViewModels?[index - 1] : nil
if lastMsgViewModel != nil { if lastMsgViewModel != nil {
return msgOwner != lastMsgViewModel?.bubblePosition() return msgOwner != lastMsgViewModel?.bubblePosition()
? BubbleChaining.singleMessage : BubbleChaining.lastOfSequence ? MessageSequencing.singleMessage : MessageSequencing.lastOfSequence
} }
} }
let lastMsgViewModel = indexPath.row - 1 >= 0 && indexPath.row - 1 < (self.messageViewModels?.count)! let lastMsgViewModel = index - 1 >= 0 && index - 1 < (self.messageViewModels?.count)!
? self.messageViewModels?[indexPath.row - 1] : nil ? self.messageViewModels?[index - 1] : nil
let nextMsgViewModel = indexPath.row + 1 <= (self.messageViewModels?.count)! let nextMsgViewModel = index + 1 <= (self.messageViewModels?.count)!
? self.messageViewModels?[indexPath.row + 1] : nil ? self.messageViewModels?[index + 1] : nil
var chaining = BubbleChaining.singleMessage var sequencing = MessageSequencing.singleMessage
if (lastMsgViewModel != nil) && (nextMsgViewModel != nil) { if (lastMsgViewModel != nil) && (nextMsgViewModel != nil) {
if msgOwner != lastMsgViewModel?.bubblePosition() && msgOwner == nextMsgViewModel?.bubblePosition() { if msgOwner != lastMsgViewModel?.bubblePosition() && msgOwner == nextMsgViewModel?.bubblePosition() {
chaining = BubbleChaining.firstOfSequence sequencing = MessageSequencing.firstOfSequence
} else if msgOwner != nextMsgViewModel?.bubblePosition() && msgOwner == lastMsgViewModel?.bubblePosition() { } else if msgOwner != nextMsgViewModel?.bubblePosition() && msgOwner == lastMsgViewModel?.bubblePosition() {
chaining = BubbleChaining.lastOfSequence sequencing = MessageSequencing.lastOfSequence
} else if msgOwner == nextMsgViewModel?.bubblePosition() && msgOwner == lastMsgViewModel?.bubblePosition() { } else if msgOwner == nextMsgViewModel?.bubblePosition() && msgOwner == lastMsgViewModel?.bubblePosition() {
chaining = BubbleChaining.middleOfSequence sequencing = MessageSequencing.middleOfSequence
} }
} }
return chaining return sequencing
} }
return BubbleChaining.error return MessageSequencing.unknown
} }
func applyBubbleStyleToCell(toCell cell: MessageCell, func applyBubbleStyleToCell(toCell cell: MessageCell,
withChaining chaining: BubbleChaining, cellForRowAt indexPath: IndexPath,
withContent content: String, withMessageVM messageVM: MessageViewModel) {
withType type: BubblePosition) { let type = messageVM.bubblePosition()
let bubbleColor = type == .received ? UIColor.ringMsgCellReceived : UIColor.ringMsgCellSent let bubbleColor = type == .received ? UIColor.ringMsgCellReceived : UIColor.ringMsgCellSent
cell.messageLabel.setTextWithLineSpacing(withText: content, withLineSpacing: 2) cell.messageLabel.setTextWithLineSpacing(withText: messageVM.content, withLineSpacing: 2)
cell.topCorner.isHidden = true cell.topCorner.isHidden = true
cell.topCorner.backgroundColor = bubbleColor cell.topCorner.backgroundColor = bubbleColor
...@@ -267,20 +330,60 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo ...@@ -267,20 +330,60 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
cell.bubbleBottomConstraint.constant = 8 cell.bubbleBottomConstraint.constant = 8
cell.bubbleTopConstraint.constant = 8 cell.bubbleTopConstraint.constant = 8
switch chaining { 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
}
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
}
}
}
switch adjustedSequencing {
case .middleOfSequence: case .middleOfSequence:
cell.topCorner.isHidden = false cell.topCorner.isHidden = false
cell.bottomCorner.isHidden = false cell.bottomCorner.isHidden = false
cell.bubbleBottomConstraint.constant = 1 cell.bubbleBottomConstraint.constant = 1
cell.bubbleTopConstraint.constant = 1 cell.bubbleTopConstraint.constant = messageVM.timeStringShown != nil ? 32 : 1
case .firstOfSequence: case .firstOfSequence:
cell.bottomCorner.isHidden = false cell.bottomCorner.isHidden = false
cell.bubbleBottomConstraint.constant = 1 cell.bubbleBottomConstraint.constant = 1
cell.bubbleTopConstraint.constant = messageVM.timeStringShown != nil ? 32 : 8
case .lastOfSequence: case .lastOfSequence:
cell.topCorner.isHidden = false cell.topCorner.isHidden = false
cell.bubbleTopConstraint.constant = 1 cell.bubbleTopConstraint.constant = messageVM.timeStringShown != nil ? 32 : 1
default: break default: break
} }
}
func formatCell(withCell cell: MessageCell,
cellForRowAt indexPath: IndexPath,
withMessageVM messageVM: MessageViewModel) {
// hide/show time label
formatTimeLabel(forCell: cell, withMessageVM: messageVM)
// 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
}
} }
} }
...@@ -291,38 +394,14 @@ extension ConversationViewController: UITableViewDataSource { ...@@ -291,38 +394,14 @@ extension ConversationViewController: UITableViewDataSource {
} }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let messageViewModel = self.messageViewModels?[indexPath.row] { if let messageViewModel = self.messageViewModels?[indexPath.row] {
let chaining = self.getBubbleChaining(cellForRowAt: indexPath)
if messageViewModel.bubblePosition() == .received { if messageViewModel.bubblePosition() == .received {
// left side (incoming)
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: MessageCellReceived.self) let cell = tableView.dequeueReusableCell(for: indexPath, cellType: MessageCellReceived.self)
formatCell(withCell: cell, cellForRowAt: indexPath, withMessageVM: messageViewModel)
// Format cell
applyBubbleStyleToCell(toCell: cell, withChaining: chaining, withContent: messageViewModel.content, withType: .received)
// Special cases where top/bottom margins should be larger
if isFirstMessage(cellForRowAt: indexPath) {
cell.bubbleTopConstraint.constant = 16
} else if isLastMessage(cellForRowAt: indexPath) {
cell.bubbleBottomConstraint.constant = 16
}
return cell return cell
} else { } else {
// right side (outgoing)
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: MessageCellSent.self) let cell = tableView.dequeueReusableCell(for: indexPath, cellType: MessageCellSent.self)
formatCell(withCell: cell, cellForRowAt: indexPath, withMessageVM: messageViewModel)
// Format cell
applyBubbleStyleToCell(toCell: cell, withChaining: chaining, withContent: messageViewModel.content, withType: .sent)
// Special cases where top/bottom margins should be larger
if isFirstMessage(cellForRowAt: indexPath) {
cell.bubbleTopConstraint.constant = 16
} else if isLastMessage(cellForRowAt: indexPath) {
cell.bubbleBottomConstraint.constant = 16
}
return cell return cell
} }
} }
......
...@@ -26,6 +26,14 @@ enum BubblePosition { ...@@ -26,6 +26,14 @@ enum BubblePosition {
case generated case generated
} }
enum MessageSequencing {
case singleMessage
case firstOfSequence
case lastOfSequence
case middleOfSequence
case unknown
}
enum GeneratedMessageType: String { enum GeneratedMessageType: String {
case sendContactRequest = "The invitation has been sent" case sendContactRequest = "The invitation has been sent"
case receivedContactRequest = "Contact request received" case receivedContactRequest = "Contact request received"
...@@ -37,16 +45,32 @@ class MessageViewModel { ...@@ -37,16 +45,32 @@ class MessageViewModel {
fileprivate let accountService: AccountsService fileprivate let accountService: AccountsService
fileprivate var message: MessageModel fileprivate var message: MessageModel
var timeStringShown: String?
var sequencing: MessageSequencing = .unknown
init(withInjectionBag injectionBag: InjectionBag, init(withInjectionBag injectionBag: InjectionBag,
withMessage message: MessageModel) { withMessage message: MessageModel) {
self.accountService = injectionBag.accountService self.accountService = injectionBag.accountService
self.message = message self.message = message
self.timeStringShown = nil
} }
var content: String { var content: String {
return self.message.content return self.message.content
} }
var receivedDate: Date {
return self.message.receivedDate
}
var id: Int64 {
return self.message.id
}
var status: MessageStatus {
return self.message.status
}
func bubblePosition() -> BubblePosition { func bubblePosition() -> BubblePosition {
if self.message.isGenerated { if self.message.isGenerated {
return .generated return .generated
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment