import { SymbolView } from "./AppSymbol"; import { Image } from "expo-image"; import { useLayoutEffect, useMemo, useState } from "react"; import { View } from "react-native"; import type { EnvironmentId } from "@t3tools/contracts"; import { getProjectFaviconCacheKey, getProjectFaviconResourceKey, isProjectFaviconFallbackUrl, } from "@t3tools/shared/projectFavicon"; import { useAtomValue } from "@effect/atom-react"; import { Atom } from "effect/unstable/reactivity"; import { projectFaviconUrlAtom } from "../state/assets"; import { beginProjectFaviconRequest, createProjectFaviconRequest, hasLoadedProjectFavicon, markProjectFaviconFailed, markProjectFaviconLoaded, } from "./projectFaviconCache"; const EMPTY_FAVICON_URL = Atom.make(null); /* ─── Component ──────────────────────────────────────────────────────── */ export function ProjectFavicon(props: { readonly environmentId: EnvironmentId; readonly open?: boolean; readonly size?: number; readonly projectTitle: string; readonly workspaceRoot?: string | null; readonly faviconPath?: string | null; }) { const size = props.size ?? 42; const faviconUrl = useAtomValue( props.workspaceRoot == null ? EMPTY_FAVICON_URL : projectFaviconUrlAtom({ environmentId: props.environmentId, cwd: props.workspaceRoot, faviconPath: props.faviconPath, }), ); const renderableFaviconUrl = isProjectFaviconFallbackUrl(faviconUrl) ? null : faviconUrl; // Inline images are self-contained; remote URLs key on their revision so signed-token // rotation reuses the disk cache while a changed icon starts from the loading state. const cacheKey = renderableFaviconUrl && props.workspaceRoot ? renderableFaviconUrl.startsWith("data:") ? getProjectFaviconResourceKey(props.environmentId, props.workspaceRoot, props.faviconPath) : getProjectFaviconCacheKey(props.environmentId, props.workspaceRoot, renderableFaviconUrl) : null; return ( ); } function ProjectFaviconImage(props: { readonly cacheKey: string | null; readonly faviconUrl: string | null; readonly open?: boolean; readonly projectTitle: string; readonly size: number; }) { const faviconRequest = useMemo( () => createProjectFaviconRequest(props.cacheKey, props.faviconUrl), [props.cacheKey, props.faviconUrl], ); const [activeFaviconRequest, setActiveFaviconRequest] = useState(null); useLayoutEffect(() => { if (faviconRequest === null) return; const endRequest = beginProjectFaviconRequest(faviconRequest); setActiveFaviconRequest(faviconRequest); return endRequest; }, [faviconRequest]); const [status, setStatus] = useState<"loading" | "loaded" | "error">(() => props.faviconUrl?.startsWith("data:") || hasLoadedProjectFavicon(props.cacheKey) ? "loaded" : "loading", ); const requestIsActive = faviconRequest !== null && activeFaviconRequest === faviconRequest; const showImage = requestIsActive && status === "loaded"; return ( {/* Folder icon fallback (matches web's FolderIcon) */} {!showImage ? ( ) : null} {/* Favicon image (hidden until loaded) */} {requestIsActive ? ( { if (!markProjectFaviconLoaded(faviconRequest)) return; setStatus("loaded"); }} onError={() => { if (!markProjectFaviconFailed(faviconRequest)) return; setStatus("error"); }} /> ) : null} ); }