From 7b6f67f42ea602eade6fb585f396baf9b5983356 Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Tue, 19 May 2026 17:50:37 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20feed=20flicker=20=E2=80=94=20fixed-heigh?= =?UTF-8?q?t=20quote=20box=20+=20content-aware=20row=20estimate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two remaining causes of upward-scroll flicker in the virtualized feed: - QuotePreview rendered nothing until fetchNoteById resolved, then popped in a box — a 70-200px late growth that desynced the virtualizer every time a quote note mounted. It now renders a fixed 80px (h-20) box, reserved whether or not the quoted note has loaded; a 2-line clamp keeps resolved content inside it. - estimateSize was a flat 140px, so every row snapped from 140 to its real height (96-1660px) on first measurement. New estimateNoteHeight predicts height from note content (text, media boxes, quote box) so the first-measure correction is small enough to be imperceptible. Remaining: a <=25px transient on some notes' first few views (a flex-wrap row reflowing as async content lands); self-heals as caches warm. --- src/components/feed/Feed.tsx | 11 +++- src/components/feed/NoteContent.tsx | 35 +++++++----- src/lib/feedEstimate.ts | 86 +++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 src/lib/feedEstimate.ts diff --git a/src/components/feed/Feed.tsx b/src/components/feed/Feed.tsx index d1b686a..847be9d 100644 --- a/src/components/feed/Feed.tsx +++ b/src/components/feed/Feed.tsx @@ -8,6 +8,7 @@ import { useWoTStore } from "../../stores/wot"; import { fetchFollowFeed, getNDK, ensureConnected } from "../../lib/nostr"; import { diagWrapFetch, logDiag } from "../../lib/feedDiagnostics"; import { detectScript, getEventLanguageTag, FILTER_SCRIPTS } from "../../lib/language"; +import { estimateNoteHeight } from "../../lib/feedEstimate"; import { NoteCard } from "./NoteCard"; import { ArticleCard } from "../article/ArticleCard"; import { ComposeBox } from "./ComposeBox"; @@ -147,7 +148,15 @@ export function Feed() { const virtualizer = useVirtualizer({ count: filteredNotes.length, getScrollElement: () => scrollRef.current, - estimateSize: () => 140, + // Content-aware estimate: a flat estimate makes every row snap to its real + // height on first measurement → upward-scroll flicker. estimateNoteHeight + // predicts the height from the note's content so the snap is negligible. + estimateSize: (index) => { + const e = filteredNotes[index]; + if (!e) return 140; + // content column = scroll width minus card padding (32) + avatar col (48) + return estimateNoteHeight(e, (scrollRef.current?.clientWidth ?? 600) - 80); + }, overscan: 6, // Key measurements by note id, not list index. The feed list mutates order // constantly — new notes prepend (flushPendingNotes), the WoT filter removes diff --git a/src/components/feed/NoteContent.tsx b/src/components/feed/NoteContent.tsx index 0497f2e..0dc7505 100644 --- a/src/components/feed/NoteContent.tsx +++ b/src/components/feed/NoteContent.tsx @@ -93,25 +93,32 @@ function QuotePreview({ eventId }: { eventId: string }) { fetchNoteById(eventId).then(setEvent); }, [eventId]); - if (!event) return null; - const rawName = profile?.displayName || profile?.name; - const name = (typeof rawName === "string" ? rawName : null) || shortenPubkey(event.pubkey); - const preview = event.content.slice(0, 160) + (event.content.length > 160 ? "…" : ""); + const name = (typeof rawName === "string" ? rawName : null) || (event ? shortenPubkey(event.pubkey) : ""); + const preview = event ? event.content.slice(0, 160) + (event.content.length > 160 ? "…" : "") : ""; + // Fixed-height box, reserved whether or not the quoted note has resolved. + // QuotePreview used to render null until fetchNoteById resolved, then pop in + // a box — that late growth (~70-200px) desynced the virtualizer and was the + // main cause of upward-scroll flicker. The box height never changes now; the + // 2-line clamp keeps resolved content inside the reserved space. return (
{preview}
+ {event && ( + <> +{preview}
+ > + )}