Files
siftlode/frontend/src/lib/useChannelPriority.ts
T
peter 02754f479f 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).
2026-08-11 03:29:08 +02:00

80 lines
3.3 KiB
TypeScript

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 };
}