mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 06:34:24 +01:00
refactor: use pubkey (hex) as Map key instead of encoded nprofile/npub strings
- Changed useProfileLabels to use pubkey as key for canonical identification - Updated replaceNostrUrisInMarkdownWithProfileLabels to extract pubkey and use it for lookup - This fixes the key mismatch issue where different nprofile encodings map to the same pubkey - Multiple nprofile strings can refer to the same pubkey (different relay hints) - Using pubkey as key is the Nostr standard way to identify profiles
This commit is contained in:
@@ -45,6 +45,7 @@ export function useProfileLabels(
|
|||||||
}, [content])
|
}, [content])
|
||||||
|
|
||||||
// Initialize labels synchronously from cache on first render to avoid delay
|
// Initialize labels synchronously from cache on first render to avoid delay
|
||||||
|
// Use pubkey (hex) as the key instead of encoded string for canonical identification
|
||||||
const initialLabels = useMemo(() => {
|
const initialLabels = useMemo(() => {
|
||||||
if (profileData.length === 0) {
|
if (profileData.length === 0) {
|
||||||
return new Map<string, string>()
|
return new Map<string, string>()
|
||||||
@@ -54,18 +55,18 @@ export function useProfileLabels(
|
|||||||
const cachedProfiles = loadCachedProfiles(allPubkeys)
|
const cachedProfiles = loadCachedProfiles(allPubkeys)
|
||||||
const labels = new Map<string, string>()
|
const labels = new Map<string, string>()
|
||||||
|
|
||||||
profileData.forEach(({ encoded, pubkey }) => {
|
profileData.forEach(({ pubkey }) => {
|
||||||
const cachedProfile = cachedProfiles.get(pubkey)
|
const cachedProfile = cachedProfiles.get(pubkey)
|
||||||
if (cachedProfile) {
|
if (cachedProfile) {
|
||||||
const displayName = extractProfileDisplayName(cachedProfile)
|
const displayName = extractProfileDisplayName(cachedProfile)
|
||||||
if (displayName) {
|
if (displayName) {
|
||||||
// Only add @ prefix if we have a real name, otherwise use fallback format directly
|
// Only add @ prefix if we have a real name, otherwise use fallback format directly
|
||||||
const label = displayName.startsWith('@') ? displayName : `@${displayName}`
|
const label = displayName.startsWith('@') ? displayName : `@${displayName}`
|
||||||
labels.set(encoded, label)
|
labels.set(pubkey, label)
|
||||||
} else {
|
} else {
|
||||||
// Use fallback npub display if profile has no name
|
// Use fallback npub display if profile has no name
|
||||||
const fallback = getNpubFallbackDisplay(pubkey)
|
const fallback = getNpubFallbackDisplay(pubkey)
|
||||||
labels.set(encoded, fallback)
|
labels.set(pubkey, fallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -78,6 +79,7 @@ export function useProfileLabels(
|
|||||||
|
|
||||||
// Batching strategy: Collect profile updates and apply them in batches via RAF to prevent UI flicker
|
// Batching strategy: Collect profile updates and apply them in batches via RAF to prevent UI flicker
|
||||||
// when many profiles resolve simultaneously. We use refs to avoid stale closures in async callbacks.
|
// when many profiles resolve simultaneously. We use refs to avoid stale closures in async callbacks.
|
||||||
|
// Use pubkey (hex) as the key for canonical identification
|
||||||
const pendingUpdatesRef = useRef<Map<string, string>>(new Map())
|
const pendingUpdatesRef = useRef<Map<string, string>>(new Map())
|
||||||
const rafScheduledRef = useRef<number | null>(null)
|
const rafScheduledRef = useRef<number | null>(null)
|
||||||
|
|
||||||
@@ -125,13 +127,13 @@ export function useProfileLabels(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Use a functional update to access current state without including it in dependencies
|
// Use a functional update to access current state without including it in dependencies
|
||||||
setProfileLabels(prevLabels => {
|
setProfileLabels(prevLabels => {
|
||||||
const currentEncodedIds = new Set(Array.from(prevLabels.keys()))
|
const currentPubkeys = new Set(Array.from(prevLabels.keys()))
|
||||||
const newEncodedIds = new Set(profileData.map(p => p.encoded))
|
const newPubkeys = new Set(profileData.map(p => p.pubkey))
|
||||||
|
|
||||||
// If the content changed significantly (different set of profiles), reset state
|
// If the content changed significantly (different set of profiles), reset state
|
||||||
const hasDifferentProfiles =
|
const hasDifferentProfiles =
|
||||||
currentEncodedIds.size !== newEncodedIds.size ||
|
currentPubkeys.size !== newPubkeys.size ||
|
||||||
!Array.from(newEncodedIds).every(id => currentEncodedIds.has(id))
|
!Array.from(newPubkeys).every(pk => currentPubkeys.has(pk))
|
||||||
|
|
||||||
if (hasDifferentProfiles) {
|
if (hasDifferentProfiles) {
|
||||||
// Clear pending updates and cancel RAF for old profiles
|
// Clear pending updates and cancel RAF for old profiles
|
||||||
@@ -145,10 +147,10 @@ export function useProfileLabels(
|
|||||||
} else {
|
} else {
|
||||||
// Same profiles, merge initial labels with existing state (initial labels take precedence for missing ones)
|
// Same profiles, merge initial labels with existing state (initial labels take precedence for missing ones)
|
||||||
const merged = new Map(prevLabels)
|
const merged = new Map(prevLabels)
|
||||||
for (const [encoded, label] of initialLabels.entries()) {
|
for (const [pubkey, label] of initialLabels.entries()) {
|
||||||
// Only update if missing or if initial label has a better value (not a fallback)
|
// Only update if missing or if initial label has a better value (not a fallback)
|
||||||
if (!merged.has(encoded) || (!prevLabels.get(encoded)?.startsWith('@') && label.startsWith('@'))) {
|
if (!merged.has(pubkey) || (!prevLabels.get(pubkey)?.startsWith('@') && label.startsWith('@'))) {
|
||||||
merged.set(encoded, label)
|
merged.set(pubkey, label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return merged
|
return merged
|
||||||
@@ -157,12 +159,12 @@ export function useProfileLabels(
|
|||||||
|
|
||||||
// Reset loading state when content changes significantly
|
// Reset loading state when content changes significantly
|
||||||
setProfileLoading(prevLoading => {
|
setProfileLoading(prevLoading => {
|
||||||
const currentEncodedIds = new Set(Array.from(prevLoading.keys()))
|
const currentPubkeys = new Set(Array.from(prevLoading.keys()))
|
||||||
const newEncodedIds = new Set(profileData.map(p => p.encoded))
|
const newPubkeys = new Set(profileData.map(p => p.pubkey))
|
||||||
|
|
||||||
const hasDifferentProfiles =
|
const hasDifferentProfiles =
|
||||||
currentEncodedIds.size !== newEncodedIds.size ||
|
currentPubkeys.size !== newPubkeys.size ||
|
||||||
!Array.from(newEncodedIds).every(id => currentEncodedIds.has(id))
|
!Array.from(newPubkeys).every(pk => currentPubkeys.has(pk))
|
||||||
|
|
||||||
if (hasDifferentProfiles) {
|
if (hasDifferentProfiles) {
|
||||||
return new Map()
|
return new Map()
|
||||||
@@ -198,16 +200,17 @@ export function useProfileLabels(
|
|||||||
|
|
||||||
// Build labels from localStorage cache and eventStore
|
// Build labels from localStorage cache and eventStore
|
||||||
// initialLabels already has all cached profiles, so we only need to check eventStore
|
// initialLabels already has all cached profiles, so we only need to check eventStore
|
||||||
|
// Use pubkey (hex) as the key for canonical identification
|
||||||
const labels = new Map<string, string>(initialLabels)
|
const labels = new Map<string, string>(initialLabels)
|
||||||
const loading = new Map<string, boolean>()
|
const loading = new Map<string, boolean>()
|
||||||
|
|
||||||
const pubkeysToFetch: string[] = []
|
const pubkeysToFetch: string[] = []
|
||||||
|
|
||||||
profileData.forEach(({ encoded, pubkey }) => {
|
profileData.forEach(({ pubkey }) => {
|
||||||
// Skip if already resolved from initial cache
|
// Skip if already resolved from initial cache
|
||||||
if (labels.has(encoded)) {
|
if (labels.has(pubkey)) {
|
||||||
loading.set(encoded, false)
|
loading.set(pubkey, false)
|
||||||
console.log(`[profile-loading-debug][profile-labels-loading] ${encoded.slice(0, 16)}... in cache, not loading`)
|
console.log(`[profile-loading-debug][profile-labels-loading] ${pubkey.slice(0, 16)}... in cache, not loading`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,69 +223,59 @@ export function useProfileLabels(
|
|||||||
if (displayName) {
|
if (displayName) {
|
||||||
// Only add @ prefix if we have a real name, otherwise use fallback format directly
|
// Only add @ prefix if we have a real name, otherwise use fallback format directly
|
||||||
const label = displayName.startsWith('@') ? displayName : `@${displayName}`
|
const label = displayName.startsWith('@') ? displayName : `@${displayName}`
|
||||||
labels.set(encoded, label)
|
labels.set(pubkey, label)
|
||||||
} else {
|
} else {
|
||||||
// Use fallback npub display if profile has no name
|
// Use fallback npub display if profile has no name
|
||||||
const fallback = getNpubFallbackDisplay(pubkey)
|
const fallback = getNpubFallbackDisplay(pubkey)
|
||||||
labels.set(encoded, fallback)
|
labels.set(pubkey, fallback)
|
||||||
}
|
}
|
||||||
loading.set(encoded, false)
|
loading.set(pubkey, false)
|
||||||
console.log(`[profile-loading-debug][profile-labels-loading] ${encoded.slice(0, 16)}... in eventStore, not loading`)
|
console.log(`[profile-loading-debug][profile-labels-loading] ${pubkey.slice(0, 16)}... in eventStore, not loading`)
|
||||||
} else {
|
} else {
|
||||||
// No profile found yet, will use fallback after fetch or keep empty
|
// No profile found yet, will use fallback after fetch or keep empty
|
||||||
// We'll set fallback labels for missing profiles at the end
|
// We'll set fallback labels for missing profiles at the end
|
||||||
// Mark as loading since we'll fetch it
|
// Mark as loading since we'll fetch it
|
||||||
pubkeysToFetch.push(pubkey)
|
pubkeysToFetch.push(pubkey)
|
||||||
loading.set(encoded, true)
|
loading.set(pubkey, true)
|
||||||
console.log(`[profile-loading-debug][profile-labels-loading] ${encoded.slice(0, 16)}... not found, SET LOADING=true`)
|
console.log(`[profile-loading-debug][profile-labels-loading] ${pubkey.slice(0, 16)}... not found, SET LOADING=true`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set fallback labels for profiles that weren't found
|
// Set fallback labels for profiles that weren't found
|
||||||
profileData.forEach(({ encoded, pubkey }) => {
|
profileData.forEach(({ pubkey }) => {
|
||||||
if (!labels.has(encoded)) {
|
if (!labels.has(pubkey)) {
|
||||||
const fallback = getNpubFallbackDisplay(pubkey)
|
const fallback = getNpubFallbackDisplay(pubkey)
|
||||||
labels.set(encoded, fallback)
|
labels.set(pubkey, fallback)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setProfileLabels(new Map(labels))
|
setProfileLabels(new Map(labels))
|
||||||
setProfileLoading(new Map(loading))
|
setProfileLoading(new Map(loading))
|
||||||
console.log(`[profile-loading-debug][profile-labels-loading] Initial loading state:`, Array.from(loading.entries()).map(([e, l]) => `${e.slice(0, 16)}...=${l}`))
|
console.log(`[profile-loading-debug][profile-labels-loading] Initial loading state:`, Array.from(loading.entries()).map(([pk, l]) => `${pk.slice(0, 16)}...=${l}`))
|
||||||
|
|
||||||
// Fetch missing profiles asynchronously with reactive updates
|
// Fetch missing profiles asynchronously with reactive updates
|
||||||
if (pubkeysToFetch.length > 0 && relayPool && eventStore) {
|
if (pubkeysToFetch.length > 0 && relayPool && eventStore) {
|
||||||
console.log(`[profile-loading-debug][profile-labels-loading] Starting fetch for ${pubkeysToFetch.length} profiles:`, pubkeysToFetch.map(p => p.slice(0, 16) + '...'))
|
console.log(`[profile-loading-debug][profile-labels-loading] Starting fetch for ${pubkeysToFetch.length} profiles:`, pubkeysToFetch.map(p => p.slice(0, 16) + '...'))
|
||||||
const pubkeysToFetchSet = new Set(pubkeysToFetch)
|
|
||||||
// Create a map from pubkey to encoded identifier for quick lookup
|
|
||||||
const pubkeyToEncoded = new Map<string, string>()
|
|
||||||
profileData.forEach(({ encoded, pubkey }) => {
|
|
||||||
if (pubkeysToFetchSet.has(pubkey)) {
|
|
||||||
pubkeyToEncoded.set(pubkey, encoded)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reactive callback: collects profile updates and batches them via RAF to prevent flicker
|
// Reactive callback: collects profile updates and batches them via RAF to prevent flicker
|
||||||
// Strategy: Collect updates in ref, schedule RAF on first update, apply all in batch
|
// Strategy: Collect updates in ref, schedule RAF on first update, apply all in batch
|
||||||
const handleProfileEvent = (event: NostrEvent) => {
|
const handleProfileEvent = (event: NostrEvent) => {
|
||||||
const encoded = pubkeyToEncoded.get(event.pubkey)
|
// Use pubkey directly as the key
|
||||||
if (!encoded) {
|
const pubkey = event.pubkey
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the label for this profile using centralized utility
|
// Determine the label for this profile using centralized utility
|
||||||
const displayName = extractProfileDisplayName(event)
|
const displayName = extractProfileDisplayName(event)
|
||||||
const label = displayName ? (displayName.startsWith('@') ? displayName : `@${displayName}`) : getNpubFallbackDisplay(event.pubkey)
|
const label = displayName ? (displayName.startsWith('@') ? displayName : `@${displayName}`) : getNpubFallbackDisplay(pubkey)
|
||||||
|
|
||||||
// Add to pending updates and schedule batched application
|
// Add to pending updates and schedule batched application
|
||||||
pendingUpdatesRef.current.set(encoded, label)
|
pendingUpdatesRef.current.set(pubkey, label)
|
||||||
scheduleBatchedUpdate()
|
scheduleBatchedUpdate()
|
||||||
|
|
||||||
// Clear loading state for this profile when it resolves
|
// Clear loading state for this profile when it resolves
|
||||||
console.log(`[profile-loading-debug][profile-labels-loading] Profile resolved for ${encoded.slice(0, 16)}..., CLEARING LOADING`)
|
console.log(`[profile-loading-debug][profile-labels-loading] Profile resolved for ${pubkey.slice(0, 16)}..., CLEARING LOADING`)
|
||||||
setProfileLoading(prevLoading => {
|
setProfileLoading(prevLoading => {
|
||||||
const updated = new Map(prevLoading)
|
const updated = new Map(prevLoading)
|
||||||
updated.set(encoded, false)
|
updated.set(pubkey, false)
|
||||||
return updated
|
return updated
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -298,13 +291,10 @@ export function useProfileLabels(
|
|||||||
setProfileLoading(prevLoading => {
|
setProfileLoading(prevLoading => {
|
||||||
const updated = new Map(prevLoading)
|
const updated = new Map(prevLoading)
|
||||||
pubkeysToFetch.forEach(pubkey => {
|
pubkeysToFetch.forEach(pubkey => {
|
||||||
const encoded = pubkeyToEncoded.get(pubkey)
|
const wasLoading = updated.get(pubkey)
|
||||||
if (encoded) {
|
updated.set(pubkey, false)
|
||||||
const wasLoading = updated.get(encoded)
|
|
||||||
updated.set(encoded, false)
|
|
||||||
if (wasLoading) {
|
if (wasLoading) {
|
||||||
console.log(`[profile-loading-debug][profile-labels-loading] ${encoded.slice(0, 16)}... CLEARED loading after fetch complete`)
|
console.log(`[profile-loading-debug][profile-labels-loading] ${pubkey.slice(0, 16)}... CLEARED loading after fetch complete`)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return updated
|
return updated
|
||||||
@@ -323,10 +313,7 @@ export function useProfileLabels(
|
|||||||
setProfileLoading(prevLoading => {
|
setProfileLoading(prevLoading => {
|
||||||
const updated = new Map(prevLoading)
|
const updated = new Map(prevLoading)
|
||||||
pubkeysToFetch.forEach(pubkey => {
|
pubkeysToFetch.forEach(pubkey => {
|
||||||
const encoded = pubkeyToEncoded.get(pubkey)
|
updated.set(pubkey, false)
|
||||||
if (encoded) {
|
|
||||||
updated.set(encoded, false)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return updated
|
return updated
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -310,9 +310,9 @@ export function replaceNostrUrisInMarkdownWithTitles(
|
|||||||
* This converts: nostr:npub1... to [@username](link) and nostr:naddr1... to [Article Title](link)
|
* This converts: nostr:npub1... to [@username](link) and nostr:naddr1... to [Article Title](link)
|
||||||
* Labels update progressively as profiles load
|
* Labels update progressively as profiles load
|
||||||
* @param markdown The markdown content to process
|
* @param markdown The markdown content to process
|
||||||
* @param profileLabels Map of encoded identifier -> display name (e.g., npub1... -> @username)
|
* @param profileLabels Map of pubkey (hex) -> display name (e.g., pubkey -> @username)
|
||||||
* @param articleTitles Map of naddr -> title for resolved articles
|
* @param articleTitles Map of naddr -> title for resolved articles
|
||||||
* @param profileLoading Map of encoded identifier -> boolean indicating if profile is loading
|
* @param profileLoading Map of pubkey (hex) -> boolean indicating if profile is loading
|
||||||
*/
|
*/
|
||||||
export function replaceNostrUrisInMarkdownWithProfileLabels(
|
export function replaceNostrUrisInMarkdownWithProfileLabels(
|
||||||
markdown: string,
|
markdown: string,
|
||||||
@@ -326,12 +326,6 @@ export function replaceNostrUrisInMarkdownWithProfileLabels(
|
|||||||
return replaceNostrUrisSafely(markdown, (encoded) => {
|
return replaceNostrUrisSafely(markdown, (encoded) => {
|
||||||
const link = createNostrLink(encoded)
|
const link = createNostrLink(encoded)
|
||||||
|
|
||||||
// Check if we have a resolved profile name
|
|
||||||
if (profileLabels.has(encoded)) {
|
|
||||||
const displayName = profileLabels.get(encoded)!
|
|
||||||
return `[${displayName}](${link})`
|
|
||||||
}
|
|
||||||
|
|
||||||
// For articles, use the resolved title if available
|
// For articles, use the resolved title if available
|
||||||
try {
|
try {
|
||||||
const decoded = decode(encoded)
|
const decoded = decode(encoded)
|
||||||
@@ -340,25 +334,21 @@ export function replaceNostrUrisInMarkdownWithProfileLabels(
|
|||||||
return `[${title}](${link})`
|
return `[${title}](${link})`
|
||||||
}
|
}
|
||||||
|
|
||||||
// For npub/nprofile, check if loading and show loading state
|
// For npub/nprofile, extract pubkey and use it as the lookup key
|
||||||
if (decoded.type === 'npub' || decoded.type === 'nprofile') {
|
if (decoded.type === 'npub' || decoded.type === 'nprofile') {
|
||||||
const hasLoading = profileLoading.has(encoded)
|
const pubkey = decoded.type === 'npub' ? decoded.data : decoded.data.pubkey
|
||||||
const isLoading = profileLoading.get(encoded)
|
|
||||||
|
|
||||||
// Debug: Check if there's a key mismatch
|
// Check if we have a resolved profile name using pubkey as key
|
||||||
if (!hasLoading && profileLoading.size > 0) {
|
if (profileLabels.has(pubkey)) {
|
||||||
// Check if there's a similar key (for debugging)
|
const displayName = profileLabels.get(pubkey)!
|
||||||
const matchingKey = Array.from(profileLoading.keys()).find(k => k.includes(encoded.slice(0, 20)) || encoded.includes(k.slice(0, 20)))
|
return `[${displayName}](${link})`
|
||||||
if (matchingKey) {
|
|
||||||
console.log(`[profile-loading-debug][nostr-uri-resolve] KEY MISMATCH: encoded="${encoded.slice(0, 50)}..." vs Map key="${matchingKey.slice(0, 50)}..."`)
|
|
||||||
console.log(`[profile-loading-debug][nostr-uri-resolve] Full encoded length=${encoded.length}, Full key length=${matchingKey.length}`)
|
|
||||||
console.log(`[profile-loading-debug][nostr-uri-resolve] encoded === key? ${encoded === matchingKey}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasLoading && isLoading) {
|
// Check loading state using pubkey as key
|
||||||
|
const isLoading = profileLoading.get(pubkey)
|
||||||
|
if (isLoading === true) {
|
||||||
const label = getNostrUriLabel(encoded)
|
const label = getNostrUriLabel(encoded)
|
||||||
console.log(`[profile-loading-debug][nostr-uri-resolve] ${encoded.slice(0, 16)}... is LOADING, showing loading state`)
|
console.log(`[profile-loading-debug][nostr-uri-resolve] ${pubkey.slice(0, 16)}... is LOADING, showing loading state`)
|
||||||
// Wrap in span with profile-loading class for CSS styling
|
// Wrap in span with profile-loading class for CSS styling
|
||||||
return `[<span class="profile-loading">${label}</span>](${link})`
|
return `[<span class="profile-loading">${label}</span>](${link})`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user