import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import type { ComponentChildren } from 'preact'; import { Globe } from 'lucide-preact'; import type { Cipher } from '@/lib/types'; import { getWebsiteIconImageUrl, getWebsiteIconStatus, preloadWebsiteIcon, subscribeWebsiteIconStatus, } from '@/lib/website-icon-cache'; import { firstCipherUri, hostFromUri, websiteIconUrl } from '@/lib/website-utils'; const ICON_LOAD_ROOT_MARGIN = '180px 0px'; interface WebsiteIconProps { cipher: Cipher; fallback?: ComponentChildren; } export default function WebsiteIcon(props: WebsiteIconProps) { const host = useMemo(() => hostFromUri(firstCipherUri(props.cipher)), [props.cipher]); const src = host ? websiteIconUrl(host) : ''; const nodeRef = useRef(null); const [shouldLoad, setShouldLoad] = useState(() => (host ? getWebsiteIconStatus(host) === 'loaded' : true)); const [status, setStatus] = useState(() => (host ? getWebsiteIconStatus(host) : 'idle')); const [imageUrl, setImageUrl] = useState(() => (host ? getWebsiteIconImageUrl(host) : '')); useEffect(() => { if (!host) { setShouldLoad(true); setStatus('idle'); setImageUrl(''); return; } const nextStatus = getWebsiteIconStatus(host); setShouldLoad(nextStatus === 'loaded'); setStatus(nextStatus); setImageUrl(getWebsiteIconImageUrl(host)); return subscribeWebsiteIconStatus(host, (next) => { setStatus(next); setImageUrl(getWebsiteIconImageUrl(host)); }); }, [host]); useEffect(() => { if (!host || shouldLoad || status === 'loaded' || status === 'error') return; const node = nodeRef.current; if (!node) return; if (typeof IntersectionObserver !== 'function') { setShouldLoad(true); return; } let cancelled = false; const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (!entry.isIntersecting && entry.intersectionRatio <= 0) continue; if (!cancelled) setShouldLoad(true); observer.disconnect(); break; } }, { rootMargin: ICON_LOAD_ROOT_MARGIN } ); observer.observe(node); return () => { cancelled = true; observer.disconnect(); }; }, [host, shouldLoad, status]); useEffect(() => { if (!host || !src || !shouldLoad || status === 'loaded' || status === 'error') return; let disposed = false; void preloadWebsiteIcon(host, src).then((nextStatus) => { if (disposed) return; setStatus(nextStatus); setImageUrl(getWebsiteIconImageUrl(host)); }); return () => { disposed = true; }; }, [host, src, shouldLoad, status]); if (!host || status === 'error') { return {props.fallback ?? }; } return ( {status !== 'loaded' && {props.fallback ?? }} {status === 'loaded' && imageUrl && ( )} ); }