fix(nav,plex,playlists): extend account-pin re-hydrate to every affected provider
The fresh-tab restore bug wasn't Channels-specific: any provider that mounts above <App> and reads per-account state at useState-init hit it, since <App> pins the tab's account only after /api/me. Audited all of them — FeedFilters/FeedView/Prefs/App already self-correct via their own me?.id server-prefs adopt effects, so the still-broken ones were the pure-localStorage providers with no server adopt: NavigationProvider (landing page), PlexProvider (scope/show/sort/filters/q), PlaylistsProvider (selected playlist). Each now adopts the shared useAccountRehydrate hook (markDirty in the setters; page uses replaceState so the restore doesn't add a phantom Back step). Also baked re-hydrate into useAccountPersistedState/Object so their consumers (persisted tabs, player prefs) are robust even if one ever mounts before the pin. Self-E2E'd on a genuinely fresh tab: it lands on Plex with Movies/In-progress/Year/search all restored on the FIRST load.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useState, useSyncExternalStore } from "react";
|
||||
import { useAccountRehydrate } from "./useAccountRehydrate";
|
||||
|
||||
// Central registry of every localStorage key the app uses. One place to see them all, rename
|
||||
// safely, and avoid collisions — instead of `"siftlode.x"` string literals scattered across
|
||||
@@ -180,14 +181,17 @@ export function writeJSON(key: string, value: unknown): void {
|
||||
/** A `useState` whose string value is mirrored to localStorage, scoped to the current account (see
|
||||
* accountKey), so one account's persisted tab/view position doesn't carry over to another in the
|
||||
* same browser. Survives reload (F5); validation is deferred to the caller (clamp at render) so it
|
||||
* can be used before an async-loaded option list is known, keeping hook order stable. These hooks
|
||||
* run in components that only mount once signed in, so the account is known. */
|
||||
* can be used before an async-loaded option list is known, keeping hook order stable. Consumers
|
||||
* usually mount only once signed in (account known), but re-read on the account-pin transition too so
|
||||
* one that mounts before the pin (a fresh tab's first paint) still restores. */
|
||||
export function useAccountPersistedState(
|
||||
base: string,
|
||||
fallback = "",
|
||||
): [string, (value: string) => void] {
|
||||
const [value, setValue] = useState<string>(() => getAccountRaw(base) ?? fallback);
|
||||
const markDirty = useAccountRehydrate(() => setValue(getAccountRaw(base) ?? fallback));
|
||||
const set = (next: string) => {
|
||||
markDirty();
|
||||
setValue(next);
|
||||
setAccountRaw(base, next);
|
||||
};
|
||||
@@ -202,15 +206,17 @@ export function useAccountPersistedObject<T extends object>(
|
||||
defaults: T,
|
||||
): [T, (patch: Partial<T>) => void] {
|
||||
const [value, setValue] = useState<T>(() => readAccountMerged(base, defaults));
|
||||
const markDirty = useAccountRehydrate(() => setValue(readAccountMerged(base, defaults)));
|
||||
const patch = useCallback(
|
||||
(p: Partial<T>) => {
|
||||
markDirty();
|
||||
setValue((prev) => {
|
||||
const next = { ...prev, ...p };
|
||||
writeAccount(base, next);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[base],
|
||||
[base, markDirty],
|
||||
);
|
||||
return [value, patch];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user