From 361f19b7627efded3848ef64cd69b2fbd8e04543 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 29 Aug 2026 20:21:08 +0200 Subject: [PATCH] Fence the instance browser's first load, not only its next page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pagination carried the login it was requested for, but the load that fills the tab in the first place did not. Its subscriptions, feed and playlist requests all resolve well after the credential behind them can have been replaced, and cancelling the previous task does not settle it: a request that already came back races the cancellation and installs the previous person's private videos over the purge that was meant to remove them. Popular and trending stay unfenced — they are unauthenticated, and discarding them on a change of login would cost a fetch for nothing. Discarding the account-scoped content now also forgets how far the feed had been read. The high-water mark belongs to the feed that was thrown away; left behind, it gates pagination against a page the next login never loaded, and that login cannot ask for a second page until its first exceeds a count it knows nothing about. --- .../Views/Instances/InstanceBrowseView.swift | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Yattee/Views/Instances/InstanceBrowseView.swift b/Yattee/Views/Instances/InstanceBrowseView.swift index b262c6f1..e326b5b4 100644 --- a/Yattee/Views/Instances/InstanceBrowseView.swift +++ b/Yattee/Views/Instances/InstanceBrowseView.swift @@ -1208,6 +1208,11 @@ struct InstanceBrowseView: View { feedPage = 1 hasMoreFeedResults = false isLoadingMoreFeed = false + // The high-water mark belongs to the discarded feed. Left behind, it + // gates pagination against the previous login's count: the next one + // would have to exceed a page it never loaded before it could ask for + // a second. + feedLoadedVideoCount = 0 } private func refreshLoginState() { @@ -1253,6 +1258,13 @@ struct InstanceBrowseView: View { isLoading = !hasData // Only show loading spinner when no existing data errorMessage = nil + // Which login the account-scoped tabs are being loaded for. Cancelling + // the previous task is not enough on its own: a request that already + // came back races the cancellation, and its results would be installed + // after the purge that was meant to remove them. Popular and trending + // are unauthenticated and deliberately not fenced. + let generation = accountContentGeneration + do { switch selectedTab { case .popular: @@ -1280,6 +1292,7 @@ struct InstanceBrowseView: View { // Load subscriptions and feed based on instance type let subscriptionChannels: [Channel] let videos: [Video] + let hasMore: Bool switch instance.type { case .invidious: @@ -1302,7 +1315,7 @@ struct InstanceBrowseView: View { subscriptionChannels = enrichedSubscriptions.map { $0.toChannel(baseURL: instance.url) } videos = feedResponse.videos - hasMoreFeedResults = feedResponse.hasMore + hasMore = feedResponse.hasMore case .piped: let api = PipedAPI(httpClient: appEnvironment.httpClient) @@ -1316,7 +1329,7 @@ struct InstanceBrowseView: View { subscriptionChannels = pipedSubscriptions.map { $0.toChannel() } videos = feedVideos // Piped doesn't support pagination for feed - hasMoreFeedResults = false + hasMore = false default: errorMessage = String(localized: "feed.error.notSupported") @@ -1324,6 +1337,9 @@ struct InstanceBrowseView: View { return } + guard generation == accountContentGeneration else { return } + + hasMoreFeedResults = hasMore feedSubscriptions = subscriptionChannels.map { $0.enrichedThumbnail(using: appEnvironment.dataManager) } feedVideos = videos prefetchBranding(for: videos) @@ -1343,6 +1359,9 @@ struct InstanceBrowseView: View { let api = InvidiousAPI(httpClient: appEnvironment.httpClient) let playlists = try await api.userPlaylists(instance: instance, sid: credential) + + guard generation == accountContentGeneration else { return } + userPlaylists = playlists } } catch is CancellationError {