refactor(ui): reuse the store, media-query and deleted-tag rules the review found rebuilt, and name a loading pick
E32 review round 1 (F3–F6). - Channels' open-row store and the chat dock use lib/store's createStore instead of a hand-rolled listener set (the chat dock gets a test of its own). - The Plex backdrop reads prefers-reduced-motion through useMediaQuery; the query string is one constant (REDUCED_MOTION) for the hook and the carousel's one-off read. - A link-restored collection filter showed its raw rating_key in the picker while the titles loaded: usePlexCollectionTitles now says it is pending, the picker draws a placeholder (labelsPending, as the channel picker does) and so does the chip row. - The "deleted tag" rule was written twice with two i18n keys and had drifted: the channel manager showed NO chip for a pick while the tag list loaded, or ever after a failed load, while the pick still narrowed the table. One rule now (lib/tagPicks), one key (common.deletedTag); the manager's row shows every pick, as a placeholder while loading and as its id after a failure.
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { createStore, type Store } from "../lib/store";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { Ban, Eye, EyeOff, History, LayoutGrid, Pencil, RefreshCw, Table, X } from "lucide-react";
|
||||
import ExcludedChannelList from "./ExcludedChannelList";
|
||||
@@ -91,24 +92,8 @@ const channelKey = (c: ManagedChannel) => c.id;
|
||||
// Which channel's row menu is open, for its "…" (`aria-expanded`, and staying shown while open). A
|
||||
// tiny store rather than a context: a context change walks the whole table to find its readers (~20
|
||||
// ms on 400 rows, each open and close), while a store calls its subscribers and re-renders only the
|
||||
// two buttons whose answer changed.
|
||||
function createOpenRowStore() {
|
||||
let open: string | null = null;
|
||||
const listeners = new Set<() => void>();
|
||||
return {
|
||||
get: () => open,
|
||||
set: (next: string | null) => {
|
||||
if (next === open) return;
|
||||
open = next;
|
||||
listeners.forEach((l) => l());
|
||||
},
|
||||
subscribe: (l: () => void) => {
|
||||
listeners.add(l);
|
||||
return () => void listeners.delete(l);
|
||||
},
|
||||
};
|
||||
}
|
||||
type OpenRowStore = ReturnType<typeof createOpenRowStore>;
|
||||
// two buttons whose answer changed (each reads a boolean, so a change elsewhere renders nothing).
|
||||
type OpenRowStore = Store<string | null>;
|
||||
|
||||
function RowMoreButton({
|
||||
id,
|
||||
@@ -349,7 +334,7 @@ export default function Channels() {
|
||||
// stays one line and the trailing "…" actions can't wrap.
|
||||
const tagChips = tagFilterChips({
|
||||
userTags,
|
||||
allTags,
|
||||
tags: tagsQuery,
|
||||
picked: tagFilter,
|
||||
noTagOn,
|
||||
toggle: toggleTagFilter,
|
||||
@@ -397,7 +382,7 @@ export default function Channels() {
|
||||
// reposition, and so re-rendered the whole table — ~0.8 s to open the menu on 400 rows.
|
||||
const { buttonProps: rowButtonProps, triggerProps: rowTriggerProps } = rowMenu;
|
||||
const openRowId = rowMenu.open ? (rowMenu.target?.id ?? null) : null;
|
||||
const [openRow] = useState(createOpenRowStore);
|
||||
const [openRow] = useState(() => createStore<string | null>(null));
|
||||
useEffect(() => openRow.set(openRowId), [openRow, openRowId]);
|
||||
const channelActions = useChannelActions({
|
||||
onView,
|
||||
|
||||
Reference in New Issue
Block a user