fix(nav): the address keeps what the app rewrites around it (review round 3)

- The Feed press over a live search ends only the search: the feed's own view stays named.
- Leaving a search steps back only off an entry the app pushed (_ytOwn); one a link or a reload
  opened is cleared in place, also where the Navigation API is missing.
- useUrlMirror rewrites a changed view through replaceView, so a reload's overlay marks go; its
  first write and its landing writes keep them for an overlay that reopens.
- Files and Playlists: a box emptied and filled again settles at once, never naming the ended search.
- Playlists: a linked player is scoped to the playlist the store started with, and only the rail's
  own move to the first playlist is no pick.
- Contract: writer objects are scanned to their matching brace, a computed or shorthand key is
  refused, and every useHistorySubview call hands a named definition.
- Lint: playwright/no-skipped-test also refuses fixme (gate snapshot regenerated).
- FEED_DEFAULT_SORT at the three remaining sites; linkedNotListed says to search by name.
This commit is contained in:
2026-10-27 23:33:22 +01:00
parent 66e427dfa4
commit 02906d0acb
18 changed files with 282 additions and 53 deletions
+3 -2
View File
@@ -24,6 +24,7 @@ import {
import { YouTubeIcon } from "../../lib/brandIcons";
import {
api,
FEED_DEFAULT_SORT,
HttpError,
refusalReason,
type FeedFilters,
@@ -318,7 +319,7 @@ export default function Feed({
const qEmpty = !filters.q.trim();
useEffect(() => {
if (qEmpty && parseSort(filters.sort).key === "relevance") {
setFilters({ ...filters, sort: buildSort("date", "desc") });
setFilters({ ...filters, sort: FEED_DEFAULT_SORT });
}
// Naming `filters` is safe precisely because the write below falsifies the condition above: the
// sort it sets is "date", so the one re-run it causes does nothing. The old `[qEmpty]` was
@@ -624,7 +625,7 @@ export default function Feed({
.catch((e: unknown) =>
notifyActionError(e, i18n.t("errors.writeFailed.clearSearch")),
);
setFilters({ ...filters, librarySource: "organic", sort: "newest" });
setFilters({ ...filters, librarySource: "organic", sort: FEED_DEFAULT_SORT });
onExitYtSearch();
dropFeedCaches();
qc.removeQueries({ queryKey: qk.ytSearch() });
+3 -2
View File
@@ -1,4 +1,5 @@
import { useTranslation } from "react-i18next";
import { FEED_DEFAULT_SORT } from "../../lib/api";
import { YouTubeIcon } from "../../lib/brandIcons";
import { useMe } from "../../lib/useMe";
import { PageHeader } from "../../components/ui/PageShell";
@@ -25,9 +26,9 @@ export default function FeedHeader() {
placeholder={t("header.searchPlaceholder")}
onChange={(q) => {
// When a search first appears, rank the feed by relevance — set atomically with the
// query (race-free vs. per-keystroke updates). Only overrides the default "newest"
// query (race-free vs. per-keystroke updates). Only overrides the default
// sort; a custom sort the user chose is left alone. Cleared in Feed.
const startSearch = !filters.q.trim() && !!q.trim() && filters.sort === "newest";
const startSearch = !filters.q.trim() && !!q.trim() && filters.sort === FEED_DEFAULT_SORT;
setFilters({ ...filters, q, ...(startSearch ? { sort: "relevance" } : {}) });
}}
// Turned off on this instance, the box only filters: no Go that could only be refused.