Commit c0b0b063 authored by Andreas Traczyk's avatar Andreas Traczyk

conversations: respond to message status changes

- Provides a UI elements that respond to states(sending, failure)
  for each outgoing message.

- Updates the message models' statuses at loading to correct
  erroneously stored data.

- Ignores the message status IDLE which is not intended
  for client use.

Change-Id: Ie6027d59ae519b96de204ba8d98bc2dd8eb9b4e4
Reviewed-by: Kateryna Kostiuk's avatarKateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
parent 3ad3fb19
......@@ -22,7 +22,6 @@
typedef NS_ENUM(int, MessageStatus) {
MessageStatusUnknown = 0,
MessageStatusIdle,
MessageStatusSending,
MessageStatusSent,
MessageStatusRead,
......
......@@ -59,10 +59,10 @@ static id <MessagesAdapterDelegate> _delegate;
confHandlers.insert(exportable_callback<ConfigurationSignal::AccountMessageStatusChanged>([&](const std::string& account_id, uint64_t message_id, const std::string& to, int state) {
if (MessagesAdapter.delegate) {
NSString* fromAccountId = [NSString stringWithUTF8String:account_id.c_str()];
NSString* toAccount = [NSString stringWithUTF8String:to.c_str()];
NSString* toUri = [NSString stringWithUTF8String:to.c_str()];
[MessagesAdapter.delegate messageStatusChanged:(MessageStatus)state
for:message_id from:fromAccountId
to:toAccount];
to:toUri];
}
}));
......
......@@ -21,6 +21,7 @@
import UIKit
import Reusable
import RxSwift
class MessageCell: UITableViewCell, NibReusable {
......@@ -33,4 +34,8 @@ class MessageCell: UITableViewCell, NibReusable {
@IBOutlet weak var timeLabel: UILabel!
@IBOutlet weak var leftDivider: UIView!
@IBOutlet weak var rightDivider: UIView!
@IBOutlet weak var sendingIndicator: UIActivityIndicatorView!
@IBOutlet weak var failedStatusLabel: UILabel!
let disposeBag = DisposeBag()
}
......@@ -15,7 +15,7 @@
<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="3QB-g7-MaS" id="Dkz-SA-3Af">
<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 clipsSubviews="YES" contentMode="scaleToFill" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="xVQ-Jk-Sxy" customClass="MessageBubble" customModule="Ring" customModuleProvider="target">
......
......@@ -16,7 +16,7 @@
<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="hdz-AQ-xHI" userLabel="Bottom Corner">
......@@ -80,15 +80,27 @@
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="78h-fZ-7yf" userLabel="Sending Indicator">
<rect key="frame" x="275.5" y="16" width="20" height="20"/>
</activityIndicatorView>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Failed" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="P5a-HI-uhr" userLabel="Failed Status Label">
<rect key="frame" x="253" y="16" width="42.5" height="19.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.94117647058823528" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<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 firstItem="78h-fZ-7yf" firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="leading" constant="-8" id="4ME-jl-Uol"/>
<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="ogn-wv-fZy" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" constant="-2" id="Fxg-Wa-Rb9"/>
<constraint firstItem="78h-fZ-7yf" firstAttribute="top" secondItem="kZJ-Ay-LTR" secondAttribute="top" constant="8" id="Gei-s7-aWx"/>
<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="P5a-HI-uhr" firstAttribute="top" secondItem="kZJ-Ay-LTR" secondAttribute="top" constant="8" id="O07-uI-R80"/>
<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 firstItem="h8N-aw-5lV" firstAttribute="centerY" secondItem="ogn-wv-fZy" secondAttribute="centerY" id="Xdu-7c-MbP"/>
......@@ -99,6 +111,7 @@
<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="P5a-HI-uhr" firstAttribute="trailing" secondItem="kZJ-Ay-LTR" secondAttribute="leading" constant="-8" id="zI5-Gc-i6d"/>
<constraint firstItem="hdz-AQ-xHI" firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" id="zWA-Jg-F6Q"/>
</constraints>
</tableViewCellContentView>
......@@ -107,9 +120,11 @@
<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="failedStatusLabel" destination="P5a-HI-uhr" id="6Sq-NU-j0d"/>
<outlet property="leftDivider" destination="2U4-l3-KET" id="y4j-CT-gez"/>
<outlet property="messageLabel" destination="lyR-7c-S2k" id="hd3-pz-Pwh"/>
<outlet property="rightDivider" destination="h8N-aw-5lV" id="9pc-93-BG6"/>
<outlet property="sendingIndicator" destination="78h-fZ-7yf" id="GrK-FT-q39"/>
<outlet property="timeLabel" destination="ogn-wv-fZy" id="7yt-vi-cSp"/>
<outlet property="topCorner" destination="EMh-bG-ilg" id="nHl-hn-BZ1"/>
</connections>
......
......@@ -384,6 +384,19 @@ class ConversationViewController: UIViewController, UITextFieldDelegate, Storybo
} else if self.messageViewModels?.count == indexPath.row + 1 {
cell.bubbleBottomConstraint.constant = 16
}
if messageVM.bubblePosition() == .sent {
messageVM.status.asObservable()
.observeOn(MainScheduler.instance)
.map { value in value == MessageStatus.sending ? true : false }
.bind(to: cell.sendingIndicator.rx.isAnimating)
.disposed(by: disposeBag)
messageVM.status.asObservable()
.observeOn(MainScheduler.instance)
.map { value in value == MessageStatus.failure ? false : true }
.bind(to: cell.failedStatusLabel.rx.isHidden)
.disposed(by: disposeBag)
}
}
}
......
......@@ -19,6 +19,7 @@
*/
import RxSwift
import SwiftyBeaver
enum BubblePosition {
case received
......@@ -42,17 +43,41 @@ enum GeneratedMessageType: String {
class MessageViewModel {
fileprivate let log = SwiftyBeaver.self
fileprivate let accountService: AccountsService
fileprivate let conversationsService: ConversationsService
fileprivate var message: MessageModel
var timeStringShown: String?
var sequencing: MessageSequencing = .unknown
private let disposeBag = DisposeBag()
init(withInjectionBag injectionBag: InjectionBag,
withMessage message: MessageModel) {
self.accountService = injectionBag.accountService
self.conversationsService = injectionBag.conversationsService
self.message = message
self.timeStringShown = nil
self.status.onNext(message.status)
// subscribe to message status updates for outgoing messages
self.conversationsService
.sharedResponseStream
.filter({ messageUpdateEvent in
let account = self.accountService.getAccount(fromAccountId: messageUpdateEvent.getEventInput(.id)!)
let accountHelper = AccountModelHelper(withAccount: account!)
return messageUpdateEvent.eventType == ServiceEventType.messageStateChanged &&
messageUpdateEvent.getEventInput(.messageId) == self.message.id &&
accountHelper.ringId == self.message.author
})
.subscribe(onNext: { [unowned self] messageUpdateEvent in
if let status: MessageStatus = messageUpdateEvent.getEventInput(.messageStatus) {
self.status.onNext(status)
}
})
.disposed(by: self.disposeBag)
}
var content: String {
......@@ -67,9 +92,7 @@ class MessageViewModel {
return UInt64(self.message.id)!
}
var status: MessageStatus {
return self.message.status
}
var status = BehaviorSubject<MessageStatus>(value: .unknown)
func bubblePosition() -> BubblePosition {
if self.message.isGenerated {
......
......@@ -33,6 +33,9 @@ class ConversationsService: MessagesAdapterDelegate {
fileprivate let disposeBag = DisposeBag()
fileprivate let textPlainMIMEType = "text/plain"
fileprivate let responseStream = PublishSubject<ServiceEvent>()
var sharedResponseStream: Observable<ServiceEvent>
private var realm: Realm!
fileprivate let results: Results<ConversationModel>
......@@ -40,6 +43,8 @@ class ConversationsService: MessagesAdapterDelegate {
var conversations: Observable<Results<ConversationModel>>
init(withMessageAdapter adapter: MessagesAdapter) {
self.responseStream.disposed(by: disposeBag)
self.sharedResponseStream = responseStream.share()
guard let realm = try? Realm() else {
fatalError("Enable to instantiate Realm")
......@@ -50,8 +55,31 @@ class ConversationsService: MessagesAdapterDelegate {
results = realm.objects(ConversationModel.self)
conversations = Observable.collection(from: results, synchronousStart: true)
MessagesAdapter.delegate = self
/**
If the app was closed prior to messages receiving a "stable"
status, incorrect status values will remain in the database.
Get updated message status from the daemon for each
message as conversations are loaded from the database.
Only sent messages having an 'unknown' or 'sending' status
are considered for updating.
*/
for conversation in results.toArray() {
for message in (conversation.messages) {
if message.id != "" && (message.status == .unknown || message.status == .sending ) {
let updatedMessageStatus = self.status(forMessageId: message.id)
if updatedMessageStatus != message.status {
self.setMessageStatus(withMessage: message, withStatus: updatedMessageStatus)
.subscribe(onCompleted: { [] in
print("Message status updated - load")
})
.disposed(by: self.disposeBag)
}
}
}
}
}
func sendMessage(withContent content: String,
......@@ -142,8 +170,8 @@ class ConversationsService: MessagesAdapterDelegate {
})
}
func status(forMessageId messageId: UInt64) -> MessageStatus {
return self.messageAdapter.status(forMessageId: messageId)
func status(forMessageId messageId: String) -> MessageStatus {
return self.messageAdapter.status(forMessageId: UInt64(messageId)!)
}
func setMessagesAsRead(forConversation conversation: ConversationModel) -> Completable {
......@@ -172,6 +200,24 @@ class ConversationsService: MessagesAdapterDelegate {
})
}
func setMessageStatus(withMessage message: MessageModel,
withStatus status: MessageStatus) -> Completable {
return Completable.create(subscribe: { [unowned self] completable in
do {
try self.realm.write {
message.status = status
}
completable(.completed)
} catch let error {
self.log.error("\(error)")
}
return Disposables.create { }
})
}
func deleteConversation(conversation: ConversationModel) {
do {
......@@ -211,8 +257,33 @@ class ConversationsService: MessagesAdapterDelegate {
func messageStatusChanged(_ status: MessageStatus,
for messageId: UInt64,
from senderAccountId: String,
to receiverAccount: String) {
log.debug("messageStatusChanged: \(status.rawValue) for: \(messageId) from: \(senderAccountId) to: \(receiverAccount)")
from accountId: String,
to uri: String) {
//Get conversations for this sender
let conversation = self.results.filter({ conversation in
return conversation.recipientRingId == uri &&
conversation.accountId == accountId
}).first
//Find message
if let message = conversation?.messages.filter({ messages in
return !messages.id.isEmpty && messages.id == String(messageId) && messages.status != status
}).first {
self.setMessageStatus(withMessage: message,
withStatus: status)
.subscribe(onCompleted: { [unowned self] in
self.log.info("Message status updated")
var event = ServiceEvent(withEventType: .messageStateChanged)
event.addEventInput(.messageStatus, value: status)
event.addEventInput(.messageId, value: String(messageId))
event.addEventInput(.id, value: accountId)
event.addEventInput(.uri, value: uri)
self.responseStream.onNext(event)
})
.disposed(by: disposeBag)
}
log.debug("messageStatusChanged: \(status.rawValue) for: \(messageId) from: \(accountId) to: \(uri)")
}
}
......@@ -23,6 +23,6 @@
func didReceiveMessage(_ message: [String: String], from senderAccount: String,
to receiverAccountId: String)
func messageStatusChanged(_ status: MessageStatus, for messageId: UInt64, from senderAccountId: String,
to receiverAccount: String)
func messageStatusChanged(_ status: MessageStatus, for messageId: UInt64, from accountId: String,
to uri: String)
}
......@@ -29,6 +29,7 @@ enum ServiceEventType {
case accountsChanged
case registrationStateChanged
case presenceUpdated
case messageStateChanged
}
/**
......@@ -40,6 +41,8 @@ enum ServiceEventInput {
case registrationState
case uri
case presenceStatus
case messageStatus
case messageId
}
/**
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment