Only a deletion the app can point at moves the selection

`correctIfContextRemoved` read a locally missing account as a deleted one
and reset the context with `setContext(.local)` — a new timestamp and a
pending upload. Now that foreground activation calls it, it ran before
the fetch that delivers the account record: a perfectly good remote
choice was reset and then published over everybody else's. It now asks
`AccountsManager` for a repair keyed on the deletion tombstone, which is
the same repair a deletion performs — a signed-in login on the same
server when there is one, the replaced choice's timestamp, nothing
uploaded. The toast names where the selection actually landed.

Adopting a synced context that named a deleted account repaired it but
kept whatever upload the previous choice still owed, so the sync engine
published the automatic fallback under that choice's timestamp — exactly
what not stamping it "now" exists to prevent. The adoption clears the
flag, and only on adoption: a local choice that outranks the repair keeps
its own upload.

The channel button's subscription lookup is fenced to the channel and the
round it was asked in, so an answer that lands after the user has walked
on — or after the account behind it changed — no longer writes itself
into the button on screen. Context and credential changes ask again. A
destination toggle is compared against the effective context rather than
the raw selection: the menu offers Local precisely when the selected
account cannot be written to, and that toggle has to be the one the
button then shows.

The destination rows forget their checkmarks when the logins change. The
answers describe credentials that have been replaced, and the tvOS picker
deliberately stays open across exactly that.

The subscriptions settings screen counts and exports what it is actually
reading from. The fetch behind both already followed the effective
context while the branch and the footer followed the selection, so an
unavailable account meant local data described as the server's.

The legacy `subscriptionAccount` migration refused to adopt a context
whose account had not synced yet and was never retried, leaving a fresh
install on local until the next launch. A synced account arriving now
retries it; the migration's own guards keep it a one-way move that never
overrides a choice made in the meantime.

The remaining negative-able Piped counts — likes, channel subscribers and
videos, playlist video counts — go through `PipedSentinel.count` like the
views and upload dates already did.
This commit is contained in:
Arkadiusz Fal
2026-09-04 23:56:39 +02:00
parent 69179c9fe9
commit fbeae586b0
8 changed files with 350 additions and 48 deletions

View File

@@ -61,6 +61,16 @@ struct SubscriptionsSettingsView: View {
appEnvironment?.accountsManager.currentContext ?? .local
}
/// What the counts and the export are actually read from.
///
/// While the selected account is unavailable that is the local store — so a
/// count or a file described as the account's would name a source it never
/// came from. The selection itself is untouched, and the moment the account
/// is usable again this follows it back.
private var effectiveContext: DataContext {
appEnvironment?.dataContextValidator.effectiveContext ?? currentContext
}
var body: some View {
List {
accountSection
@@ -68,7 +78,10 @@ struct SubscriptionsSettingsView: View {
exportSection
localDataSection
}
.task(id: currentContext) {
// Keyed to the effective context, not the selection: an account going
// unavailable — or coming back — changes which store the count is read
// from without the selection moving at all.
.task(id: effectiveContext) {
await refreshAccountSubscriptionCount()
}
.navigationTitle(String(localized: "settings.subscriptions.title"))
@@ -282,11 +295,15 @@ struct SubscriptionsSettingsView: View {
}
}
/// Exports subscriptions of the active account: the local store in local
/// mode, or the server's list for Invidious/Piped accounts.
/// Exports subscriptions of whatever is being read from: the local store in
/// local mode, or the server's list for Invidious/Piped accounts.
///
/// The branch follows the effective context because the fetch below does —
/// branching on the selection instead would take the server path only to
/// export local data through it.
private func performExport() async {
let subscriptions: [Subscription]
if currentContext.isLocal {
if effectiveContext.isLocal {
guard let dataManager else { return }
subscriptions = dataManager.allSubscriptions
} else {
@@ -342,7 +359,7 @@ struct SubscriptionsSettingsView: View {
if let cached = subscriptionService.cachedSubscriptionCount {
accountSubscriptionCount = cached
}
if !subscriptionService.currentContext.isLocal {
if !effectiveContext.isLocal {
if let channels = try? await subscriptionService.fetchSubscriptions() {
accountSubscriptionCount = channels.count
}