fix(channels): re-hydrate per-account manager state once the account is pinned
The provider tree mounts above <App>, which pins this tab's account only after /api/me resolves — so on a genuinely fresh tab (empty sessionStorage) the manager's sort, filters, view and layout initialised from an unknown account and stayed at defaults until a second reload. Make the active account reactive (a subscribable store + useActiveAccountId via useSyncExternalStore; setActiveAccount/clearActiveAccount notify) and re-read the stored value when the account transitions null→id, via a shared useAccountRehydrate hook. A markDirty guard (called by each owning setter) prevents clobbering a value the user changed in the brief pre-pin window; the effect is keyed on the account id only, so the user's own writes can't loop it. useSortState, useFilterState and ChannelsProvider (view/layout) all adopt it. Affects sort identically — this was a long-standing caveat, not new to R13.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useState, useSyncExternalStore } from "react";
|
||||
|
||||
// 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
|
||||
@@ -83,6 +83,25 @@ export function activeAccountId(): number | null {
|
||||
}
|
||||
}
|
||||
|
||||
// Reactive active-account. setActiveAccount/clearActiveAccount write sessionStorage — a silent op that
|
||||
// triggers no React render — so this tab's per-account state (sort/filter/view/layout, all keyed by the
|
||||
// account) would never re-read once the account is finally known. That bites exactly once, on first
|
||||
// load: the provider tree above <App> mounts with NO account, then <App> pins it after /api/me resolves.
|
||||
// The account setters call notifyActiveAccountChange() so useActiveAccountId subscribers can re-hydrate.
|
||||
const accountListeners = new Set<() => void>();
|
||||
function subscribeActiveAccount(cb: () => void): () => void {
|
||||
accountListeners.add(cb);
|
||||
return () => accountListeners.delete(cb);
|
||||
}
|
||||
export function notifyActiveAccountChange(): void {
|
||||
for (const cb of [...accountListeners]) cb();
|
||||
}
|
||||
/** The current tab's active account id, as reactive state — re-renders when it is (re)pinned. The
|
||||
* snapshot is a primitive (number | null), so useSyncExternalStore's Object.is check is stable. */
|
||||
export function useActiveAccountId(): number | null {
|
||||
return useSyncExternalStore(subscribeActiveAccount, activeAccountId, activeAccountId);
|
||||
}
|
||||
|
||||
/** Suffix a base localStorage key with an account id so per-account UI state (filters, selected
|
||||
* playlist, notifications, tab positions, cached prefs…) is NOT shared across the accounts signed
|
||||
* into one browser — a bare shared key leaks one account's state into another (and, with per-tab
|
||||
|
||||
Reference in New Issue
Block a user