From 9a93b0347e924e17791dfc10b5286ca9649e1cbf Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 28 Aug 2026 23:58:20 +0200 Subject: [PATCH] Tell the views when a credential is replaced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Yattee/Core/AccountsManager.swift | 4 ++++ Yattee/Core/AppEnvironment.swift | 17 +++++++++++++++++ Yattee/Views/Instances/InstanceBrowseView.swift | 7 +++++++ .../Views/Subscriptions/SubscriptionsView.swift | 6 ++++++ 4 files changed, 34 insertions(+) diff --git a/Yattee/Core/AccountsManager.swift b/Yattee/Core/AccountsManager.swift index b9481b12..972a8d86 100644 --- a/Yattee/Core/AccountsManager.swift +++ b/Yattee/Core/AccountsManager.swift @@ -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") } diff --git a/Yattee/Core/AppEnvironment.swift b/Yattee/Core/AppEnvironment.swift index 494b477c..41dfb511 100644 --- a/Yattee/Core/AppEnvironment.swift +++ b/Yattee/Core/AppEnvironment.swift @@ -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. diff --git a/Yattee/Views/Instances/InstanceBrowseView.swift b/Yattee/Views/Instances/InstanceBrowseView.swift index 1349c86d..f563a27a 100644 --- a/Yattee/Views/Instances/InstanceBrowseView.swift +++ b/Yattee/Views/Instances/InstanceBrowseView.swift @@ -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 diff --git a/Yattee/Views/Subscriptions/SubscriptionsView.swift b/Yattee/Views/Subscriptions/SubscriptionsView.swift index 194b0a25..d2eb14c0 100644 --- a/Yattee/Views/Subscriptions/SubscriptionsView.swift +++ b/Yattee/Views/Subscriptions/SubscriptionsView.swift @@ -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()