Tell the views when a credential is replaced

Signing in again as a different person on the same server changes
neither the account ID, the context, nor usually the status — so nothing
the views observe moved, and the previous user's subscriptions and feed
stayed on screen while actions went out under the new credential. There
is now a notification for exactly that, and the subscriptions view and
the instance browser discard and reload on it.

An account deleted after its instance disappeared retires its legacy
Keychain item too. Identifying that item needs the instance, but
checking it does not: the adoption marker is specific to service,
account, scope and the exact secret, so asking both managers retires it
if either owns it and does nothing otherwise.
This commit is contained in:
Arkadiusz Fal
2026-08-28 23:58:20 +02:00
parent 8ee0e38fbd
commit 9a93b0347e
4 changed files with 34 additions and 0 deletions

View File

@@ -892,5 +892,9 @@ extension Notification.Name {
static let accountsDidChange = Notification.Name("stream.yattee.accountsDidChange")
/// An account's usability changed — not the list of accounts.
static let accountStatusesDidChange = Notification.Name("stream.yattee.accountStatusesDidChange")
/// An account's stored credential was replaced. The account itself is
/// unchanged, so anything holding data fetched with the old one has to be
/// told: it may belong to an entirely different person on that server.
static let accountCredentialDidChange = Notification.Name("stream.yattee.accountCredentialDidChange")
static let dataContextDidChange = Notification.Name("stream.yattee.dataContextDidChange")
}

View File

@@ -441,6 +441,17 @@ final class AppEnvironment {
guard let account, let credentials = self.accountCredentialsManager(for: account) else {
self.invidiousCredentialsManager.deleteCredential(forAccountID: accountID)
self.pipedCredentialsManager.deleteCredential(forAccountID: accountID)
// The unscoped legacy item needs the instance to be identified,
// but not to be *checked*: the adoption marker is specific to
// service, account, scope and the exact secret, so asking both
// managers retires it if either owns it and does nothing
// otherwise. Worth asking — this account was explicitly deleted,
// and the instance may simply have gone first.
if let account {
self.invidiousCredentialsManager.deleteLegacyCredentialIfOwned(for: account)
self.pipedCredentialsManager.deleteLegacyCredentialIfOwned(for: account)
}
return
}
@@ -696,6 +707,12 @@ final class AppEnvironment {
if accountsManager.currentContext == .account(account.id) {
SubscriptionFeedCache.shared.handleAccountChange()
}
// Views hold their own copies of this account's data, and signing in
// again as somebody else changes neither the account ID, the context,
// nor usually the status — so nothing they observe would move. Told
// explicitly instead.
NotificationCenter.default.post(name: .accountCredentialDidChange, object: nil)
}
/// Drops everything cached for whichever account was being shown.

View File

@@ -455,6 +455,13 @@ struct InstanceBrowseView: View {
refreshLoginState()
Task { await startContentLoad() }
}
// Same reason, without the context moving: the credential behind this
// instance's account was replaced.
.onReceive(NotificationCenter.default.publisher(for: .accountCredentialDidChange)) { _ in
discardAccountScopedContent()
refreshLoginState()
Task { await startContentLoad() }
}
.onChange(of: selectedTab) { _, _ in
isLoading = true
errorMessage = nil

View File

@@ -527,6 +527,12 @@ struct SubscriptionsView: View {
.onReceive(NotificationCenter.default.publisher(for: .instancesDidChange)) { _ in
reloadIfEffectiveContextMoved()
}
// Signing in again keeps the same account and context, so
// nothing above would fire — but the data on screen may now
// belong to a different person on that server.
.onReceive(NotificationCenter.default.publisher(for: .accountCredentialDidChange)) { _ in
reloadForContextChange()
}
.task(id: instanceConfigurationID) {
LoggingService.shared.debug("SubscriptionsView task triggered, instanceConfigurationID: \(instanceConfigurationID)", category: .general)
await loadSubscriptionsAsync()