fix(boot): a new tab knows its account before anything renders
With Settings -> Appearance -> Startup turned off, every NEW tab or window still opened on Home; only a same-tab reload honoured it. The provider tree mounted before /api/me answered, so NavigationProvider's initializer could not read the per-account opt-out, landed on Home and mirrored ?page=home into the address bar. When App pinned the account a moment later, the re-hydrate re-read that mirror and stayed on Home. The same mount-then-patch shape made a new tab start on default filters, the default theme and the browser language, and correct each after the first paint. BootGate (main.tsx, above the providers) now owns the setup-status and /api/me queries and mounts the tree only once the identity is settled: it pins the tab's account and adopts the account's language in a layout effect, so the first frame of the app is already right. The tree is keyed on the account id, which makes "per-account state is read once, for this account" a structural guarantee. With that, the whole re-hydrate machinery is gone: useAccountRehydrate, the reactive active-account store, the markDirty guards, the pin-deferred saved-view seed and the present-dimension bookkeeping of the shared Channels/Plex links. PrefsProvider, FeedFiltersProvider, usePref and App's panel layouts seed from the me cache instead of adopting it in an effect; the theme is applied in layout effects. The app chunk is fetched in parallel with the probe, and the boot screen only fades its label in after 400 ms, on one clock across the gate and the Suspense fallback. Module switches no longer flash a placeholder either. Measured frame by frame in Chromium and WebKit, every first visit to a module showed "Loading..." for 17-70 ms: the lazy chunk's Suspense fallback, then often the first fetch's LoadingState. Pages are now lazyWithPreload components, and App fetches the chunks of every module the account can reach (plus the channel page) once the first page is idle; a preloaded page renders synchronously and never suspends. LoadingState, the one shared loading visual, fades in only after 300 ms, so a fast load shows nothing in between and a slow one still gets its spinner.
This commit is contained in:
@@ -12,7 +12,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { urlWithParams, type Page } from "../lib/urlState";
|
||||
import { pageTitleKey } from "../lib/pageMeta";
|
||||
import { setAccountRaw } from "../lib/storage";
|
||||
import { useAccountRehydrate } from "../lib/useAccountRehydrate";
|
||||
import { setNavigator } from "../lib/nav";
|
||||
import {
|
||||
closeOverlaysThenNavigate,
|
||||
@@ -102,27 +101,6 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
|
||||
const channelViewRef = useRef(channelView);
|
||||
channelViewRef.current = channelView;
|
||||
|
||||
// The landing page is per-account (PAGE_KEY), but this provider mounts above <App>, which pins the
|
||||
// tab's account only after /api/me — so on a fresh tab `loadInitialPage` reads an unknown account and
|
||||
// falls back to "feed". Re-read once the account is pinned and correct the CURRENT history entry
|
||||
// (replaceState, not push) so Back isn't given a phantom "feed" step. markDirty (set by setPage)
|
||||
// skips this if the user already navigated in the pre-pin window.
|
||||
const markDirty = useAccountRehydrate(() => {
|
||||
const stored = loadInitialPage();
|
||||
if (stored === pageRef.current) return;
|
||||
setPageState(stored);
|
||||
setAccountRaw(PAGE_KEY, stored); // no-op re-write, keeps storage authoritative
|
||||
// Correct the CURRENT entry (replace, not push) and PRESERVE any markers already on it — a channel
|
||||
// (_chan) or YT search (_yt) opened in the pre-pin window survives on top of the restored page,
|
||||
// rather than being orphaned from history so Back can't close it. Mirror the corrected page to the
|
||||
// address bar too (urlWithParams keeps any ?chan/filter params already there).
|
||||
window.history.replaceState(
|
||||
{ ...(window.history.state || {}), sfPage: stored },
|
||||
"",
|
||||
urlWithParams({ page: stored === "feed" ? null : stored }),
|
||||
);
|
||||
});
|
||||
|
||||
const setPage = useCallback(
|
||||
(next: Page) =>
|
||||
// Never under an open overlay — see the interface doc. The refs are read when the push
|
||||
@@ -131,7 +109,6 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
|
||||
// A channel page overlays the content column, so a nav click must close it even when the
|
||||
// underlying page is unchanged (e.g. "Feed" while a channel opened from the feed).
|
||||
if (next === pageRef.current && !channelViewRef.current) return;
|
||||
markDirty();
|
||||
setChannelView(null); // leave any open channel page when navigating via the rail
|
||||
setPageState(next);
|
||||
setAccountRaw(PAGE_KEY, next);
|
||||
@@ -163,15 +140,12 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
|
||||
);
|
||||
notifyNav();
|
||||
}),
|
||||
[markDirty],
|
||||
[],
|
||||
);
|
||||
|
||||
// A sub-view (channel / YT search) is an OVERLAY, not a page change, so it does NOT markDirty: a
|
||||
// pre-pin account re-hydrate should still restore the stored page underneath — the re-hydrate's
|
||||
// replaceState spreads the _chan/_yt markers, so the overlay survives on top of the restored page.
|
||||
//
|
||||
// Like setPage it never lands under an open overlay: the player's channel link and the palette's
|
||||
// channel rows call it from inside one.
|
||||
// A sub-view (channel / YT search) is an OVERLAY, not a page change. Like setPage it never lands
|
||||
// under an open overlay: the player's channel link and the palette's channel rows call it from
|
||||
// inside one.
|
||||
const openChannel = useCallback((id: string, name?: string) => {
|
||||
closeOverlaysThenNavigate(() => {
|
||||
const url = urlWithParams({ chan: id });
|
||||
@@ -197,7 +171,6 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
|
||||
}, []);
|
||||
|
||||
const enterYtSearch = useCallback((q: string) => {
|
||||
// Overlay, not a page change — see openChannel: no markDirty, the re-hydrate preserves _yt.
|
||||
setYtSearch(q);
|
||||
// Drop any prior _ytScrape marker — the new search's source isn't known yet; Feed re-stamps it
|
||||
// once the results resolve, so a reload only restores a confirmed scrape-sourced search.
|
||||
|
||||
Reference in New Issue
Block a user