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..