fix(search): make the YouTube-search view its own history entry

The live-search results view had no browser-history entry of its own, so a
player opened over the results sat directly on the feed page entry. Pressing
Back (e.g. the mouse back button over the player) could pop past both the
player and the search in one step, bouncing from the search results to the
normal feed instead of just closing the player.

The search is now a feed sub-view that owns a history entry (history.state._yt):
entering a search pushes it, the popstate handler derives ytSearch from it, and
"Back to feed" pops it. Back now steps player -> search -> feed: the first Back
closes only the player (results stay), the second returns to the normal feed.
A reload drops any stale _yt so the first Back can't resurrect a gone search.
This commit is contained in:
npeter83 2026-06-29 23:03:25 +02:00
parent 1d6dfaf486
commit ff9b0601c3
2 changed files with 35 additions and 12 deletions

View file

@ -124,7 +124,22 @@ export default function App() {
const [page, setPageState] = useState<Page>(loadInitialPage);
// Live YouTube search term (null = normal feed). Ephemeral: not persisted and not in the URL,
// so a reload returns to the normal feed (a search spends quota, so we never auto-replay it).
// The search is a feed SUB-VIEW that owns a browser-history entry (history.state._yt): the
// popstate handler below derives ytSearch from it, so Back steps player → search → feed in
// that order (a player opened over the results pops first, then the search, then the feed) —
// instead of the search vanishing because it had no history entry of its own.
const [ytSearch, setYtSearch] = useState<string | null>(null);
const enterYtSearch = useCallback((q: string) => {
setYtSearch(q);
const st = window.history.state || {};
if (st._yt) window.history.replaceState({ ...st, _yt: q }, ""); // refine current search
else window.history.pushState({ ...st, sfPage: "feed", _yt: q }, ""); // new sub-view entry
}, []);
const exitYtSearch = useCallback(() => {
// Pop our search entry (so Forward still works); fall back to a plain clear if we don't own one.
if (window.history.state?._yt) window.history.back();
else setYtSearch(null);
}, []);
const [wizardOpen, setWizardOpen] = useState(false);
// Channel status chip, persisted so a reload (F5) keeps it; clamp a stale value at render.
const [channelFilterRaw, setChannelFilter] = usePersistedState(LS.channelFilter, "all");
@ -230,10 +245,10 @@ export default function App() {
// from history.state on popstate. (stripUrlParams preserves history.state, so this stamp
// survives a later query-string strip.)
useEffect(() => {
window.history.replaceState(
{ ...window.history.state, sfPage: page },
""
);
// Drop any stale _yt from a prior session (a reload starts on the normal feed; ytSearch
// begins null), so the first Back doesn't resurrect a search we're no longer showing.
const { _yt: _staleYt, ...rest } = window.history.state || {};
window.history.replaceState({ ...rest, sfPage: page }, "");
function onPop(e: PopStateEvent) {
const p = (e.state?.sfPage as Page) ?? "feed";
// Guard a Back step that leaves Settings with unsaved changes: re-assert the Settings
@ -255,6 +270,9 @@ export default function App() {
}
setPageState(p);
localStorage.setItem(PAGE_KEY, p);
// The YouTube-search sub-view rides in history.state._yt, so Back/Forward restores or
// clears it to match the entry we landed on (and a player popped first leaves it intact).
setYtSearch((e.state?._yt as string) ?? null);
}
window.addEventListener("popstate", onPop);
return () => window.removeEventListener("popstate", onPop);
@ -505,7 +523,7 @@ export default function App() {
filters={filters}
setFilters={setFilters}
page={page}
onYtSearch={setYtSearch}
onYtSearch={enterYtSearch}
onGoToFullHistory={() => {
setChannelFilter("needs_full");
setChannelsView("subscribed"); // the status filter applies to the subscriptions tab
@ -579,7 +597,8 @@ export default function App() {
isDemo={meQuery.data!.is_demo}
onOpenWizard={() => setWizardOpen(true)}
ytSearch={ytSearch}
setYtSearch={setYtSearch}
onYtSearch={enterYtSearch}
onExitYtSearch={exitYtSearch}
/>
)}
</main>