diff --git a/Ring/Ring/Bridging/MessagesAdapter.h b/Ring/Ring/Bridging/MessagesAdapter.h index 511f6ba3c9f91d0b71e67f96d0446601cc177d2e..95a96bc1ef75934de1cec609d35375624ab96b43 100644 --- a/Ring/Ring/Bridging/MessagesAdapter.h +++ b/Ring/Ring/Bridging/MessagesAdapter.h @@ -24,7 +24,7 @@ typedef NS_ENUM(int, MessageStatus) { MessageStatusUnknown = 0, MessageStatusSending, MessageStatusSent, - MessageStatusRead, + MessageStatusDisplayed, MessageStatusFailure }; @@ -42,4 +42,9 @@ typedef NS_ENUM(int, MessageStatus) { fromAccount:(NSString*)accountID isComposing:(BOOL)isComposing; +- (void)setMessageDisplayedFrom:(NSString*)peer + byAccount:(NSString*)accountID + messageId:(NSString*)messageId + status:(MessageStatus)status; + @end diff --git a/Ring/Ring/Bridging/MessagesAdapter.mm b/Ring/Ring/Bridging/MessagesAdapter.mm index db88c51acfd463a9bd65885c8206446f2cc406a4..f2833fbc4597a614980542880ef9185ef47de4fd 100644 --- a/Ring/Ring/Bridging/MessagesAdapter.mm +++ b/Ring/Ring/Bridging/MessagesAdapter.mm @@ -107,6 +107,16 @@ static id <MessagesAdapterDelegate> _delegate; isComposing); } +- (void)setMessageDisplayedFrom:(NSString*)peer + byAccount:(NSString*)accountID + messageId:(NSString*)messageId + status:(MessageStatus)status { + setMessageDisplayed(std::string([accountID UTF8String]), + std::string([peer UTF8String]), + std::string([messageId UTF8String]), + status); +} + #pragma mark AccountAdapterDelegate + (id <MessagesAdapterDelegate>)delegate { return _delegate; diff --git a/Ring/Ring/Database/DBManager.swift b/Ring/Ring/Database/DBManager.swift index 2e59f0903e32428e0f3e0cc627bfe4ce248f89e1..ed4c36fcbab177d5ebab1897f4fff34f812b0ba9 100644 --- a/Ring/Ring/Database/DBManager.swift +++ b/Ring/Ring/Database/DBManager.swift @@ -76,7 +76,7 @@ enum InteractionStatus: String { case sending = "SENDING" case failed = "FAILED" case succeed = "SUCCEED" - case read = "READ" + case displayed = "DISPLAYED" case unread = "UNREAD" case transferCreated = "TRANSFER_CREATED" case transferAwaiting = "TRANSFER_AWAITING" @@ -92,7 +92,7 @@ enum InteractionStatus: String { case .sending: return MessageStatus.sending case .failed: return MessageStatus.failure case .succeed: return MessageStatus.sent - case .read: return MessageStatus.read + case .displayed: return MessageStatus.displayed case .unread: return MessageStatus.unknown default: return MessageStatus.unknown } @@ -103,7 +103,7 @@ enum InteractionStatus: String { case .unknown: self = .unknown case .sending: self = .sending case .sent: self = .succeed - case .read: self = .read + case .displayed: self = .displayed case .failure: self = .failed @unknown default: self = .unknown @@ -541,6 +541,16 @@ class DBManager { ? participantProfile.uri : "" if let message = self.convertToMessage(interaction: interaction, author: author) { messages.append(message) + let displayedMessage = author.isEmpty && message.status == .displayed + let isLater = conversationModel + .lastDisplayedMessage.id == -1 || + conversationModel + .lastDisplayedMessage.timestamp < message.receivedDate + if displayedMessage && isLater { + conversationModel + .lastDisplayedMessage = (message.messageId, + message.receivedDate) + } } } conversationModel.messages = messages diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift index b4bc50835d703f1a9db3529254ee8959dbdb5dd1..477be4390c66aeed13fb2b85d37f3c0e54d16563 100644 --- a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift +++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift @@ -53,6 +53,7 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { @IBOutlet weak var sendingIndicator: UIActivityIndicatorView! @IBOutlet weak var failedStatusLabel: UILabel! @IBOutlet weak var bubbleViewMask: UIView? + @IBOutlet weak var messageReadIndicator: UIView? private var transferImageView = UIImageView() private var transferProgressView = ProgressView() @@ -409,6 +410,13 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { .map { value in value == MessageStatus.failure ? false : true } .bind(to: self.failedStatusLabel.rx.isHidden) .disposed(by: self.disposeBag) + if self.messageReadIndicator != nil { + item.displayReadIndicator.asObservable() + .observeOn(MainScheduler.instance) + .map {value in return !value} + .bind(to: self.messageReadIndicator!.rx.isHidden) + .disposed(by: self.disposeBag) + } } } else if item.bubblePosition() == .received { // When the message contains only emoji diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.xib b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.xib index 62f48b698438724538eb3961ff13e502833c3b0d..a833b39b79a0de8012975e8e4518a364ba6b18f5 100644 --- a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.xib +++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellSent.xib @@ -1,9 +1,9 @@ <?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES"> <device id="retina6_5" orientation="portrait" appearance="light"/> <dependencies> <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <objects> @@ -95,12 +95,29 @@ <color key="textColor" red="0.94117647058823528" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/> <nil key="highlightedColor"/> </label> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="nk1-r0-5jJ"> + <rect key="frame" x="498" y="31" width="8" height="8"/> + <color key="backgroundColor" systemColor="systemGreenColor" red="0.20392156859999999" green="0.78039215689999997" blue="0.34901960780000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstAttribute="width" constant="8" id="IKx-gD-ktV"/> + <constraint firstAttribute="height" constant="8" id="Kmy-bf-VRp"/> + </constraints> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius"> + <real key="value" value="4"/> + </userDefinedRuntimeAttribute> + <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/> + </userDefinedRuntimeAttributes> + </view> </subviews> <constraints> <constraint firstAttribute="bottom" secondItem="kZJ-Ay-LTR" secondAttribute="bottom" constant="8" id="1QQ-bu-6Bl" userLabel="Bubble Bottom Constraint"/> + <constraint firstAttribute="trailing" secondItem="nk1-r0-5jJ" secondAttribute="trailing" constant="4" id="1g3-Kl-0eG"/> <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 firstItem="nk1-r0-5jJ" firstAttribute="leading" secondItem="kZJ-Ay-LTR" secondAttribute="trailing" constant="4" id="4ht-GX-AOD"/> <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="bottom" secondItem="nk1-r0-5jJ" secondAttribute="bottom" id="AHa-wE-zfg"/> <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="centerY" secondItem="kZJ-Ay-LTR" secondAttribute="centerY" id="HCm-lG-Bmg"/> @@ -135,6 +152,7 @@ <outlet property="messageLabelLeadingConstraint" destination="8m5-sR-xnh" id="ShD-gb-tcN"/> <outlet property="messageLabelMarginConstraint" destination="Fxg-Wa-Rb9" id="zuW-JC-pWN"/> <outlet property="messageLabelTrailingConstraint" destination="uzV-kG-oGN" id="q3j-c6-l76"/> + <outlet property="messageReadIndicator" destination="nk1-r0-5jJ" id="ODJ-u6-wPb"/> <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"/> diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift index 5adab8a20c1d91cec188427e195b794c05c91554..1ce9d8112f20ae40ec00571ffd76f0dc5c6bc220 100644 --- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift @@ -956,6 +956,11 @@ extension ConversationViewController: UITableViewDataSource { item.bubblePosition() == .generated ? MessageCellGenerated.self : MessageCellGenerated.self } + if item.message.incoming && item.message.status != .displayed { + self.viewModel + .setMessageAsRead(daemonId: item.message.daemonId, + messageId: item.message.messageId) + } let cell = tableView.dequeueReusableCell(for: indexPath, cellType: type) cell.configureFromItem(viewModel, self.messageViewModels, cellForRowAt: indexPath) diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift index a256d3ea56c22ddf6a64b69a30ede3d8066de9ad..3c23dad2a99c6de4f74722f9c73850b134241dab 100644 --- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift @@ -121,7 +121,8 @@ class ConversationViewModel: Stateable, ViewModel { .flatMap({ conversation in conversation.messages.map({ message -> MessageViewModel? in if let injBag = self?.injectionBag { - return MessageViewModel(withInjectionBag: injBag, withMessage: message) + let lastDisplayed = self?.isLastDisplayed(messageId: message.messageId) ?? false + return MessageViewModel(withInjectionBag: injBag, withMessage: message, isLastDisplayed: lastDisplayed) } return nil }) @@ -144,7 +145,7 @@ class ConversationViewModel: Stateable, ViewModel { content: " ", authorURI: self.conversation.value.participantUri, incoming: true) - let composingIndicator = MessageViewModel(withInjectionBag: self.injectionBag, withMessage: msgModel) + let composingIndicator = MessageViewModel(withInjectionBag: self.injectionBag, withMessage: msgModel, isLastDisplayed: false) composingIndicator.isComposingIndicator = true msg.append(composingIndicator) } @@ -364,10 +365,28 @@ class ConversationViewModel: Stateable, ViewModel { }).disposed(by: disposeBag) } + func setMessageAsRead (daemonId: String, messageId: Int64) { + guard let account = self.accountService.currentAccount else { + return + } + guard let accountURI = AccountModelHelper(withAccount: account).ringId else { + return + } + self.conversationsService + .setMessageAsRead(daemonId: daemonId, + messageID: messageId, + from: self.conversation.value.hash, + accountId: account.id, + accountURI: accountURI) + self.conversation.value.messages.filter { (message) -> Bool in + return message.daemonId == daemonId && message.messageId == messageId + }.first?.status = .displayed + } + fileprivate var unreadMessagesCount: Int { let unreadMessages = self.conversation.value.messages .filter({ message in - return message.status != .read && + return message.status != .displayed && !message.isTransfer && message.incoming }) return unreadMessages.count @@ -580,7 +599,7 @@ class ConversationViewModel: Stateable, ViewModel { content: " ", authorURI: self.conversation.value.participantUri, incoming: true) - let composingIndicator = MessageViewModel(withInjectionBag: self.injectionBag, withMessage: msgModel) + let composingIndicator = MessageViewModel(withInjectionBag: self.injectionBag, withMessage: msgModel, isLastDisplayed: false) composingIndicator.isComposingIndicator = true messagesValue.append(composingIndicator) self.messages.value = messagesValue @@ -600,4 +619,8 @@ class ConversationViewModel: Stateable, ViewModel { } self.messages.value = conversationsMsg } + + func isLastDisplayed(messageId: Int64) -> Bool { + return messageId == self.conversation.value.lastDisplayedMessage.id + } } diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift index ede9f181cf1d6ed8224a3f1d82b1d2e7ed5c763f..35a8ea36c86c7ba99e11a26a5a56a15a85c8e483 100644 --- a/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift +++ b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift @@ -63,7 +63,7 @@ class MessageViewModel { let injectBug: InjectionBag init(withInjectionBag injectionBag: InjectionBag, - withMessage message: MessageModel) { + withMessage message: MessageModel, isLastDisplayed: Bool) { self.accountService = injectionBag.accountService self.conversationsService = injectionBag.conversationsService self.dataTransferService = injectionBag.dataTransferService @@ -72,6 +72,7 @@ class MessageViewModel { self.initialTransferStatus = message.transferStatus self.timeStringShown = nil self.status.onNext(message.status) + self.displayReadIndicator.onNext(isLastDisplayed) if isTransfer { if let transferId = daemonId, @@ -118,6 +119,26 @@ class MessageViewModel { } }) .disposed(by: self.disposeBag) + self.conversationsService + .sharedResponseStream + .filter({ [weak self] messageUpdateEvent in + let event = messageUpdateEvent.eventType == ServiceEventType.lastDisplayedMessageUpdated + let message = messageUpdateEvent + .getEventInput(.oldDisplayedMessage) == self?.message.messageId || + messageUpdateEvent + .getEventInput(.newDisplayedMessage) == self?.message.messageId + return event && message + }) + .subscribe(onNext: { [weak self] messageUpdateEvent in + if let oldMessage: Int64 = messageUpdateEvent.getEventInput(.oldDisplayedMessage), + oldMessage == self?.message.messageId { + self?.displayReadIndicator.onNext(false) + } else if let newMessage: Int64 = messageUpdateEvent.getEventInput(.newDisplayedMessage), + newMessage == self?.message.messageId { + self?.displayReadIndicator.onNext(true) + } + }) + .disposed(by: self.disposeBag) } } @@ -159,6 +180,7 @@ class MessageViewModel { } var status = BehaviorSubject<MessageStatus>(value: .unknown) + var displayReadIndicator = BehaviorSubject<Bool>(value: false) var transferStatus = BehaviorSubject<DataTransferStatus>(value: .unknown) var lastTransferStatus: DataTransferStatus = .unknown diff --git a/Ring/Ring/Models/ConversationModel.swift b/Ring/Ring/Models/ConversationModel.swift index 1b4cc2073fa36c4429301a1b4f1d64e24b0ed9eb..8b8734a17a3c30a6b576d8a5f37347c414b55e48 100644 --- a/Ring/Ring/Models/ConversationModel.swift +++ b/Ring/Ring/Models/ConversationModel.swift @@ -29,6 +29,7 @@ class ConversationModel: Equatable { var accountId: String = "" var participantProfile: Profile? var conversationId: String = "" + var lastDisplayedMessage: (id: Int64, timestamp: Date) = (-1, Date()) convenience init(withParticipantUri participantUri: JamiURI, accountId: String) { self.init() diff --git a/Ring/Ring/Services/ConversationsService.swift b/Ring/Ring/Services/ConversationsService.swift index 649d103b5701a5b03bd12698f350d05de1b5bfed..10d47aed08365d592c5ba5b44682977978c659cb 100644 --- a/Ring/Ring/Services/ConversationsService.swift +++ b/Ring/Ring/Services/ConversationsService.swift @@ -304,19 +304,43 @@ class ConversationsService { return self.messageAdapter.status(forMessageId: status) } + func setMessageAsRead(daemonId: String, messageID: Int64, + from: String, accountId: String, accountURI: String) { + self.messageAdapter + .setMessageDisplayedFrom(from, + byAccount: accountId, + messageId: daemonId, + status: .displayed) + self.dbManager + .setMessagesAsRead(messagesIDs: [messageID], + withStatus: .displayed, + accountId: accountId) + .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) + .subscribe() + .disposed(by: self.disposeBag) + } + func setMessagesAsRead(forConversation conversation: ConversationModel, accountId: String, accountURI: String) -> Completable { return Completable.create(subscribe: { [unowned self] completable in //Filter out read, outgoing, and transfer messages let unreadMessages = conversation.messages.filter({ messages in - return messages.status != .read && messages.incoming && !messages.isTransfer + return messages.status != .displayed && messages.incoming && !messages.isTransfer }) let messagesIds = unreadMessages.map({$0.messageId}).filter({$0 >= 0}) + let messagesDaemonIds = unreadMessages.map({$0.daemonId}).filter({!$0.isEmpty}) + messagesDaemonIds.forEach { (msgId) in + self.messageAdapter + .setMessageDisplayedFrom(conversation.hash, + byAccount: accountId, + messageId: msgId, + status: .displayed) + } self.dbManager .setMessagesAsRead(messagesIDs: messagesIds, - withStatus: .read, + withStatus: .displayed, accountId: accountId) .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) .subscribe(onCompleted: { [unowned self] in @@ -387,6 +411,16 @@ class ConversationsService { (status == .failure && message.status == .sending)) }) { if let message = messages.first { + let displayedMessage = status == .displayed && !message.incoming + let oldDisplayedMessage = conversation?.lastDisplayedMessage.id + let isLater = (conversation?.lastDisplayedMessage.id ?? 0) < Int64(0) || conversation?.lastDisplayedMessage.timestamp ?? Date() < message.receivedDate + if displayedMessage && isLater { + conversation?.lastDisplayedMessage = (message.messageId, message.receivedDate) + var event = ServiceEvent(withEventType: .lastDisplayedMessageUpdated) + event.addEventInput(.oldDisplayedMessage, value: oldDisplayedMessage) + event.addEventInput(.newDisplayedMessage, value: message.messageId) + self.responseStream.onNext(event) + } self.dbManager .updateMessageStatus(daemonID: message.daemonId, withStatus: status, diff --git a/Ring/Ring/Services/ServiceEvent.swift b/Ring/Ring/Services/ServiceEvent.swift index fac3bd13504f7c86f118d9c25035c48ebe8761c8..8be118dd78209e5bb62ea905e6ec9ac4c4b21fbe 100644 --- a/Ring/Ring/Services/ServiceEvent.swift +++ b/Ring/Ring/Services/ServiceEvent.swift @@ -50,6 +50,7 @@ enum ServiceEventType { case newOutgoingMessage case messageTypingIndicator case migrationEnded + case lastDisplayedMessageUpdated } /** @@ -77,6 +78,8 @@ enum ServiceEventInput { case accountUri case name case callUUID + case oldDisplayedMessage + case newDisplayedMessage } /** diff --git a/Ring/Ring/TabBar/ChatTabBarItemViewModel.swift b/Ring/Ring/TabBar/ChatTabBarItemViewModel.swift index fd8462d237e845906cb6f4e8a93582e836284a95..791283d02ac259ed454242e77560f652f7a3d635 100644 --- a/Ring/Ring/TabBar/ChatTabBarItemViewModel.swift +++ b/Ring/Ring/TabBar/ChatTabBarItemViewModel.swift @@ -33,7 +33,7 @@ class ChatTabBarItemViewModel: ViewModel, TabBarItemViewModel { return conversations.map({ conversation in let unreadMsg = conversation.messages.filter({ message in //filtre out read messages, outgoing messages and messages that are displayed in contactrequest conversation - return message.status != .read && !message.isTransfer && message.incoming + return message.status != .displayed && !message.isTransfer && message.incoming && (contactsService.contactRequest(withRingId: JamiURI.init(schema: URIType.ring, infoHach: message.authorURI).hash ?? "") == nil) }) return unreadMsg.count