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 <kateryna.kostiuk@savoirfairelinux.com>
diff --git a/Ring/Ring/Bridging/MessagesAdapter.h b/Ring/Ring/Bridging/MessagesAdapter.h
index 4cc6c85..5b30bc5 100644
--- a/Ring/Ring/Bridging/MessagesAdapter.h
+++ b/Ring/Ring/Bridging/MessagesAdapter.h
@@ -22,7 +22,6 @@
typedef NS_ENUM(int, MessageStatus) {
MessageStatusUnknown = 0,
- MessageStatusIdle,
MessageStatusSending,
MessageStatusSent,
MessageStatusRead,
diff --git a/Ring/Ring/Bridging/MessagesAdapter.mm b/Ring/Ring/Bridging/MessagesAdapter.mm
index 69f90d4..29e95cd 100644
--- a/Ring/Ring/Bridging/MessagesAdapter.mm
+++ b/Ring/Ring/Bridging/MessagesAdapter.mm
@@ -59,10 +59,10 @@
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];
}
}));
diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift
index 8eea0f5..292f5ae 100644
--- a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift
@@ -21,6 +21,7 @@
import UIKit
import Reusable
+import RxSwift
class MessageCell: UITableViewCell, NibReusable {
@@ -33,4 +34,8 @@
@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()
}
diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellGenerated.xib b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellGenerated.xib
index 84e8450..17b1311 100644
--- a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellGenerated.xib
+++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellGenerated.xib
@@ -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">
diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.xib b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.xib
index 4b7eda3..8ead1fd 100644
--- a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.xib
+++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.xib
@@ -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>
diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
index f84ab0b..db5d913 100644
--- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
@@ -384,6 +384,19 @@
} 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)
+ }
}
}
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift
index b3b174a..941ad9f 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift
@@ -19,6 +19,7 @@
*/
import RxSwift
+import SwiftyBeaver
enum BubblePosition {
case received
@@ -42,17 +43,41 @@
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 @@
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 {
diff --git a/Ring/Ring/Services/ConversationsService.swift b/Ring/Ring/Services/ConversationsService.swift
index a61e297..0eff2be 100644
--- a/Ring/Ring/Services/ConversationsService.swift
+++ b/Ring/Ring/Services/ConversationsService.swift
@@ -33,6 +33,9 @@
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 @@
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 @@
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 @@
})
}
- 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 @@
})
}
+ 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 @@
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)")
}
}
diff --git a/Ring/Ring/Services/MessagesAdapterDelegate.swift b/Ring/Ring/Services/MessagesAdapterDelegate.swift
index 22ba215..f7419ce 100644
--- a/Ring/Ring/Services/MessagesAdapterDelegate.swift
+++ b/Ring/Ring/Services/MessagesAdapterDelegate.swift
@@ -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)
}
diff --git a/Ring/Ring/Services/ServiceEvent.swift b/Ring/Ring/Services/ServiceEvent.swift
index 836d4b7..1f6150f 100644
--- a/Ring/Ring/Services/ServiceEvent.swift
+++ b/Ring/Ring/Services/ServiceEvent.swift
@@ -29,6 +29,7 @@
case accountsChanged
case registrationStateChanged
case presenceUpdated
+ case messageStateChanged
}
/**
@@ -40,6 +41,8 @@
case registrationState
case uri
case presenceStatus
+ case messageStatus
+ case messageId
}
/**