refactor(channels): share useSortState across the manager + discovery (R13 S1)

ChannelsProvider and ChannelDiscovery each hand-rolled the same sort plumbing
(useState<SortState> + parseSortState + a persist key). Both now use the shared
useSortState hook. hydrateSort gains an empty-options rule: with no vocabulary
passed it parses without healing (these owners heal for display via
ColumnSortControl against the live columns), so the un-healed provider store is
preserved. plexSortKeys made module-private (knip).

Closes R13 S1: feed, Plex, and the two channel tables all sort through one
SortControl + one SortState model. Gate green: tsc/eslint(0 err)/prettier/knip
clean, vitest 94.
This commit is contained in:
2026-08-13 23:41:12 +02:00
parent e4bdc5ea6a
commit 45bd588af7
6 changed files with 30 additions and 25 deletions
+5 -10
View File
@@ -1,12 +1,13 @@
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { qk } from "../lib/queryKeys";
import { UserPlus } from "lucide-react";
import { api, type DiscoveredChannel } from "../lib/api";
import { accountKey, LS, readAccount, writeAccount } from "../lib/storage";
import { accountKey, LS } from "../lib/storage";
import { formatCountOrDash } from "../lib/format";
import { parseSortState, sortRows, type SortState } from "../lib/columnSort";
import { sortRows } from "../lib/columnSort";
import { useSortState } from "../lib/useSortState";
import { useCardPager } from "../lib/useCardPager";
import { subsColumn } from "./channelColumns";
import { notify } from "../lib/notifications";
@@ -44,13 +45,7 @@ export default function ChannelDiscovery({
// The table/cards density is shared with the Subscriptions tab (ChannelsProvider). The sort is
// this tab's own (its columns differ), lifted out of the table so the card layout shares it.
const layout = useChannelLayout();
const [sort, setSortState] = useState<SortState>(() =>
parseSortState(readAccount(LS.channelDiscoverySort, null)),
);
const setSort = (s: SortState) => {
setSortState(s);
writeAccount(LS.channelDiscoverySort, s);
};
const [sort, setSort] = useSortState(LS.channelDiscoverySort);
const query = useQuery({
queryKey: qk.discoveredChannels(),
+6 -10
View File
@@ -8,9 +8,10 @@ import {
useState,
type ReactNode,
} from "react";
import { getAccountRaw, LS, readAccount, setAccountRaw, writeAccount } from "../lib/storage";
import { getAccountRaw, LS, setAccountRaw } from "../lib/storage";
import { parseChannelLayout, type ChannelLayout } from "../lib/channelLayout";
import { parseSortState, type SortState } from "../lib/columnSort";
import { type SortState } from "../lib/columnSort";
import { useSortState } from "../lib/useSortState";
import { useNavigation, useNavigationActions } from "./NavigationProvider";
import type { ChannelStatusFilter, ChannelsView } from "./Channels";
@@ -117,9 +118,9 @@ export function ChannelsProvider({ children }: { children: ReactNode }) {
const [layout, setLayoutState] = useState<ChannelLayout>(() =>
parseChannelLayout(getAccountRaw(LS.channelLayout)),
);
const [sort, setSortState] = useState<SortState>(() =>
parseSortState(readAccount(LS.channelSort, null)),
);
// Sort is owned here (lifted out of DataTable) so the card layout shares it; ColumnSortControl heals
// it against the live columns for display, so the provider stores it un-healed (no options passed).
const [sort, setSort] = useSortState(LS.channelSort);
const [filtersResetToken, setFiltersResetToken] = useState(0);
const [focusName, setFocusName] = useState<string | null>(null);
const [focusToken, setFocusToken] = useState(0);
@@ -157,11 +158,6 @@ export function ChannelsProvider({ children }: { children: ReactNode }) {
setLayoutState(l);
setAccountRaw(LS.channelLayout, l);
}, []);
const setSort = useCallback((s: SortState) => {
setSortState(s);
writeAccount(LS.channelSort, s);
}, []);
const focusChannel = useCallback(
(name: string) => {
setFocusName(name);