import type { Video, VideoStatus } from "../../lib/api"; import { FEED_VIEW_SPEC, type FeedView } from "../../lib/feedView"; import type { NEXT_PAGE } from "../../lib/nextPage"; import VideoCard from "../../features/video/VideoCard"; import { VideoMenu } from "../../features/video/VideoMenu"; import type { VideoActionHandlers } from "../../features/video/useVideoActions"; import { useContextMenu } from "../../components/ui/useContextMenu"; import VirtualGrid from "../../components/ui/VirtualGrid"; // Estimated row heights before measurement; real heights are measured per-row so these only affect // the very first paint and the scrollbar's initial guess. Taken from measuring each mode in the // browser rather than guessed — a bad estimate makes the scrollbar jump as you scroll in. // RE-MEASURE these when a mode's layout changes: they have gone stale twice already, most recently // when the action row moved onto the card's picture (cards 340 -> 262, small 257 -> 226). // The card figures are the roughest of the set by nature — a card's height follows its column width // (262 measured at 4 columns in a 1268px window), so no single constant is right for every window. const ROW_EST: Record = { cards: 262, cardsSmall: 226, rows: 117, rowsCompact: 46, tiles: 138, }; export interface VirtualFeedProps { items: Video[]; view: FeedView; onState: (id: string, status: VideoStatus) => void; onToggleSave: (id: string, saved: boolean) => void; onResetState: (id: string) => void; onOpen: (v: Video, startAt?: number | null) => void; /** The card menu's channel-filter row (see `useVideoActions`); omitted where it has no place. */ channelFilter?: VideoActionHandlers["channelFilter"]; hasNextPage: boolean; isFetching: boolean; fetchNextPage: (options: typeof NEXT_PAGE) => unknown; } // The feed's videos on the shared VirtualGrid engine — this wrapper only maps a FeedView to the // grid's layout spec and renders a VideoCard per item. All the virtualization mechanics (measure, // chunk, place, infinite-scroll) live in VirtualGrid. export default function VirtualFeed({ items, view, onState, onToggleSave, onResetState, onOpen, channelFilter, hasNextPage, isFetching, fetchNextPage, }: VirtualFeedProps) { const { minCol, colMaxWidth, maxCols, thumb } = FEED_VIEW_SPEC[view]; // The card menu (the video's whole action list) lives HERE rather than in each of the // feed's callers: this is the list, and `useContextMenu`'s contract is one menu per list. Mounting // it at this level is also what gives the channel page the menu for free — it renders // ``, so it is the same component all the way down. const menu = useContextMenu