Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added an "aggressive paging" mode to PagedNoteDataSource #1206

Merged
merged 8 commits into from
Jun 4, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Fixed a bug where some profiles wouldn't load old notes.

## [0.1.16] - 2024-05-31Z

- Added feedback to the copy button in Settings.
Expand Down
56 changes: 53 additions & 3 deletions Nos/Controller/PagedNoteDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class PagedNoteDataSource<Header: View, EmptyPlaceholder: View>: NSObject, UICol
var limitedFilter = relayFilter
limitedFilter.limit = pageSize
self.pager = await relayService.subscribeToPagedEvents(matching: limitedFilter)
loadMoreIfNeeded(for: IndexPath(row: 0, section: 0))
}
}

Expand All @@ -82,6 +83,7 @@ class PagedNoteDataSource<Header: View, EmptyPlaceholder: View>: NSObject, UICol
)
self.fetchedResultsController.delegate = self
try? self.fetchedResultsController.performFetch()
loadMoreIfNeeded(for: IndexPath(row: 0, section: 0))
}

// MARK: - UICollectionViewDataSource
Expand Down Expand Up @@ -174,13 +176,61 @@ class PagedNoteDataSource<Header: View, EmptyPlaceholder: View>: NSObject, UICol
/// Instructs the pager to load more data if we are getting close to the end of the object in the list.
/// - Parameter indexPath: the indexPath last loaded by the collection view.
func loadMoreIfNeeded(for indexPath: IndexPath) {
largestLoadedRowIndex = max(largestLoadedRowIndex, indexPath.row)
let lastPageStartIndex = (fetchedResultsController.fetchedObjects?.count ?? 0) - pageSize
if indexPath.row > lastPageStartIndex {
// we are at the end of the list, load aggressively
pager?.loadMore()
// we are on the last page, load aggressively
startAggressivePaging()
return
} else if indexPath.row.isMultiple(of: pageSize / 2) {
pager?.loadMore()
}
}
}

/// A timer used for aggressive paging when we reach the end of the data. See `startAggressivePaging()`.
private var aggressivePagingTimer: Timer?

/// The largest row index seen by `loadMoreIfNeeded(for:)`
private var largestLoadedRowIndex: Int = 0

/// This function puts the data source into "aggressive paging" mode, which basically changes the paging
/// code from executing when the user scrolls (more efficient) to executing on a repeating timer. This timer will
/// automatically call `stopAggressivePaging` when it has loaded enough data.
///
/// We need to use this mode when we have an empty or nearly empty list of notes, or when the user reaches the end
/// of the results before more have loaded. We can't just wait on the existing paging requests to return (like a
/// normal REST paging API) because often we can't request exactly the notes we want from relays. For instance when
/// we are fetching root notes only on the profile screen we can only ask relays for all kind 1 notes. This means
/// we could get a page full of reply notes from the relays, none of which will match our NSFetchRequest and show
/// up in the UICollectionViewDataSource - meaning `cellForRowAtIndexPath` won't be called which means
/// `loadMoreIfNeeded(for:)` won't be called which means we'll never ask for the next page. So we need the timer.
private func startAggressivePaging() {
if aggressivePagingTimer == nil {
aggressivePagingTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] timer in
guard let self else {
timer.invalidate()
return
}

let lastPageStartIndex = (self.fetchedResultsController.fetchedObjects?.count ?? 0) - self.pageSize

if self.largestLoadedRowIndex > lastPageStartIndex {
// we are still on the last page of results, keep loading
self.pager?.loadMore()
} else {
// we've loaded enough, go back to normal paging
self.stopAggressivePaging()
}
}
}
}

/// Takes this data source out of "aggressive paging" mode. See `startAggressivePaging()`.
private func stopAggressivePaging() {
if let aggressivePagingTimer {
aggressivePagingTimer.invalidate()
self.aggressivePagingTimer = nil
}
}

// MARK: - NSFetchedResultsControllerDelegate
Expand Down
2 changes: 1 addition & 1 deletion Nos/Service/Relay/PagedRelaySubscription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class PagedRelaySubscription {
}

newUntilDates[subscription.relayAddress] = newDate
await subscriptionManager.decrementSubscriptionCount(for: subscriptionID)
relayService.decrementSubscriptionCount(for: subscriptionID)
}
}

Expand Down
6 changes: 6 additions & 0 deletions Nos/Service/Relay/RelaySubscriptionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ actor RelaySubscriptionManagerActor: RelaySubscriptionManager {
removeSubscription(with: subscriptionID)
}

/// Lets the manager know that there is one less subscriber for the given subscription. If there are no
/// more subscribers this function returns `true`.
///
/// Note that this does not send a close message on the websocket or close the socket. Right now those actions
/// are performed by the RelayService. It's yucky though. Maybe we should make the RelaySubscriptionManager
/// do that in the future.
@discardableResult
func decrementSubscriptionCount(for subscriptionID: RelaySubscription.ID) async -> Bool {
if var subscription = subscription(from: subscriptionID) {
Expand Down
Loading