From 6b308693f28e21f2a0936d088aeb90b2489c1575 Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Mon, 30 Dec 2024 10:34:31 -0800 Subject: [PATCH] changes --- .../Shared/Views/Chat/ChatItemsLoader.swift | 16 +++-- .../Shared/Views/Chat/ChatItemsMerger.swift | 7 +- .../Shared/Views/Chat/ChatScrollHelpers.swift | 68 ++++++++++++++++--- apps/ios/Shared/Views/Chat/ChatView.swift | 68 ++++++++++--------- apps/ios/Shared/Views/Chat/ReverseList.swift | 37 ++++++++-- apps/ios/SimpleXChat/APITypes.swift | 2 +- 6 files changed, 144 insertions(+), 54 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift index 5064a3d757..046ab6c208 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift @@ -17,7 +17,7 @@ func apiLoadMessages( _ pagination: ChatPagination, _ chatState: ActiveChatState, _ search: String = "", - _ visibleItemIndexesNonReversed: () -> ClosedRange = { 0 ... 0 } + _ visibleItemIndexesNonReversed: @MainActor () -> ClosedRange = { 0 ... 0 } ) async { let chat: Chat let navInfo: NavigationInfo @@ -67,8 +67,9 @@ func apiLoadMessages( guard let indexInCurrentItems else { return } let (newIds, _) = mapItemsToIds(chat.chatItems) let wasSize = newItems.count + let visibleItemIndexes = await MainActor.run { visibleItemIndexesNonReversed() } let modifiedSplits = removeDuplicatesAndModifySplitsOnBeforePagination( - unreadAfterItemId, &newItems, newIds, chatState.splits, visibleItemIndexesNonReversed + unreadAfterItemId, &newItems, newIds, chatState.splits, visibleItemIndexes ) let insertAt = max((indexInCurrentItems - (wasSize - newItems.count) + modifiedSplits.trimmedIds.count), 0) newItems.insert(contentsOf: chat.chatItems, at: insertAt) @@ -151,18 +152,21 @@ private func removeDuplicatesAndModifySplitsOnBeforePagination( _ newItems: inout [ChatItem], _ newIds: Set, _ splits: [Int64], - _ visibleItemIndexesNonReversed: () -> ClosedRange + _ visibleItemIndexes: ClosedRange ) -> ModifiedSplits { var oldUnreadSplitIndex: Int = -1 var newUnreadSplitIndex: Int = -1 - let visibleItemIndexes = visibleItemIndexesNonReversed() var lastSplitIndexTrimmed: Int? = nil var allowedTrimming = true var index = 0 /** keep the newest [TRIM_KEEP_COUNT] items (bottom area) and oldest [TRIM_KEEP_COUNT] items, trim others */ - let trimRange = visibleItemIndexes.upperBound + TRIM_KEEP_COUNT ... newItems.count - TRIM_KEEP_COUNT + let trimLowerBound = visibleItemIndexes.upperBound + TRIM_KEEP_COUNT + let trimUpperBound = newItems.count - TRIM_KEEP_COUNT + let trimRange = trimUpperBound >= trimLowerBound ? trimLowerBound ... trimUpperBound : -1 ... -1 var trimmedIds = Set() - let prevItemTrimRange = visibleItemIndexes.upperBound + TRIM_KEEP_COUNT + 1 ... newItems.count - TRIM_KEEP_COUNT + let prevTrimLowerBound = visibleItemIndexes.upperBound + TRIM_KEEP_COUNT + 1 + let prevTrimUpperBound = newItems.count - TRIM_KEEP_COUNT + let prevItemTrimRange = prevTrimUpperBound >= prevTrimLowerBound ? prevTrimLowerBound ... prevTrimUpperBound : -1 ... -1 var newSplits = splits newItems.removeAll(where: { diff --git a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift index be1e83a0c8..5976249b39 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift @@ -245,8 +245,9 @@ class ActiveChatState { let currentIndex = nonReversedItems.firstIndex(where: { $0.id == unreadAfterItemId }) let newIndex = nonReversedItems.firstIndex(where: { $0.id == toItemId }) guard let currentIndex, let newIndex else { - return unreadAfterItemId = toItemId + return } + unreadAfterItemId = toItemId let unreadDiff = newIndex > currentIndex ? -nonReversedItems[currentIndex + 1.. fromIndex ? -nonReversedItems[fromIndex + 1..: Hashable { } extension ReverseList.Controller { + @MainActor func visibleItemIndexesNonReversed(_ mergedItems: Binding) -> ClosedRange { let zero = 0 ... 0 if itemCount == 0 { diff --git a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift index 06d7f584d9..dc909b35b4 100644 --- a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift +++ b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift @@ -30,26 +30,78 @@ func loadLastItems(_ loadingMoreItems: Binding, _ chatInfo: ChatInfo) { } } -func preloadItems(_ mergedItems: MergedItems, _ state: ListState, _ ignoreLoadingRequests: Binding<[Int64]>, _ loadItems: @escaping (ChatPagination) -> Void) { +func preloadItems(_ mergedItems: MergedItems, _ allowLoadMoreItems: Bool, _ listState: ListState, _ ignoreLoadingRequests: Binding<[Int64]>, _ loadItems: @escaping (ChatPagination) async -> Bool) async { + let allowLoad = allowLoadMoreItems || mergedItems.items.count == listState.lastVisibleItemIndex + 1 let remaining = ChatPagination.UNTIL_PRELOAD_COUNT - let firstVisibleIndex = state.firstVisibleItemIndex + let firstVisibleIndex = listState.firstVisibleItemIndex - preloadItemsAfter() + await preloadItemsBefore() + await preloadItemsAfter() - func preloadItemsAfter() { + func preloadItemsBefore() async { + let splits = mergedItems.splits + let lastVisibleIndex = listState.lastVisibleItemIndex + var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits) + logger.debug("LALAL LASTINDEX TO LOAD FROM \(lastIndexToLoadFrom ?? -1)") + let items: [ChatItem] = ItemsModel.shared.reversedChatItems.reversed() + if splits.isEmpty && !items.isEmpty && lastVisibleIndex > mergedItems.items.count - remaining && items.count >= ChatPagination.INITIAL_COUNT { + lastIndexToLoadFrom = items.count - 1 + } + logger.debug("LALAL LASTINDEX TO LOAD FROM after \(lastIndexToLoadFrom ?? -1), count \(items.count)") + let loadFromItemId: Int64? + if allowLoad, let lastIndexToLoadFrom { + let index = items.count - 1 - lastIndexToLoadFrom + loadFromItemId = index >= 0 ? items[index].id : nil + logger.debug("LALAL LASTINDEX TO LOAD FROM inside \(loadFromItemId ?? -1)") + } else { + logger.debug("LALAL LOADFROMNIL") + loadFromItemId = nil + } + guard let loadFromItemId, !ignoreLoadingRequests.wrappedValue.contains(loadFromItemId) else { + return + } + let sizeWas = items.count + let firstItemIdWas = items.first?.id + let triedToLoad = await loadItems(ChatPagination.before(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT)) + logger.debug("LALAL PRELOAD BEFORE \(String(describing: splits)) \(firstVisibleIndex) \(sizeWas) \(triedToLoad) \(lastIndexToLoadFrom ?? -1)") + if triedToLoad && sizeWas == ItemsModel.shared.reversedChatItems.count && firstItemIdWas == ItemsModel.shared.reversedChatItems.last?.id { + ignoreLoadingRequests.wrappedValue.append(loadFromItemId) + } + } + + func preloadItemsAfter() async { let items: [ChatItem] = ItemsModel.shared.reversedChatItems.reversed() let splits = mergedItems.splits let split = splits.last(where: { $0.indexRangeInParentItems.contains(firstVisibleIndex) }) // we're inside a splitRange (top --- [end of the splitRange --- we're here --- start of the splitRange] --- bottom) - logger.debug("LALAL PRELOAD AFTER \(String(describing: split)) \(firstVisibleIndex) \(items.count - 1 - (split?.indexRangeInReversed.lowerBound ?? -1))") + logger.debug("LALAL PRELOAD AFTER \(String(describing: splits)) \(firstVisibleIndex) \((split?.indexRangeInParentItems.lowerBound ?? 0) + remaining) \(items.count - 1 - (split?.indexRangeInParentItems.lowerBound ?? -1))") if let split, split.indexRangeInParentItems.lowerBound + remaining > firstVisibleIndex { let index = items.count - 1 - split.indexRangeInReversed.lowerBound if index >= 0 { let loadFromItemId = items[index].id - loadItems(.after(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT)) + _ = await loadItems(ChatPagination.after(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT)) } - } } - } + +private func findLastIndexToLoadFromInSplits(_ firstVisibleIndex: Int, _ lastVisibleIndex: Int, _ remaining: Int, _ splits: [SplitRange]) -> Int? { + for split in splits { + // before any split + if split.indexRangeInParentItems.lowerBound > firstVisibleIndex { + if lastVisibleIndex > (split.indexRangeInParentItems.lowerBound - remaining) { + return split.indexRangeInReversed.lowerBound - 1 + } + break + } + let containsInRange = split.indexRangeInParentItems.contains(firstVisibleIndex) + if containsInRange { + if lastVisibleIndex > (split.indexRangeInParentItems.upperBound - remaining) { + return split.indexRangeInReversed.upperBound + } + break + } + } + return nil +} + diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 74d91967c6..59e21fa6c0 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -33,7 +33,6 @@ struct ChatView: View { @State private var customUserProfile: Profile? @State private var connectionCode: String? @State private var loadingItems = false - @State private var firstPage = false @State private var searchMode = false @State private var searchText: String = "" @FocusState private var searchFocussed @@ -47,6 +46,7 @@ struct ChatView: View { @State private var selectedChatItems: Set? = nil @State private var showDeleteSelectedMessages: Bool = false @State private var allowToDeleteSelectedMessagesForAll: Bool = false + @State private var allowLoadMoreItems: Bool = false @State private var ignoreLoadingRequests: [Int64] = [] @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial @@ -202,7 +202,9 @@ struct ChatView: View { } else { .last(count: loadItemsPerPage) } - loadChatItems(chat.chatInfo, pagination) + Task { + _ = await loadChatItems(chat.chatInfo, pagination) + } } } .environmentObject(scrollModel) @@ -442,7 +444,7 @@ struct ChatView: View { return GeometryReader { g in let _ = logger.debug("LALAL RELOAD \(im.reversedChatItems.count)") // LALAL CAN I CHANGE BINDING LIKE THIS IN ignoreLoadingRequests? - ReverseList(items: im.reversedChatItems, mergedItems: $mergedItems, revealedItems: $revealedItems, unreadCount: Binding.constant(chat.chatStats.unreadCount), scrollState: $scrollModel.state, ignoreLoadingRequests: searchValueIsEmpty ? $ignoreLoadingRequests : Binding.constant([])) { index, mergedItem in + ReverseList(items: im.reversedChatItems, mergedItems: $mergedItems, revealedItems: $revealedItems, unreadCount: Binding.constant(chat.chatStats.unreadCount), scrollState: $scrollModel.state, allowLoadMoreItems: $allowLoadMoreItems, ignoreLoadingRequests: searchValueIsEmpty ? $ignoreLoadingRequests : Binding.constant([])) { index, mergedItem in let ci = switch mergedItem { case let .single(item, _): item.item case let .grouped(items, _, _, _, _, _, _): items.boxedValue.last!.item @@ -476,11 +478,15 @@ struct ChatView: View { .environmentObject(theme) // crashes without this line when scrolling to the first unread in ReverseList .id(ci.id) // Required to trigger `onAppear` on iOS15 } loadItems: { pagination, visibleItemIndexesNonReversed in - loadChatItems(cInfo, pagination, visibleItemIndexesNonReversed) + await loadChatItems(cInfo, pagination, visibleItemIndexesNonReversed) } .onAppear { mergedItems = MergedItems.create(im.reversedChatItems, chat.chatStats.unreadCount, revealedItems, ItemsModel.shared.chatState) loadLastItems($loadingItems, chat.chatInfo) + allowLoadMoreItems = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + allowLoadMoreItems = true + } } .onChange(of: im.reversedChatItems) { items in mergedItems = MergedItems.create(items, chat.chatStats.unreadCount, revealedItems, ItemsModel.shared.chatState) @@ -493,6 +499,10 @@ struct ChatView: View { } .onChange(of: chat.id) { _ in loadLastItems($loadingItems, chat.chatInfo) + allowLoadMoreItems = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + allowLoadMoreItems = true + } } .opacity(ItemsModel.shared.isLoading ? 0 : 1) .padding(.vertical, -InvertedTableView.inset) @@ -893,34 +903,30 @@ struct ChatView: View { } } - private func loadChatItems(_ cInfo: ChatInfo, _ pagination: ChatPagination, _ visibleItemIndexesNonReversed: @escaping () -> ClosedRange = { 0 ... 0 }) { - Task { - if loadingItems || firstPage { return } - loadingItems = true - var chatItemsAvailable = true - var itemsCountChanged = false - // Load additional items until the page is +50 large after merging - //while chatItemsAvailable && filtered(im.reversedChatItems).count < loadItemsPerPage { - let oldCount = im.reversedChatItems.count - await apiLoadMessages( - cInfo.chatType, - cInfo.apiId, - pagination, - im.chatState, - searchText, - visibleItemIndexesNonReversed - ) - itemsCountChanged = im.reversedChatItems.count != oldCount - chatItemsAvailable = itemsCountChanged - //} - logger.debug("LALAL ITEMCOUNTCHANGED \(itemsCountChanged) \(oldCount) \(im.reversedChatItems.count)") - await MainActor.run { - if !itemsCountChanged { - firstPage = true - } - loadingItems = false - } + private func loadChatItems(_ cInfo: ChatInfo, _ pagination: ChatPagination, _ visibleItemIndexesNonReversed: @escaping () -> ClosedRange = { 0 ... 0 }) async -> Bool { + if loadingItems { return false } + loadingItems = true + var chatItemsAvailable = true + var itemsCountChanged = false + // Load additional items until the page is +50 large after merging + //while chatItemsAvailable && filtered(im.reversedChatItems).count < loadItemsPerPage { + let oldCount = im.reversedChatItems.count + await apiLoadMessages( + cInfo.chatType, + cInfo.apiId, + pagination, + im.chatState, + searchText, + visibleItemIndexesNonReversed + ) + itemsCountChanged = im.reversedChatItems.count != oldCount + chatItemsAvailable = itemsCountChanged + //} + logger.debug("LALAL ITEMCOUNTCHANGED \(itemsCountChanged) \(oldCount) \(im.reversedChatItems.count)") + await MainActor.run { + loadingItems = false } + return true } func stopAudioPlayer() { diff --git a/apps/ios/Shared/Views/Chat/ReverseList.swift b/apps/ios/Shared/Views/Chat/ReverseList.swift index 9669a68428..a79b28ba4b 100644 --- a/apps/ios/Shared/Views/Chat/ReverseList.swift +++ b/apps/ios/Shared/Views/Chat/ReverseList.swift @@ -18,6 +18,7 @@ struct ReverseList: UIViewControllerRepresentable { @Binding var unreadCount: Int @Binding var scrollState: ReverseListScrollModel.State + @Binding var allowLoadMoreItems: Bool @Binding var ignoreLoadingRequests: [Int64] /// Closure, that returns user interface for a given item @@ -25,7 +26,7 @@ struct ReverseList: UIViewControllerRepresentable { let content: (Int, MergedItem) -> Content // pagination, visibleItemIndexesNonReversed - let loadItems: (ChatPagination, @escaping () -> ClosedRange) -> Void + let loadItems: (ChatPagination, @escaping @MainActor () -> ClosedRange) async -> Bool func makeUIViewController(context: Context) -> Controller { Controller(representer: self) @@ -78,6 +79,7 @@ struct ReverseList: UIViewControllerRepresentable { var runBlockOnEndDecelerating: (() -> Void)? = nil private var scrollToRowOnAppear = 0 + private var translationToApply: CGPoint? = nil init(representer: ReverseList) { self.representer = representer @@ -112,12 +114,24 @@ struct ReverseList: UIViewControllerRepresentable { logger.debug("LALAL ITEM \(indexPath.item)") Task { // do not put it outside Task because it will be stackoverflow error - if let state = self.getListState(), prevFirstVisible != state.firstVisibleItemIndex { + let state = await MainActor.run { + self.getListState() + } + if let state, prevFirstVisible != state.firstVisibleItemIndex { prevFirstVisible = state.firstVisibleItemIndex logger.debug("LALAL LOADING before") - preloadItems(self.representer.mergedItems, state, self.representer.$ignoreLoadingRequests) { pagination in + await preloadItems(self.representer.mergedItems, self.representer.allowLoadMoreItems, state, self.representer.$ignoreLoadingRequests) { pagination in logger.debug("LALAL LOADING INSIDE") - self.representer.loadItems(pagination, { self.visibleItemIndexesNonReversed(Binding.constant(self.representer.mergedItems)) }) + let triedToLoad = await self.representer.loadItems(pagination, { self.visibleItemIndexesNonReversed(Binding.constant(self.representer.mergedItems)) }) + if triedToLoad { + let t = self.tableView.panGestureRecognizer.translation(in: self.tableView.superview!) + if t.y != 0 { + self.translationToApply = t + } + // stop scrolling + self.tableView.setContentOffset(self.tableView.contentOffset, animated: false) + } + return triedToLoad } } } @@ -240,7 +254,7 @@ struct ReverseList: UIViewControllerRepresentable { if #available(iOS 16.0, *) { animated = true } - if let index, tableView.numberOfRows(inSection: 0) != 0 { + if let index, tableView.numberOfRows(inSection: 0) > index { tableView.scrollToRow( at: IndexPath(row: index, section: 0), at: position, @@ -283,7 +297,7 @@ struct ReverseList: UIViewControllerRepresentable { })) itemsInCurrentSnapshot = new - if (wasCount == 101 && self.representer.mergedItems.items.count == 101) || snap.itemIdentifiers == prevSnapshot.itemIdentifiers { + if snap.itemIdentifiers == prevSnapshot.itemIdentifiers { logger.debug("LALAL SAME ITEMS, not rebuilding the tableview") return } @@ -350,6 +364,17 @@ struct ReverseList: UIViewControllerRepresentable { ) let state = getListState()! logger.debug("LALAL NOW FIRST VISIBLE \(state.firstVisibleItemIndex) \(state.firstVisibleItemOffset)") + + if let t = translationToApply { +// self.tableView.panGestureRecognizer.setTranslation(CGPointMake(t.x, t.y), in: self.tableView.superview!) + let o = self.tableView.contentOffset +// UIView.animate(withDuration: 0.5) { +// self.tableView.setContentOffset(CGPointMake(o.x + t.x, o.y + t.y), animated: false) +// self.tableView.layoutIfNeeded() +// } + self.tableView.setContentOffset(CGPointMake(o.x + t.x, o.y + t.y), animated: true) + translationToApply = nil + } //} } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index d9eee1097d..2e38087ab3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -1185,7 +1185,7 @@ struct NewUser: Encodable { public enum ChatPagination { public static let INITIAL_COUNT = 50 public static let PRELOAD_COUNT = 50 - public static let UNTIL_PRELOAD_COUNT = 50 + public static let UNTIL_PRELOAD_COUNT = 20 case last(count: Int) case after(chatItemId: Int64, count: Int)