fix(channel): the channel page's address names the channel and its own view, not the page under it

Opening a channel kept every key of the page under it (`?q=music&sort=oldest&chan=…`, a sort the
channel did not show), and the channel's own tab, sort and content types lived in its state alone, so a
reload or a bookmark lost them. Now the channel's address is `channelUrl`: the shell's keys and `chan`,
shared by `openChannel` and the channel link's href. The page writes its view (`chtab`, `chsort`,
`chtypes`, lib/channelPageUrlState) and reopens it; these are shell keys, cleared by a page switch and
by closing it in place. While a channel stands over a page, no module store writes its keys (`urlPage`);
Back finds them on the page's own entry. A link written by an older build opens without the page's keys.
This commit is contained in:
2026-10-26 00:19:02 +01:00
parent 9bd7304bb9
commit 91d36b3357
17 changed files with 565 additions and 46 deletions
+36 -6
View File
@@ -34,6 +34,12 @@ import { useFeedView, useFeedViewActions } from "./feedViewContext";
import { useShowChannelInFeed } from "./useFeedPageHooks";
import { SHOW_IDS } from "./feedFiltersInit";
import { usePref } from "../../lib/usePref";
import { useUrlMirror } from "../../lib/useUrlMirror";
import {
channelPageParamUpdates,
channelPageViewFrom,
type ChannelTab,
} from "../../lib/channelPageUrlState";
import { LS } from "../../lib/storage";
import { useConfirm } from "../../components/ui/confirmContext";
import { notify } from "../../lib/notifications";
@@ -88,7 +94,21 @@ export default function ChannelPage({
// to: the feed defaults to scope "my", so filtering it to a channel you don't follow would just
// land you on an empty page.
const onShowInFeed = useShowChannelInFeed(channelId);
const [tab, setTab] = useState<"videos" | "about">("videos");
// The view this page opens in: what its address names (a reload, a bookmark), else a fresh page —
// the videos, newest first, in the feed's content types (see `ownFilters` below). Read once, at
// mount: the page is keyed by its channel, and from here on it writes its address itself.
const [opened] = useState(() =>
channelPageViewFrom(new URLSearchParams(window.location.search), {
tab: "videos",
sort: "newest",
types: {
includeNormal: globalFilters.includeNormal,
includeShorts: globalFilters.includeShorts,
includeLive: globalFilters.includeLive,
},
}),
);
const [tab, setTab] = useState<ChannelTab>(opened.tab);
const panel = useTabPanel();
// The whole channel chrome (banner, identity, tabs, toolbar) is fixed: it portals into the shell's
// fixed band so only the video grid scrolls. This node sits at the bottom of that chrome and is
@@ -266,17 +286,27 @@ export default function ChannelPage({
tags: [],
tagModes: DEFAULT_TAG_MODES,
q: "",
sort: "newest",
sort: opened.sort,
scope: "all",
librarySource: "all",
// Content-type prefs inherited from the global feed (seeded once) — no longer forced.
includeNormal: globalFilters.includeNormal,
includeShorts: globalFilters.includeShorts,
includeLive: globalFilters.includeLive,
...opened.types,
show: "unwatched",
channelIds: [channelId],
}));
const filters: FeedFilters = { ...ownFilters, show };
// The address is this view: a reload or a bookmark reopens the tab, the sort and the content types.
useUrlMirror(
channelPageParamUpdates({
tab,
sort: ownFilters.sort,
types: {
includeNormal: ownFilters.includeNormal,
includeShorts: ownFilters.includeShorts,
includeLive: ownFilters.includeLive,
},
}),
);
const setFilters = (next: FeedFilters) => {
if (next.show !== show) setShow(next.show);
setOwnFilters(next);
@@ -436,7 +466,7 @@ export default function ChannelPage({
idBase={panel.idBase}
className="flex gap-1"
active={tab}
onChange={(id) => setTab(id as "videos" | "about")}
onChange={(id) => setTab(id as ChannelTab)}
tabs={[
{ id: "videos", label: t("channel.tabVideos") },
{ id: "about", label: t("channel.tabAbout") },