import { Skeleton } from '@/components/ui/skeleton' import { cn } from '@/lib/utils' import client from '@/services/client.service' import { TImetaInfo } from '@/types' import { getHashFromURL } from 'blossom-client-sdk' import { decode } from 'blurhash' import { ImageOff } from 'lucide-react' import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react' export default function Image({ image: { url, blurHash, pubkey, dim }, alt, className = '', classNames = {}, hideIfError = false, errorPlaceholder = , ...props }: HTMLAttributes & { classNames?: { wrapper?: string errorPlaceholder?: string } image: TImetaInfo alt?: string hideIfError?: boolean errorPlaceholder?: React.ReactNode }) { const [isLoading, setIsLoading] = useState(true) const [displaySkeleton, setDisplaySkeleton] = useState(true) const [hasError, setHasError] = useState(false) const [imageUrl, setImageUrl] = useState(url) const [tried, setTried] = useState(new Set()) useEffect(() => { setImageUrl(url) setIsLoading(true) setHasError(false) setDisplaySkeleton(true) setTried(new Set()) }, [url]) if (hideIfError && hasError) return null const handleError = async () => { let oldImageUrl: URL | undefined let hash: string | null = null try { oldImageUrl = new URL(imageUrl) hash = getHashFromURL(oldImageUrl) } catch (error) { console.error('Invalid image URL:', error) } if (!pubkey || !hash || !oldImageUrl) { setIsLoading(false) setHasError(true) return } const ext = oldImageUrl.pathname.match(/\.\w+$/i) setTried((prev) => new Set(prev.add(oldImageUrl.hostname))) const blossomServerList = await client.fetchBlossomServerList(pubkey) const urls = blossomServerList .map((server) => { try { return new URL(server) } catch (error) { console.error('Invalid Blossom server URL:', server, error) return undefined } }) .filter((url) => !!url && !tried.has(url.hostname)) const nextUrl = urls[0] if (!nextUrl) { setIsLoading(false) setHasError(true) return } nextUrl.pathname = '/' + hash + ext setImageUrl(nextUrl.toString()) } const handleLoad = () => { setIsLoading(false) setHasError(false) setTimeout(() => setDisplaySkeleton(false), 600) } return (
{displaySkeleton && (
{blurHash ? ( ) : ( )}
)} {!hasError && ( {alt} )} {hasError && (
{errorPlaceholder}
)}
) } const blurHashWidth = 32 const blurHashHeight = 32 function BlurHashCanvas({ blurHash, className = '' }: { blurHash: string; className?: string }) { const canvasRef = useRef(null) const pixels = useMemo(() => { if (!blurHash) return null try { return decode(blurHash, blurHashWidth, blurHashHeight) } catch (error) { console.warn('Failed to decode blurhash:', error) return null } }, [blurHash]) useEffect(() => { if (!pixels || !canvasRef.current) return const canvas = canvasRef.current const ctx = canvas.getContext('2d') if (!ctx) return const imageData = ctx.createImageData(blurHashWidth, blurHashHeight) imageData.data.set(pixels) ctx.putImageData(imageData, 0, 0) }, [pixels]) if (!blurHash) return null return ( ) }