import { Skeleton } from '@/components/ui/skeleton' import { cn } from '@/lib/utils' import blossomService from '@/services/blossom.service' import { TImetaInfo } from '@/types' import { decode } from 'blurhash' import { ImageOff } from 'lucide-react' import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react' import { thumbHashToDataURL } from 'thumbhash' export default function Image({ image: { url, blurHash, thumbHash, pubkey, dim, variant, sha256 }, alt, className = '', classNames = {}, hideIfError = false, errorPlaceholder = , originalUrl, ...props }: HTMLAttributes & { classNames?: { wrapper?: string errorPlaceholder?: string skeleton?: string } image: TImetaInfo alt?: string hideIfError?: boolean errorPlaceholder?: React.ReactNode originalUrl?: string }) { const [isLoading, setIsLoading] = useState(true) const [displaySkeleton, setDisplaySkeleton] = useState(true) const [hasError, setHasError] = useState(false) const [imageUrl, setImageUrl] = useState() const [naturalDim, setNaturalDim] = useState<{ width: number; height: number } | null>(null) const [showTooltip, setShowTooltip] = useState(false) const timeoutRef = useRef(null) useEffect(() => { setIsLoading(true) setHasError(false) setDisplaySkeleton(true) if (pubkey) { // BlossomService now actively validates URLs and races mirrors. // The promise will resolve with the best available URL. blossomService.getValidUrl(url, pubkey).then((validUrl) => { setImageUrl(validUrl) if (timeoutRef.current) { clearTimeout(timeoutRef.current) timeoutRef.current = null } }) // Fallback timeout in case something goes wrong with the service timeoutRef.current = setTimeout(() => { if (!imageUrl) { setImageUrl(url) } }, 3000) } else { setImageUrl(url) } return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current) timeoutRef.current = null } } }, [url]) if (hideIfError && hasError) return null const handleError = async () => { const nextUrl = await blossomService.tryNextUrl(url) if (nextUrl) { setImageUrl(nextUrl) } else { setIsLoading(false) setHasError(true) } } const handleLoad = (e: React.SyntheticEvent) => { const img = e.currentTarget setNaturalDim({ width: img.naturalWidth, height: img.naturalHeight }) setIsLoading(false) setHasError(false) setTimeout(() => setDisplaySkeleton(false), 600) blossomService.markAsSuccess(url, imageUrl || url) } return (
setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} {...props} > {/* Debug tooltip overlay */} {showTooltip && !isLoading && !hasError && (
{variant && (
variant {variant}
)}
url: {imageUrl || url}
{naturalDim && (
rendered: {naturalDim.width}x{naturalDim.height}
)} {dim && (
declared: {dim.width}x{dim.height}
)} {variant && originalUrl && (
original: {originalUrl}
)} {sha256 && (
sha256: {sha256}
)}
)} {/* Spacer: transparent image to maintain dimensions when image is loading */} {isLoading && dim?.width && dim?.height && ( )} {displaySkeleton && (
{thumbHash ? ( ) : blurHash ? ( ) : ( )}
)} {!hasError && ( {alt} )} {hasError && (typeof errorPlaceholder === 'string' ? ( {alt} ) : (
{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 ( ) } function ThumbHashPlaceholder({ thumbHash, className = '' }: { thumbHash: Uint8Array className?: string }) { const dataUrl = useMemo(() => { if (!thumbHash) return null try { return thumbHashToDataURL(thumbHash) } catch (error) { console.warn('failed to decode thumbhash:', error) return null } }, [thumbHash]) if (!dataUrl) return null return (
) }