diff --git a/Ring/Ring/Bridging/MessagesAdapter.h b/Ring/Ring/Bridging/MessagesAdapter.h index 4cc6c8553e0aaf3a7859629ddbdcfda0f04d2df4..5b30bc533cf44acca718c9efb12d88d515fb204f 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 69f90d4a37c886b27a71da16f7dabb4b0900f5eb..29e95cda1056b1f3e9c0ab3f82abf0ae26154475 100644 --- a/Ring/Ring/Bridging/MessagesAdapter.mm +++ b/Ring/Ring/Bridging/MessagesAdapter.mm @@ -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]; } })); diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift index 8eea0f533594c28b4ad8ae5454e95e12bc5d0021..292f5aeb37f22c4120c4fe86e81b9e4f8b3bcebe 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 @@ 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() } diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellGenerated.xib b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellGenerated.xib index 84e845047a4bef715839a7d6d62f49acfc89e897..17b1311d1bbbc2df5b1054c409d68a93c768f018 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 4b7eda38ec91e96d643fa9a6c1c618a4e5048a95..8ead1fd573df5d031238a968e861b0d5ceb48720 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 f84ab0b664ad2910d7a94ebf52e9a1305a44209a..db5d9135fbe6c80ba5ec7ca4953ae9a697fa8ebf 100644 --- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift @@ -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) + } } } diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift index b3b174a7f8a40bc19906300e27167585f40a6384..941ad9f845a57e61e2f7be5106a89036bd0eb805 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 @@ 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 { diff --git a/Ring/Ring/Services/ConversationsService.swift b/Ring/Ring/Services/ConversationsService.swift index a61e297a984c481c8ca32bc2a6088c5ec89fb1ce..0eff2be4b1d75720a4a1e6c30911a5c2645b6ddb 100644 --- a/Ring/Ring/Services/ConversationsService.swift +++ b/Ring/Ring/Services/ConversationsService.swift @@ -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)") } } diff --git a/Ring/Ring/Services/MessagesAdapterDelegate.swift b/Ring/Ring/Services/MessagesAdapterDelegate.swift index 22ba2158afe5bf78020baed589b83035399faea5..f7419cec9c99ebeaf766b29e18acaca042656fd6 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 836d4b7bf1eb4a5773500ccc954148d14fd48f95..1f6150f958e38ee05c552bf27a4c7b0ef93757eb 100644 --- a/Ring/Ring/Services/ServiceEvent.swift +++ b/Ring/Ring/Services/ServiceEvent.swift @@ -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 } /**