diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx index b1a17cd9..a2472e9e 100644 --- a/src/components/UserAvatar/index.tsx +++ b/src/components/UserAvatar/index.tsx @@ -163,18 +163,30 @@ function useDeferRemoteProfileAvatar( setAllowRemote(true) return } - const el = containerRef.current - if (!el) return - const io = new IntersectionObserver( - (entries) => { - if (entries.some((e) => e.isIntersecting)) { - setAllowRemote(true) - } - }, - { root: null, rootMargin: `${AVATAR_VIEWPORT_MARGIN_PX}px`, threshold: 0.01 } - ) - io.observe(el) - return () => io.disconnect() + let io: IntersectionObserver | null = null + let raf = 0 + const attach = () => { + const el = containerRef.current + if (!el || io) return + io = new IntersectionObserver( + (entries) => { + if (entries.some((e) => e.isIntersecting)) { + setAllowRemote(true) + } + }, + { root: null, rootMargin: `${AVATAR_VIEWPORT_MARGIN_PX}px`, threshold: 0.01 } + ) + io.observe(el) + } + attach() + // Ref can still be null on the first effect tick (layout ordering); retry once after paint. + if (!containerRef.current) { + raf = window.requestAnimationFrame(() => attach()) + } + return () => { + if (raf) cancelAnimationFrame(raf) + io?.disconnect() + } }, [remoteHttp, allowRemote, containerRef, deferRemote]) if (sizeBlocked) return fallbackSrc diff --git a/src/hooks/useFetchProfile.tsx b/src/hooks/useFetchProfile.tsx index 80dd2f6e..405dae67 100644 --- a/src/hooks/useFetchProfile.tsx +++ b/src/hooks/useFetchProfile.tsx @@ -89,9 +89,23 @@ export function useFetchProfile(id?: string, skipCache = false) { return null } - // CRITICAL: Check cooldown period first to prevent cascade of duplicate fetches after timeout + // CRITICAL: Check cooldown period first to prevent cascade of duplicate fetches after timeout. + // Still hydrate from session/IndexedDB — otherwise new rows remount after a timeout and stay on + // identicons until cooldown ends with no effect re-run (deps unchanged). const cooldownExpiry = globalFetchCooldowns.get(pubkey) if (cooldownExpiry && Date.now() < cooldownExpiry) { + const cachedDuringCooldown = await tryHydrateProfileFromLocalCaches(pubkey, skipCache) + if (!cancelled.current && cachedDuringCooldown) { + setProfile(cachedDuringCooldown) + setIsFetching(false) + initializedPubkeysRef.current.add(pubkey) + if (checkIntervalRef.current) { + clearInterval(checkIntervalRef.current) + checkIntervalRef.current = null + } + effectRunCountRef.current.delete(pubkey) + return cachedDuringCooldown + } logger.debug('[useFetchProfile] In cooldown period after timeout, skipping fetch', { pubkey: pubkey.substring(0, 8), remainingMs: cooldownExpiry - Date.now()