Commit Graph

7 Commits

Author SHA1 Message Date
Arkadiusz Fal
0c6c7763db Let an untouched Save keep the name it was already showing 2026-09-09 23:59:57 +02:00
Arkadiusz Fal
2e1f411cc8 Let the label the account syncs be one the user can set 2026-09-09 23:52:54 +02:00
Arkadiusz Fal
fbeae586b0 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.
2026-09-04 23:56:39 +02:00
Arkadiusz Fal
5a77e93558 Quarantine the unresolved account namespace
The accounts.unresolved.* UserDefaults namespace was found on a real
device holding a stale, contradictory snapshot: a tombstoned account
listed live, context reset to local, every sync outbox still pending.
With iCloud Drive off the identity scope dips to unresolved on every
foreground activation, so that snapshot was loaded — and rewritten —
in an entirely ordinary configuration.

Close it off on four fronts:

- Gate the unprompted writers. reconcileLegacyAccounts() and
  migrateLegacyContextIfNeeded() now no-op while the identity scope is
  unresolved; reloadForCurrentIdentity() re-runs both once a scope
  resolves, so nothing is lost, and nothing synthesizes accounts or
  queues uploads under a namespace that answers to no one.

- Fence the reload gap. Between a scope change and this manager's
  observer reloading, namespace already names the new identity while
  memory still holds the old snapshot. loadedNamespace records what
  memory belongs to; save(), the stored-context flag, and every
  coordinator path that builds, enqueues, applies or confirms
  account-category records refuses when it mismatches. Staged
  downloads stay durable and re-apply after the reload.

- Tie confirmations and conflict retries to the identity that sent
  them. Every outbound account-category record carries an
  identityNamespace field; didSave honours only confirmations whose
  record carries the current identity's stamp, conflict resolutions
  inherit the local record's stamp and are dropped when foreign, and
  a skipped confirmation re-registers the durable outbox so nothing
  strands. The abandoning conflict branches re-register it too.

- Purge the stale blob. Once a scope resolves, the unresolved
  namespace keys and the unresolved credential-fingerprint store are
  removed; the activation-time validators no longer correct the
  context or rewrite the legacy selection from unresolved state.

Hardened through four adversarial review rounds; the last found
nothing.
2026-09-01 23:55:47 +02:00
Arkadiusz Fal
cc0fd3e6f4 Fence stale feed results and stop guessing at unknown account state
Signing in again keeps the account ID and the context, so a request
already in flight under the previous credential could land afterwards
and write that user's private feed back into the cache and onto disk.
Feed results now carry the account they were requested for and are
dropped if it has moved.

An account with no recorded status was reported as "sign in required".
While the identity scope is unresolved nothing can read the Keychain
under the right scope, so that is the absence of an answer, not one — on
a tokenless or first-seen Apple account it told users with working
logins to sign in again and dropped reads to local data. It is now a
state of its own.

Transport failures mark the account unreachable instead of leaving it
"signed in", which is what the documented local read fallback keys on.
Neither that nor a rejection is overwritten by the Keychain-based
refresh any more: only the server can clear what the server said.

Disabling sync deliberately does not rewrite Keychain items to local.
That means deleting them, and deleting a synchronizable item propagates
to every device — "stop syncing this login" would sign the user out
elsewhere, which is not what the toggle says. The reasoning is recorded
at the call site.
2026-08-26 23:55:50 +02:00
Arkadiusz Fal
d366982ad8 Close account lifecycle gaps found in review
A synced context change had no effect on the pre-revamp
`subscriptionAccount` setting several views still read, nor on the feed
cache, which is global rather than account-keyed — so a selection made
on another device left this one showing the previous account's
subscriptions and videos. Both now follow every context change,
whoever made it.

Instances replaced wholesale by iCloud bypassed `saveInstances()` and so
posted no change notification. A legacy authenticated instance arriving
that way never reached account migration: no account row, and no action
to adopt its saved login, until an unrelated edit or a relaunch.

Deleting an instance now deletes the accounts on it, while the instance
is still resolvable — afterwards the account's credential manager cannot
be determined from its type and the credential would be stranded. Only
an explicit deletion cascades; an instance vanishing from iCloud leaves
its accounts orphaned, because absence is not a deletion.

A context whose instance has been switched off is temporarily
unavailable rather than valid. The account stays signed in, so nothing
about the account said the server had been turned off, and requests kept
going to it while the switcher already hid it.
2026-08-25 23:57:43 +02:00
Arkadiusz Fal
66f721e3ac Add Account model, AccountsManager and multi-login UI
Yattee 2 could hold at most one login per instance: a Keychain item keyed by
the instance UUID and a Bool on the instance. This adds the account layer
the rest of the revamp builds on.

DataContext (.local or .account) replaces SubscriptionAccount as the thing
that selects a data set. The legacy value maps onto it as an identity
mapping, because migrated accounts deliberately reuse their instance UUID.
Contexts carry an app-managed changedAt, and a migrated one is stamped at
the epoch so any explicit choice on any device outranks it whatever order
they arrive in.

AccountCapabilities makes what a backend supports a single switch in
InstanceType.accountCapabilities, verified against each API surface -
Invidious has history and playlist privacy, Piped has neither. Call sites
ask for capabilities instead of switching on instance type, which is the
seam the planned per-user Yattee Server accounts land on.

AccountsManager owns accounts and the selected context, stored per identity
scope so a switch of Apple Account cannot show or upload the previous one's
list. Legacy single-logins become accounts through a repeatable
reconciliation rather than a one-shot at launch, because instances arrive
over iCloud whenever they arrive; a permanent marker set is what makes
re-running safe, since usesAccountLogin stays true on the instance forever
and would otherwise recreate a deliberately deleted account. A dirty-flag
outbox records what still needs uploading so an offline change survives a
crash. The legacy subscriptionAccount selection is migrated one way,
reading local defaults first and then the raw KVS value - the previous
release published that key to iCloud but never read it back, so on a fresh
install it exists only there.

DataContextValidator replaces SubscriptionAccountValidator, which corrected
any selection it could not immediately verify. That was safe when a context
was just "an instance I am logged into", but with credentials arriving
asynchronously it would discard the user's choice during a slow Keychain
sync. Validity is now tri-state: anything that might still resolve keeps
the selection and falls back to local for reads only, with server mutations
disabled rather than silently retargeted at local data; only an actual
deletion corrects the stored context.

EditSourceView's single log in/out pair becomes a list of accounts with
per-row status, sign-in-again and sign-out-and-remove, plus the explicit
"use saved login from this device" action - the one-tap adoption that
replaced automatic adoption after the Keychain provenance measurement. On
tvOS a credential-less account offers a local sign-in instead, since Apple
never syncs app Keychain items there and the state will not resolve on its
own.

InstanceLoginView now returns the identifier alongside the credential so
the caller can file it in the Keychain beside the secret, keeping login
identifiers out of synced metadata.
2026-08-24 23:52:01 +02:00