diff --git a/frontend/src/components/ChannelPicker.tsx b/frontend/src/components/ChannelPicker.tsx
index e09f037b..3c4386de 100644
--- a/frontend/src/components/ChannelPicker.tsx
+++ b/frontend/src/components/ChannelPicker.tsx
@@ -1,11 +1,8 @@
-import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query";
import { qk } from "../lib/queryKeys";
-import { X } from "lucide-react";
import { api, type FeedFilters } from "../lib/api";
-import { useScrollFade } from "../lib/useScrollFade";
-import { inputCls } from "./ui/form";
+import Combobox, { type ComboboxOption } from "./Combobox";
// Pick channels to narrow the feed to — several OR together, so every other filter then applies
// across all of them. Until this existed the filter was settable only via a shared "?channel=" link:
@@ -22,86 +19,29 @@ export default function ChannelPicker({
setFilters: (f: FeedFilters) => void;
}) {
const { t } = useTranslation();
- const [query, setQuery] = useState("");
- // Same edge-fade affordance the nav rail and the panel body use to hint at more content.
- const fade = useScrollFade();
const channelsQuery = useQuery({
queryKey: qk.channels(),
queryFn: api.channels,
staleTime: 5 * 60_000,
});
- const q = query.trim().toLowerCase();
const all = channelsQuery.data ?? [];
- const matches = q ? all.filter((c) => (c.title ?? c.id).toLowerCase().includes(q)) : all;
- const picked = filters.channelIds;
- const nameOf = (id: string) => all.find((c) => c.id === id)?.title ?? id;
- const toggle = (id: string) =>
- setFilters({
- ...filters,
- channelIds: picked.includes(id) ? picked.filter((x) => x !== id) : [...picked, id],
- });
+ // The list order isn't alphabetical — it's the user's channel priority — and Combobox preserves the
+ // options order, so it stays meaningful. selectedLabels keeps a picked channel named even if a later
+ // narrowing ever drops it from the visible rows.
+ const options: ComboboxOption[] = all.map((c) => ({ value: c.id, label: c.title ?? c.id }));
+ const selectedLabels = Object.fromEntries(all.map((c) => [c.id, c.title ?? c.id]));
return (
- <>
- {/* The picks sit above the list so they stay visible once the list scrolls away. */}
- {picked.length > 0 && (
-
- {picked.map((id) => (
-
- ))}
-
- )}
- setQuery(e.target.value)}
- placeholder={t("sidebar.channelSearch")}
- aria-label={t("sidebar.channelSearch")}
- className={inputCls}
- />
- {/* The order isn't arbitrary and isn't alphabetical — say so, or the top of the list looks
- random to anyone who hasn't set a priority. Only while unfiltered: a search reorders nothing
- but does drop the rows that explain it. */}
- {!q && all.length > 0 && (
-