feat(feed): virtualize the feed list and consume the keyset cursor
Render only the visible rows via @tanstack/react-virtual (useVirtualizer against the app's <main> scroll container, per-row dynamic measurement), so the DOM no longer accumulates every loaded card. A ResizeObserver keeps the responsive grid column count in sync and chunks cards into virtualized rows; the list view virtualizes single-card rows. Infinite scroll now triggers from the virtual range and pages via next_cursor instead of offset; feed/count drops its unused paging args.
This commit is contained in:
parent
424b19f3b6
commit
0058ba7ccf
5 changed files with 228 additions and 55 deletions
|
|
@ -79,7 +79,8 @@ export interface VideoDetail {
|
|||
export interface FeedResponse {
|
||||
items: Video[];
|
||||
has_more: boolean;
|
||||
offset: number;
|
||||
// Opaque keyset cursor for the next page, or null when this is the last page.
|
||||
next_cursor: string | null;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
|
|
@ -308,11 +309,11 @@ function filterParams(f: FeedFilters): URLSearchParams {
|
|||
return p;
|
||||
}
|
||||
|
||||
function feedQuery(f: FeedFilters, offset: number, limit: number): string {
|
||||
function feedQuery(f: FeedFilters, cursor: string | null, limit: number): string {
|
||||
const p = filterParams(f);
|
||||
p.set("sort", f.sort);
|
||||
if (f.sort === "shuffle" && f.seed) p.set("seed", String(f.seed));
|
||||
p.set("offset", String(offset));
|
||||
if (cursor) p.set("cursor", cursor);
|
||||
p.set("limit", String(limit));
|
||||
return p.toString();
|
||||
}
|
||||
|
|
@ -511,10 +512,10 @@ export const api = {
|
|||
version: (): Promise<VersionInfo> => req("/api/version"),
|
||||
tags: (): Promise<Tag[]> => req("/api/tags"),
|
||||
status: (): Promise<SyncStatus> => req("/api/sync/status"),
|
||||
feed: (f: FeedFilters, offset: number, limit: number): Promise<FeedResponse> =>
|
||||
req(`/api/feed?${feedQuery(f, offset, limit)}`),
|
||||
feed: (f: FeedFilters, cursor: string | null, limit: number): Promise<FeedResponse> =>
|
||||
req(`/api/feed?${feedQuery(f, cursor, limit)}`),
|
||||
feedCount: (f: FeedFilters): Promise<{ count: number }> =>
|
||||
req(`/api/feed/count?${feedQuery(f, 0, 0)}`),
|
||||
req(`/api/feed/count?${filterParams(f).toString()}`),
|
||||
facets: (f: FeedFilters): Promise<{ counts: Record<string, number> }> =>
|
||||
req(`/api/facets?${filterParams(f).toString()}`),
|
||||
// idempotent: setting a state / saving a progress checkpoint is safe to replay, so these
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue