diff --git a/src/renderer/src/components/browser-favicon.tsx b/src/renderer/src/components/browser-favicon.tsx new file mode 100644 index 000000000000..ecde4065f03b --- /dev/null +++ b/src/renderer/src/components/browser-favicon.tsx @@ -0,0 +1,59 @@ +import { useState } from 'react' +import { Globe } from 'lucide-react' +import { cn } from '@/lib/utils' + +function displayableFaviconUrl(faviconUrl: string | null | undefined): string | null { + const trimmed = faviconUrl?.trim() + if (!trimmed) { + return null + } + if (trimmed.startsWith('data:image/')) { + return trimmed + } + try { + const url = new URL(trimmed) + return url.protocol === 'http:' || url.protocol === 'https:' ? trimmed : null + } catch { + return null + } +} + +export function BrowserFavicon({ + faviconUrl, + className, + fallbackClassName +}: { + faviconUrl: string | null | undefined + className?: string + fallbackClassName?: string +}): React.JSX.Element { + const displayUrl = displayableFaviconUrl(faviconUrl) + const [failedUrl, setFailedUrl] = useState(null) + + // Why: reset during render on any favicon identity change โ€” including a clear to null while + // a page loads โ€” so navigating back to the same url retries instead of keeping the fallback. + if (failedUrl !== null && failedUrl !== displayUrl) { + setFailedUrl(null) + } + + if (displayUrl && failedUrl !== displayUrl) { + return ( + setFailedUrl(displayUrl)} + /> + ) + } + + return