Make synced accounts usable the moment they arrive

A fresh device never adopted the synced context. The local value is a
default stamped when the manager is built, so it is always newer than a
choice made on another device, and ordering against it discarded that
choice. With no local decision to protect, the first synced context is
simply adopted.

An account arriving mid-session sat at "sign in required" until the app
was relaunched: nothing recomputed its status once the metadata — or its
instance — turned up, even when iCloud Keychain had already delivered
the credential. Both arrivals now recompute statuses and re-run the
legacy mirror, which was similarly stuck whenever the instance had not
arrived yet.

Signing in again keeps the same account and the same context, so nothing
downstream saw a change — but the credential may now belong to somebody
else, and their subscriptions and feed stayed on screen. The provider
and the account's caches are discarded on every credential replacement.

The deletion repair also cancels a context upload the deleted choice had
queued; sending it would have published the fallback under that choice's
timestamp, which not stamping it "now" exists to prevent.

Piped subscribe/unsubscribe and Invidious unsubscribe now report their
outcome like every other authenticated call, so a credential rejected by
one of them stops claiming to be signed in. Removing the last account
updates the source screen, which kept offering authenticated imports
that could only fail.
This commit is contained in:
Arkadiusz Fal
2026-08-25 23:59:38 +02:00
parent 7c966b18a9
commit ff78cc0121
6 changed files with 97 additions and 14 deletions

View File

@@ -110,8 +110,12 @@ final class InvidiousSubscriptionProvider: SubscriptionProvider {
func unsubscribe(from channelID: String) async throws {
let (instance, sid) = try await getAuthenticatedInstance()
// Unsubscribe on Invidious server
try await invidiousAPI.unsubscribe(from: channelID, instance: instance, sid: sid)
// Unsubscribe on Invidious server. Tracked like every other
// authenticated call: a rejected credential noticed here must mark the
// account, or it keeps reporting itself as signed in.
try await tracking {
try await invidiousAPI.unsubscribe(from: channelID, instance: instance, sid: sid)
}
// Update in-memory cache only
cachedChannelIDs.remove(channelID)

View File

@@ -93,8 +93,12 @@ final class PipedSubscriptionProvider: SubscriptionProvider {
func subscribe(to channel: Channel) async throws {
let (instance, authToken) = try await getAuthenticatedInstance()
// Subscribe on Piped server
try await pipedAPI.subscribe(channelID: channel.id.channelID, instance: instance, authToken: authToken)
// Subscribe on Piped server. Tracked like every other authenticated
// call: a rejected credential noticed here must mark the account, or it
// keeps reporting itself as signed in.
try await tracking {
try await pipedAPI.subscribe(channelID: channel.id.channelID, instance: instance, authToken: authToken)
}
// Update in-memory cache only
cachedChannelIDs.insert(channel.id.channelID)
@@ -109,7 +113,9 @@ final class PipedSubscriptionProvider: SubscriptionProvider {
let (instance, authToken) = try await getAuthenticatedInstance()
// Unsubscribe on Piped server
try await pipedAPI.unsubscribe(channelID: channelID, instance: instance, authToken: authToken)
try await tracking {
try await pipedAPI.unsubscribe(channelID: channelID, instance: instance, authToken: authToken)
}
// Update in-memory cache only
cachedChannelIDs.remove(channelID)