diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift index 292f5aeb37f22c4120c4fe86e81b9e4f8b3bcebe..49d789c05a05e390d1b685718abc2c5b5afeba3f 100644 --- a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift +++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift @@ -36,6 +36,8 @@ class MessageCell: UITableViewCell, NibReusable { @IBOutlet weak var rightDivider: UIView! @IBOutlet weak var sendingIndicator: UIActivityIndicatorView! @IBOutlet weak var failedStatusLabel: UILabel! + @IBOutlet weak var profileImage: UIImageView! + @IBOutlet weak var fallbackAvatar: UILabel! let disposeBag = DisposeBag() } diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellReceived.xib b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellReceived.xib index 63530ba25131ad89c38cb6aa02939d0a9258a9f2..28372bb4abf8361a19106cef16d552fa16fd90b1 100644 --- a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellReceived.xib +++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellReceived.xib @@ -19,8 +19,37 @@ <rect key="frame" x="0.0" y="0.0" width="510" height="47"/> <autoresizingMask key="autoresizingMask"/> <subviews> + <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="skH-sJ-Ip9" userLabel="Profile Image"> + <rect key="frame" x="16" y="4" width="32" height="32"/> + <constraints> + <constraint firstAttribute="width" constant="32" id="fov-2e-ylg"/> + <constraint firstAttribute="height" constant="32" id="sTy-db-CSI"/> + </constraints> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius"> + <integer key="value" value="16"/> + </userDefinedRuntimeAttribute> + </userDefinedRuntimeAttributes> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="R" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="F9a-6w-Efg" userLabel="Fallback Avatar"> + <rect key="frame" x="16" y="4" width="32" height="32"/> + <color key="backgroundColor" red="1" green="0.5" blue="0.0" alpha="1" colorSpace="calibratedRGB"/> + <constraints> + <constraint firstAttribute="height" constant="32" id="C5R-RV-ZRw"/> + <constraint firstAttribute="width" constant="32" id="jnE-tr-sYI"/> + </constraints> + <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/> + <color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <nil key="highlightedColor"/> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius"> + <integer key="value" value="16"/> + </userDefinedRuntimeAttribute> + </userDefinedRuntimeAttributes> + </label> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WBd-CS-7Qv" userLabel="Top Corner"> - <rect key="frame" x="16" y="8" width="15" height="15"/> + <rect key="frame" x="64" 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"/> @@ -28,7 +57,7 @@ </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"/> + <rect key="frame" x="64" 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"/> @@ -36,7 +65,7 @@ </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="190.5" height="30.5"/> + <rect key="frame" x="64" 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="10" y="8" width="170.5" height="14.5"/> @@ -83,6 +112,7 @@ </label> </subviews> <constraints> + <constraint firstItem="skH-sJ-Ip9" firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" constant="-2" id="005-jN-Zor"/> <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"/> @@ -93,11 +123,14 @@ <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 firstItem="skH-sJ-Ip9" firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="leading" constant="-16" id="YCa-xJ-gRb"/> <constraint firstItem="zuX-zz-1Qq" firstAttribute="trailing" secondItem="mhg-uK-iD9" secondAttribute="leading" constant="-16" id="aUU-d6-Dse"/> + <constraint firstItem="F9a-6w-Efg" firstAttribute="leading" secondItem="skH-sJ-Ip9" secondAttribute="leading" id="fx8-vF-4od"/> <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="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="64" id="nWe-5k-Qpn"/> <constraint firstItem="eza-Ni-w3g" firstAttribute="centerY" secondItem="mhg-uK-iD9" secondAttribute="centerY" id="vhB-Uv-04a"/> + <constraint firstItem="F9a-6w-Efg" firstAttribute="bottom" secondItem="skH-sJ-Ip9" secondAttribute="bottom" id="w03-vx-kON"/> <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 firstAttribute="trailingMargin" secondItem="eza-Ni-w3g" secondAttribute="trailing" constant="16" id="yMp-aN-6PX"/> @@ -108,8 +141,10 @@ <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="fallbackAvatar" destination="F9a-6w-Efg" id="JGo-mt-PVe"/> <outlet property="leftDivider" destination="zuX-zz-1Qq" id="9Jc-cV-VTA"/> <outlet property="messageLabel" destination="lyR-7c-S2k" id="hd3-pz-Pwh"/> + <outlet property="profileImage" destination="skH-sJ-Ip9" id="pM2-t7-YhV"/> <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"/> diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift index db5d9135fbe6c80ba5ec7ca4953ae9a697fa8ebf..fe7fc2fb408403e36e562e71ad3dd3b1ff796bc8 100644 --- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift @@ -38,6 +38,8 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo var bottomOffset: CGFloat = 0 let scrollOffsetThreshold: CGFloat = 600 + fileprivate var backgroundColorObservable: Observable<UIColor>! + override func viewDidLoad() { super.viewDidLoad() @@ -79,6 +81,19 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo func setupUI() { self.viewModel.userName.asObservable().bind(to: self.navigationItem.rx.title).disposed(by: disposeBag) + // UIColor that observes "best Id" prefix + self.backgroundColorObservable = 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 @@ -351,7 +366,9 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo } } - switch adjustedSequencing { + messageVM.sequencing = adjustedSequencing + + switch messageVM.sequencing { case .middleOfSequence: cell.topCorner.isHidden = false cell.bottomCorner.isHidden = false @@ -396,6 +413,42 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo .map { value in value == MessageStatus.failure ? false : true } .bind(to: cell.failedStatusLabel.rx.isHidden) .disposed(by: disposeBag) + } else { + // 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 + + // Avatar placeholder initial + viewModel.userName.asObservable() + .observeOn(MainScheduler.instance) + .map { value in value.prefixString().capitalized } + .bind(to: fallbackAvatar.rx.text) + .disposed(by: disposeBag) + + // Set placeholder avatar to backgroundColorObservable + self.backgroundColorObservable + .subscribe(onNext: { backgroundColor in + fallbackAvatar.backgroundColor = backgroundColor + }) + .disposed(by: disposeBag) + + // Set image if any + cell.profileImage?.image = nil + if let imageData = viewModel.profileImageData { + if let image = UIImage(data: imageData) { + cell.profileImage?.image = image + fallbackAvatar.isHidden = true + } + } else { + fallbackAvatar.isHidden = false + } + } } }