mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 06:34:24 +01:00
fix: batch profile label updates to prevent UI flickering
- Use requestAnimationFrame to batch rapid profile label updates - Collect pending updates in a ref instead of updating state immediately - Apply all pending updates in one render cycle - Add cleanup to cancel pending RAF on unmount/effect cleanup This prevents flickering when multiple profiles stream in quickly while still maintaining progressive updates as profiles arrive.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState, useEffect } from 'react'
|
import { useMemo, useState, useEffect, useRef } from 'react'
|
||||||
import { Hooks } from 'applesauce-react'
|
import { Hooks } from 'applesauce-react'
|
||||||
import { Helpers, IEventStore } from 'applesauce-core'
|
import { Helpers, IEventStore } from 'applesauce-core'
|
||||||
import { getContentPointers } from 'applesauce-factory/helpers'
|
import { getContentPointers } from 'applesauce-factory/helpers'
|
||||||
@@ -85,6 +85,10 @@ export function useProfileLabels(content: string, relayPool?: RelayPool | null):
|
|||||||
|
|
||||||
const [profileLabels, setProfileLabels] = useState<Map<string, string>>(initialLabels)
|
const [profileLabels, setProfileLabels] = useState<Map<string, string>>(initialLabels)
|
||||||
|
|
||||||
|
// Refs for batching updates to prevent flickering
|
||||||
|
const pendingUpdatesRef = useRef<Map<string, string>>(new Map())
|
||||||
|
const rafScheduledRef = useRef<number | null>(null)
|
||||||
|
|
||||||
// Build initial labels: localStorage cache -> eventStore -> fetch from relays
|
// Build initial labels: localStorage cache -> eventStore -> fetch from relays
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Extract all pubkeys
|
// Extract all pubkeys
|
||||||
@@ -185,7 +189,7 @@ export function useProfileLabels(content: string, relayPool?: RelayPool | null):
|
|||||||
console.log(`[profile-labels] Fetching ${pubkeysToFetch.length} profiles from relays`)
|
console.log(`[profile-labels] Fetching ${pubkeysToFetch.length} profiles from relays`)
|
||||||
console.log(`[profile-labels] Calling fetchProfiles with relayPool and ${pubkeysToFetch.length} pubkeys`)
|
console.log(`[profile-labels] Calling fetchProfiles with relayPool and ${pubkeysToFetch.length} pubkeys`)
|
||||||
|
|
||||||
// Reactive callback: update labels as profiles stream in
|
// Reactive callback: batch updates to prevent flickering
|
||||||
const handleProfileEvent = (event: NostrEvent) => {
|
const handleProfileEvent = (event: NostrEvent) => {
|
||||||
const encoded = pubkeyToEncoded.get(event.pubkey)
|
const encoded = pubkeyToEncoded.get(event.pubkey)
|
||||||
if (!encoded) {
|
if (!encoded) {
|
||||||
@@ -194,44 +198,85 @@ export function useProfileLabels(content: string, relayPool?: RelayPool | null):
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[profile-labels] Received profile event for ${encoded.slice(0, 20)}...`)
|
console.log(`[profile-labels] Received profile event for ${encoded.slice(0, 20)}...`)
|
||||||
setProfileLabels(prevLabels => {
|
|
||||||
const updatedLabels = new Map(prevLabels)
|
// Determine the label for this profile
|
||||||
|
let label: string
|
||||||
try {
|
try {
|
||||||
const profileData = JSON.parse(event.content || '{}') as { name?: string; display_name?: string; nip05?: string }
|
const profileData = JSON.parse(event.content || '{}') as { name?: string; display_name?: string; nip05?: string }
|
||||||
const displayName = profileData.display_name || profileData.name || profileData.nip05
|
const displayName = profileData.display_name || profileData.name || profileData.nip05
|
||||||
if (displayName) {
|
if (displayName) {
|
||||||
updatedLabels.set(encoded, `@${displayName}`)
|
label = `@${displayName}`
|
||||||
console.log(`[profile-labels] Updated label reactively for ${encoded.slice(0, 20)}... to @${displayName}`)
|
console.log(`[profile-labels] Updated label reactively for ${encoded.slice(0, 20)}... to @${displayName}`)
|
||||||
} else {
|
} else {
|
||||||
// Use fallback npub display if profile has no name
|
// Use fallback npub display if profile has no name
|
||||||
const fallback = getNpubFallbackDisplay(event.pubkey)
|
label = getNpubFallbackDisplay(event.pubkey)
|
||||||
updatedLabels.set(encoded, fallback)
|
console.log(`[profile-labels] Profile for ${encoded.slice(0, 20)}... has no name, keeping fallback: ${label}`)
|
||||||
console.log(`[profile-labels] Profile for ${encoded.slice(0, 20)}... has no name, keeping fallback: ${fallback}`)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Use fallback npub display if parsing fails
|
// Use fallback npub display if parsing fails
|
||||||
const fallback = getNpubFallbackDisplay(event.pubkey)
|
label = getNpubFallbackDisplay(event.pubkey)
|
||||||
updatedLabels.set(encoded, fallback)
|
|
||||||
console.warn(`[profile-labels] Error parsing profile for ${encoded.slice(0, 20)}..., using fallback:`, error)
|
console.warn(`[profile-labels] Error parsing profile for ${encoded.slice(0, 20)}..., using fallback:`, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add to pending updates
|
||||||
|
pendingUpdatesRef.current.set(encoded, label)
|
||||||
|
|
||||||
|
// Schedule batched update if not already scheduled
|
||||||
|
if (rafScheduledRef.current === null) {
|
||||||
|
rafScheduledRef.current = requestAnimationFrame(() => {
|
||||||
|
// Apply all pending updates in one batch
|
||||||
|
setProfileLabels(prevLabels => {
|
||||||
|
const updatedLabels = new Map(prevLabels)
|
||||||
|
const pendingUpdates = pendingUpdatesRef.current
|
||||||
|
|
||||||
|
// Apply all pending updates
|
||||||
|
for (const [encoded, label] of pendingUpdates.entries()) {
|
||||||
|
updatedLabels.set(encoded, label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear pending updates
|
||||||
|
pendingUpdates.clear()
|
||||||
|
rafScheduledRef.current = null
|
||||||
|
|
||||||
return updatedLabels
|
return updatedLabels
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchProfiles(relayPool, eventStore as unknown as IEventStore, pubkeysToFetch, undefined, handleProfileEvent)
|
fetchProfiles(relayPool, eventStore as unknown as IEventStore, pubkeysToFetch, undefined, handleProfileEvent)
|
||||||
.then((fetchedProfiles) => {
|
.then((fetchedProfiles) => {
|
||||||
console.log(`[profile-labels] Fetch completed (EOSE), received ${fetchedProfiles.length} profiles total`)
|
console.log(`[profile-labels] Fetch completed (EOSE), received ${fetchedProfiles.length} profiles total`)
|
||||||
// Labels have already been updated reactively via handleProfileEvent
|
// Ensure any pending batched updates are applied
|
||||||
// Just log final state for debugging
|
if (rafScheduledRef.current !== null) {
|
||||||
|
// Wait for the scheduled RAF to complete
|
||||||
|
requestAnimationFrame(() => {
|
||||||
setProfileLabels(prevLabels => {
|
setProfileLabels(prevLabels => {
|
||||||
console.log(`[profile-labels] Final labels after EOSE:`, Array.from(prevLabels.entries()).map(([enc, label]) => ({ encoded: enc.slice(0, 20) + '...', label })))
|
console.log(`[profile-labels] Final labels after EOSE:`, Array.from(prevLabels.entries()).map(([enc, label]) => ({ encoded: enc.slice(0, 20) + '...', label })))
|
||||||
return prevLabels // No change needed, already updated reactively
|
return prevLabels
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
// No pending updates, just log final state
|
||||||
|
setProfileLabels(prevLabels => {
|
||||||
|
console.log(`[profile-labels] Final labels after EOSE:`, Array.from(prevLabels.entries()).map(([enc, label]) => ({ encoded: enc.slice(0, 20) + '...', label })))
|
||||||
|
return prevLabels
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(`[profile-labels] Error fetching profiles:`, error)
|
console.error(`[profile-labels] Error fetching profiles:`, error)
|
||||||
// Silently handle fetch errors
|
// Silently handle fetch errors
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Cleanup: cancel any pending RAF and clear pending updates
|
||||||
|
return () => {
|
||||||
|
if (rafScheduledRef.current !== null) {
|
||||||
|
cancelAnimationFrame(rafScheduledRef.current)
|
||||||
|
rafScheduledRef.current = null
|
||||||
|
}
|
||||||
|
pendingUpdatesRef.current.clear()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (pubkeysToFetch.length === 0) {
|
if (pubkeysToFetch.length === 0) {
|
||||||
console.log(`[profile-labels] No profiles to fetch`)
|
console.log(`[profile-labels] No profiles to fetch`)
|
||||||
|
|||||||
Reference in New Issue
Block a user