diff --git a/src/PageManager.tsx b/src/PageManager.tsx index a3b351e9..2269001e 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -2165,7 +2165,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { const primaryFrozen = secondaryStack.length > 0 && (isSmallScreen || panelMode === 'double') - useEffect(() => { + useLayoutEffect(() => { noteStatsService.setBackgroundStatsPaused(primaryFrozen) if (primaryFrozen) { client.interruptBackgroundQueries() diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 8367fc83..af0a0fa5 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -995,6 +995,11 @@ const NoteList = forwardRef( const primaryPageCtx = usePrimaryPageOptional() const primaryPageCurrent = primaryPageCtx?.current ?? null const primaryPanelFrozen = primaryPageCtx?.frozen ?? false + /** Only pause timelines on the active primary page feed — not secondary-panel profiles, search, etc. */ + const pauseTimelineForPrimaryFreeze = + primaryPanelFrozen && + hostPrimaryPageName != null && + hostPrimaryPageName === primaryPageCurrent /** Clears text/author/time/full-search; does not change panel open state. */ const clearFeedClientSearchCriteria = useCallback(() => { @@ -1890,7 +1895,7 @@ const NoteList = forwardRef( timelineEstablishedCloserRef.current?.() timelineEstablishedCloserRef.current = null - if (primaryPanelFrozen) { + if (pauseTimelineForPrimaryFreeze) { return () => {} } @@ -3251,12 +3256,12 @@ const NoteList = forwardRef( progressiveWarmupQuery, hostPrimaryPageName, relayAuthoritativeFeedOnly, - primaryPanelFrozen + pauseTimelineForPrimaryFreeze ]) useEffect(() => { if (oneShotFetch) return - if (primaryPanelFrozen) { + if (pauseTimelineForPrimaryFreeze) { followingFeedDeltaCloserRef.current?.() followingFeedDeltaCloserRef.current = null return @@ -3519,7 +3524,7 @@ const NoteList = forwardRef( showKind1OPs, showKind1Replies, showKind1111, - primaryPanelFrozen + pauseTimelineForPrimaryFreeze ]) const oneShotDebugPrevLoadingRef = useRef(false) diff --git a/src/hooks/useProfileReportsEvents.tsx b/src/hooks/useProfileReportsEvents.tsx index f1dc264d..03cb99c9 100644 --- a/src/hooks/useProfileReportsEvents.tsx +++ b/src/hooks/useProfileReportsEvents.tsx @@ -236,7 +236,8 @@ export function useProfileReportsEvents({ const fetched = await client.fetchEvents(provisionalUrls, filter, { cache: true, eoseTimeout: 4500, - globalTimeout: 14_000 + globalTimeout: 14_000, + foreground: true }) if (!cancelled) { for (const e of fetched) pool.set(e.id, e) @@ -266,7 +267,8 @@ export function useProfileReportsEvents({ const fetchedDelta = await client.fetchEvents(deltaUrls, filter, { cache: true, eoseTimeout: 4500, - globalTimeout: 14_000 + globalTimeout: 14_000, + foreground: true }) if (!cancelled) { for (const e of fetchedDelta) pool.set(e.id, e) @@ -295,13 +297,14 @@ export function useProfileReportsEvents({ } setIsLoading(true) - - await Promise.all([ - loadMode('received', receivedCacheKey, setReceived), - loadMode('made', madeCacheKey, setMade) - ]) - - if (!cancelled) setIsLoading(false) + try { + await Promise.all([ + loadMode('received', receivedCacheKey, setReceived), + loadMode('made', madeCacheKey, setMade) + ]) + } finally { + if (!cancelled) setIsLoading(false) + } } void run() diff --git a/src/hooks/useProfileWall.tsx b/src/hooks/useProfileWall.tsx index de89e9da..5f38830f 100644 --- a/src/hooks/useProfileWall.tsx +++ b/src/hooks/useProfileWall.tsx @@ -65,7 +65,8 @@ async function fetchBadgeDefinitionOnRelays( { replaceableRace: true, eoseTimeout: METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS, - globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS + globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS, + foreground: true } ) const matches = rows.filter((e) => e.kind === ExtendedKind.BADGE_DEFINITION) @@ -110,8 +111,6 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine useEffect(() => { let cancelled = false - let idleHandle: number | undefined - let idleTimeout: ReturnType | undefined const run = async () => { const mem = wallCacheByKey.get(cacheKey) @@ -123,112 +122,106 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine } setIsLoading(true) + try { + const pkNorm = userIdToPubkey(pubkey) || pubkey + if (!isValidPubkey(pkNorm)) { + return + } - const pkNorm = userIdToPubkey(pubkey) || pubkey - if (!isValidPubkey(pkNorm)) { - if (!cancelled) setIsLoading(false) - return - } - - const emptyAuthor = { read: [] as string[], write: [] as string[], httpRead: [] as string[], httpWrite: [] as string[] } - const authorRl = await client.peekRelayListFromStorage(pubkey).catch(() => emptyAuthor) - if (cancelled) return - - const relayUrls = buildProfilePageReadRelayUrls( - favoriteRelaysRef.current, - blockedRelaysRef.current, - authorRl, - false, - false, - [ExtendedKind.COMMENT, ExtendedKind.PROFILE_BADGES_LIST, ExtendedKind.BADGE_DEFINITION], - useGlobalRelayBootstrapRef.current - ) - - // --- Badges (NIP-58): IndexedDB + profile read relays (favorites / fast-read), not inbox-only --- - let listEvent = await fetchProfileBadgesListEvent(pkNorm, relayUrls) - if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) { - const legacy = await fetchLegacyProfileBadgesListEvent(pkNorm, relayUrls) - if (legacy && isNip58ProfileBadgesListEvent(legacy)) listEvent = legacy - } - - const entries = parseProfileBadgeEntries(listEvent) - const defCoords = [...new Set(entries.map((e) => e.definitionCoordinate))] - const defByCoord = new Map() + const emptyAuthor = { + read: [] as string[], + write: [] as string[], + httpRead: [] as string[], + httpWrite: [] as string[] + } + const authorRl = await client.peekRelayListFromStorage(pubkey).catch(() => emptyAuthor) + if (cancelled) return + + const relayUrls = buildProfilePageReadRelayUrls( + favoriteRelaysRef.current, + blockedRelaysRef.current, + authorRl, + false, + false, + [ExtendedKind.COMMENT, ExtendedKind.PROFILE_BADGES_LIST, ExtendedKind.BADGE_DEFINITION], + useGlobalRelayBootstrapRef.current + ) + + // --- Badges (NIP-58): IndexedDB + profile read relays (favorites / fast-read), not inbox-only --- + let listEvent = await fetchProfileBadgesListEvent(pkNorm, relayUrls) + if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) { + const legacy = await fetchLegacyProfileBadgesListEvent(pkNorm, relayUrls) + if (legacy && isNip58ProfileBadgesListEvent(legacy)) listEvent = legacy + } - await Promise.all( - defCoords.map(async (coord) => { - defByCoord.set(coord, await fetchBadgeDefinitionOnRelays(coord, relayUrls)) - }) - ) - - const resolvedBadges = entries.map((entry) => - resolveBadgeDisplayFromDefinition(entry, defByCoord.get(entry.definitionCoordinate)) - ) - - // --- Wall comments (kind 1111 on profile kind 0) --- - let wallComments: Event[] = [] - const profileId = profileEventId?.trim().toLowerCase() - if (profileId && /^[0-9a-f]{64}$/.test(profileId) && relayUrls.length > 0) { - const profileCoord = getReplaceableCoordinate(kinds.Metadata, pkNorm, '') - const filters: Filter[] = [ - { kinds: [ExtendedKind.COMMENT], '#e': [profileId], limit: 200 }, - { kinds: [ExtendedKind.COMMENT], '#a': [profileCoord], limit: 200 } - ] - const pool = new Map() - try { - const rows = await Promise.all( - filters.map((filter) => - client.fetchEvents(relayUrls, filter, { - cache: true, - eoseTimeout: 4500, - globalTimeout: 14_000 - }) + const entries = parseProfileBadgeEntries(listEvent) + const defCoords = [...new Set(entries.map((e) => e.definitionCoordinate))] + const defByCoord = new Map() + + await Promise.all( + defCoords.map(async (coord) => { + defByCoord.set(coord, await fetchBadgeDefinitionOnRelays(coord, relayUrls)) + }) + ) + + const resolvedBadges = entries.map((entry) => + resolveBadgeDisplayFromDefinition(entry, defByCoord.get(entry.definitionCoordinate)) + ) + + // --- Wall comments (kind 1111 on profile kind 0) --- + let wallComments: Event[] = [] + const profileId = profileEventId?.trim().toLowerCase() + if (profileId && /^[0-9a-f]{64}$/.test(profileId) && relayUrls.length > 0) { + const profileCoord = getReplaceableCoordinate(kinds.Metadata, pkNorm, '') + const filters: Filter[] = [ + { kinds: [ExtendedKind.COMMENT], '#e': [profileId], limit: 200 }, + { kinds: [ExtendedKind.COMMENT], '#a': [profileCoord], limit: 200 } + ] + const pool = new Map() + try { + const rows = await Promise.all( + filters.map((filter) => + client.fetchEvents(relayUrls, filter, { + cache: true, + eoseTimeout: 4500, + globalTimeout: 14_000, + foreground: true + }) + ) ) - ) - for (const batch of rows) { - for (const e of batch) pool.set(e.id, e) + for (const batch of rows) { + for (const e of batch) pool.set(e.id, e) + } + } catch { + /* ignore */ } - } catch { - /* ignore */ + + wallComments = [...pool.values()] + .filter( + (e) => + !isEventDeletedRef.current(e) && + isDirectProfileWallComment(e, profileId, pkNorm) + ) + .sort((a, b) => b.created_at - a.created_at) } - wallComments = [...pool.values()] - .filter( - (e) => - !isEventDeletedRef.current(e) && - isDirectProfileWallComment(e, profileId, pkNorm) - ) - .sort((a, b) => b.created_at - a.created_at) + if (cancelled) return + setBadges(resolvedBadges) + setComments(wallComments) + wallCacheByKey.set(cacheKey, { + badges: resolvedBadges, + comments: wallComments, + lastUpdated: Date.now() + }) + } finally { + if (!cancelled) setIsLoading(false) } - - if (cancelled) return - setBadges(resolvedBadges) - setComments(wallComments) - wallCacheByKey.set(cacheKey, { - badges: resolvedBadges, - comments: wallComments, - lastUpdated: Date.now() - }) - setIsLoading(false) } - const scheduleRun = () => { - if (typeof requestIdleCallback === 'function') { - idleHandle = requestIdleCallback(() => void run(), { timeout: 4_000 }) - } else { - idleTimeout = setTimeout(() => void run(), 400) - } - } - scheduleRun() + void run() return () => { cancelled = true - if (idleHandle !== undefined && typeof cancelIdleCallback === 'function') { - cancelIdleCallback(idleHandle) - } - if (idleTimeout !== undefined) { - clearTimeout(idleTimeout) - } } }, [pubkey, profileEventId, cacheKey, refreshToken, relayListsKey]) diff --git a/src/services/client.service.ts b/src/services/client.service.ts index cdb45857..7d3436ce 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -3419,7 +3419,8 @@ class ClientService extends EventTarget { globalTimeout, firstRelayResultGraceMs, replaceableRace, - immediateReturn + immediateReturn, + foreground }: { onevent?: (evt: NEvent) => void cache?: boolean @@ -3428,6 +3429,8 @@ class ClientService extends EventTarget { firstRelayResultGraceMs?: number | false replaceableRace?: boolean immediateReturn?: boolean + /** When true, ignore {@link QueryService.interruptBackgroundQueries} (e.g. secondary-panel profile loads). */ + foreground?: boolean } = {} ) { const originalDedupedRelays = Array.from(new Set(urls)) @@ -3467,7 +3470,8 @@ class ClientService extends EventTarget { globalTimeout, firstRelayResultGraceMs, replaceableRace, - immediateReturn + immediateReturn, + foreground }) if (cache) { events.forEach((evt) => {