|
|
|
@ -218,6 +218,13 @@ const NoteList = forwardRef( |
|
|
|
* That stacks subscriptions on strict relays (e.g. ≤10 subs) and triggers rejections / rate limits. |
|
|
|
* That stacks subscriptions on strict relays (e.g. ≤10 subs) and triggers rejections / rate limits. |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const timelineEstablishedCloserRef = useRef<(() => void) | null>(null) |
|
|
|
const timelineEstablishedCloserRef = useRef<(() => void) | null>(null) |
|
|
|
|
|
|
|
/** Session snapshot was written to state; log once after commit (see feed-paint layout effect). */ |
|
|
|
|
|
|
|
const feedPaintSessionPendingRef = useRef(false) |
|
|
|
|
|
|
|
/** Relay / one-shot data was written to state; log once after commit. */ |
|
|
|
|
|
|
|
const feedPaintRelayPendingRef = useRef(false) |
|
|
|
|
|
|
|
const feedPaintRelayMetaRef = useRef<Record<string, unknown> | null>(null) |
|
|
|
|
|
|
|
/** First live `onEvents` paint per timeline init (rows or terminal EOSE). */ |
|
|
|
|
|
|
|
const feedPaintLiveRelayDoneRef = useRef(false) |
|
|
|
|
|
|
|
|
|
|
|
const [feedProfileBatch, setFeedProfileBatch] = useState<{ |
|
|
|
const [feedProfileBatch, setFeedProfileBatch] = useState<{ |
|
|
|
profiles: Map<string, TProfile> |
|
|
|
profiles: Map<string, TProfile> |
|
|
|
@ -298,7 +305,11 @@ const NoteList = forwardRef( |
|
|
|
return JSON.stringify([...showKinds].sort((a, b) => a - b)) |
|
|
|
return JSON.stringify([...showKinds].sort((a, b) => a - b)) |
|
|
|
}, [showKinds]) |
|
|
|
}, [showKinds]) |
|
|
|
|
|
|
|
|
|
|
|
/** Session snapshot identity: same feed + kind / reply UI toggles so restore matches filtering. */ |
|
|
|
/** |
|
|
|
|
|
|
|
* Session snapshot identity: feed + kind UI toggles that affect **REQ** / merged rows. |
|
|
|
|
|
|
|
* Do **not** include {@link hideReplies}: Notes vs Replies only changes client-side filtering; the same |
|
|
|
|
|
|
|
* raw timeline should restore for both tabs (otherwise Replies can show cache while Notes looks empty). |
|
|
|
|
|
|
|
*/ |
|
|
|
const sessionSnapshotIdentityKey = useMemo( |
|
|
|
const sessionSnapshotIdentityKey = useMemo( |
|
|
|
() => |
|
|
|
() => |
|
|
|
JSON.stringify({ |
|
|
|
JSON.stringify({ |
|
|
|
@ -306,10 +317,9 @@ const NoteList = forwardRef( |
|
|
|
kinds: showKindsKey, |
|
|
|
kinds: showKindsKey, |
|
|
|
op: showKind1OPs, |
|
|
|
op: showKind1OPs, |
|
|
|
rep: showKind1Replies, |
|
|
|
rep: showKind1Replies, |
|
|
|
c1111: showKind1111, |
|
|
|
c1111: showKind1111 |
|
|
|
hr: hideReplies |
|
|
|
|
|
|
|
}), |
|
|
|
}), |
|
|
|
[timelineSubscriptionKey, showKindsKey, showKind1OPs, showKind1Replies, showKind1111, hideReplies] |
|
|
|
[timelineSubscriptionKey, showKindsKey, showKind1OPs, showKind1Replies, showKind1111] |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
const showKindsRef = useRef(showKinds) |
|
|
|
const showKindsRef = useRef(showKinds) |
|
|
|
@ -402,6 +412,45 @@ const NoteList = forwardRef( |
|
|
|
}) |
|
|
|
}) |
|
|
|
}, [events, showCount, shouldHideEvent, showKinds, showKind1OPs, showKind1Replies, showKind1111]) |
|
|
|
}, [events, showCount, shouldHideEvent, showKinds, showKind1OPs, showKind1Replies, showKind1111]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useLayoutEffect(() => { |
|
|
|
|
|
|
|
if (!feedPaintSessionPendingRef.current && !feedPaintRelayPendingRef.current) return |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const shorten = (s: string, max: number) => |
|
|
|
|
|
|
|
s.length > max ? `${s.slice(0, max)}…` : s |
|
|
|
|
|
|
|
const feedKeyShort = shorten(timelineSubscriptionKey, 200) |
|
|
|
|
|
|
|
const snapshotKeyShort = shorten(sessionSnapshotIdentityKey, 160) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (feedPaintSessionPendingRef.current) { |
|
|
|
|
|
|
|
feedPaintSessionPendingRef.current = false |
|
|
|
|
|
|
|
logger.info('[FeedPaint] Session cache committed (DOM)', { |
|
|
|
|
|
|
|
feedKey: feedKeyShort, |
|
|
|
|
|
|
|
snapshotKey: snapshotKeyShort, |
|
|
|
|
|
|
|
eventCount: events.length, |
|
|
|
|
|
|
|
filteredVisibleRows: filteredEvents.length, |
|
|
|
|
|
|
|
pubkeySlice: pubkey ? `${pubkey.slice(0, 12)}…` : undefined |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (feedPaintRelayPendingRef.current) { |
|
|
|
|
|
|
|
feedPaintRelayPendingRef.current = false |
|
|
|
|
|
|
|
const meta = feedPaintRelayMetaRef.current |
|
|
|
|
|
|
|
feedPaintRelayMetaRef.current = null |
|
|
|
|
|
|
|
logger.info('[FeedPaint] Relay/network results committed (DOM)', { |
|
|
|
|
|
|
|
feedKey: feedKeyShort, |
|
|
|
|
|
|
|
snapshotKey: snapshotKeyShort, |
|
|
|
|
|
|
|
committedEventCount: events.length, |
|
|
|
|
|
|
|
filteredVisibleRows: filteredEvents.length, |
|
|
|
|
|
|
|
pubkeySlice: pubkey ? `${pubkey.slice(0, 12)}…` : undefined, |
|
|
|
|
|
|
|
...meta |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, [ |
|
|
|
|
|
|
|
events, |
|
|
|
|
|
|
|
filteredEvents.length, |
|
|
|
|
|
|
|
timelineSubscriptionKey, |
|
|
|
|
|
|
|
sessionSnapshotIdentityKey, |
|
|
|
|
|
|
|
pubkey |
|
|
|
|
|
|
|
]) |
|
|
|
|
|
|
|
|
|
|
|
const filteredNewEvents = useMemo(() => { |
|
|
|
const filteredNewEvents = useMemo(() => { |
|
|
|
const idSet = new Set<string>() |
|
|
|
const idSet = new Set<string>() |
|
|
|
|
|
|
|
|
|
|
|
@ -576,6 +625,11 @@ const NoteList = forwardRef( |
|
|
|
let effectActive = true |
|
|
|
let effectActive = true |
|
|
|
|
|
|
|
|
|
|
|
async function init() { |
|
|
|
async function init() { |
|
|
|
|
|
|
|
feedPaintSessionPendingRef.current = false |
|
|
|
|
|
|
|
feedPaintRelayPendingRef.current = false |
|
|
|
|
|
|
|
feedPaintRelayMetaRef.current = null |
|
|
|
|
|
|
|
feedPaintLiveRelayDoneRef.current = false |
|
|
|
|
|
|
|
|
|
|
|
// Re-subscribe with rows visible (e.g. relay URL expansion): don't flash global loading / skeleton.
|
|
|
|
// Re-subscribe with rows visible (e.g. relay URL expansion): don't flash global loading / skeleton.
|
|
|
|
const keepRowsVisible = |
|
|
|
const keepRowsVisible = |
|
|
|
preserveTimelineOnSubRequestsChange && |
|
|
|
preserveTimelineOnSubRequestsChange && |
|
|
|
@ -588,6 +642,7 @@ const NoteList = forwardRef( |
|
|
|
|
|
|
|
|
|
|
|
if (!keepExistingTimelineEvents) { |
|
|
|
if (!keepExistingTimelineEvents) { |
|
|
|
if (restoredFromSession && sessionSnap) { |
|
|
|
if (restoredFromSession && sessionSnap) { |
|
|
|
|
|
|
|
feedPaintSessionPendingRef.current = true |
|
|
|
setEvents(sessionSnap) |
|
|
|
setEvents(sessionSnap) |
|
|
|
lastEventsForTimelinePrefetchRef.current = sessionSnap |
|
|
|
lastEventsForTimelinePrefetchRef.current = sessionSnap |
|
|
|
setNewEvents([]) |
|
|
|
setNewEvents([]) |
|
|
|
@ -716,11 +771,25 @@ const NoteList = forwardRef( |
|
|
|
} |
|
|
|
} |
|
|
|
setEvents(merged) |
|
|
|
setEvents(merged) |
|
|
|
lastEventsForTimelinePrefetchRef.current = merged |
|
|
|
lastEventsForTimelinePrefetchRef.current = merged |
|
|
|
|
|
|
|
feedPaintRelayPendingRef.current = true |
|
|
|
|
|
|
|
feedPaintRelayMetaRef.current = { |
|
|
|
|
|
|
|
variant: 'one_shot_fetch', |
|
|
|
|
|
|
|
mergedCount: merged.length, |
|
|
|
|
|
|
|
mergedWithPriorSession: !!(sessionSnap?.length && !userPulledRefresh) |
|
|
|
|
|
|
|
} |
|
|
|
} catch (err) { |
|
|
|
} catch (err) { |
|
|
|
if (oneShotDebugLabel) { |
|
|
|
if (oneShotDebugLabel) { |
|
|
|
logger.warn(`[${oneShotDebugLabel}] one-shot fetch threw`, err) |
|
|
|
logger.warn(`[${oneShotDebugLabel}] one-shot fetch threw`, err) |
|
|
|
} |
|
|
|
} |
|
|
|
if (effectActive) setEvents([]) |
|
|
|
if (effectActive) { |
|
|
|
|
|
|
|
feedPaintRelayPendingRef.current = true |
|
|
|
|
|
|
|
feedPaintRelayMetaRef.current = { |
|
|
|
|
|
|
|
variant: 'one_shot_fetch', |
|
|
|
|
|
|
|
mergedCount: 0, |
|
|
|
|
|
|
|
fetchThrew: true |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
setEvents([]) |
|
|
|
|
|
|
|
} |
|
|
|
} finally { |
|
|
|
} finally { |
|
|
|
if (effectActive) { |
|
|
|
if (effectActive) { |
|
|
|
setLoading(false) |
|
|
|
setLoading(false) |
|
|
|
@ -761,6 +830,28 @@ const NoteList = forwardRef( |
|
|
|
onEvents: (batch: Event[], eosed: boolean) => { |
|
|
|
onEvents: (batch: Event[], eosed: boolean) => { |
|
|
|
if (!effectActive) return |
|
|
|
if (!effectActive) return |
|
|
|
const narrowed = narrowLiveBatch(batch) |
|
|
|
const narrowed = narrowLiveBatch(batch) |
|
|
|
|
|
|
|
if (!feedPaintLiveRelayDoneRef.current) { |
|
|
|
|
|
|
|
if (narrowed.length > 0) { |
|
|
|
|
|
|
|
feedPaintLiveRelayDoneRef.current = true |
|
|
|
|
|
|
|
feedPaintRelayPendingRef.current = true |
|
|
|
|
|
|
|
feedPaintRelayMetaRef.current = { |
|
|
|
|
|
|
|
variant: 'live_subscription', |
|
|
|
|
|
|
|
mode: 'rows', |
|
|
|
|
|
|
|
narrowedInBatch: narrowed.length, |
|
|
|
|
|
|
|
batchIncoming: batch.length, |
|
|
|
|
|
|
|
eosed |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (eosed) { |
|
|
|
|
|
|
|
feedPaintLiveRelayDoneRef.current = true |
|
|
|
|
|
|
|
feedPaintRelayPendingRef.current = true |
|
|
|
|
|
|
|
feedPaintRelayMetaRef.current = { |
|
|
|
|
|
|
|
variant: 'live_subscription', |
|
|
|
|
|
|
|
mode: 'eose_no_visible_rows', |
|
|
|
|
|
|
|
batchIncoming: batch.length, |
|
|
|
|
|
|
|
eosed |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
if (batch.length > 0) { |
|
|
|
if (batch.length > 0) { |
|
|
|
if (narrowed.length > 0) { |
|
|
|
if (narrowed.length > 0) { |
|
|
|
if (preserveTimelineOnSubRequestsChange) { |
|
|
|
if (preserveTimelineOnSubRequestsChange) { |
|
|
|
@ -874,6 +965,10 @@ const NoteList = forwardRef( |
|
|
|
timelineEstablishedCloserRef.current = closer |
|
|
|
timelineEstablishedCloserRef.current = closer |
|
|
|
timelineKey = result.timelineKey |
|
|
|
timelineKey = result.timelineKey |
|
|
|
setTimelineKey(timelineKey) |
|
|
|
setTimelineKey(timelineKey) |
|
|
|
|
|
|
|
// subscribeTimeline resolves once shards are wired; EOSE / merge callbacks can be delayed or
|
|
|
|
|
|
|
|
// skipped on edge paths (all relays fail, strict NOTICE closes, etc.). Do not keep the global
|
|
|
|
|
|
|
|
// skeleton until the first onEvents(..., eosed) — that can freeze the feed indefinitely.
|
|
|
|
|
|
|
|
setLoading(false) |
|
|
|
return closer |
|
|
|
return closer |
|
|
|
} catch (_error) { |
|
|
|
} catch (_error) { |
|
|
|
setLoading(false) |
|
|
|
setLoading(false) |
|
|
|
@ -1393,9 +1488,18 @@ const NoteList = forwardRef( |
|
|
|
<NoteCardLoadingSkeleton key={i} /> |
|
|
|
<NoteCardLoadingSkeleton key={i} /> |
|
|
|
))} |
|
|
|
))} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
) : events.length > 0 && (hasMore || loading) ? ( |
|
|
|
) : events.length > 0 && hasMore ? ( |
|
|
|
<div ref={bottomRef}> |
|
|
|
<div |
|
|
|
<NoteCardLoadingSkeleton /> |
|
|
|
ref={bottomRef} |
|
|
|
|
|
|
|
className={ |
|
|
|
|
|
|
|
filteredEvents.length === 0 && !loading |
|
|
|
|
|
|
|
? 'min-h-[35vh] py-4' |
|
|
|
|
|
|
|
: loading |
|
|
|
|
|
|
|
? 'min-h-8' |
|
|
|
|
|
|
|
: 'min-h-4' |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{loading ? <NoteCardLoadingSkeleton /> : null} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
) : events.length > 0 ? ( |
|
|
|
) : events.length > 0 ? ( |
|
|
|
<div className="text-center text-sm text-muted-foreground mt-2">{t('no more notes')}</div> |
|
|
|
<div className="text-center text-sm text-muted-foreground mt-2">{t('no more notes')}</div> |
|
|
|
|