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:
@@ -26,7 +26,17 @@ import { useConfirm } from "./ConfirmProvider";
|
||||
// Compact, order-independent signature of a filter set (reuses the share serializer, which
|
||||
// emits only non-default values and ignores the transient shuffle seed). Used to highlight the
|
||||
// saved view that matches the feed's current filters.
|
||||
const keyOf = (f: FeedFilters) => filtersToParams(f).toString();
|
||||
const keyOf = (f: FeedFilters) => filtersToParams(asFilters(f)).toString();
|
||||
|
||||
// A saved view's `filters` is a JSON blob straight from the DB — the FeedFilters type says nothing
|
||||
// about what an older build actually wrote there, so coerce the array fields before the blob reaches
|
||||
// app state or a serializer. Deliberately NOT a migration: a view saved against a dropped field just
|
||||
// loses that filter.
|
||||
const asFilters = (raw: FeedFilters): FeedFilters => ({
|
||||
...raw,
|
||||
tags: raw.tags ?? [],
|
||||
channelIds: raw.channelIds ?? [],
|
||||
});
|
||||
|
||||
function syncDefaultMirror(views: SavedView[]): void {
|
||||
const key = accountKey(LS.defaultViewFilters, getActiveAccount());
|
||||
@@ -99,7 +109,7 @@ export default function SavedViewsWidget({
|
||||
|
||||
async function share(v: SavedView) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl(v.filters));
|
||||
await navigator.clipboard.writeText(shareUrl(asFilters(v.filters)));
|
||||
notify({ level: "success", message: t("views.shareCopied") });
|
||||
} catch {
|
||||
notify({ level: "warning", message: t("sidebar.shareFailed") });
|
||||
@@ -138,7 +148,7 @@ export default function SavedViewsWidget({
|
||||
view={v}
|
||||
active={keyOf(v.filters) === currentKey}
|
||||
renaming={renameId === v.id}
|
||||
onApply={() => onApply(v.filters)}
|
||||
onApply={() => onApply(asFilters(v.filters))}
|
||||
onStartRename={() => setRenameId(v.id)}
|
||||
onRename={(name) => rename(v.id, name)}
|
||||
onCancelRename={() => setRenameId(null)}
|
||||
|
||||
Reference in New Issue
Block a user