feat(nav): each module declares its URL keys; a page switch clears them and a page opens without another's

The shell cleared a hand-kept key list on a page switch (it missed the Files and feed keys) and cleared
nothing when a page opened from a link carrying another page's keys. ModuleDef.urlKeys now names every
key a page's view writes, the shell derives both from the registry, and moduleUrl.contract.test holds the
declarations true: one owner per key, no shell key, exactly what each codec writes, and no undeclared
key literal in the app.
This commit is contained in:
2026-10-25 20:48:14 +01:00
parent 4aff19a9a4
commit b30c60d1a5
24 changed files with 246 additions and 28 deletions
@@ -3,7 +3,6 @@ import { useLatest } from "../../lib/useLatest";
import { useTranslation } from "react-i18next";
import { useMe } from "../../lib/useMe";
import { resolvePage, retiredParams, urlWithParams, type Page } from "../../lib/urlState";
import { ACTIVITY_KEYS } from "../../lib/activityUrlState";
import { useDocumentObjectTitle } from "../../lib/documentTitle";
import { getAccountRaw, setAccountRaw } from "../../lib/storage";
import {
@@ -13,7 +12,7 @@ import {
notifyNav,
rememberNavPosition,
} from "../../lib/history";
import { pageTitleKey, reachablePage } from "./moduleRegistry";
import { foreignUrlKeys, moduleUrlKeys, pageTitleKey, reachablePage } from "./moduleRegistry";
import {
channelViewFrom,
loadInitialPage,
@@ -37,21 +36,13 @@ import {
//
// The contexts, their value types and the hooks live in navigationContext.ts.
/** The address bar of `page`'s root: the page (feed implicit) with every view-specific key cleared. */
/** The address bar of `page`'s root: the page (feed implicit) with every module's own keys cleared — a
* page switch resets to that module's root, and its store mirrors what it holds back in. */
function pageRootParams(page: Page): Record<string, string | null> {
return {
page: page === "feed" ? null : page,
chan: null,
v: null,
pl: null,
pscope: null,
psort: null,
pshow: null,
pf: null,
ctab: null,
csort: null,
cf: null,
...Object.fromEntries(ACTIVITY_KEYS.map((k) => [k, null])),
...Object.fromEntries(moduleUrlKeys().map((k) => [k, null])),
};
}
@@ -116,12 +107,10 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
setAccountRaw(PAGE_KEY, next);
// Push an in-app history entry so browser/mouse Back steps through pages instead of leaving
// the app. A fresh { sfPage } (no carried-over _yt/_chan markers) so each page starts at its
// root, and the address bar reflects the page (feed is implicit) while clearing the
// view-specific params (chan/v/pl and the plex content keys pscope/psort/pshow/pf) — a page
// switch resets to that module's root. Feed filters (owned by FeedFiltersProvider) are left
// intact; the plex/channels keys re-mirror from PlexProvider / ChannelsProvider on return to
// their page. The item it was opened from is still focused: the state setters above have not
// re-rendered anything yet.
// root, and the address bar reflects the page (feed is implicit) while clearing `chan` and
// every module's own keys (pageRootParams) — a page switch resets to that module's root, and
// each module's store mirrors its state back in on its page. The item it was opened from is
// still focused: the state setters above have not re-rendered anything yet.
rememberNavPosition();
window.history.pushState(
{ sfPage: next, sfScroll: nextNavId() },
@@ -244,7 +233,10 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
// part of the page that replaced it (an `at` tab).
const linked = new URLSearchParams(window.location.search).get("page");
const remembered = getAccountRaw(PAGE_KEY);
// A link can carry another page's keys (hand-edited, or written by an older build): the page
// opens without them, so its address names only what it shows.
const url = urlWithParams({
...Object.fromEntries(foreignUrlKeys(mountPage).map((k) => [k, null])),
...retiredParams(linked ?? (resolvePage(remembered) === mountPage ? remembered : null)),
page: mountPage === "feed" ? null : mountPage,
});