|
|
|
@ -944,6 +944,9 @@ const NoteList = forwardRef( |
|
|
|
}>(() => ({ profiles: new Map(), pending: new Set(), version: 0 })) |
|
|
|
}>(() => ({ profiles: new Map(), pending: new Set(), version: 0 })) |
|
|
|
const feedProfileLoadedRef = useRef<Set<string>>(new Set()) |
|
|
|
const feedProfileLoadedRef = useRef<Set<string>>(new Set()) |
|
|
|
const feedProfileBatchGenRef = useRef(0) |
|
|
|
const feedProfileBatchGenRef = useRef(0) |
|
|
|
|
|
|
|
/** Dedupes layout-time pending sync so a new `events` array reference alone cannot loop setState. */ |
|
|
|
|
|
|
|
const lastProfilePrefetchPubkeysKeyRef = useRef('') |
|
|
|
|
|
|
|
const clientFilteredVisibleCountRef = useRef(0) |
|
|
|
|
|
|
|
|
|
|
|
const noteFeedProfileContextValue = useMemo<NoteFeedProfileContextValue>( |
|
|
|
const noteFeedProfileContextValue = useMemo<NoteFeedProfileContextValue>( |
|
|
|
() => ({ |
|
|
|
() => ({ |
|
|
|
@ -1074,13 +1077,16 @@ const NoteList = forwardRef( |
|
|
|
|
|
|
|
|
|
|
|
useLayoutEffect(() => { |
|
|
|
useLayoutEffect(() => { |
|
|
|
publicReadFallbackAttemptedRef.current = false |
|
|
|
publicReadFallbackAttemptedRef.current = false |
|
|
|
setFeedTimelineEmptyUiReady(false) |
|
|
|
if (!pauseTimelineForPrimaryFreeze) { |
|
|
|
setFeedSubscribeRelayOutcomes([]) |
|
|
|
setFeedTimelineEmptyUiReady(false) |
|
|
|
}, [timelineSubscriptionKey, subRequestsKey, refreshCount]) |
|
|
|
setFeedSubscribeRelayOutcomes([]) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, [timelineSubscriptionKey, subRequestsKey, refreshCount, pauseTimelineForPrimaryFreeze]) |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
useEffect(() => { |
|
|
|
feedProfileBatchGenRef.current += 1 |
|
|
|
feedProfileBatchGenRef.current += 1 |
|
|
|
feedProfileLoadedRef.current.clear() |
|
|
|
feedProfileLoadedRef.current.clear() |
|
|
|
|
|
|
|
lastProfilePrefetchPubkeysKeyRef.current = '' |
|
|
|
setFeedProfileBatch({ profiles: new Map(), pending: new Set(), version: 0 }) |
|
|
|
setFeedProfileBatch({ profiles: new Map(), pending: new Set(), version: 0 }) |
|
|
|
}, [timelineSubscriptionKey, refreshCount]) |
|
|
|
}, [timelineSubscriptionKey, refreshCount]) |
|
|
|
|
|
|
|
|
|
|
|
@ -1093,22 +1099,30 @@ const NoteList = forwardRef( |
|
|
|
for (const e of newEvents) { |
|
|
|
for (const e of newEvents) { |
|
|
|
collectProfilePrefetchPubkeysFromEvent(e, candidates) |
|
|
|
collectProfilePrefetchPubkeysFromEvent(e, candidates) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const pubkeysKey = [...candidates].sort().join('\n') |
|
|
|
|
|
|
|
if (pubkeysKey === lastProfilePrefetchPubkeysKeyRef.current) return |
|
|
|
|
|
|
|
lastProfilePrefetchPubkeysKeyRef.current = pubkeysKey |
|
|
|
|
|
|
|
|
|
|
|
setFeedProfileBatch((prev) => { |
|
|
|
setFeedProfileBatch((prev) => { |
|
|
|
const pending = new Set(prev.pending) |
|
|
|
const pending = new Set(prev.pending) |
|
|
|
let changed = false |
|
|
|
let changed = false |
|
|
|
for (const pk of candidates) { |
|
|
|
for (const pk of candidates) { |
|
|
|
if (!prev.profiles.has(pk) && !pending.has(pk)) { |
|
|
|
if ( |
|
|
|
pending.add(pk) |
|
|
|
prev.profiles.has(pk) || |
|
|
|
changed = true |
|
|
|
pending.has(pk) || |
|
|
|
|
|
|
|
feedProfileLoadedRef.current.has(pk) |
|
|
|
|
|
|
|
) { |
|
|
|
|
|
|
|
continue |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
pending.add(pk) |
|
|
|
|
|
|
|
changed = true |
|
|
|
} |
|
|
|
} |
|
|
|
if (!changed) return prev |
|
|
|
if (!changed) return prev |
|
|
|
// Do not bump `version` here — only the debounced batch + profile merges should notify
|
|
|
|
// Do not bump `version` here — only the debounced batch + profile merges should notify
|
|
|
|
// `useFetchProfile` (via profiles map / pending membership), not every pending-key sync.
|
|
|
|
// `useFetchProfile` (via profiles map / pending membership), not every pending-key sync.
|
|
|
|
return { ...prev, pending } |
|
|
|
return { ...prev, pending } |
|
|
|
}) |
|
|
|
}) |
|
|
|
}, [timelineEventsForFilter, newEvents]) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
const subRequestsRef = useRef(subRequests) |
|
|
|
const subRequestsRef = useRef(subRequests) |
|
|
|
subRequestsRef.current = subRequests |
|
|
|
subRequestsRef.current = subRequests |
|
|
|
@ -1508,6 +1522,10 @@ const NoteList = forwardRef( |
|
|
|
[showFeedClientFilter, applyClientFeedFilter, filteredEvents] |
|
|
|
[showFeedClientFilter, applyClientFeedFilter, filteredEvents] |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
|
|
clientFilteredVisibleCountRef.current = clientFilteredEvents.length |
|
|
|
|
|
|
|
}, [clientFilteredEvents.length]) |
|
|
|
|
|
|
|
|
|
|
|
const visibleNoteIdsForStatsPrefetchKey = useMemo( |
|
|
|
const visibleNoteIdsForStatsPrefetchKey = useMemo( |
|
|
|
() => |
|
|
|
() => |
|
|
|
clientFilteredEvents |
|
|
|
clientFilteredEvents |
|
|
|
@ -1929,6 +1947,10 @@ const NoteList = forwardRef( |
|
|
|
timelineEstablishedCloserRef.current = null |
|
|
|
timelineEstablishedCloserRef.current = null |
|
|
|
|
|
|
|
|
|
|
|
if (pauseTimelineForPrimaryFreeze) { |
|
|
|
if (pauseTimelineForPrimaryFreeze) { |
|
|
|
|
|
|
|
setLoading(false) |
|
|
|
|
|
|
|
if (eventsRef.current.length > 0) { |
|
|
|
|
|
|
|
setFeedTimelineEmptyUiReady(true) |
|
|
|
|
|
|
|
} |
|
|
|
return () => {} |
|
|
|
return () => {} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -3896,11 +3918,15 @@ const NoteList = forwardRef( |
|
|
|
const remaining = currentEvents.length - currentShowCount |
|
|
|
const remaining = currentEvents.length - currentShowCount |
|
|
|
const step = revealBatchSize ?? REVEAL_BATCH_STEP |
|
|
|
const step = revealBatchSize ?? REVEAL_BATCH_STEP |
|
|
|
const increment = Math.min(step, remaining) |
|
|
|
const increment = Math.min(step, remaining) |
|
|
|
setShowCount((prev) => prev + increment) |
|
|
|
const exhausted = bufferExhaustedForVisibleQuotaRef.current |
|
|
|
|
|
|
|
const noVisibleRowsYet = clientFilteredVisibleCountRef.current === 0 |
|
|
|
|
|
|
|
// Revealing more raw buffer rows cannot surface visible cards (aggressive filters / seen-on gate).
|
|
|
|
|
|
|
|
if (!(exhausted && noVisibleRowsYet)) { |
|
|
|
|
|
|
|
setShowCount((prev) => prev + increment) |
|
|
|
|
|
|
|
} |
|
|
|
// `showCount` is a *visible-row quota*, not an offset into the raw merged timeline. Skipping relay
|
|
|
|
// `showCount` is a *visible-row quota*, not an offset into the raw merged timeline. Skipping relay
|
|
|
|
// fetch when `events.length - showCount` is large breaks sparse feeds (e.g. only zap receipts): the
|
|
|
|
// fetch when `events.length - showCount` is large breaks sparse feeds (e.g. only zap receipts): the
|
|
|
|
// buffer can hold many raw events while every visible row is already shown — we must still REQ.
|
|
|
|
// buffer can hold many raw events while every visible row is already shown — we must still REQ.
|
|
|
|
const exhausted = bufferExhaustedForVisibleQuotaRef.current |
|
|
|
|
|
|
|
if ( |
|
|
|
if ( |
|
|
|
!exhausted && |
|
|
|
!exhausted && |
|
|
|
currentEvents.length >= 50 && |
|
|
|
currentEvents.length >= 50 && |
|
|
|
@ -4188,6 +4214,14 @@ const NoteList = forwardRef( |
|
|
|
const ev = eventsRef.current |
|
|
|
const ev = eventsRef.current |
|
|
|
const sc = showCountRef.current |
|
|
|
const sc = showCountRef.current |
|
|
|
if (sc < ev.length || hasMoreRef.current) { |
|
|
|
if (sc < ev.length || hasMoreRef.current) { |
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
|
|
sc < ev.length && |
|
|
|
|
|
|
|
!hasMoreRef.current && |
|
|
|
|
|
|
|
bufferExhaustedForVisibleQuotaRef.current && |
|
|
|
|
|
|
|
clientFilteredVisibleCountRef.current === 0 |
|
|
|
|
|
|
|
) { |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
} |
|
|
|
loadMore() |
|
|
|
loadMore() |
|
|
|
} |
|
|
|
} |
|
|
|
}, options) |
|
|
|
}, options) |
|
|
|
|