Deep-review R6 — the small items: - The video card's channel name was still a <button> (pre-existing): ctrl/middle-click did nothing there while the title above it and the player's channel name honoured them. It renders through ChannelNameLink now, which grew a forwarded ref (the clamp measurement), pass-through attributes, a `tooltip`, and an opt-in `stopPropagation` so no click reaches the card's row. - ChannelNameLink rendered an empty, still-tabbable <a> for a channel with no title yet (a stub first seen through a live search; pre-existing at this site). It falls back to the translated "Channel". - The feed's SortKey→SortOption mapping existed twice in Feed.tsx; one `feedSortOptions` in feedSort.ts serves both modes (DIRECTIONLESS is module-private again). - The backfill refresh's comment claimed a one-page cost. It refetches every loaded page of the grid, and that is the right price: backfilled videos are older, so they land below the head. Tests for call sites that could be deleted with every lane green: the channel page running the backfill refresh (knip had been blinded by the hook test's import), Feed handing the grid the live results in the chosen key AND direction, the card using the link. Plus the alt-click arm of the shared click predicate, and a plex filter-chip test that claimed it made no request while firing a real one. Verified on localdev: the feed's channel names are anchors (same 14 px muted look), a plain click opens the channel in-app. 7 mutations, each red on its assertion.
58 lines
3.4 KiB
TypeScript
58 lines
3.4 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
import type { QueryClient } from "@tanstack/react-query";
|
|
import { qk } from "./queryKeys";
|
|
import { type ChannelDetail } from "./api";
|
|
|
|
/** How often the channel page re-polls its detail (via useLiveQuery): every 4s while THIS user's
|
|
* full-history backfill is running (`deep_requested && !backfill_done`) so the stored count + the
|
|
* N/S/L breakdown climb on their own, and `false` (stop polling) once the back-catalogue is in or
|
|
* the user never requested it. Extracted so the poll-or-stop decision is unit-testable. */
|
|
export const channelDetailPollMs = (d: ChannelDetail | undefined): number | false =>
|
|
d?.deep_requested && !d.backfill_done ? 4000 : false;
|
|
|
|
/** Did the poll above just watch the running backfill change this channel's catalogue?
|
|
*
|
|
* Only the poll can tell: the backfill runs in a BackgroundTask, so the PATCH that STARTS it
|
|
* returns before it has written anything, and it emits no event of its own. The window is the
|
|
* same one the poll runs in (`deep_requested && !backfill_done` on the PREVIOUS reading), so a
|
|
* detail refetch triggered by some other mutation — which invalidates the feed itself — can't
|
|
* come through here twice. Completion counts even with an unchanged count: the last page can be
|
|
* all-duplicates and still leave the grid's `hasNextPage` reading from a one-page catalogue. */
|
|
const backfillChangedCatalog = (
|
|
prev: ChannelDetail | undefined,
|
|
next: ChannelDetail | undefined,
|
|
): boolean =>
|
|
!!prev &&
|
|
!!next &&
|
|
prev.id === next.id &&
|
|
prev.deep_requested &&
|
|
!prev.backfill_done &&
|
|
(next.stored_videos !== prev.stored_videos || next.backfill_done);
|
|
|
|
/** Keep the channel page's video grid in step with the backfill its detail poll is watching.
|
|
*
|
|
* Without this the grid is frozen until an F5: the feed's infinite query has no interval, and the
|
|
* app-wide defaults are `staleTime: 30s` + `refetchOnWindowFocus: false`, so nothing refetches it
|
|
* on its own. "Get full history" invalidates the feed on the PATCH RESPONSE — before the
|
|
* background job has written a row — which is why the list stayed on the ~19 cards the channel had
|
|
* at subscribe time while the count above it climbed into the thousands.
|
|
*
|
|
* What it costs, honestly: an invalidation refetches EVERY loaded page of the grid's infinite
|
|
* query — one request per page the viewer has scrolled through, on each 4 s tick where the count
|
|
* moved, for as long as the backfill runs (the poll pauses while the tab is hidden, and stops when
|
|
* the back-catalogue is in). That is the right price, not an accident: backfilled videos are OLDER
|
|
* than what the grid opened with, so under the newest-first order they land in the pages BELOW the
|
|
* head — refreshing only the first page would leave out exactly the rows the backfill wrote. The
|
|
* head itself comes back structurally identical and re-renders nothing. */
|
|
export function useBackfillFeedRefresh(detail: ChannelDetail | undefined, qc: QueryClient) {
|
|
const seen = useRef<ChannelDetail | undefined>(undefined);
|
|
useEffect(() => {
|
|
const prev = seen.current;
|
|
seen.current = detail;
|
|
if (!backfillChangedCatalog(prev, detail)) return;
|
|
qc.invalidateQueries({ queryKey: qk.feed() });
|
|
// …and the count pill beside it, which reads the same catalogue through its own query.
|
|
qc.invalidateQueries({ queryKey: qk.feedSummary() });
|
|
}, [detail, qc]);
|
|
}
|