import React, { useEffect, useState } from "react"; import { Image, Platform, View } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { useColorScheme } from "nativewind"; import { resolveIcon } from "../services/favicon"; import { type UploadResolver, useUploadBlobUrl } from "../hooks/useUploadBlobUrl"; export interface ItemIconProps { /** * Stored icon reference. Either a literal URL, a `custom:` * marker (fetched through the auth-aware transport), or null/empty * to fall back to the favicon proxy. */ iconUrl?: string | null; /** Site URL the favicon proxy derives a public URL from. */ siteUrl?: string | null; /** Tile size in pixels (square). Defaults to 40. */ size?: number; /** Inner image size. Defaults to roughly 60% of `size`. */ imageSize?: number; /** Tile corner radius. Defaults to 25% of `size`. */ borderRadius?: number; /** Tile background override. Defaults to the secondary surface. */ background?: string; /** Globe-glyph color override. Defaults to muted-foreground. */ fallbackColor?: string; /** Favicon proxy path on the configured API server. Defaults to `/favicon`. */ faviconPath?: string; /** Upload-fetch path prefix. Defaults to `/uploads`. */ uploadsPath?: string; /** * Resolve a `custom:` reference yourself instead of letting this * component fetch it. * * For a consumer whose uploads are not behind `apiFetch` at all — vault's * device-only vaults keep them in on-device SQLite, with no server to ask. * Without this, such a consumer has to either fork the component or * pre-resolve the reference into a `data:` URI and pass it as `iconUrl`, * which works by accident (anything that is not `custom:` is treated as a * plain URL) and hides the intent. * * Return `null` for an id that does not resolve; the fallback glyph is drawn, * exactly as for a fetch that fails. */ resolveUpload?: UploadResolver; } // react-native-web forwards `loading` and `decoding` to the underlying // ; RN's TS types don't expose these props, so we cast through a // Record for the web branch. const WEB_LAZY_PROPS = Platform.OS === "web" ? ({ loading: "lazy", decoding: "async" } as Record) : {}; /** * Per-item icon: shows the custom upload, falls back to the favicon * proxy, then to a generic globe glyph on a tile-toned square. Shared * between RN clients (mobile) and RN-Web (extension popup). */ export const ItemIcon = React.memo(function ItemIcon({ iconUrl, siteUrl, size = 40, imageSize, borderRadius, background, fallbackColor, faviconPath = "/favicon", uploadsPath = "/uploads", resolveUpload, }: ItemIconProps) { const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; const resolved = resolveIcon({ iconUrl, siteUrl, faviconPath }); const uploadId = resolved.kind === "upload" ? resolved.uploadId : null; const upload = useUploadBlobUrl(uploadId, { uploadsPath, resolve: resolveUpload }); const directUri = resolved.kind === "url" ? resolved.uri : null; const [urlErrored, setUrlErrored] = useState(false); useEffect(() => { setUrlErrored(false); }, [directUri]); const renderUri = uploadId ? upload.uri : directUri; const renderErrored = uploadId ? upload.errored : urlErrored; const onError = () => { if (uploadId) { upload.setErrored(true); } else { setUrlErrored(true); } }; const tileBg = background ?? (isDark ? "rgb(38, 38, 38)" : "rgb(245, 245, 245)"); const tileRadius = borderRadius ?? size * 0.25; const innerSize = imageSize ?? Math.round(size * 0.6); const globeColor = fallbackColor ?? (isDark ? "rgb(163, 163, 163)" : "rgb(115, 115, 115)"); const showFallback = !renderUri || renderErrored; return ( {showFallback ? ( ) : ( )} ); });