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
+18 -45
View File
@@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { qk } from "../lib/queryKeys";
import { useNavigationActions } from "./NavigationProvider";
import { PriorityStepper } from "./PriorityStepper";
import { useChannelPriority } from "../lib/useChannelPriority";
import {
ArrowLeft,
Ban,
@@ -169,46 +170,21 @@ export default function ChannelPage({
onError: (err) => notifyYouTubeActionError(err, t("channel.fullHistoryFailed")),
});
// Feed priority, editable here with the same stepper the manager uses. Optimistic on this channel's
// detail cache + a debounced flush (~400ms after the last notch) so a press-and-hold sends ONE
// request, not one per step; the pending flush fires on unmount so a last bump isn't lost.
const prioFlush = useRef<ReturnType<typeof setTimeout> | null>(null);
const flushPriority = useCallback(() => {
const latest = qc.getQueryData<ChannelDetail>(qk.channel(channelId))?.priority;
if (latest == null) return;
api
.updateChannel(channelId, { priority: latest })
.then(() => {
qc.invalidateQueries({ queryKey: qk.channels() });
qc.invalidateQueries({ queryKey: qk.feed() });
})
.catch(() => qc.invalidateQueries({ queryKey: qk.channel(channelId) }));
}, [qc, channelId]);
const setPriority = (next: number) => {
qc.setQueryData<ChannelDetail>(qk.channel(channelId), (old) =>
old ? { ...old, priority: next } : old,
);
if (prioFlush.current) clearTimeout(prioFlush.current);
prioFlush.current = setTimeout(() => {
prioFlush.current = null;
flushPriority();
}, 400);
};
// Read the LATEST value from the cache each step (not a render closure) so a hold-repeat accumulates
// correctly, exactly like the manager's arrows.
const bumpPriority = (delta: number) => {
const cur = qc.getQueryData<ChannelDetail>(qk.channel(channelId))?.priority ?? 0;
setPriority(cur + delta);
};
useEffect(
() => () => {
if (prioFlush.current) {
clearTimeout(prioFlush.current);
flushPriority();
}
// Feed priority, editable here with the same stepper + debounced-flush the manager uses (shared
// hook so the two can't drift). Optimistic on this channel's detail cache; a success re-sorts the
// manager list + feed, a failure reverts by refetching the detail.
const { setPriority, bumpPriority } = useChannelPriority({
read: (id) => qc.getQueryData<ChannelDetail>(qk.channel(id))?.priority,
optimistic: (id, next) =>
qc.setQueryData<ChannelDetail>(qk.channel(id), (old) =>
old ? { ...old, priority: next } : old,
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: qk.channels() });
qc.invalidateQueries({ queryKey: qk.feed() });
},
[flushPriority],
);
onError: (id) => qc.invalidateQueries({ queryKey: qk.channel(id) }),
});
// Personal tags, editable right here (the same control + flow the channel manager uses). Optimistic
// on this channel's detail cache; a failed attach/detach reverts by refetching, and a success
@@ -423,15 +399,12 @@ export default function ChannelPage({
</button>
)}
{ch?.subscribed && !me.is_demo && (
<div
className="inline-flex items-center gap-1.5 h-9 px-2.5 rounded-lg border border-border"
title={t("channels.row.priorityHint")}
>
<div className="inline-flex items-center gap-1.5 h-9 px-2.5 rounded-lg border border-border">
<span className="text-xs text-muted">{t("channels.cols.priority")}</span>
<PriorityStepper
priority={ch.priority}
onPriority={bumpPriority}
onSet={setPriority}
onPriority={(d) => bumpPriority(channelId, d)}
onSet={(v) => setPriority(channelId, v)}
/>
</div>
)}