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
@@ -48,8 +48,9 @@ describe("ChannelNameLink", () => {
it("is an anchor whose href opens this channel on the current page", () => {
const { link } = renderLink();
expect(link.tagName).toBe("A");
// The same URL openChannel pushes: the current page's query plus ?chan=<id>.
expect(link.getAttribute("href")).toBe(`/?page=feed&show=unwatched&chan=${CH}`);
// The same URL openChannel pushes: the current page plus ?chan=<id> — not the page's own keys,
// which named a view the channel does not show (`show=unwatched` here).
expect(link.getAttribute("href")).toBe(`/?page=feed&chan=${CH}`);
});
it("takes the in-app path on a plain left click, and suppresses the browser navigation", () => {
@@ -73,11 +74,16 @@ describe("ChannelNameLink", () => {
expect(onOpenInApp).not.toHaveBeenCalled();
});
it("does not lose the rest of the address bar", () => {
window.history.replaceState({}, "", "/?page=channels&q=abc");
// The address names only what the screen shows: the channel over the page, not the page's filters
// (a link's new tab once opened the channel under the feed's search and sort).
it("keeps the page and the shell's keys, and leaves the page's and another channel's behind", () => {
window.history.replaceState(
{},
"",
"/?page=channels&q=abc&ctab=hidden&chtab=about&m=signup#x=1",
);
const { link } = renderLink();
expect(link.getAttribute("href")).toContain("page=channels");
expect(link.getAttribute("href")).toContain("q=abc");
expect(link.getAttribute("href")).toBe(`/?page=channels&chan=${CH}&m=signup#x=1`);
});
it("gives a channel with no title yet an accessible name", () => {
@@ -1,15 +1,15 @@
import { forwardRef, type AnchorHTMLAttributes } from "react";
import { useTranslation } from "react-i18next";
import { isPlainLeftClick } from "../../lib/nav";
import { urlWithParams } from "../../lib/urlState";
import { channelUrl } from "../../lib/urlState";
// A channel name that is a REAL link. The player showed it as a `<button>`, so the browser's own
// gestures did nothing on it: middle-click and Ctrl/Cmd-click opened no tab, on a thing that looks
// exactly like a link (the YouTube icon right next to it IS an anchor and does open one). The video
// card's channel name had the same `<button>`, so it renders through here too.
//
// The href is the same URL `openChannel` pushes — `?chan=<id>` on the current page — so a new tab
// lands where the in-app navigation would have. A plain left click is still handled in-app, because
// The href is the same URL `openChannel` pushes (`channelUrl`: the channel over the current page,
// without the page's own keys) — so a new tab lands where the in-app navigation would have. A plain left click is still handled in-app, because
// the SPA path does more than navigate (the player has to close first and reclaim its own
// Back-to-close history entry; see lib/history.closeOverlaysThenNavigate).
type Props = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "onClick" | "title"> & {
@@ -33,7 +33,7 @@ const ChannelNameLink = forwardRef<HTMLAnchorElement, Props>(function ChannelNam
<a
{...rest}
ref={ref}
href={urlWithParams({ chan: channelId })}
href={channelUrl(channelId)}
title={tooltip}
onClick={(e) => {
if (stopPropagation) e.stopPropagation();