Bind results to the account and identity they were requested for

Fetched account records are staged under the identity the fetch began
under, not whichever is current when they arrive — the engine's fetch
spans several delegate events and the Apple account can move between
them, which would have filed one identity's accounts under another's.

Subscription fetches are fenced against context switches, not only
against a provider being discarded. Providers are deliberately kept
across a switch — each is one account's cache — so an earlier account's
fetch could finish afterwards and put its channels on screen while
unsubscribes went to the account now selected.

The subscriptions view reloads when the *effective* context moves, which
a credential arriving, expiring or being rejected does without the
stored context changing at all.

Feed requests report what they learn about the account, like every other
authenticated call: a credential that expired during a feed-only refresh
kept looking signed in, and the fallback never engaged. The
classification behind that verdict now exists once, on AccountsManager,
rather than in each provider.
This commit is contained in:
Arkadiusz Fal
2026-08-28 19:58:27 +02:00
parent 81aa47196f
commit e1ebb71511
8 changed files with 127 additions and 76 deletions

View File

@@ -22,6 +22,9 @@ struct SubscriptionsView: View {
@State private var feedCache = SubscriptionFeedCache.shared
@State private var subscriptions: [Subscription] = []
@State private var subscriptionsLoaded = false
/// The effective context the visible subscriptions were loaded for, so a
/// status change that does not move it does not force a reload.
@State private var loadedContext: DataContext?
@State private var selectedChannelID: String? = nil
@State private var errorMessage: String?
@State private var watchEntriesMap: [String: WatchEntry] = [:]
@@ -507,15 +510,17 @@ struct SubscriptionsView: View {
// switching between them would change nothing here and this
// view would keep showing the first account's subscriptions
// while writes went to the second.
//
// Status changes count too, through the *effective* context:
// a credential arriving or being rejected moves reads
// between the account and local without the stored context
// moving at all.
.onReceive(NotificationCenter.default.publisher(for: .dataContextDidChange)) { _ in
feedCache.handleAccountChange()
subscriptions = []
subscriptionsLoaded = false
Task {
guard let appEnvironment else { return }
await loadSubscriptionsAsync()
await feedCache.refresh(using: appEnvironment)
}
reloadForContextChange()
}
.onReceive(NotificationCenter.default.publisher(for: .accountStatusesDidChange)) { _ in
guard appEnvironment?.dataContextValidator.effectiveContext != loadedContext else { return }
reloadForContextChange()
}
.task(id: instanceConfigurationID) {
LoggingService.shared.debug("SubscriptionsView task triggered, instanceConfigurationID: \(instanceConfigurationID)", category: .general)
@@ -1333,9 +1338,28 @@ struct SubscriptionsView: View {
/// Loads subscriptions asynchronously from the current provider.
/// For Invidious, this fetches from the API and creates temporary Subscription objects for UI.
/// Drops what is on screen and reloads it for whatever context is current.
private func reloadForContextChange() {
loadedContext = appEnvironment?.dataContextValidator.effectiveContext
feedCache.handleAccountChange()
subscriptions = []
subscriptionsLoaded = false
Task {
guard let appEnvironment else { return }
await loadSubscriptionsAsync()
await feedCache.refresh(using: appEnvironment)
}
}
private func loadSubscriptionsAsync() async {
guard let subscriptionService, let appEnvironment else { return }
// Recorded on every load, not only on a context change: the first load
// of a session has to establish what is on screen, or the first status
// change would look like a move and reload for nothing.
loadedContext = appEnvironment.dataContextValidator.effectiveContext
// For local account, just load from DataManager (fast)
if appEnvironment.accountsManager.currentContext.isLocal {
subscriptions = dataManager?.subscriptions() ?? []