|
|
|
|
@ -1494,8 +1494,6 @@ const NoteList = forwardRef(
@@ -1494,8 +1494,6 @@ const NoteList = forwardRef(
|
|
|
|
|
[showFeedClientFilter, applyClientFeedFilter, filteredEvents] |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
/** Bumps when {@link noteStatsService} updates any visible row so profile batch can include boosters/likers. */ |
|
|
|
|
const [feedStatsProfileBump, setFeedStatsProfileBump] = useState(0) |
|
|
|
|
const visibleNoteIdsForStatsPrefetchKey = useMemo( |
|
|
|
|
() => |
|
|
|
|
clientFilteredEvents |
|
|
|
|
@ -1505,80 +1503,9 @@ const NoteList = forwardRef(
@@ -1505,80 +1503,9 @@ const NoteList = forwardRef(
|
|
|
|
|
[clientFilteredEvents, showCount] |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
if (!visibleNoteIdsForStatsPrefetchKey) return |
|
|
|
|
const ids = visibleNoteIdsForStatsPrefetchKey.split('\n').filter(Boolean) |
|
|
|
|
const bump = () => setFeedStatsProfileBump((n) => n + 1) |
|
|
|
|
const unsubs = ids.map((id) => noteStatsService.subscribeNoteStats(id, bump)) |
|
|
|
|
return () => { |
|
|
|
|
unsubs.forEach((u) => u()) |
|
|
|
|
} |
|
|
|
|
}, [visibleNoteIdsForStatsPrefetchKey]) |
|
|
|
|
|
|
|
|
|
const clientFilteredNewEvents = useMemo( |
|
|
|
|
() => |
|
|
|
|
showFeedClientFilter ? applyClientFeedFilter(filteredNewEvents) : filteredNewEvents, |
|
|
|
|
[showFeedClientFilter, applyClientFeedFilter, filteredNewEvents] |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
const feedClientFilterActive = useMemo( |
|
|
|
|
() => |
|
|
|
|
!!( |
|
|
|
|
showFeedClientFilter && |
|
|
|
|
(feedClientSearch.trim() || |
|
|
|
|
(feedClientAuthorMode === 'me' && !!pubkey) || |
|
|
|
|
(feedClientAuthorMode === 'npub' && feedClientAuthorNpubInput.trim() !== '') || |
|
|
|
|
feedClientKindInput.trim() !== '' || |
|
|
|
|
feedClientMinCreatedAt !== null) |
|
|
|
|
), |
|
|
|
|
[ |
|
|
|
|
showFeedClientFilter, |
|
|
|
|
feedClientSearch, |
|
|
|
|
feedClientAuthorMode, |
|
|
|
|
feedClientAuthorNpubInput, |
|
|
|
|
feedClientKindInput, |
|
|
|
|
pubkey, |
|
|
|
|
feedClientMinCreatedAt |
|
|
|
|
] |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
useLayoutEffect(() => { |
|
|
|
|
if (!onSpellFeedFirstPaint || spellFeedInstrumentToken === undefined) return |
|
|
|
|
if (filteredEvents.length === 0) return |
|
|
|
|
const first = filteredEvents[0] |
|
|
|
|
if (!first) return |
|
|
|
|
const fpKey = `${spellFeedInstrumentToken}|${timelineSubscriptionKey ?? ''}` |
|
|
|
|
if (spellFeedFirstPaintLoggedKeyRef.current === fpKey) return |
|
|
|
|
spellFeedFirstPaintLoggedKeyRef.current = fpKey |
|
|
|
|
onSpellFeedFirstPaint({ |
|
|
|
|
eventCount: filteredEvents.length, |
|
|
|
|
firstEventId: first.id |
|
|
|
|
}) |
|
|
|
|
}, [ |
|
|
|
|
onSpellFeedFirstPaint, |
|
|
|
|
spellFeedInstrumentToken, |
|
|
|
|
timelineSubscriptionKey, |
|
|
|
|
filteredEvents.length, |
|
|
|
|
filteredEvents[0]?.id |
|
|
|
|
]) |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
const handle = window.setTimeout(() => { |
|
|
|
|
const gen = feedProfileBatchGenRef.current |
|
|
|
|
const candidates = new Set<string>() |
|
|
|
|
for (const e of timelineEventsForFilter) { |
|
|
|
|
collectProfilePrefetchPubkeysFromEvent(e, candidates) |
|
|
|
|
} |
|
|
|
|
for (const e of newEvents) { |
|
|
|
|
collectProfilePrefetchPubkeysFromEvent(e, candidates) |
|
|
|
|
} |
|
|
|
|
for (const e of clientFilteredEvents.slice(0, Math.min(120, Math.max(showCount + 64, 64)))) { |
|
|
|
|
collectProfilePrefetchPubkeysFromNoteStats(noteStatsService.getNoteStats(e.id), candidates) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const need = [...candidates].filter((pk) => !feedProfileLoadedRef.current.has(pk)) |
|
|
|
|
const enqueueFeedProfilePubkeys = useCallback((need: string[]) => { |
|
|
|
|
if (need.length === 0) return |
|
|
|
|
|
|
|
|
|
const gen = feedProfileBatchGenRef.current |
|
|
|
|
need.forEach((pk) => feedProfileLoadedRef.current.add(pk)) |
|
|
|
|
|
|
|
|
|
setFeedProfileBatch((prev) => { |
|
|
|
|
@ -1647,9 +1574,124 @@ const NoteList = forwardRef(
@@ -1647,9 +1574,124 @@ const NoteList = forwardRef(
|
|
|
|
|
return { profiles: next, pending: pend, version: prev.version + 1 } |
|
|
|
|
}) |
|
|
|
|
})() |
|
|
|
|
}, []) |
|
|
|
|
|
|
|
|
|
const statsProfilePrefetchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null) |
|
|
|
|
const pendingStatsProfilePubkeysRef = useRef<Set<string>>(new Set()) |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
if (!visibleNoteIdsForStatsPrefetchKey) return |
|
|
|
|
const ids = visibleNoteIdsForStatsPrefetchKey.split('\n').filter(Boolean) |
|
|
|
|
|
|
|
|
|
const flushStatsProfiles = () => { |
|
|
|
|
statsProfilePrefetchDebounceRef.current = null |
|
|
|
|
const need = [...pendingStatsProfilePubkeysRef.current].filter( |
|
|
|
|
(pk) => !feedProfileLoadedRef.current.has(pk) |
|
|
|
|
) |
|
|
|
|
pendingStatsProfilePubkeysRef.current.clear() |
|
|
|
|
enqueueFeedProfilePubkeys(need) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const onStatsUpdate = (noteId: string) => { |
|
|
|
|
const candidates = new Set<string>() |
|
|
|
|
collectProfilePrefetchPubkeysFromNoteStats(noteStatsService.getNoteStats(noteId), candidates) |
|
|
|
|
for (const pk of candidates) { |
|
|
|
|
if (!feedProfileLoadedRef.current.has(pk)) { |
|
|
|
|
pendingStatsProfilePubkeysRef.current.add(pk) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if (pendingStatsProfilePubkeysRef.current.size === 0) return |
|
|
|
|
if (statsProfilePrefetchDebounceRef.current) { |
|
|
|
|
clearTimeout(statsProfilePrefetchDebounceRef.current) |
|
|
|
|
} |
|
|
|
|
statsProfilePrefetchDebounceRef.current = setTimeout( |
|
|
|
|
flushStatsProfiles, |
|
|
|
|
FEED_PROFILE_BATCH_DEBOUNCE_MS |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const unsubs = ids.map((id) => noteStatsService.subscribeNoteStats(id, () => onStatsUpdate(id))) |
|
|
|
|
return () => { |
|
|
|
|
unsubs.forEach((u) => u()) |
|
|
|
|
if (statsProfilePrefetchDebounceRef.current) { |
|
|
|
|
clearTimeout(statsProfilePrefetchDebounceRef.current) |
|
|
|
|
statsProfilePrefetchDebounceRef.current = null |
|
|
|
|
} |
|
|
|
|
pendingStatsProfilePubkeysRef.current.clear() |
|
|
|
|
} |
|
|
|
|
}, [visibleNoteIdsForStatsPrefetchKey, enqueueFeedProfilePubkeys]) |
|
|
|
|
|
|
|
|
|
const clientFilteredNewEvents = useMemo( |
|
|
|
|
() => |
|
|
|
|
showFeedClientFilter ? applyClientFeedFilter(filteredNewEvents) : filteredNewEvents, |
|
|
|
|
[showFeedClientFilter, applyClientFeedFilter, filteredNewEvents] |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
const feedClientFilterActive = useMemo( |
|
|
|
|
() => |
|
|
|
|
!!( |
|
|
|
|
showFeedClientFilter && |
|
|
|
|
(feedClientSearch.trim() || |
|
|
|
|
(feedClientAuthorMode === 'me' && !!pubkey) || |
|
|
|
|
(feedClientAuthorMode === 'npub' && feedClientAuthorNpubInput.trim() !== '') || |
|
|
|
|
feedClientKindInput.trim() !== '' || |
|
|
|
|
feedClientMinCreatedAt !== null) |
|
|
|
|
), |
|
|
|
|
[ |
|
|
|
|
showFeedClientFilter, |
|
|
|
|
feedClientSearch, |
|
|
|
|
feedClientAuthorMode, |
|
|
|
|
feedClientAuthorNpubInput, |
|
|
|
|
feedClientKindInput, |
|
|
|
|
pubkey, |
|
|
|
|
feedClientMinCreatedAt |
|
|
|
|
] |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
useLayoutEffect(() => { |
|
|
|
|
if (!onSpellFeedFirstPaint || spellFeedInstrumentToken === undefined) return |
|
|
|
|
if (filteredEvents.length === 0) return |
|
|
|
|
const first = filteredEvents[0] |
|
|
|
|
if (!first) return |
|
|
|
|
const fpKey = `${spellFeedInstrumentToken}|${timelineSubscriptionKey ?? ''}` |
|
|
|
|
if (spellFeedFirstPaintLoggedKeyRef.current === fpKey) return |
|
|
|
|
spellFeedFirstPaintLoggedKeyRef.current = fpKey |
|
|
|
|
onSpellFeedFirstPaint({ |
|
|
|
|
eventCount: filteredEvents.length, |
|
|
|
|
firstEventId: first.id |
|
|
|
|
}) |
|
|
|
|
}, [ |
|
|
|
|
onSpellFeedFirstPaint, |
|
|
|
|
spellFeedInstrumentToken, |
|
|
|
|
timelineSubscriptionKey, |
|
|
|
|
filteredEvents.length, |
|
|
|
|
filteredEvents[0]?.id |
|
|
|
|
]) |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
const handle = window.setTimeout(() => { |
|
|
|
|
const candidates = new Set<string>() |
|
|
|
|
for (const e of timelineEventsForFilter) { |
|
|
|
|
collectProfilePrefetchPubkeysFromEvent(e, candidates) |
|
|
|
|
} |
|
|
|
|
for (const e of newEvents) { |
|
|
|
|
collectProfilePrefetchPubkeysFromEvent(e, candidates) |
|
|
|
|
} |
|
|
|
|
for (const e of clientFilteredEvents.slice(0, Math.min(120, Math.max(showCount + 64, 64)))) { |
|
|
|
|
collectProfilePrefetchPubkeysFromNoteStats(noteStatsService.getNoteStats(e.id), candidates) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const need = [...candidates].filter((pk) => !feedProfileLoadedRef.current.has(pk)) |
|
|
|
|
enqueueFeedProfilePubkeys(need) |
|
|
|
|
}, FEED_PROFILE_BATCH_DEBOUNCE_MS) |
|
|
|
|
return () => window.clearTimeout(handle) |
|
|
|
|
}, [timelineEventsForFilter, newEvents, clientFilteredEvents, showCount, feedStatsProfileBump]) |
|
|
|
|
}, [ |
|
|
|
|
timelineEventsForFilter, |
|
|
|
|
newEvents, |
|
|
|
|
clientFilteredEvents, |
|
|
|
|
showCount, |
|
|
|
|
enqueueFeedProfilePubkeys |
|
|
|
|
]) |
|
|
|
|
|
|
|
|
|
const scrollToTop = useCallback((behavior: ScrollBehavior = 'instant') => { |
|
|
|
|
setTimeout(() => { |
|
|
|
|
|