@ -2,7 +2,7 @@ import NewNotesButton from '@/components/NewNotesButton'
import { Button } from '@/components/ui/button'
import { Button } from '@/components/ui/button'
import { ExtendedKind , FIRST_RELAY_RESULT_GRACE_MS } from '@/constants'
import { ExtendedKind , FIRST_RELAY_RESULT_GRACE_MS } from '@/constants'
import {
import {
getEmbeddedNoteBech32Id s,
collectEmbeddedEventPrefetchTarget s,
getReplaceableCoordinateFromEvent ,
getReplaceableCoordinateFromEvent ,
isMentioningMutedUsers ,
isMentioningMutedUsers ,
isReplaceableEvent ,
isReplaceableEvent ,
@ -566,18 +566,22 @@ const NoteList = forwardRef(
const evs = lastEventsForTimelinePrefetchRef . current
const evs = lastEventsForTimelinePrefetchRef . current
if ( evs . length === 0 ) return
if ( evs . length === 0 ) return
const initialEmbeddedEventIds = new Set < string > ( )
const { hexIds , nip19Pointers } = mergePrefetchTargetsFromEvents ( evs . slice ( 0 , 50 ) )
evs . slice ( 0 , 50 ) . forEach ( ( ev : Event ) = > {
const hexIdsToFetch = hexIds . filter ( ( id ) = > ! prefetchedEventIdsRef . current . has ( id ) )
extractEmbeddedEventIds ( ev ) . forEach ( ( id : string ) = > initialEmbeddedEventIds . add ( id ) )
const nip19ToFetch = nip19Pointers . filter ( ( p ) = > ! prefetchedEventIdsRef . current . has ( p ) )
} )
if ( hexIdsToFetch . length > 0 || nip19ToFetch . length > 0 ) {
const eventIdsToFetch = Array . from ( initialEmbeddedEventIds ) . filter (
hexIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . add ( id ) )
( id ) = > ! prefetchedEventIdsRef . current . has ( id )
nip19ToFetch . forEach ( ( p ) = > prefetchedEventIdsRef . current . add ( p ) )
)
const run = async ( ) = > {
if ( eventIdsToFetch . length > 0 ) {
try {
eventIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . add ( id ) )
await client . prefetchHexEventIds ( hexIdsToFetch )
Promise . all ( eventIdsToFetch . map ( ( id ) = > client . fetchEvent ( id ) ) ) . catch ( ( ) = > {
await Promise . all ( nip19ToFetch . map ( ( p ) = > client . fetchEvent ( p ) ) )
eventIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . delete ( id ) )
} catch {
} )
hexIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . delete ( id ) )
nip19ToFetch . forEach ( ( p ) = > prefetchedEventIdsRef . current . delete ( p ) )
}
}
void run ( )
}
}
} , 450 )
} , 450 )
} else if ( eosed ) {
} else if ( eosed ) {
@ -874,25 +878,22 @@ const NoteList = forwardRef(
}
}
schedulePrefetch ( ( ) = > {
schedulePrefetch ( ( ) = > {
// CRITICAL: Prefetch embedded events for newly loaded events (throttled)
const { hexIds , nip19Pointers } = mergePrefetchTargetsFromEvents ( newEvents . slice ( 0 , 30 ) )
const newEmbeddedEventIds = new Set < string > ( )
const hexIdsToFetch = hexIds . filter ( ( id ) = > ! prefetchedEventIdsRef . current . has ( id ) )
// Only prefetch for first 30 events to reduce load
const nip19ToFetch = nip19Pointers . filter ( ( p ) = > ! prefetchedEventIdsRef . current . has ( p ) )
newEvents . slice ( 0 , 30 ) . forEach ( ( ev ) = > {
if ( hexIdsToFetch . length === 0 && nip19ToFetch . length === 0 ) return
const embeddedIds = extractEmbeddedEventIds ( ev )
hexIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . add ( id ) )
embeddedIds . forEach ( ( id ) = > newEmbeddedEventIds . add ( id ) )
nip19ToFetch . forEach ( ( p ) = > prefetchedEventIdsRef . current . add ( p ) )
} )
const run = async ( ) = > {
const eventIdsToFetch = Array . from ( newEmbeddedEventIds ) . filter (
try {
( id ) = > ! prefetchedEventIdsRef . current . has ( id )
await client . prefetchHexEventIds ( hexIdsToFetch )
)
await Promise . all ( nip19ToFetch . map ( ( p ) = > client . fetchEvent ( p ) ) )
if ( eventIdsToFetch . length > 0 ) {
} catch {
// Mark as prefetched immediately to prevent duplicate requests
hexIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . delete ( id ) )
eventIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . add ( id ) )
nip19ToFetch . forEach ( ( p ) = > prefetchedEventIdsRef . current . delete ( p ) )
// Batch fetch embedded events in background (non-blocking)
}
Promise . all ( eventIdsToFetch . map ( ( id ) = > client . fetchEvent ( id ) ) ) . catch ( ( ) = > {
// On error, remove from prefetched set so we can retry later
eventIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . delete ( id ) )
} )
}
}
void run ( )
} )
} )
}
}
} catch ( _error ) {
} catch ( _error ) {
@ -942,41 +943,15 @@ const NoteList = forwardRef(
const prefetchedEventIdsRef = useRef < Set < string > > ( new Set ( ) )
const prefetchedEventIdsRef = useRef < Set < string > > ( new Set ( ) )
const prefetchEmbeddedEventsTimeoutRef = useRef < NodeJS.Timeout | null > ( null )
const prefetchEmbeddedEventsTimeoutRef = useRef < NodeJS.Timeout | null > ( null )
// Helper function to extract all embedded event IDs from an event
const mergePrefetchTargetsFromEvents = useCallback ( ( evts : Event [ ] ) = > {
const extractEmbeddedEventIds = useCallback ( ( evt : Event ) : string [ ] = > {
const hex = new Set < string > ( )
const eventIds : string [ ] = [ ]
const nip19 = new Set < string > ( )
for ( const e of evts ) {
// 1. Extract from 'e' tags (event references)
const t = collectEmbeddedEventPrefetchTargets ( e )
evt . tags
t . hexIds . forEach ( ( id ) = > hex . add ( id ) )
. filter ( ( tag ) = > tag [ 0 ] === 'e' && tag [ 1 ] && tag [ 1 ] . length === 64 )
t . nip19Pointers . forEach ( ( p ) = > nip19 . add ( p ) )
. forEach ( ( tag ) = > {
}
const eventId = tag [ 1 ]
return { hexIds : Array.from ( hex ) , nip19Pointers : Array.from ( nip19 ) }
if ( eventId && /^[0-9a-f]{64}$/ . test ( eventId ) ) {
eventIds . push ( eventId )
}
} )
// 2. Extract from 'a' tags (addressable events) - get event ID if present
evt . tags
. filter ( ( tag ) = > tag [ 0 ] === 'a' && tag [ 3 ] ) // tag[3] is the event ID for version tracking
. forEach ( ( tag ) = > {
const eventId = tag [ 3 ]
if ( eventId && /^[0-9a-f]{64}$/ . test ( eventId ) ) {
eventIds . push ( eventId )
}
} )
// 3. Extract from content (nostr: links)
// Note: getEmbeddedNoteBech32Ids returns hex IDs (despite the name)
const embeddedNoteIds = getEmbeddedNoteBech32Ids ( evt )
embeddedNoteIds . forEach ( ( id ) = > {
// The function already returns hex IDs, so use them directly
if ( id && /^[0-9a-f]{64}$/ . test ( id ) ) {
eventIds . push ( id )
}
} )
return Array . from ( new Set ( eventIds ) ) // Deduplicate
} , [ ] )
} , [ ] )
// CRITICAL: Prefetch embedded events for visible events
// CRITICAL: Prefetch embedded events for visible events
@ -989,39 +964,22 @@ const NoteList = forwardRef(
// Debounce embedded event prefetching by 400ms to reduce frequency during rapid scrolling
// Debounce embedded event prefetching by 400ms to reduce frequency during rapid scrolling
prefetchEmbeddedEventsTimeoutRef . current = setTimeout ( ( ) = > {
prefetchEmbeddedEventsTimeoutRef . current = setTimeout ( ( ) = > {
// Extract embedded event IDs from visible events (first 40, reduced to reduce load)
const visibleTargets = mergePrefetchTargetsFromEvents ( filteredEvents . slice ( 0 , 40 ) )
const visibleEmbeddedEventIds = new Set < string > ( )
const upcomingTargets = mergePrefetchTargetsFromEvents ( events . slice ( 0 , 80 ) )
filteredEvents . slice ( 0 , 40 ) . forEach ( ( ev ) = > {
const hexIds = Array . from (
const embeddedIds = extractEmbeddedEventIds ( ev )
new Set ( [ . . . visibleTargets . hexIds , . . . upcomingTargets . hexIds ] )
embeddedIds . forEach ( ( id ) = > visibleEmbeddedEventIds . add ( id ) )
} )
// Also extract from upcoming events (next 80, reduced to reduce load)
const upcomingEmbeddedEventIds = new Set < string > ( )
events . slice ( 0 , 80 ) . forEach ( ( ev ) = > {
const embeddedIds = extractEmbeddedEventIds ( ev )
embeddedIds . forEach ( ( id ) = > upcomingEmbeddedEventIds . add ( id ) )
} )
// Combine visible and upcoming
const allEmbeddedEventIds = Array . from (
new Set ( [ . . . visibleEmbeddedEventIds , . . . upcomingEmbeddedEventIds ] )
)
)
const nip19Pointers = Array . from (
if ( allEmbeddedEventIds . length === 0 ) return
new Set ( [ . . . visibleTargets . nip19Pointers , . . . upcomingTargets . nip19Pointers ] )
// Filter out already prefetched event IDs
const eventIdsToFetch = allEmbeddedEventIds . filter (
( id ) = > ! prefetchedEventIdsRef . current . has ( id )
)
)
if ( eventIdsToFetch . length === 0 ) return
const hexIdsToFetch = hexIds . filter ( ( id ) = > ! prefetchedEventIdsRef . current . has ( id ) )
const nip19ToFetch = nip19Pointers . filter ( ( p ) = > ! prefetchedEventIdsRef . current . has ( p ) )
// Mark as prefetched immediately to prevent duplicate requests
if ( hexIdsToFetch . length === 0 && nip19ToFetch . length === 0 ) return
eventIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . add ( id ) )
hexIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . add ( id ) )
// Batch fetch embedded events in background (non-blocking)
nip19ToFetch . forEach ( ( p ) = > prefetchedEventIdsRef . current . add ( p ) )
// Use requestIdleCallback if available to avoid blocking scroll
const scheduleFetch = ( callback : ( ) = > void ) = > {
const scheduleFetch = ( callback : ( ) = > void ) = > {
if ( typeof requestIdleCallback !== 'undefined' ) {
if ( typeof requestIdleCallback !== 'undefined' ) {
requestIdleCallback ( callback , { timeout : 500 } )
requestIdleCallback ( callback , { timeout : 500 } )
@ -1029,12 +987,18 @@ const NoteList = forwardRef(
setTimeout ( callback , 0 )
setTimeout ( callback , 0 )
}
}
}
}
scheduleFetch ( ( ) = > {
scheduleFetch ( ( ) = > {
Promise . all ( eventIdsToFetch . map ( ( id ) = > client . fetchEvent ( id ) ) ) . catch ( ( ) = > {
const run = async ( ) = > {
// On error, remove from prefetched set so we can retry later
try {
eventIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . delete ( id ) )
await client . prefetchHexEventIds ( hexIdsToFetch )
} )
await Promise . all ( nip19ToFetch . map ( ( p ) = > client . fetchEvent ( p ) ) )
} catch {
hexIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . delete ( id ) )
nip19ToFetch . forEach ( ( p ) = > prefetchedEventIdsRef . current . delete ( p ) )
}
}
void run ( )
} )
} )
} , 400 ) // Debounce by 400ms to reduce frequency during rapid scrolling
} , 400 ) // Debounce by 400ms to reduce frequency during rapid scrolling
@ -1044,7 +1008,7 @@ const NoteList = forwardRef(
prefetchEmbeddedEventsTimeoutRef . current = null
prefetchEmbeddedEventsTimeoutRef . current = null
}
}
}
}
} , [ filteredEvents , events , extractEmbeddedEventId s] )
} , [ filteredEvents , events , mergePrefetchTargetsFromEvent s] )
// Also prefetch when loading more events (scrolling down)
// Also prefetch when loading more events (scrolling down)
// Throttled to reduce frequency during rapid scrolling
// Throttled to reduce frequency during rapid scrolling
@ -1059,34 +1023,36 @@ const NoteList = forwardRef(
// Debounce embedded-event prefetch for newly revealed rows (profiles use NoteFeed batcher above)
// Debounce embedded-event prefetch for newly revealed rows (profiles use NoteFeed batcher above)
prefetchNewEventsTimeoutRef . current = setTimeout ( ( ) = > {
prefetchNewEventsTimeoutRef . current = setTimeout ( ( ) = > {
// CRITICAL: Prefetch embedded events for newly loaded events (reduced scope)
const { hexIds , nip19Pointers } = mergePrefetchTargetsFromEvents (
const newlyLoadedEmbeddedEventIds = new Set < string > ( )
events . slice ( showCount , showCount + 50 )
events . slice ( showCount , showCount + 50 ) . forEach ( ( ev ) = > {
const embeddedIds = extractEmbeddedEventIds ( ev )
embeddedIds . forEach ( ( id ) = > newlyLoadedEmbeddedEventIds . add ( id ) )
} )
const eventIdsToFetch = Array . from ( newlyLoadedEmbeddedEventIds ) . filter (
( id ) = > ! prefetchedEventIdsRef . current . has ( id )
)
)
if ( eventIdsToFetch . length > 0 ) {
const hexIdsToFetch = hexIds . filter ( ( id ) = > ! prefetchedEventIdsRef . current . has ( id ) )
// Mark as prefetched immediately to prevent duplicate requests
const nip19ToFetch = nip19Pointers . filter ( ( p ) = > ! prefetchedEventIdsRef . current . has ( p ) )
eventIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . add ( id ) )
if ( hexIdsToFetch . length === 0 && nip19ToFetch . length === 0 ) return
// Batch fetch embedded events in background (non-blocking) using requestIdleCallback
const scheduleFetch = ( callback : ( ) = > void ) = > {
hexIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . add ( id ) )
if ( typeof requestIdleCallback !== 'undefined' ) {
nip19ToFetch . forEach ( ( p ) = > prefetchedEventIdsRef . current . add ( p ) )
requestIdleCallback ( callback , { timeout : 500 } )
} else {
const scheduleFetch = ( callback : ( ) = > void ) = > {
setTimeout ( callback , 0 )
if ( typeof requestIdleCallback !== 'undefined' ) {
}
requestIdleCallback ( callback , { timeout : 500 } )
} else {
setTimeout ( callback , 0 )
}
}
scheduleFetch ( ( ) = > {
Promise . all ( eventIdsToFetch . map ( ( id ) = > client . fetchEvent ( id ) ) ) . catch ( ( ) = > {
// On error, remove from prefetched set so we can retry later
eventIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . delete ( id ) )
} )
} )
}
}
scheduleFetch ( ( ) = > {
const run = async ( ) = > {
try {
await client . prefetchHexEventIds ( hexIdsToFetch )
await Promise . all ( nip19ToFetch . map ( ( p ) = > client . fetchEvent ( p ) ) )
} catch {
hexIdsToFetch . forEach ( ( id ) = > prefetchedEventIdsRef . current . delete ( id ) )
nip19ToFetch . forEach ( ( p ) = > prefetchedEventIdsRef . current . delete ( p ) )
}
}
void run ( )
} )
} , 400 ) // Debounce by 400ms to reduce frequency during rapid scrolling
} , 400 ) // Debounce by 400ms to reduce frequency during rapid scrolling
return ( ) = > {
return ( ) = > {
@ -1095,7 +1061,7 @@ const NoteList = forwardRef(
prefetchNewEventsTimeoutRef . current = null
prefetchNewEventsTimeoutRef . current = null
}
}
}
}
} , [ events . length , showCount , loading , hasMore ] )
} , [ events . length , showCount , loading , hasMore , mergePrefetchTargetsFromEvents ] )
const showNewEvents = ( ) = > {
const showNewEvents = ( ) = > {
setEvents ( ( oldEvents ) = > [ . . . newEvents , . . . oldEvents ] )
setEvents ( ( oldEvents ) = > [ . . . newEvents , . . . oldEvents ] )