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 @@ @@ -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({ @@ -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 (
<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 (
<Avatar
<div
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) => {
e.stopPropagation()
navigateToProfile(toProfile(pubkey))
navigateToProfile(toProfile(displayPubkey))
}}
>
<AvatarImage src={avatar} className="object-cover object-center" />
<AvatarFallback>
<img src={defaultAvatar} alt={pubkey} />
</AvatarFallback>
</Avatar>
{!imgError && currentSrc ? (
<>
{!imageLoaded && (
<div className="absolute inset-0 bg-muted animate-pulse" />
)}
<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({ @@ -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(

7
src/components/WebPreview/index.tsx

@ -10,7 +10,6 @@ import { ExternalLink } from 'lucide-react' @@ -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? @@ -168,7 +167,6 @@ export default function WebPreview({ url, className }: { url: string; className?
<div className="flex items-center gap-2 mb-1">
{fetchedEvent ? (
<>
<SimpleUserAvatar userId={fetchedEvent.pubkey} size="xSmall" />
<Username userId={fetchedEvent.pubkey} className="text-xs" />
<span className="text-xs text-muted-foreground"></span>
<span className="text-xs text-muted-foreground">{eventTypeName}</span>
@ -211,11 +209,6 @@ export default function WebPreview({ url, className }: { url: string; className? @@ -211,11 +209,6 @@ export default function WebPreview({ url, className }: { url: string; className?
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 items-center gap-2">
{fetchedProfile ? (

13
src/lib/pubkey.ts

@ -59,12 +59,17 @@ export function isValidPubkey(pubkey: string) { @@ -59,12 +59,17 @@ export function isValidPubkey(pubkey: string) {
}
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 {
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 { @@ -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
}

Loading…
Cancel
Save