diff --git a/sphinx/Core Data/TransactionMessage/TransactionMessageQueriesExtension.swift b/sphinx/Core Data/TransactionMessage/TransactionMessageQueriesExtension.swift index 3987ec8c..b95fb209 100644 --- a/sphinx/Core Data/TransactionMessage/TransactionMessageQueriesExtension.swift +++ b/sphinx/Core Data/TransactionMessage/TransactionMessageQueriesExtension.swift @@ -827,4 +827,9 @@ extension TransactionMessage { } return groupedMessages } + + static func getCountOfMessages(forChat chat: Chat, fromId messageId: Int) -> Int { + let predicate = NSPredicate(format: "chat == %@ AND id >= %d", chat, messageId) + return CoreDataManager.sharedManager.getObjectsCountOfTypeWith(predicate: predicate, entityName: "TransactionMessage") + } } diff --git a/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/New Chat View Controller/NewChatViewController.swift b/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/New Chat View Controller/NewChatViewController.swift index 7184cea6..884a06dc 100644 --- a/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/New Chat View Controller/NewChatViewController.swift +++ b/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/New Chat View Controller/NewChatViewController.swift @@ -145,6 +145,9 @@ class NewChatViewController: NewKeyboardHandlerViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + // Save scroll state before the view disappears + chatTableDataSource?.saveSnapshotCurrentState() + if self.isMovingFromParent { chat?.setChatMessagesAsSeen() } @@ -154,7 +157,6 @@ class NewChatViewController: NewKeyboardHandlerViewController { super.viewDidDisappear(animated) if self.isMovingFromParent { - chatTableDataSource?.saveSnapshotCurrentState() chatTableDataSource?.stopListeningToResultsController() stopPlayingClip() diff --git a/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/MessagesPreloaderHelper.swift b/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/MessagesPreloaderHelper.swift index 67b2e361..fb3e20df 100644 --- a/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/MessagesPreloaderHelper.swift +++ b/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/MessagesPreloaderHelper.swift @@ -19,32 +19,13 @@ class MessagesPreloaderHelper { } struct ScrollState { - var bottomFirstVisibleRow: Int - var bottomFirstVisibleRowOffset: CGFloat - var bottomFirstVisibleRowUniqueID: Int? - var numberOfItems: Int - var shouldAdjustScroll: Bool - var shouldPreventSetMessagesAsSeen: Bool - - init( - bottomFirstVisibleRow: Int, - bottomFirstVisibleRowOffset: CGFloat, - bottomFirstVisibleRowUniqueID: Int?, - numberOfItems: Int, - shouldAdjustScroll: Bool, - shouldPreventSetMessagesAsSeen: Bool - ) { - self.bottomFirstVisibleRow = bottomFirstVisibleRow - self.bottomFirstVisibleRowOffset = bottomFirstVisibleRowOffset - self.bottomFirstVisibleRowUniqueID = bottomFirstVisibleRowUniqueID - self.numberOfItems = numberOfItems - self.shouldAdjustScroll = shouldAdjustScroll - self.shouldPreventSetMessagesAsSeen = shouldPreventSetMessagesAsSeen - } + var firstRowId: Int // message.id of the topmost visible row + var difference: CGFloat // pixel offset of that row from the top of the viewport + var isAtBottom: Bool } var chatMessages: [Int: [MessageTableCellState]] = [:] - var chatLastPositions: [Int: ScrollState] = [:] + var chatScrollState: [Int: ScrollState] = [:] var tribesData: [String: MessageTableCellState.TribeData] = [:] var linksData: [String: MessageTableCellState.LinkData] = [:] @@ -67,47 +48,18 @@ class MessagesPreloaderHelper { return nil } - func save( - bottomFirstVisibleRow: Int, - bottomFirstVisibleRowOffset: CGFloat, - bottomFirstVisibleRowUniqueID: Int?, - numberOfItems: Int, - for chatId: Int - ) { - self.chatLastPositions[chatId] = ScrollState( - bottomFirstVisibleRow: bottomFirstVisibleRow, - bottomFirstVisibleRowOffset: bottomFirstVisibleRowOffset, - bottomFirstVisibleRowUniqueID: bottomFirstVisibleRowUniqueID, - numberOfItems: numberOfItems, - shouldAdjustScroll: true, - shouldPreventSetMessagesAsSeen: true - ) + func save(firstRowId: Int, difference: CGFloat, isAtBottom: Bool, for chatId: Int) { + chatScrollState[chatId] = ScrollState(firstRowId: firstRowId, difference: difference, isAtBottom: isAtBottom) } - func getScrollState( - for chatId: Int, - with newItemIdentifiers: [MessageTableCellState] - ) -> ScrollState? { - - if let scrollState = chatLastPositions[chatId] { - - if let firstItemBeforeUpdate = scrollState.bottomFirstVisibleRowUniqueID { - - let itemUniqueIdentifiers = newItemIdentifiers.map({ $0.getUniqueIdentifier() }) - let difference = itemUniqueIdentifiers.index(of: firstItemBeforeUpdate) ?? 0 - let destinationRow = scrollState.bottomFirstVisibleRow + difference - let shouldAdjustScroll = destinationRow > 1 || (destinationRow > 0 && scrollState.bottomFirstVisibleRowOffset > 0) - - return ScrollState( - bottomFirstVisibleRow: destinationRow, - bottomFirstVisibleRowOffset: scrollState.bottomFirstVisibleRowOffset, - bottomFirstVisibleRowUniqueID: scrollState.bottomFirstVisibleRowUniqueID, - numberOfItems: scrollState.numberOfItems, - shouldAdjustScroll: shouldAdjustScroll, - shouldPreventSetMessagesAsSeen: scrollState.numberOfItems == newItemIdentifiers.count - ) - } + func getScrollState(for chatId: Int, pinnedMessageId: Int? = nil) -> ScrollState? { + if let pinnedMessageId = pinnedMessageId { + return ScrollState(firstRowId: pinnedMessageId, difference: 0, isAtBottom: false) } - return nil + return chatScrollState[chatId] + } + + func reset(for chatId: Int) { + chatScrollState.removeValue(forKey: chatId) } } diff --git a/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource+PreloaderExtension.swift b/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource+PreloaderExtension.swift index c3d3731e..3ed23c1f 100644 --- a/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource+PreloaderExtension.swift +++ b/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource+PreloaderExtension.swift @@ -34,74 +34,67 @@ extension NewChatTableDataSource { } @objc func saveSnapshotCurrentState() { - guard let chat = chat else { - return - } + // Do not overwrite a saved state that hasn't been consumed yet + guard !isRestoringScrollPosition else { return } + guard let chatId = chat?.id else { return } - if let firstVisibleRow = tableView.indexPathsForVisibleRows?.first { - - let cellRectInTable = tableView.rectForRow(at: firstVisibleRow) - let cellOffset = tableView.convert(cellRectInTable.origin, to: bottomView) - - preloaderHelper.save( - bottomFirstVisibleRow: firstVisibleRow.row, - bottomFirstVisibleRowOffset: cellOffset.y, - bottomFirstVisibleRowUniqueID: dataSource.snapshot().itemIdentifiers.first?.getUniqueIdentifier(), - numberOfItems: preloaderHelper.getPreloadedMessagesCount(for: chat.id), - for: chat.id - ) - } + // In the inverted UITableView, .last = highest index = topmost visible row on screen + guard var topIndexPath = tableView.indexPathsForVisibleRows?.last else { return } - saveMessagesToPreloader() + // Skip date-separator rows (message == nil); walk toward lower index (newer, but still visible) + while topIndexPath.row >= 0 { + let state = messageTableCellStateArray[topIndexPath.row] + if let messageId = state.message?.id { + // Found a real message row — compute pixel offset + let isAtBottom = tableView.contentOffset.y <= Constants.kChatTableContentInset + if isAtBottom { + preloaderHelper.save(firstRowId: messageId, difference: 0, isAtBottom: true, for: chatId) + } else { + let cellRect = tableView.rectForRow(at: topIndexPath) + let cellRectInView = tableView.convert(cellRect, to: tableView) + let visibleTop = tableView.contentOffset.y + tableView.contentInset.top + let difference = cellRectInView.origin.y - visibleTop + preloaderHelper.save(firstRowId: messageId, difference: difference, isAtBottom: false, for: chatId) + } + return + } + if topIndexPath.row == 0 { break } + topIndexPath = IndexPath(row: topIndexPath.row - 1, section: topIndexPath.section) + } } @objc func restoreScrollLastPosition() { - guard let chat = chat else { - return - } - + guard let chatId = chat?.id else { return } tableView.alpha = 1.0 if let pinnedMessageId = pinnedMessageId { - if let index = getTableCellStateFor( - messageId: pinnedMessageId, - and: nil - )?.0 { - tableView.scrollToRow( - at: IndexPath(row: index, section: 0), - at: .top, - animated: true - ) + if let index = getTableCellStateFor(messageId: pinnedMessageId, and: nil)?.0 { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .top, animated: true) + } + isRestoringScrollPosition = false + return + } + + if let scrollState = preloaderHelper.getScrollState(for: chatId, pinnedMessageId: nil), + !scrollState.isAtBottom { + // Find the row whose message.id matches the saved anchor + if let index = messageTableCellStateArray.firstIndex(where: { $0.message?.id == scrollState.firstRowId }) { + let rowCount = tableView.numberOfRows(inSection: 0) + guard index < rowCount else { + isRestoringScrollPosition = false + delegate?.didScrollToBottom() + return + } + tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .top, animated: false) + // Apply saved pixel offset from viewport top + tableView.contentOffset.y = tableView.contentOffset.y - scrollState.difference + isRestoringScrollPosition = false + return } } -// else { -// if let scrollState = preloaderHelper.getScrollState( -// for: chat.id, -// with: dataSource.snapshot().itemIdentifiers -// ) { -// let row = scrollState.bottomFirstVisibleRow -// let offset = scrollState.bottomFirstVisibleRowOffset -// -// if scrollState.shouldAdjustScroll && !loadingMoreItems { -// -// if tableView.numberOfRows(inSection: 0) > row { -// -// tableView.scrollToRow( -// at: IndexPath(row: row, section: 0), -// at: .top, -// animated: false -// ) -// -// tableView.contentOffset.y = tableView.contentOffset.y + (offset + tableView.contentInset.top) -// } -// } -// -// if scrollState.shouldPreventSetMessagesAsSeen { -// return -// } -// } -// } + // Fallback: scroll to bottom (most recent messages) + isRestoringScrollPosition = false if tableView.contentOffset.y <= Constants.kChatTableContentInset { delegate?.didScrollToBottom() } diff --git a/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource+ResultsControllerExtension.swift b/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource+ResultsControllerExtension.swift index a8c83366..81f052ed 100644 --- a/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource+ResultsControllerExtension.swift +++ b/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource+ResultsControllerExtension.swift @@ -69,6 +69,7 @@ extension NewChatTableDataSource { self.saveSnapshotCurrentState() self.dataSource.apply(snapshot, animatingDifferences: false) + self.isRestoringScrollPosition = true self.restoreScrollLastPosition() DelayPerformedHelper.performAfterDelay(seconds: 1.0, completion: { @@ -793,11 +794,23 @@ extension NewChatTableDataSource : NSFetchedResultsControllerDelegate { return } - messagesCountRequested = items + var requestedItems = items + + // If there is a saved scroll state mid-chat, ensure we fetch enough items to cover the anchor + if let chatId = chat.id, + let scrollState = preloaderHelper.getScrollState(for: chatId), + !scrollState.isAtBottom { + // Count how many messages exist from the anchor downward (more recent) + let anchorId = scrollState.firstRowId + let countFromAnchor = TransactionMessage.getCountOfMessages(forChat: chat, fromId: anchorId) + 20 + requestedItems = max(items, countFromAnchor) + } + + messagesCountRequested = requestedItems var fetchRequest = getFetchRequestFor( chat: chat, - with: items + with: requestedItems ) if let minIndex = getFetchMinIndex(fetchRequest: fetchRequest), !isThread { @@ -805,7 +818,7 @@ extension NewChatTableDataSource : NSFetchedResultsControllerDelegate { fetchRequest = getFetchRequestFor( chat: chat, - with: items, + with: requestedItems, and: minIndex ) } diff --git a/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource.swift b/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource.swift index 22689cbb..422c55aa 100644 --- a/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource.swift +++ b/sphinx/Scenes/Chat/View Controllers/New Chat View Controller/View Model & Data Source/NewChatTableDataSource/NewChatTableDataSource.swift @@ -103,6 +103,7 @@ class NewChatTableDataSource : NSObject { let messageBubbleHelper = NewMessageBubbleHelper() let audioPlayerHelper = AudioPlayerHelper() var podcastPlayerController = PodcastPlayerController.sharedInstance + var isRestoringScrollPosition: Bool = false ///Messages Data var messagesArray: [TransactionMessage] = []