refactor(ux): address /code-review high findings on the fixpack

- Extract the optimistic + debounced-flush feed-priority flow into a shared
  useChannelPriority hook (mirrors useChannelTagActions); the manager and the
  channel-detail page now share it instead of two drifting copies.
- NavSidebar: drop the always-true `slim` const and its dead expanded-rail
  branches; refresh the now-stale pinned/collapsible comments.
- Remove the orphaned `navCollapsed` localStorage key (no readers/writers left
  after the always-slim rail).
- Channel page: drop the wrapper native `title` that duplicated PriorityStepper's
  own Tooltip.

The keyboard-focus nav-label finding needed no change: the popover platform
already wires useFocus (visibleOnly), so a real Tab press shows the flyout label
(verified in-browser).
This commit is contained in:
2026-08-11 03:29:08 +02:00
parent 71bbf35f5a
commit 02754f479f
5 changed files with 123 additions and 123 deletions
+14 -50
View File
@@ -28,6 +28,7 @@ import ChannelLayoutGrid from "./ChannelLayoutGrid";
import { LoadingState, StateMessage } from "./QueryState";
import { ChannelTagsControl } from "./ChannelTags";
import { useChannelTagActions } from "../lib/useChannelTagActions";
import { useChannelPriority } from "../lib/useChannelPriority";
import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
import ColumnSortControl from "./ColumnSortControl";
import Pager from "./Pager";
@@ -143,56 +144,19 @@ export default function Channels() {
qc.invalidateQueries({ queryKey: qk.feedCount() });
},
});
// Priority is updated optimistically in place and NOT re-sorted/refetched, so the list
// doesn't jump (the server list is priority-sorted; refetching would reorder rows and
// lose your scroll position). The new order takes effect next time the page loads.
// Press-and-hold fires many bumps, so the PATCH is DEBOUNCED per channel: each bump reads the
// accumulated value from the cache, and the server call flushes once ~400ms after the last step
// (instead of one request per notch). Pending flushes are fired on unmount so a last-moment bump
// isn't lost.
const prioFlush = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
const flushPriority = (id: string) => {
const latest = qc
.getQueryData<ManagedChannel[]>(qk.channels())
?.find((c) => c.id === id)?.priority;
if (latest == null) return;
api
.updateChannel(id, { priority: latest })
.catch(() => qc.invalidateQueries({ queryKey: qk.channels() }));
};
// Set an absolute priority (used by both the arrows — via bumpPriority — and the inline edit),
// optimistically + a debounced flush.
const setPriority = (id: string, next: number) => {
qc.setQueryData<ManagedChannel[]>(qk.channels(), (old) =>
(old ?? []).map((ch) => (ch.id === id ? { ...ch, priority: next } : ch)),
);
const timers = prioFlush.current;
const pending = timers.get(id);
if (pending) clearTimeout(pending);
timers.set(
id,
setTimeout(() => {
timers.delete(id);
flushPriority(id);
}, 400),
);
};
const bumpPriority = (id: string, delta: number) => {
const cur =
qc.getQueryData<ManagedChannel[]>(qk.channels())?.find((c) => c.id === id)?.priority ?? 0;
setPriority(id, cur + delta);
};
useEffect(() => {
const timers = prioFlush.current;
return () => {
timers.forEach((timer, id) => {
clearTimeout(timer);
flushPriority(id);
});
timers.clear();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Priority is updated optimistically in place and NOT re-sorted/refetched, so the list doesn't
// jump (the server list is priority-sorted; refetching would reorder rows and lose your scroll
// position). The new order takes effect next time the page loads. Press-and-hold is debounced +
// flushed on unmount by the shared hook (same flow the channel detail page uses).
const { setPriority, bumpPriority } = useChannelPriority({
read: (id) =>
qc.getQueryData<ManagedChannel[]>(qk.channels())?.find((c) => c.id === id)?.priority,
optimistic: (id, next) =>
qc.setQueryData<ManagedChannel[]>(qk.channels(), (old) =>
(old ?? []).map((ch) => (ch.id === id ? { ...ch, priority: next } : ch)),
),
onError: () => qc.invalidateQueries({ queryKey: qk.channels() }),
});
// Tagging is updated optimistically in place (no refetch of the whole channel list, which
// made the toggle feel ~2s slow); only revert by refetching if the server call fails. The