feat(filters): filter the feed by several channels at once (OR)

Pick any number of channels; they OR together and every other filter applies
across the lot — "the last week's unwatched from these three".

`channelId`/`channelName` are replaced outright by `channelIds: string[]`, no
legacy reader (standing rule: no back-compat unless asked — a saved view written
against the old field simply loses its channel filter). The wire and the share
URL move with it: repeated `channel_ids` params, `?channel=a,b`. Backend ORs via
`Video.channel_id.in_()`; the count endpoint shares the same params object.

Two things the migration exposed:
- SavedViewsWidget fed DB blobs straight into serializers that now index an
  array — the FeedFilters type says nothing about what an older build wrote, so
  the blob is coerced where it enters. It white-screened the app before this.
- Chips carried channelName to label themselves. Without it they resolve names
  from the channel cache, so they now fall back to the id, not a shared
  "This channel" that would render N identical chips while the cache loads.
This commit is contained in:
2026-07-16 00:54:22 +02:00
parent 82e351bc62
commit 0d10e0f0a5
11 changed files with 88 additions and 66 deletions
+3 -6
View File
@@ -89,6 +89,7 @@ const ReleaseNotes = lazy(() => import("./components/ReleaseNotes"));
const DEFAULT_FILTERS: FeedFilters = {
tags: [],
tagMode: "or",
channelIds: [],
q: "",
sort: "newest",
scope: "my",
@@ -846,11 +847,7 @@ export default function App() {
onShowInFeed={() => {
// Keep the rest of the feed's filters — the point is "this channel, the way I
// normally read": unwatched, my tags, my date window.
setFilters({
...filters,
channelId: channelView.id,
channelName: channelView.name,
});
setFilters({ ...filters, channelIds: [channelView.id] });
closeChannel();
setPage("feed");
}}
@@ -905,7 +902,7 @@ export default function App() {
onOpenWizard={() => setWizardOpen(true)}
onViewChannel={(id, name) => openChannel(id, name)}
onFilterByTag={(tagId, name) => {
setFilters({ ...filters, tags: [tagId], channelId: undefined, channelName: undefined });
setFilters({ ...filters, tags: [tagId], channelIds: [] });
setPage("feed");
notify({ level: "info", message: t("channels.notify.filteredByTag", { name }) });
}}