diff --git a/CHANGELOG.md b/CHANGELOG.md index 6479e8af7..b112a4d50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ 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. - Fixed an issue where NIP-05 could appear as invalid. ## [0.1.16] - 2024-05-31Z diff --git a/Nos/Controller/PagedNoteDataSource.swift b/Nos/Controller/PagedNoteDataSource.swift index 61afa5a93..8390fdc2e 100644 --- a/Nos/Controller/PagedNoteDataSource.swift +++ b/Nos/Controller/PagedNoteDataSource.swift @@ -70,6 +70,7 @@ class PagedNoteDataSource: NSObject, UICol var limitedFilter = relayFilter limitedFilter.limit = pageSize self.pager = await relayService.subscribeToPagedEvents(matching: limitedFilter) + loadMoreIfNeeded(for: IndexPath(row: 0, section: 0)) } } @@ -82,6 +83,7 @@ class PagedNoteDataSource: NSObject, UICol ) self.fetchedResultsController.delegate = self try? self.fetchedResultsController.performFetch() + loadMoreIfNeeded(for: IndexPath(row: 0, section: 0)) } // MARK: - UICollectionViewDataSource @@ -174,13 +176,61 @@ class PagedNoteDataSource: 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 diff --git a/Nos/Service/Relay/PagedRelaySubscription.swift b/Nos/Service/Relay/PagedRelaySubscription.swift index 9f44cb34b..76a533aae 100644 --- a/Nos/Service/Relay/PagedRelaySubscription.swift +++ b/Nos/Service/Relay/PagedRelaySubscription.swift @@ -73,7 +73,7 @@ class PagedRelaySubscription { } newUntilDates[subscription.relayAddress] = newDate - await subscriptionManager.decrementSubscriptionCount(for: subscriptionID) + relayService.decrementSubscriptionCount(for: subscriptionID) subscriptionsToRemove.insert(subscription.id) } } diff --git a/Nos/Service/Relay/RelaySubscriptionManager.swift b/Nos/Service/Relay/RelaySubscriptionManager.swift index b8488bbda..5bf215c71 100644 --- a/Nos/Service/Relay/RelaySubscriptionManager.swift +++ b/Nos/Service/Relay/RelaySubscriptionManager.swift @@ -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) {