From 41d57714d38647138b76b89701a839e8cac1765a Mon Sep 17 00:00:00 2001 From: Phung <283886185+nvphungdev@users.noreply.github.com> Date: Tue, 19 May 2026 11:59:22 +0700 Subject: [PATCH] Improve chat message reload performance --- .../Helpers/MessagesPreloaderHelper.swift | 52 +++++-- ...atTableDataSource+PreloaderExtension.swift | 138 +++++++----------- .../NewChatViewController.swift | 2 +- 3 files changed, 91 insertions(+), 101 deletions(-) diff --git a/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/Data Source/New Chat Data Source/Helpers/MessagesPreloaderHelper.swift b/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/Data Source/New Chat Data Source/Helpers/MessagesPreloaderHelper.swift index 083eb562..17b998df 100644 --- a/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/Data Source/New Chat Data Source/Helpers/MessagesPreloaderHelper.swift +++ b/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/Data Source/New Chat Data Source/Helpers/MessagesPreloaderHelper.swift @@ -9,19 +9,19 @@ import Foundation class MessagesPreloaderHelper { - + class var sharedInstance : MessagesPreloaderHelper { struct Static { static let instance = MessagesPreloaderHelper() } return Static.instance } - + struct ScrollState { var firstRowId: Int var difference: CGFloat var isAtBottom: Bool - + init( firstRowId: Int, difference: CGFloat, @@ -32,11 +32,11 @@ class MessagesPreloaderHelper { self.isAtBottom = isAtBottom } } - + struct PreloadedMessagesState { var messageCellStates: [MessageTableCellState] var resultsControllerCount: Int - + init( messageCellStates: [MessageTableCellState], resultsControllerCount: Int @@ -45,37 +45,46 @@ class MessagesPreloaderHelper { self.resultsControllerCount = resultsControllerCount } } - + + private let maxPreloadedChats = 5 + private var preloadedChatOrder: [Int] = [] + func releaseMemory() { tribesData = [:] linksData = [:] - chatMessages = [:] } - + var chatMessages: [Int: PreloadedMessagesState] = [:] var chatScrollState: [Int: ScrollState] = [:] - + var tribesData: [String: MessageTableCellState.TribeData] = [:] var linksData: [String: MessageTableCellState.LinkData] = [:] - + func add( messageStateArray: [MessageTableCellState], resultsControllerCount: Int, for chatId: Int ) { + guard !messageStateArray.isEmpty else { + return + } + self.chatMessages[chatId] = PreloadedMessagesState( messageCellStates: messageStateArray, resultsControllerCount: resultsControllerCount ) + + markChatAsRecentlyPreloaded(chatId) + trimPreloadedChatsIfNeeded() } - + func getPreloadedMessagesState(for chatId: Int) -> PreloadedMessagesState? { if let preloadedMessagesState = chatMessages[chatId], preloadedMessagesState.messageCellStates.count > 0 { return preloadedMessagesState } return nil } - + func save( firstRowId: Int, difference: CGFloat, @@ -88,13 +97,15 @@ class MessagesPreloaderHelper { isAtBottom: isAtBottom ) } - + func reset( for chatId: Int ) { self.chatScrollState.removeValue(forKey: chatId) + self.chatMessages.removeValue(forKey: chatId) + self.preloadedChatOrder.removeAll(where: { $0 == chatId }) } - + func getScrollState( for chatId: Int, pinnedMessageId: Int? = nil @@ -111,4 +122,17 @@ class MessagesPreloaderHelper { } return nil } + + private func markChatAsRecentlyPreloaded(_ chatId: Int) { + preloadedChatOrder.removeAll(where: { $0 == chatId }) + preloadedChatOrder.append(chatId) + } + + private func trimPreloadedChatsIfNeeded() { + while preloadedChatOrder.count > maxPreloadedChats { + let oldestChatId = preloadedChatOrder.removeFirst() + chatMessages.removeValue(forKey: oldestChatId) + chatScrollState.removeValue(forKey: oldestChatId) + } + } } diff --git a/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/Data Source/New Chat Data Source/New Chat Table Data Source/NewChatTableDataSource+PreloaderExtension.swift b/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/Data Source/New Chat Data Source/New Chat Table Data Source/NewChatTableDataSource+PreloaderExtension.swift index 8cdca9f2..5fbc7d8a 100644 --- a/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/Data Source/New Chat Data Source/New Chat Table Data Source/NewChatTableDataSource+PreloaderExtension.swift +++ b/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/Data Source/New Chat Data Source/New Chat Table Data Source/NewChatTableDataSource+PreloaderExtension.swift @@ -9,97 +9,63 @@ import Foundation extension NewChatTableDataSource { -// func preloadDataForItems() { -// for index in stride(from: messageTableCellStateArray.count - 1, through: 0, by: -1) { -// let item = messageTableCellStateArray[index] -// -// if let messageId = item.message?.id { -// DispatchQueue.global(qos: .userInteractive).async { -// self.preloadDataFor( -// rowIndex: index, -// messageId: messageId -// ) -// } -// } -// } -// } -// -// func preloadDataFor( -// rowIndex: Int, -// messageId: Int -// ) { -// if let tableCellState = getTableCellStateFor( -// messageId: messageId, -// and: rowIndex -// ) { -// var mutableCellState = tableCellState -// -// if let link = mutableCellState.1.webLink?.link { -// let linkData = preloaderHelper.linksData[link] -// -// if linkData == nil { -//// loadLinkDataFor( -//// messageId: messageId, -//// and: rowIndex -//// ) -// } -// } -// } -// } - + + private static let maxPreloadedRows = 200 + @objc func restorePreloadedOrLoadMessages() { -// guard let chat = chat else { -// return -// } -// -// if let preloadedMessagesState = preloaderHelper.getPreloadedMessagesState(for: chat.id) { -// messageTableCellStateArray = preloadedMessagesState.messageCellStates -// updatePreloadedSnapshot() -// -// DelayPerformedHelper.performAfterDelay(seconds: 0.5, completion: { [weak self] in -// guard let self = self else { return } -// self.configureResultsController( -// items: max(self.dataSource.snapshot().numberOfItems, preloadedMessagesState.resultsControllerCount) -// ) -// fetchMoreItems() -// }) -// } else { + guard let chat = chat else { + return + } + + if let preloadedMessagesState = preloaderHelper.getPreloadedMessagesState(for: chat.id) { + messageTableCellStateArray = preloadedMessagesState.messageCellStates + updatePreloadedSnapshot() + + DelayPerformedHelper.performAfterDelay(seconds: 0.2, completion: { [weak self] in + guard let self = self else { return } + self.configureResultsController( + items: max(self.dataSource.snapshot().numberOfItems, preloadedMessagesState.resultsControllerCount) + ) + }) + } else { configureResultsController(items: max(dataSource.snapshot().numberOfItems, 100)) -// } + } } - + @objc func saveMessagesToPreloader() { -// let collectionViewOffsetY = collectionViewScroll.documentYOffset + collectionViewScroll.contentInsets.top -// let firstVisibleItem = collectionView.indexPathForItem(at: NSPoint(x: 0, y: collectionViewOffsetY))?.item ?? 0 -// -// guard let chat = chat, collectionView.numberOfSections > 0 && firstVisibleItem > 0 else { -// return -// } -// -// let numberOfItems = collectionView.numberOfItems(inSection: 0) -// -// preloaderHelper.add( -// messageStateArray: messageTableCellStateArray.endSubarray(size: (numberOfItems - firstVisibleItem) + 10), -// resultsControllerCount: messagesCount, -// for: chat.id -// ) + let collectionViewOffsetY = collectionViewScroll.documentYOffset + collectionViewScroll.contentInsets.top + let firstVisibleItem = collectionView.indexPathForItem(at: NSPoint(x: 0, y: collectionViewOffsetY))?.item ?? 0 + + guard let chat = chat, collectionView.numberOfSections > 0 else { + return + } + + let numberOfItems = collectionView.numberOfItems(inSection: 0) + let rowsFromVisibleItem = max(numberOfItems - firstVisibleItem, 0) + 10 + let rowsToPreload = min(rowsFromVisibleItem, Self.maxPreloadedRows) + + preloaderHelper.add( + messageStateArray: messageTableCellStateArray.endSubarray(size: rowsToPreload), + resultsControllerCount: messagesCount, + for: chat.id + ) } - + @objc func saveSnapshotCurrentState() { saveScrollPosition() -// saveMessagesToPreloader() + saveMessagesToPreloader() } - + func deleteSnapshotCurrentState() { guard let chatId = chat?.id else { return } - + self.preloaderHelper.reset( for: chatId ) } - + @objc func restoreScrollLastPosition() { guard let chatId = chat?.id else { return } @@ -110,7 +76,7 @@ extension NewChatTableDataSource { ///Find index of stored first visible item if let index = messageTableCellStateArray.firstIndex(where: { $0.getUniqueIdentifier() == scrollState.firstRowId}) { - + let numberOfItems = collectionView.numberOfItems(inSection: 0) guard index < numberOfItems else { return @@ -133,33 +99,33 @@ extension NewChatTableDataSource { return } } - + ///Scroll to bottom if it didn't scroll to spefici position let collectionViewContentSize = collectionView.collectionViewLayout?.collectionViewContentSize.height ?? 0 let offset = collectionViewContentSize - collectionViewScroll.frame.height + collectionViewScroll.contentInsets.top scrollViewDesiredOffset = offset collectionViewScroll.documentYOffset = offset - + if scrolledAtBottom { return } - + scrolledAtBottom = true - + delegate?.didScrollToBottom() scrollViewDidScroll() } - + func saveScrollPosition() { guard let _ = collectionView.enclosingScrollView else { return } if collectionView.alphaValue == 0 { return } - + guard let chatId = chat?.id else { return } - + let collectionViewOffsetY = collectionViewScroll.documentYOffset + collectionViewScroll.contentInsets.top - + ///Find first visible item if let firstVisibleRow = collectionView.indexPathForItem(at: NSPoint(x: 0, y: collectionViewOffsetY))?.item { ///Find first visible item y position @@ -167,13 +133,13 @@ extension NewChatTableDataSource { ///Find unique identifier for first visible item var firstVisibleItem = dataSource.snapshot().itemIdentifiers[firstVisibleRow] var firstRowId = dataSource.snapshot().itemIdentifiers[firstVisibleRow].getUniqueIdentifier() - + ///If first visible row is date separator, then take next since date separator can change its position based on number of items displayed if !firstVisibleItem.isMessageRow && dataSource.snapshot().itemIdentifiers.count > firstVisibleRow + 1 { firstRowId = dataSource.snapshot().itemIdentifiers[firstVisibleRow + 1].getUniqueIdentifier() firstVisibleRowY = collectionView.item(at: firstVisibleRow + 1)?.view.frame.origin.y ?? 0 } - + ///Save scroll position based on visible item and offset self.preloaderHelper.save( firstRowId: firstRowId, diff --git a/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/New Chat View Controller/NewChatViewController.swift b/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/New Chat View Controller/NewChatViewController.swift index bf4c0d6a..9509dda1 100644 --- a/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/New Chat View Controller/NewChatViewController.swift +++ b/com.stakwork.sphinx.desktop/Scenes/Dashboard/Chat/New Chat View Controller/NewChatViewController.swift @@ -174,7 +174,7 @@ class NewChatViewController: DashboardSplittedViewController { override func viewWillDisappear() { super.viewWillDisappear() - chatTableDataSource?.deleteSnapshotCurrentState() + chatTableDataSource?.saveSnapshotCurrentState() chatTableDataSource?.releaseMemory() closeThreadAndResetEscapeMonitor()