Commit d350c48e authored by Andreas Traczyk's avatar Andreas Traczyk

conversations: implement message grouping

- Calculates the grouping property of each message, determining
  whether it's first, middle, or last within a sequence either
  sent or received.

- Adjusts the top and bottom constraints for each message bubble
  according to its sequencing.

- Fixes the scroll-to-bottom feature by updating the bottoOffset
  value before conditionally scrolling, removing the scroll
  animation, and only scrolling down when the user is near the
  end of the chat.

- Applies a message bubble grouping style, adjusts line spacing
  of the content, and decouples the theming of the message
  bubble and text colors.

Change-Id: I9118c2bbca0433573c877450c73bd6dc5c9229a0
Reviewed-by: Kateryna Kostiuk's avatarKateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
parent d8b60072
......@@ -186,6 +186,8 @@
56BBC9D41EDC7A6D00CDAF8B /* libargon2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 56BBC9D31EDC7A6D00CDAF8B /* libargon2.a */; };
56BBC9DF1EDDC9D300CDAF8B /* LookupNameResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 56BBC9DE1EDDC9D300CDAF8B /* LookupNameResponse.m */; };
56C715FF1F0D36C600770048 /* ContactsAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 56C715FE1F0D36C600770048 /* ContactsAdapter.mm */; };
621231F91F880EDF009B86F0 /* UILabel+Ring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621231F81F880EDF009B86F0 /* UILabel+Ring.swift */; };
621231FB1F8D6FEE009B86F0 /* MessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621231FA1F8D6FEE009B86F0 /* MessageCell.swift */; };
62A88D371F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D361F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift */; };
62A88D391F6C323500F8AB18 /* PresenceAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D381F6C323500F8AB18 /* PresenceAdapter.mm */; };
62A88D3B1F6C3ACC00F8AB18 /* PresenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D3A1F6C3ACC00F8AB18 /* PresenceService.swift */; };
......@@ -417,6 +419,8 @@
56C715FE1F0D36C600770048 /* ContactsAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ContactsAdapter.mm; sourceTree = "<group>"; };
56C716001F0D36D900770048 /* ContactsAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsAdapterDelegate.swift; sourceTree = "<group>"; };
56C716021F0D466100770048 /* ContactsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsService.swift; sourceTree = "<group>"; };
621231F81F880EDF009B86F0 /* UILabel+Ring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+Ring.swift"; sourceTree = "<group>"; };
621231FA1F8D6FEE009B86F0 /* MessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCell.swift; sourceTree = "<group>"; };
62A88D351F6C2E5F00F8AB18 /* PresenceAdapter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PresenceAdapter.h; sourceTree = "<group>"; };
62A88D361F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceAdapterDelegate.swift; sourceTree = "<group>"; };
62A88D381F6C323500F8AB18 /* PresenceAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PresenceAdapter.mm; sourceTree = "<group>"; };
......@@ -633,6 +637,7 @@
1A2D18A51F27F7A400B2C785 /* UIViewController+Rx.swift */,
0586C94A1F684DF600613517 /* UIImage+Helpers.swift */,
0EE1B54D1F75ACDE00BA98EE /* CNContactVCardSerialization+Helpers.swift */,
621231F81F880EDF009B86F0 /* UILabel+Ring.swift */,
);
path = Extensions;
sourceTree = "<group>";
......@@ -957,6 +962,7 @@
1A2D18F11F292D7200B2C785 /* MessageCellReceived.swift */,
1A2D18F21F292D7200B2C785 /* MessageCellReceived.xib */,
1A2D18F31F292D7200B2C785 /* MessageCellSent.swift */,
621231FA1F8D6FEE009B86F0 /* MessageCell.swift */,
1A2D18F41F292D7200B2C785 /* MessageCellSent.xib */,
0E403F801F7D797300C80BC2 /* MessageCellGenerated.swift */,
0E403F821F7D79B000C80BC2 /* MessageCellGenerated.xib */,
......@@ -1274,6 +1280,7 @@
files = (
557086521E8ADB9D001A7CE4 /* SystemAdapter.mm in Sources */,
0586C94B1F684DF600613517 /* UIImage+Helpers.swift in Sources */,
621231F91F880EDF009B86F0 /* UILabel+Ring.swift in Sources */,
1A2D18AC1F29149D00B2C785 /* MeCoordinator.swift in Sources */,
1A2D18C51F29180700B2C785 /* ContactModel.swift in Sources */,
1A2D18F71F292D7200B2C785 /* MessageCellSent.swift in Sources */,
......@@ -1310,6 +1317,7 @@
56308BA71EA00E5700660275 /* NameRegistrationResponse.m in Sources */,
1A3CA32D1F13DA7200283748 /* Chameleon+Ring.swift in Sources */,
1ABE07E21F0D924700D36361 /* Strings.swift in Sources */,
621231FB1F8D6FEE009B86F0 /* MessageCell.swift in Sources */,
56AC650E1E85694D00EA1AA9 /* DesignableTextField.swift in Sources */,
1A2D189A1F2642C000B2C785 /* NotificationCenter+Ring.swift in Sources */,
1A2D18FC1F292DAD00B2C785 /* ConversationCell.swift in Sources */,
......
......@@ -58,6 +58,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
self.window = UIWindow(frame: UIScreen.main.bounds)
UserDefaults.standard.setValue(false, forKey: "_UIConstraintBasedLayoutLogUnsatisfiable")
// initialize log format
let console = ConsoleDestination()
console.format = "$Dyyyy-MM-dd HH:mm:ss.SSS$d $C$L$c: $M"
......
......@@ -31,15 +31,12 @@ extension Chameleon {
case .contrast:
contentColor = ContrastColorOf(primaryColor, returnFlat: false)
secondaryContentColor = ContrastColorOf(secondaryColor, returnFlat: false)
break
case .light:
contentColor = UIColor.white
secondaryContentColor = UIColor.white
break
case .dark:
contentColor = UIColor.flatBlackColorDark()
secondaryContentColor = UIColor.flatBlackColorDark()
break
}
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = UIColor.flatGray()
......@@ -48,12 +45,12 @@ extension Chameleon {
MessageBubble.appearance().backgroundColor = secondaryColor
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellSent.self]).tintColor = contentColor
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellSent.self]).backgroundColor = primaryColor
UILabel.appearance(whenContainedInInstancesOf: [MessageBubble.self, MessageCellSent.self]).textColor = contentColor
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellSent.self]).backgroundColor = UIColor.ringMsgCellSent
UILabel.appearance(whenContainedInInstancesOf: [MessageBubble.self, MessageCellSent.self]).textColor = UIColor.ringMsgCellSentText
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellReceived.self]).tintColor = secondaryContentColor
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellReceived.self]).backgroundColor = secondaryColor
UILabel.appearance(whenContainedInInstancesOf: [MessageBubble.self, MessageCellReceived.self]).textColor = secondaryContentColor
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellReceived.self]).backgroundColor = UIColor.ringMsgCellReceived
UILabel.appearance(whenContainedInInstancesOf: [MessageBubble.self, MessageCellReceived.self]).textColor = UIColor.ringMsgCellReceivedText
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellGenerated.self]).tintColor = UIColor.clear
MessageBubble.appearance(whenContainedInInstancesOf: [MessageCellGenerated.self]).backgroundColor = UIColor.clear
......
......@@ -33,4 +33,24 @@ extension UIColor {
blue: 96.0/255.0,
alpha: 1.0)
static let ringMsgCellSent = UIColor(colorLiteralRed: 58.0/255.0,
green: 192.0/255.0,
blue: 210.0/255.0,
alpha: 1.0)
static let ringMsgCellSentText = UIColor(colorLiteralRed: 255.0/255.0,
green: 255.0/255.0,
blue: 255.0/255.0,
alpha: 1.0)
static let ringMsgCellReceived = UIColor(colorLiteralRed: 235.0/255.0,
green: 239.0/255.0,
blue: 239.0/255.0,
alpha: 1.0)
static let ringMsgCellReceivedText = UIColor(colorLiteralRed: 48.0/255.0,
green: 48.0/255.0,
blue: 48.0/255.0,
alpha: 1.0)
}
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
*
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import Foundation
extension UILabel {
func setTextWithLineSpacing(withText: String, withLineSpacing: CGFloat) {
let attrString = NSMutableAttributedString(string: withText)
let style = NSMutableParagraphStyle()
style.lineSpacing = withLineSpacing
attrString.addAttribute(NSParagraphStyleAttributeName,
value: style,
range: NSRange(location: 0, length: withText.utf16.count))
self.attributedText = attrString
}
}
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import UIKit
import Reusable
class MessageCell: UITableViewCell, NibReusable {
@IBOutlet weak var bubble: MessageBubble!
@IBOutlet weak var bubbleBottomConstraint: NSLayoutConstraint!
@IBOutlet weak var bubbleTopConstraint: NSLayoutConstraint!
@IBOutlet weak var messageLabel: UILabel!
@IBOutlet weak var bottomCorner: UIView!
@IBOutlet weak var topCorner: UIView!
}
......@@ -21,9 +21,5 @@
import UIKit
import Reusable
class MessageCellReceived: UITableViewCell, NibReusable {
@IBOutlet weak var bubble: MessageBubble!
@IBOutlet weak var messageLabel: UILabel!
class MessageCellReceived: MessageCell {
}
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
......@@ -15,48 +15,73 @@
<rect key="frame" x="0.0" y="0.0" width="510" height="47"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="510" height="46.5"/>
<rect key="frame" x="0.0" y="0.0" width="510" height="47"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WBd-CS-7Qv" userLabel="Top Corner">
<rect key="frame" x="16" y="8" width="15" height="15"/>
<color key="backgroundColor" red="1" green="0.0" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="15" id="fjJ-O1-VNm"/>
<constraint firstAttribute="width" constant="15" id="gch-Wg-ytg"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="XcL-CH-BiH" userLabel="Bottom Corner">
<rect key="frame" x="16" y="24" width="15" height="15"/>
<color key="backgroundColor" red="1" green="0.5" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="width" constant="15" id="ocR-DU-zKZ"/>
<constraint firstAttribute="height" constant="15" id="ooc-tv-fiO"/>
</constraints>
</view>
<view clipsSubviews="YES" contentMode="scaleToFill" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="kZJ-Ay-LTR" customClass="MessageBubble" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="16" y="8" width="152.5" height="30.5"/>
<rect key="frame" x="16" y="8" width="190.5" height="30.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label Label Label Label " lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lyR-7c-S2k">
<rect key="frame" x="8" y="4" width="136.5" height="22.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label Label Label Label" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lyR-7c-S2k">
<rect key="frame" x="10" y="8" width="170.5" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="30" id="1Kj-UZ-gu7"/>
<constraint firstItem="lyR-7c-S2k" firstAttribute="leading" secondItem="kZJ-Ay-LTR" secondAttribute="leading" constant="8" id="8m5-sR-xnh"/>
<constraint firstAttribute="bottom" secondItem="lyR-7c-S2k" secondAttribute="bottom" constant="4" id="gwN-uX-PWd"/>
<constraint firstAttribute="trailing" secondItem="lyR-7c-S2k" secondAttribute="trailing" constant="8" id="uzV-kG-oGN"/>
<constraint firstItem="lyR-7c-S2k" firstAttribute="top" secondItem="kZJ-Ay-LTR" secondAttribute="top" constant="4" id="ycc-WI-Jk6"/>
<constraint firstItem="lyR-7c-S2k" firstAttribute="leading" secondItem="kZJ-Ay-LTR" secondAttribute="leading" constant="10" id="8m5-sR-xnh"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="28" id="UWN-H4-Sh9"/>
<constraint firstAttribute="bottom" secondItem="lyR-7c-S2k" secondAttribute="bottom" constant="8" id="gwN-uX-PWd"/>
<constraint firstAttribute="trailing" secondItem="lyR-7c-S2k" secondAttribute="trailing" constant="10" id="uzV-kG-oGN"/>
<constraint firstItem="lyR-7c-S2k" firstAttribute="top" secondItem="kZJ-Ay-LTR" secondAttribute="top" constant="8" id="ycc-WI-Jk6"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<real key="value" value="4"/>
<integer key="value" value="15"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" constant="8" id="1QQ-bu-6Bl"/>
<constraint firstItem="XcL-CH-BiH" firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" id="2d4-0F-VWg"/>
<constraint firstItem="WBd-CS-7Qv" firstAttribute="top" secondItem="kZJ-Ay-LTR" secondAttribute="top" id="4Zp-8q-rFJ"/>
<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="XcL-CH-BiH" firstAttribute="leading" secondItem="kZJ-Ay-LTR" secondAttribute="leading" id="GaI-yj-QFt"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" constant="64" id="TCY-7X-mFs"/>
<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="WBd-CS-7Qv" firstAttribute="leading" secondItem="kZJ-Ay-LTR" secondAttribute="leading" id="yBG-sT-w2a"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="bottomCorner" destination="XcL-CH-BiH" id="4gw-IC-EAM"/>
<outlet property="bubble" destination="kZJ-Ay-LTR" id="hdG-fG-L69"/>
<outlet property="bubbleBottomConstraint" destination="1QQ-bu-6Bl" id="a4F-pf-cXL"/>
<outlet property="bubbleTopConstraint" destination="jhd-A8-c1o" id="40k-2d-6rW"/>
<outlet property="messageLabel" destination="lyR-7c-S2k" id="hd3-pz-Pwh"/>
<outlet property="topCorner" destination="WBd-CS-7Qv" id="GCm-Hv-5Ei"/>
</connections>
<point key="canvasLocation" x="-411" y="-132"/>
<point key="canvasLocation" x="-411" y="-132.5"/>
</tableViewCell>
</objects>
</document>
......@@ -21,9 +21,5 @@
import UIKit
import Reusable
class MessageCellSent: UITableViewCell, NibReusable {
@IBOutlet weak var bubble: MessageBubble!
@IBOutlet weak var messageLabel: UILabel!
class MessageCellSent: MessageCell {
}
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
......@@ -18,45 +18,70 @@
<rect key="frame" x="0.0" y="0.0" width="510" height="46.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="hdz-AQ-xHI" userLabel="Bottom Corner">
<rect key="frame" x="479" y="24" width="15" height="15"/>
<color key="backgroundColor" red="1" green="0.5" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" constant="15" id="D0h-cW-9kB"/>
<constraint firstAttribute="width" constant="15" id="wlh-ar-Nsv"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="EMh-bG-ilg" userLabel="Top Corner">
<rect key="frame" x="479" y="8" width="15" height="15"/>
<color key="backgroundColor" red="1" green="0.0" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="width" constant="15" id="zaa-Rn-ziw"/>
<constraint firstAttribute="height" constant="15" id="zuP-4P-1GS"/>
</constraints>
</view>
<view clipsSubviews="YES" contentMode="scaleToFill" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="kZJ-Ay-LTR" customClass="MessageBubble" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="341.5" y="8" width="152.5" height="30.5"/>
<rect key="frame" x="303.5" y="8" width="190.5" height="31"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label Label Label Label " lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lyR-7c-S2k">
<rect key="frame" x="8" y="4" width="136.5" height="22.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label Label Label Label" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lyR-7c-S2k">
<rect key="frame" x="10" y="8" width="170.5" height="15"/>
<fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="16"/>
<color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="30" id="1Kj-UZ-gu7"/>
<constraint firstItem="lyR-7c-S2k" firstAttribute="leading" secondItem="kZJ-Ay-LTR" secondAttribute="leading" constant="8" id="8m5-sR-xnh"/>
<constraint firstAttribute="bottom" secondItem="lyR-7c-S2k" secondAttribute="bottom" constant="4" id="gwN-uX-PWd"/>
<constraint firstAttribute="trailing" secondItem="lyR-7c-S2k" secondAttribute="trailing" constant="8" id="uzV-kG-oGN"/>
<constraint firstItem="lyR-7c-S2k" firstAttribute="top" secondItem="kZJ-Ay-LTR" secondAttribute="top" constant="4" id="ycc-WI-Jk6"/>
<constraint firstItem="lyR-7c-S2k" firstAttribute="leading" secondItem="kZJ-Ay-LTR" secondAttribute="leading" constant="10" id="8m5-sR-xnh"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="28" id="BZE-kP-hPK"/>
<constraint firstAttribute="bottom" secondItem="lyR-7c-S2k" secondAttribute="bottom" constant="8" id="gwN-uX-PWd"/>
<constraint firstAttribute="trailing" secondItem="lyR-7c-S2k" secondAttribute="trailing" constant="10" id="uzV-kG-oGN"/>
<constraint firstItem="lyR-7c-S2k" firstAttribute="top" secondItem="kZJ-Ay-LTR" secondAttribute="top" constant="8" id="ycc-WI-Jk6"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<real key="value" value="4"/>
<integer key="value" value="15"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
</subviews>
<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" userLabel="Bubble Bottom Constraint"/>
<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="EMh-bG-ilg" firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" id="MY3-Aj-94K"/>
<constraint firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" constant="16" id="TCY-7X-mFs"/>
<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="kZJ-Ay-LTR" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="64" id="nWe-5k-Qpn"/>
<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"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="bottomCorner" destination="hdz-AQ-xHI" id="ChE-BT-0LS"/>
<outlet property="bubble" destination="kZJ-Ay-LTR" id="hdG-fG-L69"/>
<outlet property="bubbleBottomConstraint" destination="1QQ-bu-6Bl" id="woo-UQ-wXK"/>
<outlet property="bubbleTopConstraint" destination="jhd-A8-c1o" id="cll-eA-OC5"/>
<outlet property="messageLabel" destination="lyR-7c-S2k" id="hd3-pz-Pwh"/>
<outlet property="topCorner" destination="EMh-bG-ilg" id="nHl-hn-BZ1"/>
</connections>
<point key="canvasLocation" x="-411" y="-132"/>
<point key="canvasLocation" x="-411" y="-132.5"/>
</tableViewCell>
</objects>
</document>
......@@ -21,9 +21,20 @@
import UIKit
import RxSwift
import Reusable
import SwiftyBeaver
enum BubbleChaining {
case singleMessage
case firstOfSequence
case lastOfSequence
case middleOfSequence
case error
}
class ConversationViewController: UIViewController, UITextFieldDelegate, StoryboardBased, ViewModelBased {
let log = SwiftyBeaver.self
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var spinnerView: UIView!
......@@ -33,6 +44,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
var messageViewModels: [MessageViewModel]?
var textFieldShouldEndEditing = false
var bottomOffset: CGFloat = 0
let scrollOffsetThreshold: CGFloat = 600
override func viewDidLoad() {
super.viewDidLoad()
......@@ -147,7 +159,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
fileprivate func scrollToBottomIfNeed() {
if self.isBottomContentOffset {
self.scrollToBottom(animated: true)
self.scrollToBottom(animated: false)
}
}
......@@ -160,7 +172,9 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
}
fileprivate var isBottomContentOffset: Bool {
return self.tableView.contentOffset.y + self.tableView.contentInset.top >= bottomOffset
updateBottomOffset()
let offset = abs((self.tableView.contentOffset.y + self.tableView.contentInset.top) - bottomOffset)
return offset <= scrollOffsetThreshold
}
override var inputAccessoryView: UIView {
......@@ -189,6 +203,86 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
return textFieldShouldEndEditing
}
func isFirstMessage(cellForRowAt indexPath: IndexPath) -> Bool {
return indexPath.row == 0
}
func isLastMessage(cellForRowAt indexPath: IndexPath) -> Bool {
return self.messageViewModels?.count == indexPath.row + 1
}
func getBubbleChaining(cellForRowAt indexPath: IndexPath) -> BubbleChaining {
if let msgViewModel = self.messageViewModels?[indexPath.row] {
let msgOwner = msgViewModel.bubblePosition()
if self.messageViewModels?.count == 1 || indexPath.row == 0 {
if self.messageViewModels?.count == indexPath.row + 1 {
return BubbleChaining.singleMessage
}
let nextMsgViewModel = indexPath.row + 1 <= (self.messageViewModels?.count)!
? self.messageViewModels?[indexPath.row + 1] : nil
if nextMsgViewModel != nil {
return msgOwner != nextMsgViewModel?.bubblePosition()
? BubbleChaining.singleMessage : BubbleChaining.firstOfSequence
}
} else if self.messageViewModels?.count == indexPath.row + 1 {
let lastMsgViewModel = indexPath.row - 1 >= 0 && indexPath.row - 1 < (self.messageViewModels?.count)!
? self.messageViewModels?[indexPath.row - 1] : nil
if lastMsgViewModel != nil {
return msgOwner != lastMsgViewModel?.bubblePosition()
? BubbleChaining.singleMessage : BubbleChaining.lastOfSequence
}
}
let lastMsgViewModel = indexPath.row - 1 >= 0 && indexPath.row - 1 < (self.messageViewModels?.count)!
? self.messageViewModels?[indexPath.row - 1] : nil
let nextMsgViewModel = indexPath.row + 1 <= (self.messageViewModels?.count)!
? self.messageViewModels?[indexPath.row + 1] : nil
var chaining = BubbleChaining.singleMessage
if (lastMsgViewModel != nil) && (nextMsgViewModel != nil) {
if msgOwner != lastMsgViewModel?.bubblePosition() && msgOwner == nextMsgViewModel?.bubblePosition() {
chaining = BubbleChaining.firstOfSequence
} else if msgOwner != nextMsgViewModel?.bubblePosition() && msgOwner == lastMsgViewModel?.bubblePosition() {
chaining = BubbleChaining.lastOfSequence
} else if msgOwner == nextMsgViewModel?.bubblePosition() && msgOwner == lastMsgViewModel?.bubblePosition() {
chaining = BubbleChaining.middleOfSequence
}
}
return chaining
}
return BubbleChaining.error
}
func applyBubbleStyleToCell(toCell cell: MessageCell,
withChaining chaining: BubbleChaining,
withContent content: String,
withType type: BubblePosition) {
let bubbleColor = type == .received ? UIColor.ringMsgCellReceived : UIColor.ringMsgCellSent
cell.messageLabel.setTextWithLineSpacing(withText: content, withLineSpacing: 2)
cell.topCorner.isHidden = true
cell.topCorner.backgroundColor = bubbleColor
cell.bottomCorner.isHidden = true
cell.bottomCorner.backgroundColor = bubbleColor
cell.bubbleBottomConstraint.constant = 8
cell.bubbleTopConstraint.constant = 8
switch chaining {
case .middleOfSequence:
cell.topCorner.isHidden = false
cell.bottomCorner.isHidden = false
cell.bubbleBottomConstraint.constant = 1
cell.bubbleTopConstraint.constant = 1
case .firstOfSequence:
cell.bottomCorner.isHidden = false
cell.bubbleBottomConstraint.constant = 1
case .lastOfSequence:
cell.topCorner.isHidden = false
cell.bubbleTopConstraint.constant = 1
default: break
}
}
}
extension ConversationViewController: UITableViewDataSource {
......@@ -199,21 +293,38 @@ extension ConversationViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let messageViewModel = self.messageViewModels?[indexPath.row] {
let chaining = self.getBubbleChaining(cellForRowAt: indexPath)
if messageViewModel.bubblePosition() == .received {
// left side (incoming)
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: MessageCellReceived.self)
cell.messageLabel.text = messageViewModel.content
// 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
}
} else {
// right side (outgoing)
let cell = tableView.dequeueReusableCell(for: indexPath, cellType: MessageCellSent.self)
// Format cell
applyBubbleStyleToCell(toCell: cell, withChaining: chaining, withContent: messageViewModel.content, withType: .sent)