diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx
index 47937aa..2c0ee78 100644
--- a/src/components/UserAvatar/index.tsx
+++ b/src/components/UserAvatar/index.tsx
@@ -1,11 +1,9 @@
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@/hooks'
-import { generateImageByPubkey } from '@/lib/pubkey'
+import { generateImageByPubkey, userIdToPubkey } from '@/lib/pubkey'
import { toProfile } from '@/lib/link'
import { cn } from '@/lib/utils'
import { useSmartProfileNavigation } from '@/PageManager'
-import { nip19 } from 'nostr-tools'
import { useMemo, useState, useEffect } from 'react'
const UserAvatarSizeCnMap = {
@@ -30,33 +28,96 @@ export default function UserAvatar({
}) {
const { profile } = useFetchProfile(userId)
const { navigateToProfile } = useSmartProfileNavigation()
+
+ // Extract pubkey from userId if it's npub/nprofile format
+ const pubkey = useMemo(() => {
+ if (!userId) return ''
+ const decodedPubkey = userIdToPubkey(userId)
+ return decodedPubkey || profile?.pubkey || ''
+ }, [userId, profile?.pubkey])
+
const defaultAvatar = useMemo(
- () => (profile?.pubkey ? generateImageByPubkey(profile.pubkey) : ''),
- [profile]
+ () => (pubkey ? generateImageByPubkey(pubkey) : ''),
+ [pubkey]
)
+
+ // Use profile avatar if available, otherwise use default avatar
+ const avatarSrc = profile?.avatar || defaultAvatar || ''
- if (!profile) {
+ // All hooks must be called before any early returns
+ const [imgError, setImgError] = useState(false)
+ const [currentSrc, setCurrentSrc] = useState(avatarSrc)
+ const [imageLoaded, setImageLoaded] = useState(false)
+
+ // Reset error state when src changes
+ useEffect(() => {
+ setImgError(false)
+ setImageLoaded(false)
+ setCurrentSrc(avatarSrc)
+ }, [avatarSrc])
+
+ const handleImageError = () => {
+ if (profile?.avatar && defaultAvatar && currentSrc === profile.avatar) {
+ // Try default avatar if profile avatar fails
+ setCurrentSrc(defaultAvatar)
+ setImgError(false)
+ } else {
+ // Both failed, show placeholder
+ setImgError(true)
+ setImageLoaded(true)
+ }
+ }
+
+ const handleImageLoad = () => {
+ setImageLoaded(true)
+ setImgError(false)
+ }
+
+ // Use pubkey from decoded userId if profile isn't loaded yet
+ const displayPubkey = profile?.pubkey || pubkey || ''
+
+ // If we have a pubkey (from decoding npub/nprofile or profile), show avatar even without profile
+ // Otherwise show skeleton while loading
+ if (!profile && !pubkey) {
return (
)
}
- const { avatar, pubkey } = profile
-
+ // Render image directly instead of using Radix UI Avatar for better reliability
return (
- {
e.stopPropagation()
- navigateToProfile(toProfile(pubkey))
+ navigateToProfile(toProfile(displayPubkey))
}}
>
-
-
-
-
-
+ {!imgError && currentSrc ? (
+ <>
+ {!imageLoaded && (
+
+ )}
+
+ >
+ ) : (
+ // Show initials or placeholder when image fails
+
+ {displayPubkey ? displayPubkey.slice(0, 2).toUpperCase() : ''}
+
+ )}
+
)
}
@@ -73,30 +134,8 @@ export function SimpleUserAvatar({
// Always generate default avatar from userId/pubkey, even if profile isn't loaded yet
const pubkey = useMemo(() => {
if (!userId) return ''
- try {
- // Try to extract pubkey from userId (handles npub, nprofile, or hex pubkey)
- if (userId.length === 64 && /^[0-9a-f]+$/i.test(userId)) {
- return userId
- }
- // Try to decode npub/nprofile to get pubkey
- try {
- const decoded = nip19.decode(userId)
- if (decoded.type === 'npub') {
- return decoded.data
- } else if (decoded.type === 'nprofile') {
- return decoded.data.pubkey
- }
- } catch {
- // Not a valid npub/nprofile, continue
- }
- // Use profile pubkey if available
- if (profile?.pubkey) {
- return profile.pubkey
- }
- return ''
- } catch {
- return ''
- }
+ const decodedPubkey = userIdToPubkey(userId)
+ return decodedPubkey || profile?.pubkey || ''
}, [userId, profile?.pubkey])
const defaultAvatar = useMemo(
diff --git a/src/components/WebPreview/index.tsx b/src/components/WebPreview/index.tsx
index 7224d3c..2cccec8 100644
--- a/src/components/WebPreview/index.tsx
+++ b/src/components/WebPreview/index.tsx
@@ -10,7 +10,6 @@ import { ExternalLink } from 'lucide-react'
import { nip19, kinds } from 'nostr-tools'
import { useMemo } from 'react'
import Image from '../Image'
-import { SimpleUserAvatar } from '../UserAvatar'
import Username from '../Username'
// Helper function to get event type name
@@ -168,7 +167,6 @@ export default function WebPreview({ url, className }: { url: string; className?
{fetchedEvent ? (
<>
-
•
{eventTypeName}
@@ -211,11 +209,6 @@ export default function WebPreview({ url, className }: { url: string; className?
window.open(url, '_blank')
}}
>
- {fetchedProfile ? (
-
- ) : (
-
- )}
{fetchedProfile ? (
diff --git a/src/lib/pubkey.ts b/src/lib/pubkey.ts
index 309bda3..a225166 100644
--- a/src/lib/pubkey.ts
+++ b/src/lib/pubkey.ts
@@ -59,12 +59,17 @@ export function isValidPubkey(pubkey: string) {
}
const pubkeyImageCache = new LRUCache({ max: 1000 })
+
+// Version identifier to force cache invalidation when algorithm changes
+const CACHE_VERSION = 'v2'
+
export function generateImageByPubkey(pubkey: string): string {
- if (pubkeyImageCache.has(pubkey)) {
- return pubkeyImageCache.get(pubkey)!
+ const cacheKey = `${CACHE_VERSION}:${pubkey}`
+ if (pubkeyImageCache.has(cacheKey)) {
+ return pubkeyImageCache.get(cacheKey)!
}
- const paddedPubkey = pubkey.padEnd(2, '0')
+ const paddedPubkey = pubkey.padEnd(66, '0')
// Split into 3 parts for colors and the rest for control points
const colors: string[] = []
@@ -104,7 +109,7 @@ export function generateImageByPubkey(pubkey: string): string {
`
const imageData = `data:image/svg+xml;base64,${btoa(image)}`
- pubkeyImageCache.set(pubkey, imageData)
+ pubkeyImageCache.set(cacheKey, imageData)
return imageData
}