fix(feed): round-2 deep-review remediations (incl. a real coerce-on-seed bug)

The re-review of the fix round surfaced 4 findings; fixing the coverage one
exposed that the C1 migration was ineffective:
- FeedFiltersProvider.loadAccountFilters: run coerceTagModes on the RAW stored
  blob, not the object merged over DEFAULT_FILTERS. The merge injects
  DEFAULT_TAG_MODES (all "or"), which coerceTagModes prefers over the legacy
  scalar `tagMode` — so the migration never fired. Now a persisted legacy
  tagMode:'and' actually migrates into tagModes.topic (proven by a new test).
- test_feed_views: the tag_mode_other=and case was vacuous (only chC carried
  either "other" tag, so AND == OR). Give chA an "other" tag too so OR-other
  {chA,chC} differs from AND-other {chC} — the conjunctive path now narrows.
- DateFilterSelect: reset the custom reveal in useLayoutEffect (not useEffect)
  so the correction commits before paint — no one-frame "Custom range" flicker
  on the trigger after an external clear.
- SavedViewsProvider: extract the sticky-view tie-break to a pure, tested
  lib/savedViews.ts (preferSticky).

New tests: savedViews (preferSticky), FeedFiltersProvider.dom (legacy tagMode
migration on the localStorage/default-view seed).
This commit is contained in:
2026-08-22 06:55:16 +02:00
parent 7692253636
commit e4b19b5320
7 changed files with 111 additions and 20 deletions
@@ -10,6 +10,7 @@ import {
type SavedView,
} from "../lib/api";
import { filtersToParams } from "../lib/urlState";
import { preferSticky } from "../lib/savedViews";
import { notifyActionError } from "../lib/actionErrors";
import { accountKey, LS, writeJSON } from "../lib/storage";
import { useConfirm } from "./ConfirmProvider";
@@ -85,12 +86,16 @@ export function SavedViewsProvider({ children }: { children: ReactNode }) {
}, [viewsQuery.data]);
const currentKey = keyOf(filters);
const activeView = useMemo(() => {
const matches = views.filter((v) => keyOf(v.filters) === currentKey);
const activeView = useMemo(
// When several views capture identical filters, prefer the one the user explicitly applied
// (sticky) so apply(B) isn't silently re-attributed to an earlier duplicate A; else the first.
return matches.find((v) => v.id === stickyId) ?? matches[0] ?? null;
}, [views, currentKey, stickyId]);
() =>
preferSticky(
views.filter((v) => keyOf(v.filters) === currentKey),
stickyId,
),
[views, currentKey, stickyId],
);
// An exact match becomes the sticky selection, so filters that happen to equal a view (deep-link,
// manual, or an applied view) name that view in the chip and keep naming it after a later tweak.
useEffect(() => {