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:
@@ -39,7 +39,6 @@ export const LS = {
|
||||
auditTable: "siftlode.auditTable",
|
||||
statsTab: "siftlode.statsTab",
|
||||
adminUsersTab: "siftlode.adminUsersTab",
|
||||
navCollapsed: "siftlode.navCollapsed",
|
||||
filterCollapsed: "siftlode.filterCollapsed",
|
||||
playlist: "siftlode.playlist",
|
||||
plSort: "siftlode.plSort",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { api } from "./api";
|
||||
|
||||
/** The shared feed-priority flow used by both the channel manager and the channel detail page:
|
||||
* an optimistic bump/set with a debounced server flush (~400ms after the last notch) so a
|
||||
* press-and-hold sends ONE request, not one per step, and pending flushes fire on unmount so a
|
||||
* last-moment bump isn't lost. The cache-shape-specific bits are injected (the manager maps over a
|
||||
* ManagedChannel[]; the detail patches one ChannelDetail), so the two call sites can't drift — the
|
||||
* same drift the sibling `useChannelTagActions` hook prevents for tags.
|
||||
*
|
||||
* Timers are keyed by channel id, so a pending flush survives the caller switching channels: it
|
||||
* fires later and reads the right cache via `read(id)`, rather than needing the caller to flush on
|
||||
* every id change. `read`/`optimistic`/`onSuccess`/`onError` are read through a ref so the returned
|
||||
* callbacks stay stable — the unmount flush then runs only on unmount, never on every render. */
|
||||
export function useChannelPriority(opts: {
|
||||
/** Current priority for this channel from whichever cache the caller owns (undefined if absent). */
|
||||
read: (channelId: string) => number | undefined;
|
||||
/** Apply the optimistic priority to the caller's cache. */
|
||||
optimistic: (channelId: string, next: number) => void;
|
||||
/** Called after the server write lands (e.g. invalidate sibling caches). Optional. */
|
||||
onSuccess?: (channelId: string) => void;
|
||||
/** Called if the server write fails — revert by refetching the owning cache. */
|
||||
onError: (channelId: string) => void;
|
||||
/** Debounce window in ms; defaults to 400. */
|
||||
delay?: number;
|
||||
}) {
|
||||
const timers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
||||
const optsRef = useRef(opts);
|
||||
optsRef.current = opts;
|
||||
|
||||
const flush = useCallback((channelId: string) => {
|
||||
const latest = optsRef.current.read(channelId);
|
||||
if (latest == null) return;
|
||||
api
|
||||
.updateChannel(channelId, { priority: latest })
|
||||
.then(() => optsRef.current.onSuccess?.(channelId))
|
||||
.catch(() => optsRef.current.onError(channelId));
|
||||
}, []);
|
||||
|
||||
// Set an absolute priority (used by both the arrows — via bumpPriority — and the inline edit),
|
||||
// optimistically + a debounced flush.
|
||||
const setPriority = useCallback(
|
||||
(channelId: string, next: number) => {
|
||||
optsRef.current.optimistic(channelId, next);
|
||||
const pending = timers.current.get(channelId);
|
||||
if (pending) clearTimeout(pending);
|
||||
timers.current.set(
|
||||
channelId,
|
||||
setTimeout(() => {
|
||||
timers.current.delete(channelId);
|
||||
flush(channelId);
|
||||
}, optsRef.current.delay ?? 400),
|
||||
);
|
||||
},
|
||||
[flush],
|
||||
);
|
||||
|
||||
// Read the LATEST value from the cache each step (not a render closure) so a hold-repeat
|
||||
// accumulates correctly.
|
||||
const bumpPriority = useCallback(
|
||||
(channelId: string, delta: number) => {
|
||||
setPriority(channelId, (optsRef.current.read(channelId) ?? 0) + delta);
|
||||
},
|
||||
[setPriority],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const pending = timers.current;
|
||||
return () => {
|
||||
pending.forEach((timer, channelId) => {
|
||||
clearTimeout(timer);
|
||||
flush(channelId);
|
||||
});
|
||||
pending.clear();
|
||||
};
|
||||
}, [flush]);
|
||||
|
||||
return { setPriority, bumpPriority };
|
||||
}
|
||||
Reference in New Issue
Block a user