From 98e88315de76eb7eb1959560c8960d56f22d77d3 Mon Sep 17 00:00:00 2001 From: Iana Date: Fri, 8 Nov 2024 14:22:09 +0200 Subject: [PATCH 1/3] [trello.com/c/TOuHqbBz] Fix: Read/unread messages --- .../CoreData/Chatroom+CoreDataClass.swift | 39 +++--- .../Chat/View/ChatViewController.swift | 23 ++++ .../Chat/ViewModel/ChatViewModel.swift | 43 ++++++- .../ChatsList/ChatListViewController.swift | 51 ++++++-- .../DataProviders/ChatsProvider.swift | 20 ++++ .../DataProviders/AdamantChatsProvider.swift | 112 +++++++++++++++--- .../Sources/CommonKit/Core/SecuredStore.swift | 6 + 7 files changed, 243 insertions(+), 51 deletions(-) diff --git a/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift b/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift index 6512a40ab..4d8401f8a 100644 --- a/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift +++ b/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift @@ -14,30 +14,21 @@ import CoreData public class Chatroom: NSManagedObject, @unchecked Sendable { static let entityName = "Chatroom" - var hasUnread: Bool { - return hasUnreadMessages || (lastTransaction?.isUnread ?? false) - } - - func markAsReaded() { - hasUnreadMessages = false - - if let trs = transactions as? Set { - trs.filter { $0.isUnread }.forEach { $0.isUnread = false } - } - lastTransaction?.isUnread = false - } - - func markAsUnread() { - hasUnreadMessages = true - lastTransaction?.isUnread = true - } - - func getFirstUnread() -> ChatTransaction? { - if let trs = transactions as? Set { - return trs.filter { $0.isUnread }.map { $0 }.first - } - return nil - } +// func hasUnread(with lastHeight: Int64) -> Bool { +// guard let height = lastTransaction?.height else { return false } +// +// return lastHeight < height +// } +// +// func markAsReaded() { +// hasUnreadMessages = false +// lastTransaction?.isUnread = false +// } +// +// func markAsUnread() { +// hasUnreadMessages = true +// lastTransaction?.isUnread = true +// } @MainActor func getName(addressBookService: AddressBookService) -> String? { guard let partner = partner else { return nil } diff --git a/Adamant/Modules/Chat/View/ChatViewController.swift b/Adamant/Modules/Chat/View/ChatViewController.swift index 35d8399a7..3c93d1b44 100644 --- a/Adamant/Modules/Chat/View/ChatViewController.swift +++ b/Adamant/Modules/Chat/View/ChatViewController.swift @@ -207,6 +207,7 @@ final class ChatViewController: MessagesViewController { super.scrollViewDidScroll(scrollView) updateIsScrollPositionNearlyTheBottom() updateScrollDownButtonVisibility() + identifyBottomVisibleMessage() if scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating { updateDateHeaderIfNeeded() @@ -712,6 +713,27 @@ private extension ChatViewController { scrollDownButton.isHidden = isScrollPositionNearlyTheBottom } + func identifyBottomVisibleMessage() { + let targetY: CGFloat = view.frame.height - view.safeAreaInsets.bottom - targetYOffsetBottom + + guard let visibleIndexPaths = messagesCollectionView.indexPathsForVisibleItems.sorted(by: { + $0.row > $1.row + }) as [IndexPath]? else { return } + + for indexPath in visibleIndexPaths { + guard let cell = messagesCollectionView.cellForItem(at: indexPath) + else { continue } + + let cellRect = messagesCollectionView.convert(cell.frame, to: self.view) + + guard cellRect.maxY >= targetY && cellRect.minY <= targetY + else { continue } + + viewModel.checkBottomMessage(indexPath: indexPath) + break + } + } + func updateDateHeaderIfNeeded() { guard viewAppeared else { return } @@ -1093,3 +1115,4 @@ private let scrollDownButtonInset: CGFloat = 20 private let messagePadding: CGFloat = 12 private let filesToolbarViewHeight: CGFloat = 140 private let targetYOffset: CGFloat = 20 +private let targetYOffsetBottom: CGFloat = 100 diff --git a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift index 3d0ac0587..0eb8c9357 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift @@ -410,10 +410,19 @@ final class ChatViewModel: NSObject { Task { guard let chatroom = chatroom, - chatroom.hasUnreadMessages == true || chatroom.lastTransaction?.isUnread == true + let address = chatroom.partner?.address, + let lastTransaction = chatroom.lastTransaction, + await chatsProvider.isUnreadChat(chatroom: chatroom) else { return } - await chatsProvider.markChatAsRead(chatroom: chatroom) + guard let transactions = chatroom.transactions as? Set + else { return } + + await chatsProvider.setLastReadMessage( + height: lastTransaction.height, + transactions: transactions, + chatroom: address + ) } } @@ -1040,6 +1049,36 @@ extension ChatViewModel { hideHeaderTimer = nil } + func checkBottomMessage(indexPath: IndexPath) { + guard let message = messages[safe: indexPath.section], + let transaction = chatTransactions.first( + where: { $0.chatMessageId == message.id } + ) + else { + return + } + + Task { + guard + let address = chatroom?.partner?.address, + let lastReadMessage = await chatsProvider.getLastReadMessage(chatroom: address), + lastReadMessage.height <= transaction.height || transaction.height == .zero + else { + return + } + + await chatsProvider.appendLastReadMessage( + readMessage: .init( + height: transaction.height > .zero + ? transaction.height + : lastReadMessage.height, + transactionsId: [transaction.transactionId] + ), + chatroom: address + ) + } + } + func startHideDateTimer() { hideHeaderTimer?.cancel() hideHeaderTimer = Timer diff --git a/Adamant/Modules/ChatsList/ChatListViewController.swift b/Adamant/Modules/ChatsList/ChatListViewController.swift index c18e87dcf..a1e1c3911 100644 --- a/Adamant/Modules/ChatsList/ChatListViewController.swift +++ b/Adamant/Modules/ChatsList/ChatListViewController.swift @@ -201,6 +201,12 @@ final class ChatListViewController: KeyboardObservingViewController { } } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + tableView.reloadData() + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() @@ -691,7 +697,8 @@ extension ChatListViewController { cell.hasUnreadMessages = chatroom.hasUnreadMessages if let lastTransaction = chatroom.lastTransaction { - cell.hasUnreadMessages = lastTransaction.isUnread + let isUnread = chatsProvider.isUnreadChat(chatroom: chatroom) + cell.hasUnreadMessages = isUnread cell.lastMessageLabel.attributedText = shortDescription(for: lastTransaction) } else { cell.lastMessageLabel.text = nil @@ -1164,14 +1171,42 @@ extension ChatListViewController { let markAsRead = UIContextualAction( style: .normal, title: "👀" - ) { (_, _, completionHandler) in - if chatroom.hasUnread { - chatroom.markAsReaded() - } else { - chatroom.markAsUnread() + ) { [weak self] (_, _, completionHandler) in + guard let self = self else { return } + + Task { @MainActor in + defer { + completionHandler(true) + self.tableView.reloadData() + } + + guard + let address = chatroom.partner?.address, + let lastTransaction = chatroom.lastTransaction + else { + return + } + + let isUnread = await self.chatsProvider.isUnreadChat(chatroom: chatroom) + + guard let transactions = chatroom.transactions as? Set + else { return } + + if isUnread { + await self.chatsProvider.setLastReadMessage( + height: lastTransaction.height, + transactions: transactions, + chatroom: address + ) + return + } + + await self.chatsProvider.setLastReadMessage( + height: lastTransaction.height - 1, + transactions: [], + chatroom: address + ) } - try? chatroom.managedObjectContext?.save() - completionHandler(true) } markAsRead.backgroundColor = UIColor.adamant.contextMenuDefaultBackgroundColor diff --git a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift index 3ba06143a..e6540dfa3 100644 --- a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift @@ -209,6 +209,26 @@ protocol ChatsProvider: DataProvider, Actor { /// Unread messages controller. Sections by chatroom. func getUnreadMessagesController() -> NSFetchedResultsController + func setLastReadMessage( + readMessage: ReadMessage, + chatroom: String + ) + + func getLastReadMessage(chatroom: String) -> ReadMessage? + + func isUnreadChat(chatroom: Chatroom) -> Bool + + func appendLastReadMessage( + readMessage: ReadMessage, + chatroom: String + ) + + func setLastReadMessage( + height: Int64, + transactions: Set, + chatroom: String + ) + // ForceUpdate chats func update(notifyState: Bool) async -> ChatsProviderResult? diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index ebd8ec840..b16c6896c 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -12,6 +12,11 @@ import MarkdownKit import Combine import CommonKit +struct ReadMessage: Codable { + let height: Int64 + var transactionsId: Set +} + actor AdamantChatsProvider: ChatsProvider { // MARK: Dependencies @@ -473,7 +478,7 @@ extension AdamantChatsProvider { } let offset = (offset ?? 0) + messageCount - + let loadedCount = chatLoadedMessages[addressRecipient] ?? 0 chatLoadedMessages[addressRecipient] = loadedCount + messageCount @@ -532,7 +537,7 @@ extension AdamantChatsProvider { let privateKey = accountService.keypair?.privateKey else { return } - + // MARK: 3. Get transactions socketService.connect(address: address) { [weak self] result in @@ -555,6 +560,70 @@ extension AdamantChatsProvider { self.socketService.disconnect() } + func appendLastReadMessage( + readMessage: ReadMessage, + chatroom: String + ) { + var lastReadMessage = getLastReadMessage(chatroom: chatroom) ?? readMessage + + if lastReadMessage.height == readMessage.height { + lastReadMessage.transactionsId.formUnion(readMessage.transactionsId) + } else { + lastReadMessage = readMessage + } + + setLastReadMessage(readMessage: lastReadMessage, chatroom: chatroom) + } + + func setLastReadMessage( + height: Int64, + transactions: Set, + chatroom: String + ) { + let unreadTransactions = transactions.filter { + $0.height == height || $0.height == .zero + }.compactMap { $0.transactionId } + + setLastReadMessage( + readMessage: .init( + height: height, + transactionsId: Set(unreadTransactions) + ), + chatroom: chatroom + ) + } + + func setLastReadMessage( + readMessage: ReadMessage, + chatroom: String + ) { + securedStore.set(readMessage, for: StoreKey.chat.lastReadHeight(for: chatroom)) + } + + func getLastReadMessage(chatroom: String) -> ReadMessage? { + guard let result: ReadMessage = securedStore.get(StoreKey.chat.lastReadHeight(for: chatroom)) + else { + return nil + } + return result + } + + func isUnreadChat(chatroom: Chatroom) -> Bool { + guard + let address = chatroom.partner?.address, + let lastTransaction = chatroom.lastTransaction, + let lastReadMessage = getLastReadMessage(chatroom: address) else { + return false + } + + if lastReadMessage.height == lastTransaction.height + || lastTransaction.height == .zero { + return !lastReadMessage.transactionsId.contains(lastTransaction.transactionId) + } + + return lastReadMessage.height < lastTransaction.height + } + func update(notifyState: Bool) async -> ChatsProviderResult? { // MARK: 1. Check state guard isInitiallySynced, @@ -1806,19 +1875,28 @@ extension AdamantChatsProvider { } // MARK: 4. Unread messagess - if let readedLastHeight = readedLastHeight { - var unreadTransactions = newMessageTransactions.filter { $0.height > readedLastHeight } - if unreadTransactions.count == 0 { - unreadTransactions = newMessageTransactions.filter { $0.height == 0 } + let chatrooms = Dictionary(grouping: newMessageTransactions, by: ({ (t: ChatTransaction) -> Chatroom in t.chatroom! })) + + for (chatroom, trs) in chatrooms { + guard let address = chatroom.partner?.address, + let lastTransactionHeight = trs.last?.height + else { + continue } - let chatrooms = Dictionary(grouping: unreadTransactions, by: ({ (t: ChatTransaction) -> Chatroom in t.chatroom! })) - for (chatroom, trs) in chatrooms { - if let address = chatroom.partner?.address { - chatroom.isHidden = self.blockList.contains(address) - } - chatroom.hasUnreadMessages = true - trs.forEach { $0.isUnread = true } + + chatroom.isHidden = self.blockList.contains(address) + + guard getLastReadMessage(chatroom: address) == nil else { + print("ignore setLastReadMessage for \(address)") + continue } + + print("setLastReadMessage for \(address)") + setLastReadMessage( + height: lastTransactionHeight, + transactions: Set(trs), + chatroom: address + ) } // MARK: 5. Dump new transactions @@ -1955,10 +2033,10 @@ extension AdamantChatsProvider { } func markChatAsRead(chatroom: Chatroom) { - chatroom.managedObjectContext?.perform { - chatroom.markAsReaded() - try? chatroom.managedObjectContext?.save() - } +// chatroom.managedObjectContext?.perform { +// chatroom.markAsReaded() +// try? chatroom.managedObjectContext?.save() +// } } private func onConnectionToTheInternetRestored() { diff --git a/CommonKit/Sources/CommonKit/Core/SecuredStore.swift b/CommonKit/Sources/CommonKit/Core/SecuredStore.swift index 91cd2608a..51c5a9bab 100644 --- a/CommonKit/Sources/CommonKit/Core/SecuredStore.swift +++ b/CommonKit/Sources/CommonKit/Core/SecuredStore.swift @@ -66,6 +66,12 @@ public extension StoreKey { public static let autoDownloadFullMedia = "autoDownloadFullMediaEnabled" public static let saveFileEncrypted = "saveFileEncrypted" } + + enum chat { + public static func lastReadHeight(for chatRoom: String) -> String { + "lastReadHeight\(chatRoom)" + } + } } public protocol SecuredStore: AnyObject, Sendable { From 7b7cc7af482b6c3482f39ef2c88f89edc6caf135 Mon Sep 17 00:00:00 2001 From: Iana Date: Thu, 21 Nov 2024 18:15:53 +0200 Subject: [PATCH 2/3] [trello.com/c/TOuHqbBz] Fixed: Read/unread messages for new chats --- .../CoreData/Chatroom+CoreDataClass.swift | 16 ----- .../Chat/View/ChatViewController.swift | 7 +- .../DataProviders/ChatsProvider.swift | 1 - .../DataProviders/AdamantChatsProvider.swift | 67 ++++++++++--------- 4 files changed, 36 insertions(+), 55 deletions(-) diff --git a/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift b/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift index 4d8401f8a..bca0a6e4e 100644 --- a/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift +++ b/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift @@ -14,22 +14,6 @@ import CoreData public class Chatroom: NSManagedObject, @unchecked Sendable { static let entityName = "Chatroom" -// func hasUnread(with lastHeight: Int64) -> Bool { -// guard let height = lastTransaction?.height else { return false } -// -// return lastHeight < height -// } -// -// func markAsReaded() { -// hasUnreadMessages = false -// lastTransaction?.isUnread = false -// } -// -// func markAsUnread() { -// hasUnreadMessages = true -// lastTransaction?.isUnread = true -// } - @MainActor func getName(addressBookService: AddressBookService) -> String? { guard let partner = partner else { return nil } let result: String? diff --git a/Adamant/Modules/Chat/View/ChatViewController.swift b/Adamant/Modules/Chat/View/ChatViewController.swift index 3c93d1b44..389ff4f21 100644 --- a/Adamant/Modules/Chat/View/ChatViewController.swift +++ b/Adamant/Modules/Chat/View/ChatViewController.swift @@ -715,11 +715,8 @@ private extension ChatViewController { func identifyBottomVisibleMessage() { let targetY: CGFloat = view.frame.height - view.safeAreaInsets.bottom - targetYOffsetBottom - - guard let visibleIndexPaths = messagesCollectionView.indexPathsForVisibleItems.sorted(by: { - $0.row > $1.row - }) as [IndexPath]? else { return } - + let visibleIndexPaths = messagesCollectionView.indexPathsForVisibleItems + for indexPath in visibleIndexPaths { guard let cell = messagesCollectionView.cellForItem(at: indexPath) else { continue } diff --git a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift index e6540dfa3..e4ecf4f9c 100644 --- a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift @@ -268,7 +268,6 @@ protocol ChatsProvider: DataProvider, Actor { func validateMessage(_ message: AdamantMessage) -> ValidateMessageResult func blockChat(with address: String) func removeMessage(with id: String) - func markChatAsRead(chatroom: Chatroom) @MainActor func removeChatPositon(for address: String) @MainActor func setChatPositon(for address: String, position: Double?) diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index b16c6896c..cb0afcd8a 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -376,6 +376,39 @@ extension AdamantChatsProvider { privateKey: privateKey ) + if !accountService.hasStayInAccount { + chatrooms.chats?.forEach({ chatroom in + guard let lastTransaction = chatroom.lastTransaction else { return } + setLastReadMessage( + readMessage: .init( + height: lastTransaction.height, + transactionsId: [String(lastTransaction.id)] + ), + chatroom: lastTransaction.recipientId + ) + }) + +// for (chatroom, trs) in chatrooms { +// guard let address = chatroom.partner?.address, +// let lastTransactionHeight = trs.last?.height +// else { +// continue +// } +// +// chatroom.isHidden = self.blockList.contains(address) +// +// guard getLastReadMessage(chatroom: address) == nil else { +// continue +// } +// +// setLastReadMessage( +// height: lastTransactionHeight, +// transactions: Set(trs), +// chatroom: address +// ) +// } + } + if !isInitiallySynced { isInitiallySynced = true preLoadChats(array, address: address) @@ -613,7 +646,7 @@ extension AdamantChatsProvider { let address = chatroom.partner?.address, let lastTransaction = chatroom.lastTransaction, let lastReadMessage = getLastReadMessage(chatroom: address) else { - return false + return true } if lastReadMessage.height == lastTransaction.height @@ -1874,31 +1907,6 @@ extension AdamantChatsProvider { } } - // MARK: 4. Unread messagess - let chatrooms = Dictionary(grouping: newMessageTransactions, by: ({ (t: ChatTransaction) -> Chatroom in t.chatroom! })) - - for (chatroom, trs) in chatrooms { - guard let address = chatroom.partner?.address, - let lastTransactionHeight = trs.last?.height - else { - continue - } - - chatroom.isHidden = self.blockList.contains(address) - - guard getLastReadMessage(chatroom: address) == nil else { - print("ignore setLastReadMessage for \(address)") - continue - } - - print("setLastReadMessage for \(address)") - setLastReadMessage( - height: lastTransactionHeight, - transactions: Set(trs), - chatroom: address - ) - } - // MARK: 5. Dump new transactions if privateContext.hasChanges { do { @@ -2032,13 +2040,6 @@ extension AdamantChatsProvider { } } - func markChatAsRead(chatroom: Chatroom) { -// chatroom.managedObjectContext?.perform { -// chatroom.markAsReaded() -// try? chatroom.managedObjectContext?.save() -// } - } - private func onConnectionToTheInternetRestored() { onConnectionToTheInternetRestoredTasks.forEach { $0() } onConnectionToTheInternetRestoredTasks = [] From a223479570044aed459239d4220899280647258c Mon Sep 17 00:00:00 2001 From: Iana Date: Wed, 27 Nov 2024 13:00:54 +0200 Subject: [PATCH 3/3] [trello.com/c/TOuHqbBz] Removed unused code --- .../DataProviders/AdamantChatsProvider.swift | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index cb0afcd8a..98c4ca3fb 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -387,26 +387,6 @@ extension AdamantChatsProvider { chatroom: lastTransaction.recipientId ) }) - -// for (chatroom, trs) in chatrooms { -// guard let address = chatroom.partner?.address, -// let lastTransactionHeight = trs.last?.height -// else { -// continue -// } -// -// chatroom.isHidden = self.blockList.contains(address) -// -// guard getLastReadMessage(chatroom: address) == nil else { -// continue -// } -// -// setLastReadMessage( -// height: lastTransactionHeight, -// transactions: Set(trs), -// chatroom: address -// ) -// } } if !isInitiallySynced {