diff --git a/Yattee/Services/HomeInstanceCache.swift b/Yattee/Services/HomeInstanceCache.swift index a07e6cfc..1dd753e9 100644 --- a/Yattee/Services/HomeInstanceCache.swift +++ b/Yattee/Services/HomeInstanceCache.swift @@ -69,8 +69,21 @@ final class HomeInstanceCache { return } diskCacheLoaded = true + let generation = accountGeneration if let cacheData = await HomeInstanceDiskCache.shared.load() { + // The context or the Apple account can move while this is + // suspended. The in-memory cache is empty by then, so the purge + // finds nothing to drop and this would quietly reinstate the + // previous login's private feeds. + guard generation == accountGeneration else { + LoggingService.shared.info( + "Discarding a disk cache that belongs to the previous login", + category: .general + ) + return + } + cache = cacheData.videos lastUpdated = cacheData.lastUpdated let totalVideos = cache.values.map { $0.count }.reduce(0, +) diff --git a/Yattee/Services/SubscriptionFeedCache.swift b/Yattee/Services/SubscriptionFeedCache.swift index 510ceff4..d6b1899a 100644 --- a/Yattee/Services/SubscriptionFeedCache.swift +++ b/Yattee/Services/SubscriptionFeedCache.swift @@ -615,6 +615,8 @@ final class SubscriptionFeedCache { instance: instance ) + guard isCurrentAccountsResult(generation) else { return } + let serverVideos = response.toVideos() LoggingService.shared.info( "refreshFromStatelessServer: Received \(serverVideos.count) videos from server, status: \(response.status), ready: \(response.isReady)", diff --git a/Yattee/Views/Instances/InstanceBrowseView.swift b/Yattee/Views/Instances/InstanceBrowseView.swift index f563a27a..b262c6f1 100644 --- a/Yattee/Views/Instances/InstanceBrowseView.swift +++ b/Yattee/Views/Instances/InstanceBrowseView.swift @@ -26,6 +26,9 @@ struct InstanceBrowseView: View { @State private var showFilterSheet = false // Feed state (for Invidious/Piped login) + /// Which login the account-scoped content belongs to, so a page still in + /// flight cannot be appended to the next one's feed. + @State private var accountContentGeneration = 0 @State private var feedVideos: [Video] = [] @State private var feedSubscriptions: [Channel] = [] @@ -1198,9 +1201,13 @@ struct InstanceBrowseView: View { /// /// Public browsing — popular and trending — is unauthenticated and stays. private func discardAccountScopedContent() { + accountContentGeneration += 1 feedVideos = [] feedSubscriptions = [] userPlaylists = [] + feedPage = 1 + hasMoreFeedResults = false + isLoadingMoreFeed = false } private func refreshLoginState() { @@ -1434,12 +1441,19 @@ struct InstanceBrowseView: View { feedLoadedVideoCount = feedVideos.count // Mark current count to prevent re-triggering feedPage += 1 + // The login can change while this page is in flight, and appending + // afterwards would mix one account's private videos into another's + // feed. + let generation = accountContentGeneration + Task { do { let api = InvidiousAPI(httpClient: appEnvironment.httpClient) let feedResponse = try await api.feed(instance: instance, sid: sid, page: feedPage) await MainActor.run { + guard generation == accountContentGeneration else { return } + feedVideos.append(contentsOf: feedResponse.videos) hasMoreFeedResults = feedResponse.hasMore isLoadingMoreFeed = false diff --git a/Yattee/Views/Settings/EditSourceView.swift b/Yattee/Views/Settings/EditSourceView.swift index 582011d2..e85b255a 100644 --- a/Yattee/Views/Settings/EditSourceView.swift +++ b/Yattee/Views/Settings/EditSourceView.swift @@ -411,10 +411,29 @@ private struct EditRemoteServerContent: View { return false } + // Re-resolved rather than trusted: signing in takes a round trip, and + // the account can be deleted on another device while it is happening. + // Writing the credential then would file a secret under an account that + // no longer exists, and report success for it. + if let existing = target.account, appEnvironment.accountsManager.account(id: existing.id) == nil { + appEnvironment.toastManager.showError( + String(localized: "accounts.signIn.storeFailed") + ) + return false + } + let account = target.account ?? appEnvironment.accountsManager.addAccount(instanceID: instance.id) - guard let credentials = appEnvironment.accountCredentialsManager(for: account) else { return false } + guard let credentials = appEnvironment.accountCredentialsManager(for: account) else { + // Its instance may have gone in the meantime, which is what makes + // the backend unresolvable. A provisional account created moments + // ago must not be left behind. + if target.account == nil { + appEnvironment.accountsManager.rollbackAccountCreation(account.id) + } + return false + } credentials.setCredential(credential, for: account)