refactor: standardize @ prefix handling and improve npub/nprofile display

- Fix getNpubFallbackDisplay to return names without @ prefix
- Update all call sites to consistently add @ when rendering mentions
- Fix incomplete error handling in getNpubFallbackDisplay catch block
- Add nprofile support to addLoadingClassToProfileLinks
- Extract shared isProfileInCacheOrStore utility to eliminate duplicate loading state checks
- Update ResolvedMention and NostrMentionLink to use shared utility

This ensures consistent @ prefix handling across all profile display contexts
and eliminates code duplication for profile loading state detection.
This commit is contained in:
Gigi
2025-11-02 23:36:46 +01:00
parent 8cb77864bc
commit 66de230f66
5 changed files with 78 additions and 61 deletions

View File

@@ -4,7 +4,7 @@ import { useEventModel } from 'applesauce-react/hooks'
import { Hooks } from 'applesauce-react' import { Hooks } from 'applesauce-react'
import { Models, Helpers } from 'applesauce-core' import { Models, Helpers } from 'applesauce-core'
import { getProfileDisplayName } from '../utils/nostrUriResolver' import { getProfileDisplayName } from '../utils/nostrUriResolver'
import { loadCachedProfiles } from '../services/profileService' import { isProfileInCacheOrStore } from '../utils/profileLoadingUtils'
const { getPubkeyFromDecodeResult } = Helpers const { getPubkeyFromDecodeResult } = Helpers
@@ -43,14 +43,7 @@ const NostrMentionLink: React.FC<NostrMentionLinkProps> = ({
// Check if profile is in cache or eventStore for loading detection // Check if profile is in cache or eventStore for loading detection
const isInCacheOrStore = useMemo(() => { const isInCacheOrStore = useMemo(() => {
if (!pubkey) return false if (!pubkey) return false
// Check cache return isProfileInCacheOrStore(pubkey, eventStore)
const cached = loadCachedProfiles([pubkey])
if (cached.has(pubkey)) {
return true
}
// Check eventStore
const eventStoreProfile = eventStore?.getEvent(pubkey + ':0')
return !!eventStoreProfile
}, [pubkey, eventStore]) }, [pubkey, eventStore])
// Show loading if profile doesn't exist and not in cache/store (for npub/nprofile) // Show loading if profile doesn't exist and not in cache/store (for npub/nprofile)

View File

@@ -5,7 +5,7 @@ import { Hooks } from 'applesauce-react'
import { Models, Helpers } from 'applesauce-core' import { Models, Helpers } from 'applesauce-core'
import { decode, npubEncode } from 'nostr-tools/nip19' import { decode, npubEncode } from 'nostr-tools/nip19'
import { getProfileDisplayName } from '../utils/nostrUriResolver' import { getProfileDisplayName } from '../utils/nostrUriResolver'
import { loadCachedProfiles } from '../services/profileService' import { isProfileInCacheOrStore } from '../utils/profileLoadingUtils'
const { getPubkeyFromDecodeResult } = Helpers const { getPubkeyFromDecodeResult } = Helpers
@@ -28,14 +28,7 @@ const ResolvedMention: React.FC<ResolvedMentionProps> = ({ encoded }) => {
// Check if profile is in cache or eventStore // Check if profile is in cache or eventStore
const isInCacheOrStore = useMemo(() => { const isInCacheOrStore = useMemo(() => {
if (!pubkey) return false if (!pubkey) return false
// Check cache return isProfileInCacheOrStore(pubkey, eventStore)
const cached = loadCachedProfiles([pubkey])
if (cached.has(pubkey)) {
return true
}
// Check eventStore
const eventStoreProfile = eventStore?.getEvent(pubkey + ':0')
return !!eventStoreProfile
}, [pubkey, eventStore]) }, [pubkey, eventStore])
// Show loading if profile doesn't exist and not in cache/store // Show loading if profile doesn't exist and not in cache/store

View File

@@ -60,13 +60,13 @@ export function useProfileLabels(
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 // Add @ prefix (extractProfileDisplayName returns name without @)
const label = displayName.startsWith('@') ? displayName : `@${displayName}` const label = `@${displayName}`
labels.set(pubkey, 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 (add @ prefix)
const fallback = getNpubFallbackDisplay(pubkey) const fallback = getNpubFallbackDisplay(pubkey)
labels.set(pubkey, fallback) labels.set(pubkey, `@${fallback}`)
} }
} }
}) })
@@ -224,13 +224,13 @@ export function useProfileLabels(
// Extract display name using centralized utility // Extract display name using centralized utility
const displayName = extractProfileDisplayName(eventStoreProfile as NostrEvent) const displayName = extractProfileDisplayName(eventStoreProfile as NostrEvent)
if (displayName) { if (displayName) {
// Only add @ prefix if we have a real name, otherwise use fallback format directly // Add @ prefix (extractProfileDisplayName returns name without @)
const label = displayName.startsWith('@') ? displayName : `@${displayName}` const label = `@${displayName}`
labels.set(pubkey, 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 (add @ prefix)
const fallback = getNpubFallbackDisplay(pubkey) const fallback = getNpubFallbackDisplay(pubkey)
labels.set(pubkey, fallback) labels.set(pubkey, `@${fallback}`)
} }
loading.set(pubkey, false) loading.set(pubkey, false)
} else { } else {
@@ -258,8 +258,9 @@ export function useProfileLabels(
const pubkey = event.pubkey const pubkey = event.pubkey
// Determine the label for this profile using centralized utility // Determine the label for this profile using centralized utility
// Add @ prefix (both extractProfileDisplayName and getNpubFallbackDisplay return names without @)
const displayName = extractProfileDisplayName(event) const displayName = extractProfileDisplayName(event)
const label = displayName ? (displayName.startsWith('@') ? displayName : `@${displayName}`) : getNpubFallbackDisplay(pubkey) const label = displayName ? `@${displayName}` : `@${getNpubFallbackDisplay(pubkey)}`
// Apply label immediately to prevent race condition with loading state // Apply label immediately to prevent race condition with loading state
// This ensures labels are available when isLoading becomes false // This ensures labels are available when isLoading becomes false

View File

@@ -86,13 +86,15 @@ export function getNostrUriLabel(encoded: string): string {
const decoded = decode(encoded) const decoded = decode(encoded)
switch (decoded.type) { switch (decoded.type) {
case 'npub': case 'npub': {
// Remove "npub1" prefix (5 chars) and show next 7 chars // Use shared fallback display function and add @ for label
return `@${encoded.slice(5, 12)}...` const pubkey = decoded.data
return `@${getNpubFallbackDisplay(pubkey)}`
}
case 'nprofile': { case 'nprofile': {
const npub = npubEncode(decoded.data.pubkey) // Use shared fallback display function and add @ for label
// Remove "npub1" prefix (5 chars) and show next 7 chars const pubkey = decoded.data.pubkey
return `@${npub.slice(5, 12)}...` return `@${getNpubFallbackDisplay(pubkey)}`
} }
case 'note': case 'note':
return `note:${encoded.slice(5, 12)}...` return `note:${encoded.slice(5, 12)}...`
@@ -119,18 +121,19 @@ export function getNostrUriLabel(encoded: string): string {
/** /**
* Get a standardized fallback display name for a pubkey when profile has no name * Get a standardized fallback display name for a pubkey when profile has no name
* Returns npub format: @abc1234... * Returns npub format: abc1234... (without @ prefix)
* Components should add @ prefix when rendering mentions/links
* @param pubkey The pubkey in hex format * @param pubkey The pubkey in hex format
* @returns Formatted npub display string * @returns Formatted npub display string without @ prefix
*/ */
export function getNpubFallbackDisplay(pubkey: string): string { export function getNpubFallbackDisplay(pubkey: string): string {
try { try {
const npub = npubEncode(pubkey) const npub = npubEncode(pubkey)
// Remove "npub1" prefix (5 chars) and show next 7 chars // Remove "npub1" prefix (5 chars) and show next 7 chars
return `@${npub.slice(5, 12)}...` return `${npub.slice(5, 12)}...`
} catch { } catch {
// Fallback to shortened pubkey if encoding fails // Fallback to shortened pubkey if encoding fails
return `@${pubkey.slice(0, 8)}...` return `${pubkey.slice(0, 8)}...`
} }
} }
@@ -380,12 +383,17 @@ export function addLoadingClassToProfileLinks(
// Find all <a> tags with href starting with /p/ (profile links) // Find all <a> tags with href starting with /p/ (profile links)
const result = html.replace(/<a\s+[^>]*?href="\/p\/([^"]+)"[^>]*?>/g, (match, npub: string) => { const result = html.replace(/<a\s+[^>]*?href="\/p\/([^"]+)"[^>]*?>/g, (match, npub: string) => {
try { try {
// Decode npub to get pubkey // Decode npub or nprofile to get pubkey
const decoded: ReturnType<typeof decode> = decode(npub) const decoded: ReturnType<typeof decode> = decode(npub)
switch (decoded.type) {
case 'npub': {
const pubkey = decoded.data
let pubkey: string | undefined
if (decoded.type === 'npub') {
pubkey = decoded.data
} else if (decoded.type === 'nprofile') {
pubkey = decoded.data.pubkey
}
if (pubkey) {
// Check if this profile is loading // Check if this profile is loading
const isLoading = profileLoading.get(pubkey) const isLoading = profileLoading.get(pubkey)
@@ -403,11 +411,6 @@ export function addLoadingClassToProfileLinks(
} }
} }
} }
break
}
default:
// Not an npub, ignore
break
} }
} catch (error) { } catch (error) {
// Ignore processing errors // Ignore processing errors

View File

@@ -0,0 +1,27 @@
import { IEventStore } from 'applesauce-core'
import { loadCachedProfiles } from '../services/profileService'
/**
* Check if a profile exists in cache or eventStore
* Used to determine if profile loading state should be shown
* @param pubkey The pubkey in hex format
* @param eventStore Optional eventStore instance
* @returns true if profile exists in cache or eventStore, false otherwise
*/
export function isProfileInCacheOrStore(
pubkey: string,
eventStore?: IEventStore | null
): boolean {
if (!pubkey) return false
// Check cache first
const cached = loadCachedProfiles([pubkey])
if (cached.has(pubkey)) {
return true
}
// Check eventStore
const eventStoreProfile = eventStore?.getEvent(pubkey + ':0')
return !!eventStoreProfile
}