Messages: Add messages screen to read messages and reply

Add MVVM, services and adpater to receive and reply to messages

This allows to read received messages from other users and reply to
them.

The messages screen uses a TableView that scroll automatically to the
bottom if the user a new message is received. Messages bubbles are
made with simple cells with a label that move to the right or the
left.

Change-Id: I1b6bd6fd36b88daab9d39cb5745d1ed953868ead
parent 5e442943
This diff is collapsed.
......@@ -120,4 +120,18 @@ struct AccountModelHelper {
}
return self.account
}
var ringId :String? {
let accountUsernameKey = ConfigKeyModel(withKey: ConfigKey.AccountUsername)
let accountUsername = self.account.details.get(withConfigKeyModel: accountUsernameKey)
let ringIdPrefix = "ring:"
if accountUsername.contains(ringIdPrefix) {
let index = accountUsername.range(of: ringIdPrefix)?.upperBound
return accountUsername.substring(from: index!)
} else {
return nil
}
}
}
......@@ -248,12 +248,76 @@
<navigationItem key="navigationItem" id="b8m-eG-Q9D"/>
<connections>
<outlet property="tableView" destination="B6Y-MZ-L7L" id="dXp-J4-x68"/>
<segue destination="Qlv-cA-wRT" kind="show" identifier="ShowMessages" id="X75-kM-dPZ"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="rzQ-ll-5bo" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-97.5" y="-1177.8169014084508"/>
</scene>
<!--Conversation View Controller-->
<scene sceneID="N9T-Vl-P5n">
<objects>
<viewController hidesBottomBarWhenPushed="YES" id="Qlv-cA-wRT" customClass="ConversationViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="wEb-Zj-bvJ"/>
<viewControllerLayoutGuide type="bottom" id="S9d-I1-nWj"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" misplaced="YES" id="jPi-CC-dFO">
<rect key="frame" x="0.0" y="64" width="320" height="455"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="M1r-X5-oFv">
<rect key="frame" x="0.0" y="0.0" width="320" height="455"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="MessageCellId" id="8rX-Qa-Ypu">
<rect key="frame" x="0.0" y="28" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="8rX-Qa-Ypu" id="7vA-nx-B3h">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
</tableView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="PYu-2x-JXM">
<rect key="frame" x="0.0" y="0.0" width="320" height="455"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" animating="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="oH9-4M-JpG">
<rect key="frame" x="142" y="209" width="37" height="37"/>
<color key="color" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="oH9-4M-JpG" firstAttribute="centerY" secondItem="PYu-2x-JXM" secondAttribute="centerY" id="105-id-Oqu"/>
<constraint firstItem="oH9-4M-JpG" firstAttribute="centerX" secondItem="PYu-2x-JXM" secondAttribute="centerX" id="Fk6-H6-Vdq"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="M1r-X5-oFv" secondAttribute="trailing" id="4Dh-nK-p84"/>
<constraint firstItem="M1r-X5-oFv" firstAttribute="top" secondItem="jPi-CC-dFO" secondAttribute="top" id="BFo-2E-BaG"/>
<constraint firstAttribute="bottom" secondItem="M1r-X5-oFv" secondAttribute="bottom" id="PoM-IX-CFt"/>
<constraint firstAttribute="trailing" secondItem="PYu-2x-JXM" secondAttribute="trailing" id="X7H-Er-Udk"/>
<constraint firstItem="PYu-2x-JXM" firstAttribute="leading" secondItem="jPi-CC-dFO" secondAttribute="leading" id="dQk-7P-cld"/>
<constraint firstItem="PYu-2x-JXM" firstAttribute="top" secondItem="jPi-CC-dFO" secondAttribute="top" id="g9s-sd-GJC"/>
<constraint firstItem="M1r-X5-oFv" firstAttribute="leading" secondItem="jPi-CC-dFO" secondAttribute="leading" id="i0E-9E-6co"/>
<constraint firstAttribute="bottom" secondItem="PYu-2x-JXM" secondAttribute="bottom" id="j3b-gd-iBK"/>
</constraints>
</view>
<extendedEdge key="edgesForExtendedLayout"/>
<connections>
<outlet property="spinnerView" destination="PYu-2x-JXM" id="hxZ-lU-3XC"/>
<outlet property="tableView" destination="M1r-X5-oFv" id="5Lh-Iu-nQb"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bv6-qf-2Pa" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="844" y="-1179"/>
</scene>
<!--Ring-->
<scene sceneID="oqo-zJ-m0o">
<objects>
......
......@@ -80,7 +80,7 @@
<constraints>
<constraint firstItem="2fJ-Wf-1e0" firstAttribute="leading" secondItem="pFB-Jn-TNP" secondAttribute="trailing" constant="4" id="2NV-6m-dri"/>
<constraint firstItem="eug-ak-r49" firstAttribute="leading" secondItem="pFB-Jn-TNP" secondAttribute="trailing" constant="4" id="9ah-Ed-RlY"/>
<constraint firstItem="pFB-Jn-TNP" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="9mO-5E-3lA"/>
<constraint firstItem="pFB-Jn-TNP" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="9mO-5E-3lA"/>
<constraint firstItem="7Yv-cC-LKx" firstAttribute="leading" secondItem="2fJ-Wf-1e0" secondAttribute="trailing" constant="4" id="BzU-Ya-2ME"/>
<constraint firstAttribute="trailing" secondItem="eug-ak-r49" secondAttribute="trailing" constant="16" id="ITl-14-BeZ"/>
<constraint firstItem="JTE-eF-Y5s" firstAttribute="trailing" secondItem="pFB-Jn-TNP" secondAttribute="trailing" id="MgK-cd-QXM"/>
......
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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 RxSwift
class ConversationViewController: UIViewController, UITextFieldDelegate {
let disposeBag = DisposeBag()
var viewModel: ConversationViewModel?
var textFieldShouldEndEditing = false
var bottomOffset :CGFloat = 0
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var spinnerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
self.setupUI()
self.setupTableView()
self.setupBindings()
self.messageAccessoryView.messageTextField.delegate = self
/*
Register to keyboard notifications to adjust tableView insets when the keybaord appears
or disappears
*/
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(withNotification:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(withNotification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
func keyboardWillShow(withNotification notification: Notification) {
let userInfo: Dictionary = notification.userInfo!
let keyboardFrame: NSValue = userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
self.tableView.contentInset.bottom = keyboardHeight
self.tableView.scrollIndicatorInsets.bottom = keyboardHeight
self.scrollToBottom(animated: true)
self.updateBottomOffset()
}
func keyboardWillHide(withNotification notification: Notification) {
self.tableView.contentInset.bottom = 0
self.tableView.scrollIndicatorInsets.bottom = 0
self.updateBottomOffset()
}
func setupUI() {
self.viewModel?.userName.bind(to: self.navigationItem.rx.title).addDisposableTo(disposeBag)
self.tableView.contentInset.bottom = messageAccessoryView.frame.size.height
self.tableView.scrollIndicatorInsets.bottom = messageAccessoryView.frame.size.height
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.scrollToBottom(animated: false)
self.messagesLoadingFinished()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.textFieldShouldEndEditing = true
self.viewModel?.setMessagesAsRead()
}
func setupTableView() {
self.tableView.estimatedRowHeight = 50
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.separatorStyle = .none
//Register cell
self.tableView.register(UINib.init(nibName: "MessageCell", bundle: nil),
forCellReuseIdentifier: "MessageCellId")
//Bind the TableView to the ViewModel
self.viewModel?.messages.bind(to: tableView.rx.items(cellIdentifier: "MessageCellId", cellType: MessageCell.self))
{ index, messageViewModel, cell in
cell.messageLabel.text = messageViewModel.content
cell.bubblePosition = messageViewModel.bubblePosition()
}.addDisposableTo(disposeBag)
//Scroll to bottom when reloaded
self.tableView.rx.methodInvoked(#selector(UITableView.reloadData)).subscribe(onNext: { element in
self.scrollToBottomIfNeed()
self.updateBottomOffset()
}).addDisposableTo(disposeBag)
}
fileprivate func updateBottomOffset() {
self.bottomOffset = self.tableView.contentSize.height
- ( self.tableView.frame.size.height
- self.tableView.contentInset.top
- self.tableView.contentInset.bottom )
}
fileprivate func messagesLoadingFinished() {
self.spinnerView.isHidden = true
}
fileprivate func scrollToBottomIfNeed() {
if self.isBottomContentOffset {
self.scrollToBottom(animated: true)
}
}
fileprivate func scrollToBottom(animated: Bool) {
let last = IndexPath(row: self.tableView.numberOfRows(inSection: 0) - 1, section: 0)
self.tableView.scrollToRow(at: last, at: .bottom, animated: animated)
}
fileprivate var isBottomContentOffset: Bool {
return self.tableView.contentOffset.y + self.tableView.contentInset.top >= bottomOffset
}
override var inputAccessoryView: UIView {
return self.messageAccessoryView
}
override var canBecomeFirstResponder: Bool {
return true
}
lazy var messageAccessoryView: MessageAccessoryView = {
return MessageAccessoryView.instanceFromNib()
}()
func setupBindings() {
//Binds the keyboard Send button action to the ViewModel
self.messageAccessoryView.messageTextField.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: { event in
self.viewModel?.sendMessage(withContent: self.messageAccessoryView.messageTextField.text!)
self.messageAccessoryView.messageTextField.text = ""
}).addDisposableTo(disposeBag)
}
// Avoid the keyboard to be hidden when the Send button is touched
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
return textFieldShouldEndEditing
}
}
......@@ -34,28 +34,40 @@ class ConversationViewModel {
private let disposeBag = DisposeBag()
let messages :Observable<[MessageViewModel]>
private let conversationsService = AppDelegate.conversationsService
private let accountService = AppDelegate.accountService
init(withConversation conversation: ConversationModel) {
self.conversation = conversation
dateFormatter.dateStyle = .medium
hourFormatter.dateFormat = "HH:mm"
self.userName = ContactHelper.lookupUserName(forRingId: self.conversation.recipient.ringId,
nameService: AppDelegate.nameService,
disposeBag: self.disposeBag).asObservable()
//Create observable from sorted conversations and flatMap them to view models
self.messages = self.conversationsService.conversations.asObservable().map({ conversations in
return conversations.filter({ currentConversation in
return currentConversation.recipient == conversation.recipient
}).flatMap({ conversation in
conversation.messages.map({ message in
return MessageViewModel(withMessage: message)
})
})
}).observeOn(MainScheduler.instance)
}
var unreadMessages: String {
return self.unreadMessagesCount.description
}
fileprivate var unreadMessagesCount: Int {
return self.conversation.messages.filter({ message in
return message.status != .read
}).count
}
var hasUnreadMessages: Bool {
return conversation.messages.count > 0
return unreadMessagesCount > 0
}
var lastMessage: String {
......@@ -96,4 +108,43 @@ class ConversationViewModel {
return dateFormatter.string(from: lastMessageDate)
}
}
var hideNewMessagesLabel: Bool {
return self.unreadMessagesCount == 0
}
func sendMessage(withContent content: String) {
self.conversationsService
.sendMessage(withContent: content,
from: accountService.currentAccount!,
to: self.conversation.recipient)
.subscribe(onCompleted: {
let accountHelper = AccountModelHelper(withAccount: self.accountService.currentAccount!)
self.saveMessage(withContent: content, byAuthor: accountHelper.ringId!, toConversationWith: self.conversation.recipient.ringId)
}).addDisposableTo(disposeBag)
}
fileprivate func saveMessage(withContent content: String, byAuthor author: String, toConversationWith account: String) {
self.conversationsService
.saveMessage(withContent: content, byAuthor: author, toConversationWith: account)
.subscribe(onCompleted: {
print("Message saved")
})
.addDisposableTo(disposeBag)
}
func setMessagesAsRead() {
self.conversationsService
.setMessagesAsRead(forConversation: self.conversation)
.subscribe(onCompleted: {
print("Message set as read")
}).addDisposableTo(disposeBag)
}
fileprivate var unreadMessagesCount: Int {
let accountHelper = AccountModelHelper(withAccount: self.accountService.currentAccount!)
return self.conversation.messages.filter({ message in
return message.status != .read && message.author != accountHelper.ringId!
}).count
}
}
......@@ -27,28 +27,34 @@ class ConversationsService: MessagesAdapterDelegate {
fileprivate let disposeBag = DisposeBag()
fileprivate let textPlainMIMEType = "text/plain"
let conversations = Variable([ConversationModel]())
var conversations = Variable([ConversationModel]())
init(withMessageAdapter messageAdapter: MessagesAdapter) {
self.messageAdapter = messageAdapter
MessagesAdapter.delegate = self
}
func status(forMessageId messageId: UInt64) -> MessageStatus {
return self.messageAdapter.status(forMessageId: messageId)
}
//MARK: Message Adapter delegate
func sendMessage(withContent content: String, from senderAccount: AccountModel, to recipient: ContactModel) -> Completable {
func didReceiveMessage(_ message: Dictionary<String, String>, from senderAccount: String,
to receiverAccountId: String) {
return Completable.create(subscribe: { [unowned self] completable in
let contentDict = [self.textPlainMIMEType : content]
self.messageAdapter.sendMessage(withContent: contentDict, withAccountId: senderAccount.id, to: recipient.ringId)
if let content = message[textPlainMIMEType] {
let message = MessageModel(withId: nil, receivedDate: Date(), content: content, author: senderAccount)
completable(.completed)
return Disposables.create {}
})
}
func saveMessage(withContent content: String, byAuthor author: String, toConversationWith account: String) -> Completable {
return Completable.create(subscribe: { [unowned self] completable in
let message = MessageModel(withId: nil, receivedDate: Date(), content: content, author: author)
//Get conversations for this sender
var currentConversation = conversations.value.filter({ conversation in
return conversation.recipient.ringId == senderAccount
var currentConversation = self.conversations.value.filter({ conversation in
return conversation.recipient.ringId == account
}).first
//Get the current array of conversations
......@@ -56,7 +62,7 @@ class ConversationsService: MessagesAdapterDelegate {
//Create a new conversation for this sender if not exists
if currentConversation == nil {
currentConversation = ConversationModel(withRecipient: ContactModel(withRingId: senderAccount), accountId: receiverAccountId)
currentConversation = ConversationModel(withRecipient: ContactModel(withRingId: account), accountId: author)
currentConversations.append(currentConversation!)
}
......@@ -65,6 +71,55 @@ class ConversationsService: MessagesAdapterDelegate {
//Upate the value of the Variable
self.conversations.value = currentConversations
completable(.completed)
return Disposables.create { }
})
}
func status(forMessageId messageId: UInt64) -> MessageStatus {
return self.messageAdapter.status(forMessageId: messageId)
}
func setMessagesAsRead(forConversation conversation: ConversationModel) -> Completable {
return Completable.create(subscribe: { completable in
//Get the current array of conversations
let currentConversations = self.conversations.value
//Filter unread messages
let unreadMessages = conversation.messages.filter({ messages in
return messages.status != .read
})
for message in unreadMessages {
message.status = .read
}
//Upate the value of the Variable
self.conversations.value = currentConversations
completable(.completed)
return Disposables.create { }
})
}
//MARK: Message Adapter delegate
func didReceiveMessage(_ message: Dictionary<String, String>, from senderAccount: String,
to receiverAccountId: String) {
if let content = message[textPlainMIMEType] {
self.saveMessage(withContent: content, byAuthor: senderAccount, toConversationWith: senderAccount)
.subscribe(onCompleted: {
print("Message saved")
})
.addDisposableTo(disposeBag)
}
}
......
......@@ -30,6 +30,8 @@
<array>
<string>armv7</string>
</array>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
<key>UIStatusBarTintParameters</key>
<dict>
<key>UINavigationBar</key>
......
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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
class MessageAccessoryView: UIView {
@IBOutlet weak var messageTextField: UITextField!
class func instanceFromNib() -> MessageAccessoryView {
return UINib(nibName: "MessageAccessoryView", bundle: nil)
.instantiate(withOwner: nil, options: nil).first as! MessageAccessoryView
}
}
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="15G31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="Fja-dy-lIy" customClass="MessageAccessoryView" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="315" height="38"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Write a message..." textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="AJA-0c-Rp7">
<rect key="frame" x="4" y="4" width="307" height="30"/>
<nil key="textColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="send" enablesReturnKeyAutomatically="YES"/>
</textField>
</subviews>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="AJA-0c-Rp7" secondAttribute="bottom" constant="4" id="Gph-b4-ZJH"/>
<constraint firstAttribute="trailing" secondItem="AJA-0c-Rp7" secondAttribute="trailing" constant="4" id="UyK-3a-JcH"/>
<constraint firstItem="AJA-0c-Rp7" firstAttribute="top" secondItem="Fja-dy-lIy" secondAttribute="top" constant="4" id="mDu-cF-UdO"/>
<constraint firstItem="AJA-0c-Rp7" firstAttribute="leading" secondItem="Fja-dy-lIy" secondAttribute="leading" constant="4" id="tvd-4f-jdO"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="messageTextField" destination="AJA-0c-Rp7" id="ga9-yi-G2h"/>
</connections>
<point key="canvasLocation" x="-15" y="-178"/>
</view>
</objects>
</document>
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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
enum BubblePosition {
case received
case sent
}
class MessageCell: UITableViewCell {
@IBOutlet weak var bubble: UIView!
@IBOutlet weak var messageLabel: UILabel!
@IBOutlet weak var minimumLeadingConstraint: NSLayoutConstraint!
@IBOutlet weak var containerLeadingConstraint: NSLayoutConstraint!
@IBOutlet weak var minimumTrailingConstraint: NSLayoutConstraint!
@IBOutlet weak var containerTrailingConstraint: NSLayoutConstraint!
var bubblePosition = BubblePosition.received {
didSet {
if bubblePosition == .sent {
self.minimumTrailingConstraint.priority = 1
self.containerTrailingConstraint.priority = 999
self.containerLeadingConstraint.priority = 1
self.minimumLeadingConstraint.priority = 999
self.bubble.backgroundColor = Colors.ringMainColor
self.messageLabel.textColor = UIColor.white
} else {
self.minimumLeadingConstraint.priority = 1
self.containerLeadingConstraint.priority = 999
self.containerTrailingConstraint.priority = 1
self.minimumTrailingConstraint.priority = 999
self.bubble.backgroundColor = UIColor.lightGray
self.messageLabel.textColor = UIColor.black
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.bubblePosition = .received
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="15G31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="MessageCellId" rowHeight="60" id="KGk-i7-Jjw" customClass="MessageCell" customModule="Ring" customModuleProvider="target">
<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"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view clipsSubviews="YES" contentMode="scaleToFill" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="kZJ-Ay-LTR">
<rect key="frame" x="16" y="8" width="152.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"/>
<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"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="5"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" constant="8" id="1QQ-bu-6Bl"/>
<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 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"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="bubble" destination="kZJ-Ay-LTR" id="hdG-fG-L69"/>
<outlet property="containerLeadingConstraint" destination="nWe-5k-Qpn" id="Ahu-gW-4IY"/>
<outlet property="containerTrailingConstraint" destination="99Y-bR-Ioq" id="JJr-pE-UfA"/>
<outlet property="messageLabel" destination="lyR-7c-S2k" id="hd3-pz-Pwh"/>
<outlet property="minimumLeadingConstraint" destination="Eso-cy-OYs" id="jhV-mE-FMA"/>
<outlet property="minimumTrailingConstraint" destination="TCY-7X-mFs" id="xj3-4A-I9G"/>
</connections>