Keep one login's verdicts and content off another's

A feed failure recorded its verdict before checking whether anyone was
still waiting for it, and the verdict resolves the *currently* selected
account — so a 401 from a credential the app had already moved off could
mark its replacement, or a different account, as rejected indefinitely.

The instance browser drops its feed, subscriptions and playlists when
the selected login changes. Nothing else there would notice: the
instance, the tab and the "already has data" shortcut are all unchanged
between two accounts on the same server.

Caches are no longer discarded while the identity is unresolved. With
iCloud Drive off the scope drops to unresolved on every activation and
comes back the same, and the context reads as local in between — which
looked like a move and threw the caches away several times a day.

Local is always in the account picker again. Its feed needs a Yattee
Server and reports that itself; with correction no longer moving away
from local, leaving it out of the list made it unreachable once left.
And the subscriptions view reloads when an instance is switched off,
which moves reads to local without touching the context or any status.
This commit is contained in:
Arkadiusz Fal
2026-08-28 23:24:06 +02:00
parent 6a2b3250ce
commit 1c0a3e8e9a
7 changed files with 89 additions and 12 deletions

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>13</integer>
<key>logs</key>
<dict>
<key>D50ED115-45BF-4DD0-BB7E-A46C31531159</key>
<dict>
<key>className</key>
<string>IDECommandLineBuildLog</string>
<key>documentTypeString</key>
<string>&lt;nil&gt;</string>
<key>domainType</key>
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
<key>fileName</key>
<string>D50ED115-45BF-4DD0-BB7E-A46C31531159.xcactivitylog</string>
<key>hasPrimaryLog</key>
<true/>
<key>primaryObservable</key>
<dict>
<key>highLevelStatus</key>
<string>E</string>
<key>totalNumberOfAnalyzerIssues</key>
<integer>0</integer>
<key>totalNumberOfErrors</key>
<integer>2</integer>
<key>totalNumberOfTestFailures</key>
<integer>0</integer>
<key>totalNumberOfWarnings</key>
<integer>0</integer>
</dict>
<key>signature</key>
<string>Resolve Packages</string>
<key>timeStartedRecording</key>
<real>807236581.56917596</real>
<key>timeStoppedRecording</key>
<real>807236581.71271205</real>
<key>title</key>
<string>Resolve Packages</string>
<key>uniqueIdentifier</key>
<string>D50ED115-45BF-4DD0-BB7E-A46C31531159</string>
</dict>
</dict>
</dict>
</plist>

View File

@@ -629,8 +629,13 @@ final class AppEnvironment {
// logins on the same instance collapse onto the same one; and not on
// the stored context, because that does not move when a credential
// arrives or disappears and the app falls back to local and back again.
// Never while the identity is unresolved. With iCloud Drive off the
// scope drops to unresolved on every activation and comes back
// unchanged; the context reads as local in between, and treating that
// as a move would throw the caches away several times a day. "Cannot
// tell yet" is not grounds for anything destructive.
let effective = dataContextValidator.effectiveContext
if effective != lastCachedContext {
if effective != lastCachedContext, credentialIdentityScope.isResolved {
lastCachedContext = effective
clearAccountScopedCaches()
}

View File

@@ -433,8 +433,8 @@ final class SubscriptionFeedCache {
feedLoadState = .ready
} catch {
LoggingService.shared.error("Failed to fetch Piped feed: \(error.localizedDescription)", category: .general)
recordOutcome(error, backend: .piped, using: appEnvironment)
guard isCurrentAccountsResult(generation) else { return }
recordOutcome(error, backend: .piped, using: appEnvironment)
feedLoadState = .error(.networkError(error.localizedDescription))
}
@@ -508,8 +508,8 @@ final class SubscriptionFeedCache {
"Failed to fetch Invidious feed: \(error.localizedDescription)",
category: .general
)
recordOutcome(error, backend: .invidious, using: appEnvironment)
guard isCurrentAccountsResult(generation) else { return }
recordOutcome(error, backend: .invidious, using: appEnvironment)
feedLoadState = .error(.networkError(error.localizedDescription))
}

View File

@@ -74,13 +74,11 @@ final class SubscriptionAccountValidator {
var availableAccounts: [SubscriptionAccount] {
var accounts: [SubscriptionAccount] = []
// Add local if Yattee Server is configured and enabled
let hasYatteeServer = instancesManager.instances.contains {
$0.type == .yatteeServer && $0.isEnabled
}
if hasYatteeServer {
accounts.append(.local)
}
// Local is always offered. Its *feed* needs a Yattee Server and says so
// itself when there is none; that is not a reason to make the user's
// own data unselectable — and since correction no longer moves away
// from local, leaving it out would make it unreachable once left.
accounts.append(.local)
// Add logged-in and enabled Invidious instances
for instance in instancesManager.instances {

View File

@@ -447,6 +447,14 @@ struct InstanceBrowseView: View {
.onReceive(NotificationCenter.default.publisher(for: .accountsDidChange)) { _ in
refreshLoginState()
}
// Selecting the other login on this instance changes whose feed and
// playlists these are. Nothing else here would notice: the instance,
// the tab and the "already has data" shortcut are all unchanged.
.onReceive(NotificationCenter.default.publisher(for: .dataContextDidChange)) { _ in
discardAccountScopedContent()
refreshLoginState()
Task { await startContentLoad() }
}
.onChange(of: selectedTab) { _, _ in
isLoading = true
errorMessage = nil
@@ -1179,6 +1187,15 @@ struct InstanceBrowseView: View {
// MARK: - Data Loading
/// Drops what belonged to the previously selected login.
///
/// Public browsing — popular and trending — is unauthenticated and stays.
private func discardAccountScopedContent() {
feedVideos = []
feedSubscriptions = []
userPlaylists = []
}
private func refreshLoginState() {
guard instance.supportsAuthentication else { return }
isLoggedIn = appEnvironment?.isSignedIn(to: instance) ?? false

View File

@@ -518,9 +518,14 @@ struct SubscriptionsView: View {
.onReceive(NotificationCenter.default.publisher(for: .dataContextDidChange)) { _ in
reloadForContextChange()
}
// Instances count for the same reason: switching one off
// moves reads to local without touching the stored context
// or any account status.
.onReceive(NotificationCenter.default.publisher(for: .accountStatusesDidChange)) { _ in
guard appEnvironment?.dataContextValidator.effectiveContext != loadedContext else { return }
reloadForContextChange()
reloadIfEffectiveContextMoved()
}
.onReceive(NotificationCenter.default.publisher(for: .instancesDidChange)) { _ in
reloadIfEffectiveContextMoved()
}
.task(id: instanceConfigurationID) {
LoggingService.shared.debug("SubscriptionsView task triggered, instanceConfigurationID: \(instanceConfigurationID)", category: .general)
@@ -1338,6 +1343,11 @@ struct SubscriptionsView: View {
/// Loads subscriptions asynchronously from the current provider.
/// For Invidious, this fetches from the API and creates temporary Subscription objects for UI.
private func reloadIfEffectiveContextMoved() {
guard appEnvironment?.dataContextValidator.effectiveContext != loadedContext else { return }
reloadForContextChange()
}
/// Drops what is on screen and reloads it for whatever context is current.
private func reloadForContextChange() {
loadedContext = appEnvironment?.dataContextValidator.effectiveContext