You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
79 lines
2.7 KiB
79 lines
2.7 KiB
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' |
|
import { useFetchRelayInfo } from '@/hooks' |
|
import { getRelayIconOverrideSrc, relayUrlFingerprintColors } from '@/lib/relay-icon-source' |
|
import { cn } from '@/lib/utils' |
|
import logger from '@/lib/logger' |
|
import { Server } from 'lucide-react' |
|
import { useMemo } from 'react' |
|
|
|
/** |
|
* Resolve an image URL from NIP-11. Handles: |
|
* - Absolute HTTP(S) URLs → used as-is |
|
* - Relative paths (e.g. "/logo.png") → resolved against the relay's base HTTP URL |
|
* - ws(s):// URLs some relays mistakenly return → ignored |
|
* |
|
* We do not fetch `https://host/favicon.ico` as a fallback: many relays return HTML/404 there, |
|
* which triggers Firefox Opaque Response Blocking noise and broken `<img>` loads. |
|
*/ |
|
function resolveRelayImageUrl(raw: string, relayUrl: string): string | undefined { |
|
if (!raw) return undefined |
|
if (raw.startsWith('https://') || raw.startsWith('http://')) return raw |
|
if (raw.startsWith('/')) { |
|
try { |
|
const base = relayUrl.replace(/^wss?:\/\//i, 'https://').replace(/^ws:\/\//i, 'http://') |
|
const u = new URL(base) |
|
return `${u.protocol}//${u.host}${raw}` |
|
} catch { |
|
return undefined |
|
} |
|
} |
|
return undefined |
|
} |
|
|
|
export default function RelayIcon({ |
|
url, |
|
className, |
|
iconSize = 14, |
|
/** When true, do not hit NIP-11 (parent already fetches relay info, or icon-only row). */ |
|
skipRelayInfoFetch = false |
|
}: { |
|
url?: string |
|
className?: string |
|
iconSize?: number |
|
skipRelayInfoFetch?: boolean |
|
}) { |
|
const { relayInfo } = useFetchRelayInfo(skipRelayInfoFetch ? undefined : url) |
|
const iconUrl = useMemo(() => { |
|
if (!url) return undefined |
|
|
|
const override = getRelayIconOverrideSrc(url) |
|
if (override) { |
|
logger.debug('[RelayIcon] using override icon', { url, override }) |
|
return override |
|
} |
|
|
|
// Prefer the NIP-11 icon field |
|
const rawIcon = relayInfo?.icon && typeof relayInfo.icon === 'string' ? relayInfo.icon : undefined |
|
const nip11Icon = rawIcon ? resolveRelayImageUrl(rawIcon, url) : undefined |
|
if (nip11Icon) { |
|
logger.debug('[RelayIcon] using NIP-11 icon', { url, rawIcon, nip11Icon }) |
|
return nip11Icon |
|
} |
|
|
|
return undefined |
|
}, [url, relayInfo]) |
|
|
|
const fallbackColors = useMemo(() => relayUrlFingerprintColors(url), [url]) |
|
|
|
return ( |
|
<Avatar className={cn('w-6 h-6', className)}> |
|
{iconUrl && <AvatarImage src={iconUrl} className="object-cover object-center" />} |
|
<AvatarFallback |
|
className="bg-transparent" |
|
style={{ backgroundColor: fallbackColors.background, color: fallbackColors.color }} |
|
> |
|
<Server size={iconSize} className="opacity-95" aria-hidden /> |
|
</AvatarFallback> |
|
</Avatar> |
|
) |
|
}
|
|
|