From 9dd9fd9176015dcd09ae89d8a9d573262117baa1 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 18 Aug 2026 16:57:42 +0300 Subject: [PATCH] Prefetch the next room list page before reaching the bottom Growing the list only once the last row was already visible, on top of the half a second visible range throttle, meant hitting and bouncing off the end of the list while the next page loaded. Ask for the next page half a page early instead, so the growth happens while the user is still scrolling towards the boundary. The threshold is clamped so that it stays positive for accounts with fewer rooms than half a page, where it would otherwise always match and leave the list unable to shrink back to a single page. --- .../HomeScreen/View/HomeScreenContent.swift | 15 +++++++++-- .../RoomSummary/RoomSummaryProvider.swift | 27 ++++++++++++++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift index cada0a7e8..dce72e254 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift @@ -68,7 +68,10 @@ struct HomeScreenContent: View { scrollViewAdapter.scrollView = scrollView } .onReceive(scrollViewAdapter.didScroll) { _ in - updateVisibleRange() + // Live, undelayed updates while scrolling: pagination needs to see the + // range early enough to grow the list before the user reaches the bottom, + // and the scroll geometry is valid mid-scroll. + sendVisibleRange() } .onReceive(scrollViewAdapter.isScrolling) { _ in updateVisibleRange() @@ -133,13 +136,21 @@ struct HomeScreenContent: View { /// Often times the scroll view's content size isn't correct yet when this method is called e.g. when cancelling a search /// Dispatch it with a delay to allow the UI to update and the computations to be correct /// Once we move to iOS 17 we should remove all of this and use scroll anchors instead + /// Update: We're on iOS 26 now and the scroll achors still don't work properly. private func updateVisibleRange() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { delayedUpdateVisibleRange() } } private func delayedUpdateVisibleRange() { + guard scrollViewAdapter.isScrolling.value == false else { + // Scrolling reports live through didScroll + return + } + sendVisibleRange() + } + + private func sendVisibleRange() { guard let scrollView = scrollViewAdapter.scrollView, - scrollViewAdapter.isScrolling.value == false, // Ignore while scrolling context.searchQuery.isEmpty == true, // Ignore while filtering !context.viewState.visibleRooms.isEmpty else { return diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index 7960f7ef5..b7f79c504 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -19,6 +19,8 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { private let appSettings: AppSettings private let roomListPageSize: UInt32 + /// Remember how many rooms we had on the previous requests so we can deduplicate + private var roomCountOnLastPageAddRequest = -1 private let visibleItemRangePublisher = CurrentValueSubject, Never>(0..<0) @@ -182,6 +184,24 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { // MARK: - Private private func setupVisibleRangeObservers() { + // Unthrottled to add another page half way through the last one + visibleItemRangePublisher + .sink { [weak self] range in + guard let self, + !range.isEmpty, + range.upperBound >= rooms.count - Int(roomListPageSize) / 2, + rooms.count != roomCountOnLastPageAddRequest else { + return + } + + roomCountOnLastPageAddRequest = rooms.count + + MXLog.info("\(self.name): Adding a page at \(rooms.count) rooms, visible range: \(range)") + + listUpdatesSubscriptionResult?.controller().addOnePage() + } + .store(in: &cancellables) + visibleItemRangePublisher .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) .removeDuplicates() @@ -190,9 +210,8 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { MXLog.info("\(self.name): Updating visible range: \(range)") - if range.upperBound >= rooms.count { - listUpdatesSubscriptionResult?.controller().addOnePage() - } else if range.lowerBound == 0 { + if range.lowerBound == 0, range.upperBound < rooms.count { + roomCountOnLastPageAddRequest = -1 listUpdatesSubscriptionResult?.controller().resetToOnePage() } } @@ -215,6 +234,8 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { range = range.lowerBound..