From 3372626e34268df9f3e5cceec81009ab382cd6ab Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 27 Sep 2026 14:53:24 +0200 Subject: [PATCH] fix(notifications): "Find in feed" drops every narrowing that could hide the video locate() spread the reader's filters and cleared only the tag, search and age cap, so a year, a custom range, a length, a content type (a hidden Short under "no Shorts") or the scope could still leave the video out. It builds the filters from nothing now, keeping only the sort and tag modes. (Also types the C1 test's pending plan.) --- .../NotificationsPanel.menu.dom.test.tsx | 33 +++++++++++++++++-- .../src/components/NotificationsPanel.tsx | 15 ++++++--- .../usePlaylistActions.dom.test.tsx | 9 +++-- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/NotificationsPanel.menu.dom.test.tsx b/frontend/src/components/NotificationsPanel.menu.dom.test.tsx index b56592fb..5d1c7fc7 100644 --- a/frontend/src/components/NotificationsPanel.menu.dom.test.tsx +++ b/frontend/src/components/NotificationsPanel.menu.dom.test.tsx @@ -46,7 +46,28 @@ vi.mock("../lib/notifications", async (importOriginal) => ({ vi.mock("./navigationContext", () => ({ useNavigationActions: () => ({ setPage: h.setPage }) })); vi.mock("./channelsContext", () => ({ useChannelsActions: () => ({ focusChannel: vi.fn() }) })); vi.mock("./feedFiltersContext", () => ({ - useFeedFilters: () => ({ tags: [54], untagged: true, q: "rust", maxAgeDays: 30, sort: "newest" }), + // Every narrowing a feed can carry (E28 review C2: the year, the custom range, the length, the + // content types and the scope survived "Find in feed" and could still hide the video). + useFeedFilters: () => ({ + tags: [54], + tagModes: { language: "and", topic: "or", other: "or" }, + untagged: true, + q: "rust", + maxAgeDays: 30, + dateFrom: "2024-01-01", + dateTo: "2024-02-01", + year: 2024, + minDuration: 60, + maxDuration: 600, + includeNormal: true, + includeShorts: false, + includeLive: false, + scope: "my", + librarySource: "organic", + sort: "oldest", + show: "unwatched", + channelIds: ["x"], + }), useFeedFiltersActions: () => ({ setFilters: h.setFilters }), })); vi.mock("./ui/PageShell", async (importOriginal) => ({ @@ -223,12 +244,18 @@ describe("NotificationsPanel · an activity row", () => { await openMenu(() => fireEvent.contextMenu(rowOf("Activity 9"))); choose("notifications.findInFeed"); await waitFor(() => expect(h.setFilters).toHaveBeenCalled()); + // Nothing but the state and the channel narrows; the reader's sort and tag modes stay theirs. expect(h.setFilters).toHaveBeenCalledWith({ tags: [], + tagModes: { language: "and", topic: "or", other: "or" }, untagged: false, q: "", - maxAgeDays: undefined, - sort: "newest", + includeNormal: true, + includeShorts: true, + includeLive: true, + scope: "all", + librarySource: "all", + sort: "oldest", show: "watched", channelIds: ["c"], }); diff --git a/frontend/src/components/NotificationsPanel.tsx b/frontend/src/components/NotificationsPanel.tsx index f1566865..eca8753b 100644 --- a/frontend/src/components/NotificationsPanel.tsx +++ b/frontend/src/components/NotificationsPanel.tsx @@ -142,15 +142,22 @@ export default function NotificationsPanel() { // "Find in feed" / undo, ported from the old bell. Works for both hidden and watched // notices — jump to the feed filtered to that state and the video's channel. - // The narrowing filters that could leave the video out (a tag, "no tag", a search, an age cap) are - // dropped: kept, a channel outside the reader's tag filter found an empty feed. + // Built from nothing, not spread over the reader's filters: EVERY narrowing that could leave the + // video out goes — a tag, "no tag", a search, the age cap, the year, the custom range, the length, + // a content type (a hidden Short under "no Shorts"), the scope and the library source (E28 review + // C2: the spread kept whatever this list forgot). Only how the reader orders and matches stays. function locate(meta: VideoHiddenMeta | VideoWatchedMeta) { setFilters({ - ...filters, tags: [], + tagModes: filters.tagModes, untagged: false, q: "", - maxAgeDays: undefined, + includeNormal: true, + includeShorts: true, + includeLive: true, + scope: "all", + librarySource: "all", + sort: filters.sort, show: meta.kind === "video-hidden" ? "hidden" : "watched", channelIds: meta.channelId ? [meta.channelId] : [], }); diff --git a/frontend/src/components/usePlaylistActions.dom.test.tsx b/frontend/src/components/usePlaylistActions.dom.test.tsx index 957a15d1..d3b66bd8 100644 --- a/frontend/src/components/usePlaylistActions.dom.test.tsx +++ b/frontend/src/components/usePlaylistActions.dom.test.tsx @@ -161,10 +161,9 @@ describe("usePlaylistActions — a playlist's menu", () => { // offering a second push of the same list. describe("usePlaylistActions — one push at a time, across menus", () => { it("a push started from one menu is off in the other until it ends", async () => { - let finish: (v: unknown) => void = () => {}; - vi.spyOn(api, "playlistPushPlan").mockReturnValue( - new Promise((r) => (finish = r)) as ReturnType, - ); + type Plan = Awaited>; + let finish: (v: Plan) => void = () => {}; + vi.spyOn(api, "playlistPushPlan").mockReturnValue(new Promise((r) => (finish = r))); const rail = mount(); const header = mount(); const dirty = { ...linked, dirty: true }; @@ -176,7 +175,7 @@ describe("usePlaylistActions — one push at a time, across menus", () => { await act(async () => push(header).onSelect!()); expect(api.playlistPushPlan).toHaveBeenCalledTimes(1); await act(async () => { - finish({ action: "update", affordable: true }); + finish({ action: "update", affordable: true } as Plan); }); expect(push(header).disabled).toBeFalsy(); });