Browse Source

remove profile pics from fallback cards

imwald
Silberengel 4 months ago
parent
commit
e55da45f79
  1. 119
      src/components/UserAvatar/index.tsx
  2. 7
      src/components/WebPreview/index.tsx
  3. 13
      src/lib/pubkey.ts

119
src/components/UserAvatar/index.tsx

@ -1,11 +1,9 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@/hooks' import { useFetchProfile } from '@/hooks'
import { generateImageByPubkey } from '@/lib/pubkey' import { generateImageByPubkey, userIdToPubkey } from '@/lib/pubkey'
import { toProfile } from '@/lib/link' import { toProfile } from '@/lib/link'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useSmartProfileNavigation } from '@/PageManager' import { useSmartProfileNavigation } from '@/PageManager'
import { nip19 } from 'nostr-tools'
import { useMemo, useState, useEffect } from 'react' import { useMemo, useState, useEffect } from 'react'
const UserAvatarSizeCnMap = { const UserAvatarSizeCnMap = {
@ -30,33 +28,96 @@ export default function UserAvatar({
}) { }) {
const { profile } = useFetchProfile(userId) const { profile } = useFetchProfile(userId)
const { navigateToProfile } = useSmartProfileNavigation() 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( const defaultAvatar = useMemo(
() => (profile?.pubkey ? generateImageByPubkey(profile.pubkey) : ''), () => (pubkey ? generateImageByPubkey(pubkey) : ''),
[profile] [pubkey]
) )
if (!profile) { // Use profile avatar if available, otherwise use default avatar
const avatarSrc = profile?.avatar || defaultAvatar || ''
// 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 ( return (
<Skeleton className={cn('shrink-0', UserAvatarSizeCnMap[size], 'rounded-full', className)} /> <Skeleton className={cn('shrink-0', UserAvatarSizeCnMap[size], 'rounded-full', className)} />
) )
} }
const { avatar, pubkey } = profile // Render image directly instead of using Radix UI Avatar for better reliability
return ( return (
<Avatar <div
data-user-avatar data-user-avatar
className={cn('shrink-0 cursor-pointer', UserAvatarSizeCnMap[size], className)} className={cn('shrink-0 cursor-pointer relative overflow-hidden rounded-full bg-muted', UserAvatarSizeCnMap[size], className)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
navigateToProfile(toProfile(pubkey)) navigateToProfile(toProfile(displayPubkey))
}} }}
> >
<AvatarImage src={avatar} className="object-cover object-center" /> {!imgError && currentSrc ? (
<AvatarFallback> <>
<img src={defaultAvatar} alt={pubkey} /> {!imageLoaded && (
</AvatarFallback> <div className="absolute inset-0 bg-muted animate-pulse" />
</Avatar> )}
<img
src={currentSrc}
alt={displayPubkey}
className={cn(
'h-full w-full object-cover object-center transition-opacity duration-200',
imageLoaded ? 'opacity-100' : 'opacity-0'
)}
onError={handleImageError}
onLoad={handleImageLoad}
loading="lazy"
/>
</>
) : (
// Show initials or placeholder when image fails
<div className="h-full w-full flex items-center justify-center text-xs font-medium text-muted-foreground">
{displayPubkey ? displayPubkey.slice(0, 2).toUpperCase() : ''}
</div>
)}
</div>
) )
} }
@ -73,30 +134,8 @@ export function SimpleUserAvatar({
// Always generate default avatar from userId/pubkey, even if profile isn't loaded yet // Always generate default avatar from userId/pubkey, even if profile isn't loaded yet
const pubkey = useMemo(() => { const pubkey = useMemo(() => {
if (!userId) return '' if (!userId) return ''
try { const decodedPubkey = userIdToPubkey(userId)
// Try to extract pubkey from userId (handles npub, nprofile, or hex pubkey) return decodedPubkey || profile?.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 ''
}
}, [userId, profile?.pubkey]) }, [userId, profile?.pubkey])
const defaultAvatar = useMemo( const defaultAvatar = useMemo(

7
src/components/WebPreview/index.tsx

@ -10,7 +10,6 @@ import { ExternalLink } from 'lucide-react'
import { nip19, kinds } from 'nostr-tools' import { nip19, kinds } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import Image from '../Image' import Image from '../Image'
import { SimpleUserAvatar } from '../UserAvatar'
import Username from '../Username' import Username from '../Username'
// Helper function to get event type name // Helper function to get event type name
@ -168,7 +167,6 @@ export default function WebPreview({ url, className }: { url: string; className?
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
{fetchedEvent ? ( {fetchedEvent ? (
<> <>
<SimpleUserAvatar userId={fetchedEvent.pubkey} size="xSmall" />
<Username userId={fetchedEvent.pubkey} className="text-xs" /> <Username userId={fetchedEvent.pubkey} className="text-xs" />
<span className="text-xs text-muted-foreground"></span> <span className="text-xs text-muted-foreground"></span>
<span className="text-xs text-muted-foreground">{eventTypeName}</span> <span className="text-xs text-muted-foreground">{eventTypeName}</span>
@ -211,11 +209,6 @@ export default function WebPreview({ url, className }: { url: string; className?
window.open(url, '_blank') window.open(url, '_blank')
}} }}
> >
{fetchedProfile ? (
<SimpleUserAvatar userId={fetchedProfile.pubkey} size="small" />
) : (
<div className="w-7 h-7 rounded-full bg-muted flex-shrink-0" />
)}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{fetchedProfile ? ( {fetchedProfile ? (

13
src/lib/pubkey.ts

@ -59,12 +59,17 @@ export function isValidPubkey(pubkey: string) {
} }
const pubkeyImageCache = new LRUCache<string, string>({ max: 1000 }) const pubkeyImageCache = new LRUCache<string, string>({ max: 1000 })
// Version identifier to force cache invalidation when algorithm changes
const CACHE_VERSION = 'v2'
export function generateImageByPubkey(pubkey: string): string { export function generateImageByPubkey(pubkey: string): string {
if (pubkeyImageCache.has(pubkey)) { const cacheKey = `${CACHE_VERSION}:${pubkey}`
return pubkeyImageCache.get(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 // Split into 3 parts for colors and the rest for control points
const colors: string[] = [] const colors: string[] = []
@ -104,7 +109,7 @@ export function generateImageByPubkey(pubkey: string): string {
` `
const imageData = `data:image/svg+xml;base64,${btoa(image)}` const imageData = `data:image/svg+xml;base64,${btoa(image)}`
pubkeyImageCache.set(pubkey, imageData) pubkeyImageCache.set(cacheKey, imageData)
return imageData return imageData
} }

Loading…
Cancel
Save