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

View File

@@ -60,13 +60,13 @@ export function useProfileLabels(
if (cachedProfile) {
const displayName = extractProfileDisplayName(cachedProfile)
if (displayName) {
// Only add @ prefix if we have a real name, otherwise use fallback format directly
const label = displayName.startsWith('@') ? displayName : `@${displayName}`
// Add @ prefix (extractProfileDisplayName returns name without @)
const label = `@${displayName}`
labels.set(pubkey, label)
} 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)
labels.set(pubkey, fallback)
labels.set(pubkey, `@${fallback}`)
}
}
})
@@ -224,13 +224,13 @@ export function useProfileLabels(
// Extract display name using centralized utility
const displayName = extractProfileDisplayName(eventStoreProfile as NostrEvent)
if (displayName) {
// Only add @ prefix if we have a real name, otherwise use fallback format directly
const label = displayName.startsWith('@') ? displayName : `@${displayName}`
// Add @ prefix (extractProfileDisplayName returns name without @)
const label = `@${displayName}`
labels.set(pubkey, label)
} 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)
labels.set(pubkey, fallback)
labels.set(pubkey, `@${fallback}`)
}
loading.set(pubkey, false)
} else {
@@ -258,8 +258,9 @@ export function useProfileLabels(
const pubkey = event.pubkey
// Determine the label for this profile using centralized utility
// Add @ prefix (both extractProfileDisplayName and getNpubFallbackDisplay return names without @)
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
// 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)
switch (decoded.type) {
case 'npub':
// Remove "npub1" prefix (5 chars) and show next 7 chars
return `@${encoded.slice(5, 12)}...`
case 'npub': {
// Use shared fallback display function and add @ for label
const pubkey = decoded.data
return `@${getNpubFallbackDisplay(pubkey)}`
}
case 'nprofile': {
const npub = npubEncode(decoded.data.pubkey)
// Remove "npub1" prefix (5 chars) and show next 7 chars
return `@${npub.slice(5, 12)}...`
// Use shared fallback display function and add @ for label
const pubkey = decoded.data.pubkey
return `@${getNpubFallbackDisplay(pubkey)}`
}
case 'note':
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
* 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
* @returns Formatted npub display string
* @returns Formatted npub display string without @ prefix
*/
export function getNpubFallbackDisplay(pubkey: string): string {
try {
const npub = npubEncode(pubkey)
// Remove "npub1" prefix (5 chars) and show next 7 chars
return `@${npub.slice(5, 12)}...`
return `${npub.slice(5, 12)}...`
} catch {
// Fallback to shortened pubkey if encoding fails
return `@${pubkey.slice(0, 8)}...`
return `${pubkey.slice(0, 8)}...`
}
}
@@ -380,34 +383,34 @@ export function addLoadingClassToProfileLinks(
// Find all <a> tags with href starting with /p/ (profile links)
const result = html.replace(/<a\s+[^>]*?href="\/p\/([^"]+)"[^>]*?>/g, (match, npub: string) => {
try {
// Decode npub to get pubkey
// Decode npub or nprofile to get pubkey
const decoded: ReturnType<typeof decode> = decode(npub)
switch (decoded.type) {
case 'npub': {
const pubkey = decoded.data
// Check if this profile is loading
const isLoading = profileLoading.get(pubkey)
if (isLoading === true) {
// Add profile-loading class if not already present
if (!match.includes('profile-loading')) {
// Insert class before the closing >
const classMatch = /class="([^"]*)"/.exec(match)
if (classMatch) {
const updated = match.replace(/class="([^"]*)"/, `class="$1 profile-loading"`)
return updated
} else {
const updated = match.replace(/(<a\s+[^>]*?)>/, '$1 class="profile-loading">')
return updated
}
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
const isLoading = profileLoading.get(pubkey)
if (isLoading === true) {
// Add profile-loading class if not already present
if (!match.includes('profile-loading')) {
// Insert class before the closing >
const classMatch = /class="([^"]*)"/.exec(match)
if (classMatch) {
const updated = match.replace(/class="([^"]*)"/, `class="$1 profile-loading"`)
return updated
} else {
const updated = match.replace(/(<a\s+[^>]*?)>/, '$1 class="profile-loading">')
return updated
}
}
break
}
default:
// Not an npub, ignore
break
}
} catch (error) {
// 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
}