diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ff3dfcf..719da76 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,7 @@ import { saveLocalTheme, type ThemePrefs, } from "./lib/theme"; -import { hasFilterParams, paramsToFilters, readPage, syncUrl, type Page } from "./lib/urlState"; +import { hasFilterParams, paramsToFilters, readPage, stripUrlParams, type Page } from "./lib/urlState"; import { loadLayout, normalizeLayout, @@ -85,12 +85,10 @@ export default function App() { function setFilters(next: FeedFilters) { setFiltersState(next); localStorage.setItem(FILTERS_KEY, JSON.stringify(next)); - syncUrl(next, page); } function setPage(next: Page) { setPageState(next); - syncUrl(filters, next); } function setSidebarLayout(next: SidebarLayout) { @@ -101,8 +99,16 @@ export default function App() { useEffect(() => applyTheme(theme), [theme]); - // Reflect the initial filters + page into the address bar so the URL is always shareable. - useEffect(() => syncUrl(filters, page), []); // eslint-disable-line react-hooks/exhaustive-deps + // Filters live in localStorage, not the address bar. If we arrived via a "Share view" + // link, its params have already hydrated the initial state — persist them and strip the + // query so the URL stays clean from here on. + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (hasFilterParams(params) || params.has("page")) { + localStorage.setItem(FILTERS_KEY, JSON.stringify(filters)); + stripUrlParams(); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps const meQuery = useQuery({ queryKey: ["me"], queryFn: api.me }); diff --git a/frontend/src/lib/urlState.ts b/frontend/src/lib/urlState.ts index 8498865..4b745f3 100644 --- a/frontend/src/lib/urlState.ts +++ b/frontend/src/lib/urlState.ts @@ -1,12 +1,14 @@ -// Serialize feed filters to/from a compact, human-readable URL query string, so a -// pasted URL reproduces exactly what's on screen (handy for "here's the URL, I see -// this bug"). Only non-default values are emitted to keep URLs clean. +// Serialize feed filters to/from a compact, human-readable URL query string. Filters are +// NOT kept in the address bar during normal use (localStorage is the single source of +// truth); this serializer powers the explicit "Share view" link and the one-time hydration +// when someone opens such a link. Only non-default values are emitted to keep URLs clean. import type { FeedFilters } from "./api"; const KEYS = [ "q", "show", "sort", + "scope", "normal", "shorts", "live", @@ -25,6 +27,7 @@ export function filtersToParams(f: FeedFilters): URLSearchParams { if (f.q) p.set("q", f.q); if (f.show && f.show !== "unwatched") p.set("show", f.show); if (f.sort && f.sort !== "newest") p.set("sort", f.sort); + if (f.scope === "all") p.set("scope", "all"); if (!f.includeNormal) p.set("normal", "0"); if (f.includeShorts) p.set("shorts", "1"); if (f.includeLive) p.set("live", "1"); @@ -51,6 +54,7 @@ export function paramsToFilters(params: URLSearchParams, base: FeedFilters): Fee if (params.has("q")) f.q = params.get("q") ?? ""; if (params.has("show")) f.show = params.get("show") || "unwatched"; if (params.has("sort")) f.sort = params.get("sort") || "newest"; + if (params.has("scope")) f.scope = params.get("scope") === "all" ? "all" : "my"; if (params.has("normal")) f.includeNormal = params.get("normal") !== "0"; if (params.has("shorts")) f.includeShorts = params.get("shorts") === "1"; if (params.has("live")) f.includeLive = params.get("live") === "1"; @@ -81,11 +85,19 @@ export function readPage(): Page { return p === "channels" || p === "stats" ? p : "feed"; } -/** Reflect the current filters + page into the address bar without adding history entries. */ -export function syncUrl(f: FeedFilters, page: Page = "feed"): void { +/** Build a shareable absolute URL that reproduces the current filters (+ page). Used by the + * opt-in "Share view" action — filters are not otherwise written to the address bar. */ +export function shareUrl(f: FeedFilters, page: Page = "feed"): string { const params = filtersToParams(f); if (page !== "feed") params.set("page", page); const qs = params.toString(); - const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname; - window.history.replaceState(null, "", url); + return `${window.location.origin}${window.location.pathname}${qs ? `?${qs}` : ""}`; +} + +/** Strip any filter/page query params from the address bar (after hydrating from a share + * link), leaving a clean URL without touching history. */ +export function stripUrlParams(): void { + if (window.location.search) { + window.history.replaceState(null, "", window.location.pathname); + } }