feat(search): keep scrape-sourced YouTube results across a reload
A live YouTube search was always dropped on reload so it could never re-spend quota. But the default scrape source costs no quota, so there's no reason to bounce the user back to the feed — they can stay on their results. Feed now stamps history.state._ytScrape=true once a search resolves via the scrape source; App restores _yt on reload only when that flag is set (and re-runs the search, which is free). An api-source search is left unmarked and still drops to the feed on reload, since re-fetching it would cost ~100 units. The flag is cleared when a new search starts (Feed re-stamps it once the new results' source is known).
This commit is contained in:
parent
703ae41f2d
commit
b498755067
2 changed files with 33 additions and 8 deletions
|
|
@ -130,10 +130,18 @@ export default function App() {
|
|||
// 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 [ytSearch, setYtSearch] = useState<string | null>(() => {
|
||||
// Restore a live search across a reload ONLY when it was served by the zero-quota scrape
|
||||
// source (re-fetching is free) — an api-source search would re-spend ~100 units, so it's
|
||||
// dropped to the normal feed as before. The scrape flag rides in history.state (survives F5).
|
||||
const st = window.history.state;
|
||||
return st?._yt && st?._ytScrape ? (st._yt as string) : null;
|
||||
});
|
||||
const enterYtSearch = useCallback((q: string) => {
|
||||
setYtSearch(q);
|
||||
const st = window.history.state || {};
|
||||
// Drop any prior _ytScrape marker — the new search's source isn't known yet; Feed re-stamps
|
||||
// it once the results resolve, so a reload only restores a confirmed scrape-sourced search.
|
||||
const { _ytScrape: _drop, ...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
|
||||
}, []);
|
||||
|
|
@ -271,12 +279,17 @@ export default function App() {
|
|||
// from history.state on popstate. (stripUrlParams preserves history.state, so this stamp
|
||||
// survives a later query-string strip.)
|
||||
useEffect(() => {
|
||||
// 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.
|
||||
// Drop a stale _yt (a reload starts on the normal feed; a search would re-spend quota), but
|
||||
// KEEP _chan/_chanName so the open channel page survives F5 (channelView restores from it above).
|
||||
const { _yt: _staleYt, ...rest } = window.history.state || {};
|
||||
window.history.replaceState({ ...rest, sfPage: page }, "");
|
||||
// A reload returns to the normal feed by dropping a stale _yt — EXCEPT when the search was
|
||||
// scrape-sourced (zero quota): then we keep _yt + _ytScrape so it restores and re-fetches for
|
||||
// free (ytSearch above already initialised from it). An api-source search is still dropped (it
|
||||
// would re-spend ~100 units). _chan/_chanName are always kept so a channel page survives F5.
|
||||
const st = window.history.state || {};
|
||||
if (st._yt && st._ytScrape) {
|
||||
window.history.replaceState({ ...st, sfPage: page }, "");
|
||||
} else {
|
||||
const { _yt: _staleYt, _ytScrape: _staleScrape, ...rest } = st;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -151,6 +151,18 @@ export default function Feed({
|
|||
retry: false,
|
||||
});
|
||||
|
||||
// Remember (in history.state) that this live search was served by the zero-quota scrape source,
|
||||
// so a reload restores the results instead of dropping to the feed — re-fetching scrape pages
|
||||
// costs no quota. An api-source search is left unmarked (a reload would re-spend ~100 units).
|
||||
useEffect(() => {
|
||||
if (!ytActive) return;
|
||||
const st = window.history.state;
|
||||
if (!st || st._yt !== ytSearch) return; // only mark our own sub-view entry
|
||||
if (ytQuery.data?.pages?.[0]?.source === "scrape" && !st._ytScrape) {
|
||||
window.history.replaceState({ ...st, _ytScrape: true }, "");
|
||||
}
|
||||
}, [ytActive, ytSearch, ytQuery.data]);
|
||||
|
||||
// Switching to the relevance sort when a search starts happens atomically in the header's
|
||||
// input onChange (race-free with the query update). Here we only handle the reverse: when
|
||||
// the term is cleared, fall back to the default sort — relevance has no dropdown option and
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue