diff --git a/src/components/Feed.tsx b/src/components/Feed.tsx index ebba592c..7b3446cc 100644 --- a/src/components/Feed.tsx +++ b/src/components/Feed.tsx @@ -9,7 +9,8 @@ import { Button } from '@/components/ui/button'; import { Loader2 } from 'lucide-react'; import LoginDialog from '@/components/auth/LoginDialog'; import SignupDialog from '@/components/auth/SignupDialog'; -import { useFeed } from '@/hooks/useFeed'; +import { useNostr } from '@nostrify/react'; +import { useFeed, prefetchFeedData } from '@/hooks/useFeed'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { cn } from '@/lib/utils'; import type { FeedItem } from '@/hooks/useFeed'; @@ -27,6 +28,7 @@ export function Feed() { } }, [user]); + const { nostr } = useNostr(); const queryClient = useQueryClient(); const { @@ -67,6 +69,15 @@ export function Feed() { return items; }, [data?.pages]); + // After each new page renders, prefetch authors and stats in the background. + // Runs after render so it never blocks pagination — the 400px early sentinel + // gives enough lead time for profiles to arrive before the user scrolls to them. + useEffect(() => { + if (feedItems.length > 0) { + prefetchFeedData(feedItems, nostr, queryClient); + } + }, [feedItems, nostr, queryClient]); + const handleLogin = () => { setLoginDialogOpen(false); setSignupDialogOpen(false); diff --git a/src/hooks/useFeed.ts b/src/hooks/useFeed.ts index cb735630..05d78e58 100644 --- a/src/hooks/useFeed.ts +++ b/src/hooks/useFeed.ts @@ -41,15 +41,15 @@ function parseRepostContent(repost: NostrEvent): NostrEvent | undefined { } /** - * Fire-and-forget: seed ['author', pubkey] and ['event-stats', id] caches - * for a page of feed items so that NoteCard's per-card hooks resolve from - * cache rather than firing individual relay queries. + * Prefetch author profiles and interaction stats for a set of feed items, + * seeding the TanStack Query cache so per-card hooks resolve without opening + * individual relay subscriptions. * - * Runs in the background — does NOT block the queryFn from returning items. - * Includes post authors, repost authors, and mentioned p-tag pubkeys so that - * @mentions and reply-to lines also resolve without individual round trips. + * Called from a useEffect in Feed.tsx — runs after render, never blocks + * the queryFn. Authors use a 1500ms timeout (partial results are fine); + * stats are fully fire-and-forget. */ -async function prefetchPageData( +export function prefetchFeedData( items: FeedItem[], nostr: ReturnType['nostr'], queryClient: ReturnType, @@ -71,11 +71,8 @@ async function prefetchPageData( .map((item) => item.event.id) .filter((id) => queryClient.getQueryData(['event-stats', id]) === undefined); - // Await author prefetch with a short deadline — whatever profiles arrive - // in time get cached; cards whose authors miss the window fall back to - // individual useAuthor queries (acceptable for a minority of cards). if (pubkeysToFetch.length > 0) { - await nostr.query( + nostr.query( [{ kinds: [0], authors: pubkeysToFetch }], { signal: AbortSignal.timeout(1500) }, ).then((profileEvents) => { @@ -87,7 +84,6 @@ async function prefetchPageData( }).catch(() => {}); } - // Stats are fire-and-forget — nice to have pre-cached but not worth blocking on. if (eventIdsToFetch.length > 0) { nostr.query( [ @@ -203,11 +199,6 @@ export function useFeed(tab: 'follows' | 'global') { .map((ev) => ({ event: ev, sortTimestamp: ev.created_at })); } - // Prefetch authors and stats in parallel before returning. - // This ensures cache is populated before NoteCards mount, so individual - // useAuthor/useEventStats calls hit cache and never open relay subscriptions. - await prefetchPageData(items, nostr, queryClient); - return items; }, getNextPageParam: (lastPage) => {