refactor(channels): scope module state in ChannelsProvider

Lift the Channels manager's shared local state (name filter, status chip,
active tab, column-reset token, focus-a-channel intent) out of App into a
split-context ChannelsProvider, nested inside NavigationProvider so its
focusChannel/goToFullHistory actions can drive navigation. The Channels page,
the header search box + sync chip, the feed filter sidebar, and the
notifications inbox all read it via useChannels()/useChannelsActions() instead
of props drilled through App. Channels also internalises onViewChannel /
onFilterByTag / onFocusChannel (nav + feed-filters straight from providers) and
takes only { me, onOpenWizard } now. App loses five useStates, the focusChannel
helper, the focus-clear effect, and a large block of per-page prop plumbing.
(Platform S6b.2)
This commit is contained in:
2026-07-19 00:08:34 +02:00
parent 6c32e5c8d6
commit a303f307cd
7 changed files with 199 additions and 134 deletions
+30 -40
View File
@@ -15,7 +15,7 @@ import {
UserMinus,
X,
} from "lucide-react";
import { api, type ManagedChannel, type Tag } from "../lib/api";
import { api, type ManagedChannel, type Me, type Tag } from "../lib/api";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
import { accountKey, LS } from "../lib/storage";
import { useDismiss } from "../lib/useDismiss";
@@ -30,6 +30,9 @@ import ChannelLink from "./ChannelLink";
import ChannelDiscovery from "./ChannelDiscovery";
import TagManager from "./TagManager";
import { useConfirm } from "./ConfirmProvider";
import { useChannels, useChannelsActions } from "./ChannelsProvider";
import { useNavigationActions } from "./NavigationProvider";
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
export type ChannelsView = "subscribed" | "discovery" | "blocked";
@@ -42,49 +45,36 @@ const STATUS_FILTERS: { id: ChannelStatusFilter; labelKey: string }[] = [
{ id: "hidden", labelKey: "channels.filters.hidden" },
];
export default function Channels({
canWrite,
isAdmin,
search,
setSearch,
onViewChannel,
onFilterByTag,
onFocusChannel,
focusChannelName,
focusChannelToken,
statusFilter,
setStatusFilter,
view,
setView,
filtersResetToken,
onOpenWizard,
}: {
canWrite: boolean;
isAdmin: boolean;
// The channel-name filter, lifted to App so the shared top SearchBar drives it.
search: string;
setSearch: (q: string) => void;
onViewChannel: (id: string, name: string) => void;
onFilterByTag: (tagId: number, name: string) => void;
onFocusChannel: (name: string) => void;
focusChannelName: string | null;
// Bumped every time a focus-channel intent is (re-)issued, so re-clicking the same channel
// re-seeds the search box even when the name is unchanged.
focusChannelToken: number;
statusFilter: ChannelStatusFilter;
setStatusFilter: (f: ChannelStatusFilter) => void;
// The active tab lives in App so navigation intents that target the subscriptions table
// (the header's "without full history" link, focus-channel) can force it back here.
view: ChannelsView;
setView: (v: ChannelsView) => void;
// Bumped by the header's "without full history" intent to clear a stale column filter.
filtersResetToken: number;
onOpenWizard: () => void;
}) {
export default function Channels({ me, onOpenWizard }: { me: Me; onOpenWizard: () => void }) {
const canWrite = me.can_write;
const isAdmin = me.role === "admin";
// The manager's name filter, status chip, active tab, reset token, and focus-channel intent live
// in ChannelsProvider (shared with the header search box + the cross-module "focus this channel"
// callers); this page is their primary editor.
const {
search,
statusFilter,
view,
filtersResetToken,
focusName: focusChannelName,
focusToken: focusChannelToken,
} = useChannels();
const { setSearch, setStatusFilter, setView, focusChannel: onFocusChannel } = useChannelsActions();
const { openChannel: onViewChannel, setPage } = useNavigationActions();
const feedFilters = useFeedFilters();
const { setFilters: setFeedFilters } = useFeedFiltersActions();
const { t } = useTranslation();
const qc = useQueryClient();
const confirm = useConfirm();
// Clicking a tag chip filters the feed by that tag and jumps there. Internalised (was an App
// callback) now that Channels reads the feed filters + navigation straight from their providers.
const onFilterByTag = (tagId: number, name: string) => {
setFeedFilters({ ...feedFilters, tags: [tagId], channelIds: [] });
setPage("feed");
notify({ level: "info", message: t("channels.notify.filteredByTag", { name }) });
};
const channelsQuery = useQuery({ queryKey: ["channels"], queryFn: api.channels });
const tagsQuery = useQuery({ queryKey: ["tags"], queryFn: api.tags });
const statusQuery = useQuery({ queryKey: ["my-status"], queryFn: api.myStatus });