Feed virtualization Stage 2: infinite scroll

Adds older-notes pagination to the Global feed. fetchGlobalFeed gains an
optional until param (fetch events before a timestamp, no since bound).
The feed store's new loadOlderNotes action fetches 50 older notes when
triggered, dedups, merges, re-sorts. MAX_FEED_SIZE raised 200 to 1000 —
virtualization bounds the DOM-node/bitmap count, so this just caps the JS
notes array as hygiene.

Feed.tsx auto-triggers loadOlderNotes when the user scrolls within ~8 rows
of the bottom; a 'Loading older notes' row shows during the fetch. Global
tab only (Following uses a separate path, Trending is a ranked snapshot).

Pending: sandboxed memory verification.
This commit is contained in:
Jure
2026-05-17 20:26:15 +02:00
parent 73c1bd1ac9
commit 7678ad2f1f
3 changed files with 73 additions and 7 deletions
+19 -1
View File
@@ -34,6 +34,8 @@ export function Feed() {
const connect = useFeedStore((s) => s.connect);
const loadCachedFeed = useFeedStore((s) => s.loadCachedFeed);
const loadFeed = useFeedStore((s) => s.loadFeed);
const loadOlderNotes = useFeedStore((s) => s.loadOlderNotes);
const loadingOlder = useFeedStore((s) => s.loadingOlder);
const trendingNotes = useFeedStore((s) => s.trendingNotes);
const trendingLoading = useFeedStore((s) => s.trendingLoading);
const loadTrendingFeed = useFeedStore((s) => s.loadTrendingFeed);
@@ -166,6 +168,18 @@ export function Feed() {
if (focusedNoteIndex >= 0) virtualizer.scrollToIndex(focusedNoteIndex, { align: "center" });
}, [focusedNoteIndex, virtualizer]);
// Infinite scroll (Global tab only): when the user nears the bottom of the
// virtualized list, load the next page of older notes. loadOlderNotes
// self-guards against concurrent calls and end-of-feed.
const virtualItems = virtualizer.getVirtualItems();
useEffect(() => {
if (tab !== "global") return;
const last = virtualItems[virtualItems.length - 1];
if (last && last.index >= filteredNotes.length - 8) {
loadOlderNotes();
}
}, [virtualItems, filteredNotes.length, tab, loadOlderNotes]);
return (
<div className="h-full flex flex-col">
{/* Header */}
@@ -307,7 +321,7 @@ export function Feed() {
{/* Virtualized list — only the visible window of cards stays in the DOM */}
{filteredNotes.length > 0 && (
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative", width: "100%" }}>
{virtualizer.getVirtualItems().map((vi) => {
{virtualItems.map((vi) => {
const event = filteredNotes[vi.index];
return (
<div
@@ -326,6 +340,10 @@ export function Feed() {
})}
</div>
)}
{tab === "global" && loadingOlder && (
<div className="py-4 text-center text-text-dim text-[12px]">Loading older notes</div>
)}
</div>
</div>
);