Replace DomainFavicon with ExternalFavicon using configurable favicon URL template
The old DomainFavicon component had a multi-step fallback chain (HTML scraping, direct URL guessing, then the configured provider) which meant the configurable setting was almost never reached. The new ExternalFavicon component uses the configured favicon URL template directly via RFC 6570 URI templates. - Rename config field faviconProvider -> faviconUrl - Add faviconUrl() utility with uri-templates for RFC 6570 support - Remove local Favicon component from ProfileRightSidebar - Update all consumers to use ExternalFavicon
This commit is contained in:
Generated
+7
@@ -72,6 +72,7 @@
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uri-templates": "^0.2.0",
|
||||
"vaul": "^0.9.3",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
@@ -9755,6 +9756,12 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-templates": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/uri-templates/-/uri-templates-0.2.0.tgz",
|
||||
"integrity": "sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg==",
|
||||
"license": "http://geraintluff.github.io/tv4/LICENSE.txt"
|
||||
},
|
||||
"node_modules/use-callback-ref": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uri-templates": "^0.2.0",
|
||||
"vaul": "^0.9.3",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ const defaultConfig: AppConfig = {
|
||||
nip85OnlyMode: true,
|
||||
blossomServers: ['https://blossom.primal.net/'],
|
||||
defaultZapComment: 'Zapped with Mew!',
|
||||
faviconProvider: 'https://favicon.shakespeare.diy/?url={href}',
|
||||
faviconUrl: 'https://favicon.shakespeare.diy/?url={href}',
|
||||
corsProxy: 'https://proxy.shakespeare.diy/?url={href}',
|
||||
contentWarningPolicy: 'blur',
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export function AdvancedSettings() {
|
||||
const [statsPubkey, setStatsPubkey] = useState(config.nip85StatsPubkey);
|
||||
const [newBlossomUrl, setNewBlossomUrl] = useState('');
|
||||
const [defaultZapComment, setDefaultZapComment] = useState(config.defaultZapComment);
|
||||
const [faviconProvider, setFaviconProvider] = useState(config.faviconProvider);
|
||||
const [faviconUrl, setFaviconUrl] = useState(config.faviconUrl);
|
||||
const [corsProxy, setCorsProxy] = useState(config.corsProxy);
|
||||
|
||||
const handleAddBlossomServer = () => {
|
||||
@@ -352,25 +352,25 @@ export function AdvancedSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Favicon Provider */}
|
||||
{/* Favicon URL */}
|
||||
<div>
|
||||
<Label htmlFor="favicon-provider" className="text-sm font-medium">
|
||||
Favicon Provider
|
||||
<Label htmlFor="favicon-url" className="text-sm font-medium">
|
||||
Favicon URL
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1 mb-2">
|
||||
URI template for fetching site favicons. Use <code className="bg-muted px-1 rounded">{'{href}'}</code> as a placeholder for the page URL.
|
||||
URI template for fetching site favicons. Supports RFC 6570 variables: <code className="bg-muted px-1 rounded">{'{href}'}</code>, <code className="bg-muted px-1 rounded">{'{hostname}'}</code>, <code className="bg-muted px-1 rounded">{'{origin}'}</code>, etc.
|
||||
</p>
|
||||
<Input
|
||||
id="favicon-provider"
|
||||
value={faviconProvider}
|
||||
id="favicon-url"
|
||||
value={faviconUrl}
|
||||
onChange={(e) => {
|
||||
setFaviconProvider(e.target.value);
|
||||
setFaviconUrl(e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
const trimmed = faviconProvider.trim();
|
||||
if (trimmed && trimmed !== config.faviconProvider) {
|
||||
updateConfig(() => ({ faviconProvider: trimmed }));
|
||||
toast({ title: 'Favicon provider updated' });
|
||||
const trimmed = faviconUrl.trim();
|
||||
if (trimmed && trimmed !== config.faviconUrl) {
|
||||
updateConfig(() => ({ faviconUrl: trimmed }));
|
||||
toast({ title: 'Favicon URL updated' });
|
||||
}
|
||||
}}
|
||||
placeholder="https://favicon.shakespeare.diy/?url={href}"
|
||||
|
||||
@@ -63,7 +63,7 @@ const AppConfigSchema = z.object({
|
||||
nip85OnlyMode: z.boolean(),
|
||||
blossomServers: z.array(z.url()),
|
||||
defaultZapComment: z.string(),
|
||||
faviconProvider: z.string(),
|
||||
faviconUrl: z.string(),
|
||||
corsProxy: z.string(),
|
||||
contentWarningPolicy: z.enum(['blur', 'hide', 'show']),
|
||||
});
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppContext } from '@/hooks/useAppContext';
|
||||
|
||||
interface DomainFaviconProps {
|
||||
/** Full URL or just domain name */
|
||||
domain: string;
|
||||
/** Size in pixels (default: 16) */
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the HTML of a domain via CORS proxy and parses for favicon link tags.
|
||||
* Returns the discovered favicon URL or null if not found.
|
||||
*/
|
||||
async function discoverFavicon(origin: string, corsProxy: string): Promise<string | null> {
|
||||
try {
|
||||
// Use CORS proxy to fetch the HTML
|
||||
const proxyUrl = corsProxy.replace('{href}', encodeURIComponent(origin));
|
||||
const response = await fetch(proxyUrl, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'text/html' }
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
// Parse favicon from <link> tags
|
||||
// Look for rel="icon", rel="shortcut icon", or rel="apple-touch-icon"
|
||||
const iconRegex = /<link[^>]*rel=["'](?:shortcut )?icon["'][^>]*>/gi;
|
||||
const appleIconRegex = /<link[^>]*rel=["']apple-touch-icon["'][^>]*>/gi;
|
||||
|
||||
const matches = [...html.matchAll(iconRegex), ...html.matchAll(appleIconRegex)];
|
||||
|
||||
for (const match of matches) {
|
||||
const linkTag = match[0];
|
||||
const hrefMatch = linkTag.match(/href=["']([^"']+)["']/i);
|
||||
if (hrefMatch && hrefMatch[1]) {
|
||||
let href = hrefMatch[1];
|
||||
// Make absolute URL if relative
|
||||
if (href.startsWith('/')) {
|
||||
href = `${origin}${href}`;
|
||||
} else if (!href.startsWith('http')) {
|
||||
href = `${origin}/${href}`;
|
||||
}
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a favicon for a domain or URL.
|
||||
* Strategy: Scrape HTML for favicon → try common paths → fallback to favicon provider → hide if all fail
|
||||
*/
|
||||
export function DomainFavicon({ domain, size = 16, className }: DomainFaviconProps) {
|
||||
const { config } = useAppContext();
|
||||
const [faviconUrl, setFaviconUrl] = useState<string | null>(null);
|
||||
const [fallbackIndex, setFallbackIndex] = useState(0);
|
||||
const [triedProvider, setTriedProvider] = useState(false);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
// Get origin from domain
|
||||
const origin = useMemo(() => {
|
||||
try {
|
||||
if (domain.startsWith('http://') || domain.startsWith('https://')) {
|
||||
return new URL(domain).origin;
|
||||
}
|
||||
return `https://${domain}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [domain]);
|
||||
|
||||
// Direct favicon URLs to try (svg, ico, png)
|
||||
const directUrls = useMemo(() => {
|
||||
if (!origin) return [];
|
||||
return [
|
||||
`${origin}/favicon.svg`,
|
||||
`${origin}/favicon.ico`,
|
||||
`${origin}/favicon.png`,
|
||||
];
|
||||
}, [origin]);
|
||||
|
||||
// Favicon provider fallback URL (configurable template)
|
||||
const providerUrl = useMemo(() => {
|
||||
if (!origin) return null;
|
||||
return config.faviconProvider.replace('{href}', encodeURIComponent(origin));
|
||||
}, [origin, config.faviconProvider]);
|
||||
|
||||
// Discover favicon from HTML on mount
|
||||
useEffect(() => {
|
||||
if (!origin) {
|
||||
setFailed(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let mounted = true;
|
||||
|
||||
// Try to discover favicon from HTML
|
||||
discoverFavicon(origin, config.corsProxy).then((url) => {
|
||||
if (mounted) {
|
||||
if (url) {
|
||||
// Found favicon in HTML, use it
|
||||
setFaviconUrl(url);
|
||||
} else {
|
||||
// No favicon in HTML, start with first direct URL
|
||||
setFaviconUrl(directUrls[0] || null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [origin, directUrls]);
|
||||
|
||||
const handleLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
const img = e.currentTarget;
|
||||
|
||||
// If this is a favicon provider service, check if it returned the default placeholder
|
||||
if (faviconUrl === providerUrl) {
|
||||
// Google's default placeholder is exactly 16x16 gray globe
|
||||
// Real favicons from Google are usually larger or have different aspect ratios
|
||||
if (img.naturalWidth === 16 && img.naturalHeight === 16) {
|
||||
// This is likely the default globe - reject it
|
||||
setFailed(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
// If we haven't tried all direct URLs yet
|
||||
if (fallbackIndex < directUrls.length - 1) {
|
||||
setFallbackIndex(fallbackIndex + 1);
|
||||
setFaviconUrl(directUrls[fallbackIndex + 1]);
|
||||
} else if (!triedProvider && providerUrl) {
|
||||
// All direct URLs failed, try favicon provider
|
||||
setTriedProvider(true);
|
||||
setFaviconUrl(providerUrl);
|
||||
} else {
|
||||
// Even Google failed, hide the favicon
|
||||
setFailed(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (!faviconUrl || failed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
className={cn('shrink-0', className)}
|
||||
style={{ width: size, height: size }}
|
||||
loading="lazy"
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useAppContext } from '@/hooks/useAppContext';
|
||||
import { faviconUrl } from '@/lib/faviconUrl';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
|
||||
interface ExternalFaviconProps {
|
||||
/** The URL to fetch the favicon for */
|
||||
url: string | URL | undefined;
|
||||
/** Size of the favicon in pixels */
|
||||
size?: number;
|
||||
/** Fallback element to display if favicon fails to load */
|
||||
fallback?: ReactNode;
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ExternalFavicon component that fetches and displays a favicon for a given URL
|
||||
* using the configurable favicon service from app settings.
|
||||
*/
|
||||
export function ExternalFavicon({
|
||||
url,
|
||||
size = 16,
|
||||
fallback,
|
||||
className = '',
|
||||
}: ExternalFaviconProps) {
|
||||
const { config } = useAppContext();
|
||||
|
||||
// Generate the favicon URL using the configured template
|
||||
const faviconSrc = useMemo(() => {
|
||||
if (!url) return;
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
// Strip `ai.` and `api.` subdomains for a better chance at finding a favicon
|
||||
parsedUrl.hostname = parsedUrl.hostname.replace(/^(ai\.|api\.|enclave\.)/, '');
|
||||
|
||||
// Normalize the URL to ensure it has a protocol
|
||||
return faviconUrl({ template: config.faviconUrl, url: parsedUrl });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}, [url, config.faviconUrl]);
|
||||
|
||||
// If faviconSrc is not available, render the fallback directly
|
||||
if (!faviconSrc) {
|
||||
return (
|
||||
<span className={cn('inline-flex items-center justify-center', className)}>
|
||||
<span>{fallback}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={cn('inline-flex items-center justify-center', className)}>
|
||||
<img
|
||||
src={faviconSrc}
|
||||
alt=""
|
||||
className="object-contain"
|
||||
style={{ width: size, height: size }}
|
||||
onError={(e) => {
|
||||
// Hide the image and show the fallback on error
|
||||
e.currentTarget.style.display = 'none';
|
||||
const fallbackElement = e.currentTarget.nextElementSibling as HTMLElement;
|
||||
if (fallbackElement) {
|
||||
fallbackElement.style.display = 'inline-block';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{fallback && (
|
||||
<span style={{ display: 'none' }}>
|
||||
{fallback}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DomainFavicon } from '@/components/DomainFavicon';
|
||||
import { ExternalFavicon } from '@/components/ExternalFavicon';
|
||||
import { getNip05Domain, getNip05User } from '@/lib/nip05';
|
||||
|
||||
interface Nip05BadgeProps {
|
||||
@@ -54,7 +54,7 @@ export function Nip05Badge({ nip05, className, iconSize = 16 }: Nip05BadgeProps)
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title={`View ${domain} feed`}
|
||||
>
|
||||
<DomainFavicon domain={domain} size={iconSize} className="shrink-0" />
|
||||
<ExternalFavicon url={`https://${domain}`} size={iconSize} className="shrink-0" />
|
||||
</Link>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/hover-card';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { DomainFavicon } from '@/components/DomainFavicon';
|
||||
import { ExternalFavicon } from '@/components/ExternalFavicon';
|
||||
import { EmojifiedText } from '@/components/CustomEmoji';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
@@ -83,7 +83,7 @@ export function ProfileHoverCard({ pubkey, children, asChild }: ProfileHoverCard
|
||||
<div className="flex items-center gap-1 text-sm text-muted-foreground mt-0.5">
|
||||
<span className="truncate">@{nip05Display}</span>
|
||||
{nip05Domain && (
|
||||
<DomainFavicon domain={nip05Domain} size={14} className="shrink-0" />
|
||||
<ExternalFavicon url={`https://${nip05Domain}`} size={14} className="shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@@ -6,6 +6,7 @@ import { Check, Copy, QrCode, ExternalLink, Bitcoin, ShieldAlert } from 'lucide-
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ExternalFavicon } from '@/components/ExternalFavicon';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useFeedSettings } from '@/hooks/useFeedSettings';
|
||||
import { getEnabledFeedKinds } from '@/lib/extraKinds';
|
||||
@@ -119,41 +120,6 @@ function eventLink(item: MediaItem): string {
|
||||
return `/${nip19.neventEncode({ id: item.eventId, author: item.authorPubkey })}`;
|
||||
}
|
||||
|
||||
/** Try multiple favicon paths: /favicon.ico, then /favicon.svg. */
|
||||
function Favicon({ url }: { url: string }) {
|
||||
const candidates = useMemo(() => {
|
||||
try {
|
||||
const origin = new URL(url).origin;
|
||||
return [`${origin}/favicon.ico`, `${origin}/favicon.svg`];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
const [index, setIndex] = useState(0);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
if (candidates.length === 0 || failed) return null;
|
||||
|
||||
const src = candidates[index];
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
className="size-4 shrink-0"
|
||||
loading="lazy"
|
||||
onError={() => {
|
||||
if (index < candidates.length - 1) {
|
||||
setIndex(index + 1);
|
||||
} else {
|
||||
setFailed(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Bitcoin QR code modal */
|
||||
function BitcoinQRModal({ address }: { address: string }) {
|
||||
const [qrUrl, setQrUrl] = useState('');
|
||||
@@ -282,7 +248,7 @@ function ProfileFieldRow({ field }: { field: ProfileField }) {
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 text-sm text-primary hover:underline truncate mt-0.5"
|
||||
>
|
||||
<Favicon url={field.value} />
|
||||
<ExternalFavicon url={field.value} size={16} className="shrink-0" />
|
||||
<span className="truncate">{field.value.replace(/^https?:\/\//, '')}</span>
|
||||
</a>
|
||||
) : (
|
||||
|
||||
@@ -83,8 +83,8 @@ export interface AppConfig {
|
||||
blossomServers: string[];
|
||||
/** Default comment attached to zaps */
|
||||
defaultZapComment: string;
|
||||
/** Favicon provider URI template. Use {href} as placeholder for the page URL. */
|
||||
faviconProvider: string;
|
||||
/** Favicon URI template. Supports RFC 6570 variables: {href}, {origin}, {hostname}, etc. */
|
||||
faviconUrl: string;
|
||||
/** CORS proxy URI template. Use {href} as placeholder for the target URL (URL-encoded). */
|
||||
corsProxy: string;
|
||||
/** How to handle NIP-36 content-warning events (blur, hide, or show). Default: "blur". */
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import UriTemplate from 'uri-templates';
|
||||
|
||||
export interface FaviconUrlOpts {
|
||||
template: string;
|
||||
url: string | URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a favicon URL from a template and input URL
|
||||
* @param opts - Options object
|
||||
* @param opts.template - URL template with placeholders like {hostname}, {origin}, etc.
|
||||
* @param opts.url - The URL to extract parts from
|
||||
* @returns The hydrated favicon URL
|
||||
*/
|
||||
export function faviconUrl(opts: FaviconUrlOpts): string {
|
||||
const u = new URL(opts.url);
|
||||
|
||||
return UriTemplate(opts.template).fill({
|
||||
href: u.href,
|
||||
origin: u.origin,
|
||||
protocol: u.protocol,
|
||||
username: u.username,
|
||||
password: u.password,
|
||||
host: u.host,
|
||||
hostname: u.hostname,
|
||||
port: u.port,
|
||||
pathname: u.pathname,
|
||||
hash: u.hash,
|
||||
search: u.search,
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { NoteCard } from '@/components/NoteCard';
|
||||
import { DomainFavicon } from '@/components/DomainFavicon';
|
||||
import { ExternalFavicon } from '@/components/ExternalFavicon';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useFeedSettings } from '@/hooks/useFeedSettings';
|
||||
import { useAppContext } from '@/hooks/useAppContext';
|
||||
@@ -104,7 +104,7 @@ export function DomainFeedPage() {
|
||||
<ArrowLeft className="size-5" />
|
||||
</button>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<DomainFavicon domain={domain ?? ''} size={20} />
|
||||
<ExternalFavicon url={domain ? `https://${domain}` : undefined} size={20} />
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-lg font-bold truncate leading-tight">{domain}</h1>
|
||||
{pubkeys && pubkeys.length > 0 && (
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useLayoutOptions } from '@/contexts/LayoutContext';
|
||||
import { ProfileRightSidebar } from '@/components/ProfileRightSidebar';
|
||||
import { NoteCard } from '@/components/NoteCard';
|
||||
import { ZapDialog } from '@/components/ZapDialog';
|
||||
import { DomainFavicon } from '@/components/DomainFavicon';
|
||||
import { ExternalFavicon } from '@/components/ExternalFavicon';
|
||||
import { Nip05Badge } from '@/components/Nip05Badge';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
@@ -426,7 +426,7 @@ function ProfileFieldInline({ field }: { field: { label: string; value: string }
|
||||
if (isUrl) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<DomainFavicon domain={field.value} size={16} />
|
||||
<ExternalFavicon url={field.value} size={16} className="shrink-0" />
|
||||
<span className="text-sm text-muted-foreground shrink-0">{field.label}</span>
|
||||
<a
|
||||
href={field.value}
|
||||
|
||||
@@ -57,7 +57,7 @@ export function TestApp({ children }: TestAppProps) {
|
||||
nip85OnlyMode: false,
|
||||
blossomServers: ['https://blossom.primal.net/'],
|
||||
defaultZapComment: 'Zapped with Mew!',
|
||||
faviconProvider: 'https://favicon.shakespeare.diy/?url={href}',
|
||||
faviconUrl: 'https://favicon.shakespeare.diy/?url={href}',
|
||||
corsProxy: 'https://proxy.shakespeare.diy/?url={href}',
|
||||
contentWarningPolicy: 'blur',
|
||||
};
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
declare module 'uri-templates' {
|
||||
interface UriTemplate {
|
||||
fill(vars: Record<string, string>): string;
|
||||
}
|
||||
function UriTemplate(template: string): UriTemplate;
|
||||
export default UriTemplate;
|
||||
}
|
||||
Reference in New Issue
Block a user