1import type { ScopedThreadRef } from "@t3tools/contracts";
2import { type ReactNode, useState } from "react";
3
4import { useFaviconForThreadUrl } from "~/browserFaviconStore";
5import { cn } from "~/lib/utils";
6
7import { BrowserMockup } from "./BrowserMockup";
8
9export function FaviconImage(props: {
10 sources: ReadonlyArray<string | null | undefined>;
11 fallback: ReactNode;
12 className?: string | undefined;
13}) {
14 const sources = props.sources.filter((source): source is string => Boolean(source));
15 return (
16 <FaviconImageAttempt
17 key={sources.join("\0")}
18 sources={sources}
19 fallback={props.fallback}
20 className={props.className}
21 />
22 );
23}
24
25function FaviconImageAttempt(props: {
26 sources: ReadonlyArray<string>;
27 fallback: ReactNode;
28 className?: string | undefined;
29}) {
30 const [failed, setFailed] = useState<ReadonlySet<string>>(() => new Set());
31 const source = props.sources.find((candidate) => !failed.has(candidate));
32 if (!source) return props.fallback;
33 return (
34 <img
35 src={source}
36 alt=""
37 aria-hidden="true"
38 draggable={false}
39 className={props.className}
40 onError={() => setFailed((current) => new Set(current).add(source))}
41 />
42 );
43}
44
45export function PreviewFaviconIcon(props: {
46 threadRef: ScopedThreadRef;
47 url: string;
48 className?: string | undefined;
49}) {
50 const source = useFaviconForThreadUrl(props.threadRef, props.url);
51 const fallback = <BrowserMockup className={cn("size-7 shrink-0", props.className)} />;
52 return (
53 <FaviconImage
54 sources={[source]}
55 fallback={fallback}
56 className={cn("size-7 shrink-0 rounded object-contain", props.className)}
57 />
58 );
59}