Commit Graph

5 Commits

Author SHA1 Message Date
Arkadiusz Fal
40d6f4e71f Let a status flap fence the feed instead of throwing it away
MAC3-R1 from the fix verification: refreshCachesForEffectiveContext purged
on every effective-context move, so the cache the Subscriptions fix meant
to keep was already empty when its keep-path ran. A move under an unchanged
selection now fences the feed cache non-destructively; only a genuine
selection move still purges. Also settles the destination loader's
observer callback onto the main actor without a capture-of-var warning.
2026-09-11 21:14:53 +02:00
Arkadiusz Fal
b0b95195b6 Let Subscribe On know its memberships and refuse a doomed destination
MAC5: membership lives in an observable loader started from ChannelView's
task, outside the NSMenu content SwiftUI never runs lifecycle for; rows are
Toggles so the mark renders on macOS, and writes toast Subscribed or
Unsubscribed with the destination name.
MAC13: a destination whose context fails validation is disabled, the same
gate the plain Subscribe button uses.
2026-09-11 08:20:24 +02:00
Arkadiusz Fal
f44c1d91c3 Refuse to repair a context that is not this identity's
Foreground activation repairs a selection whose account this device
knows was deleted. The check for whether that is safe was the identity
scope being resolved, which it already is in the window between an Apple
account change moving the namespace and the reload refilling memory —
so the repair weighed the previous identity's selection against the
previous identity's tombstones and announced the result to a user who
had not been shown an account yet. Fenced on the loaded namespace, the
same way saves are.

Three smaller things around the same theme:

- The destination rows forgot their membership answers on a credential
  or account change but not on an instance change, which adds a row to
  the picker while it is open. Unknown reads as unsubscribed, so that
  row offered to subscribe a channel the account already has.

- The subscriptions settings count applied whatever the fetch returned.
  A selection change is fenced by the service's provider generation, but
  the fallback to local is driven by availability and announces nothing,
  so the context is captured before the fetch and rechecked after it.

- Deleting a source from the browser runs the same cascade as deleting
  it from Settings, and was the one dialog left that did not say the
  logins go with it.
2026-09-04 23:59:27 +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
0c5745e4fb Let a subscribe name the account it lands on
Subscribing went wherever the selected account happened to be, and the
button offering it read the local store first — so under a server account
it claimed a channel was subscribed when only the device knew about it.
The button now asks the active provider and nothing else, except under a
local context, where the local store is that provider and can answer
without a hop.

The channel menu gains a destination list: local, plus every account
whose backend does subscriptions, whose instance is switched on, and
which has a credential on this device. A row names its own target, so it
deliberately does not care whether the selected context can be written
to — that is precisely the case where naming somewhere else is the only
way to subscribe at all. The main button is disabled instead, with the
reason beside it. tvOS reaches the same list through its own button and a
full-screen picker, because a menu cannot be driven with a remote.

Membership is asked for only once the rows are on screen: an unwarmed
provider answers by fetching that account's entire subscription list, and
asking every destination up front is a burst nobody requested. Answers
carry the round they were asked under, and "not asked yet" is kept apart
from "asked, not subscribed" — a checkmark the user acts on must not be a
guess.

SubscriptionService grows the destination-parameterized subscribe,
unsubscribe and membership calls the menu writes through, with the
context-implicit ones left as wrappers over the current context. A
membership answer that outlived its login now says nothing rather than
"no". Building a provider moves behind SubscriptionProviderFactory, which
is what lets a test see which destination a write was routed to without a
server behind it.

The unsubscribe-only screens are untouched: they act on the account being
browsed, which is the account whose list they are showing.
2026-09-04 23:18:04 +02:00