import { useMemo } from 'react'; import { useAppContext } from '@/hooks/useAppContext'; import type { RelayMetadata } from '@/contexts/AppContext'; /** Host (no scheme) for display, falling back to the raw value. */ export function relayHost(url: string): string { try { return new URL(url).host; } catch { return url; } } /** Normalise user input into a `ws(s)://` relay URL, or null if invalid. */ export function normalizeRelayUrl(input: string): string | null { const raw = input.trim(); if (!raw) return null; try { const url = new URL(/^wss?:\/\//i.test(raw) ? raw : `wss://${raw}`); if (url.protocol === 'http:') url.protocol = 'ws:'; if (url.protocol === 'https:') url.protocol = 'wss:'; if (url.protocol !== 'ws:' && url.protocol !== 'wss:') return null; if (!url.host) return null; return url.toString().replace(/\/$/, ''); } catch { return null; } } /** Hosts that are retired or dead and are pruned from every relay list. */ export const RETIRED_RELAY_HOSTS = new Set([ 'goskatespots.nostr1.com', // legacy hosted relay — retired 2026-10-12 'wotr.relatr.xyz', // dead (does not resolve) ]); /** True when `url` points at a retired/dead relay host. */ export function isRetiredRelay(url: string): boolean { try { return RETIRED_RELAY_HOSTS.has(new URL(url).host.toLowerCase()); } catch { return false; } } /** * Remove retired/dead relays from a stored RelayMetadata. Returns a new object * only when something actually changed (so callers can skip a no-op write), else * null. */ export function pruneRetiredRelays(metadata: RelayMetadata): RelayMetadata | null { const relays = metadata.relays.filter((r) => !isRetiredRelay(r.url)); const local = metadata.localRelays?.filter((u) => !isRetiredRelay(u)); const dm = metadata.dmRelays?.filter((u) => !isRetiredRelay(u)); const changed = relays.length !== metadata.relays.length || (local?.length ?? 0) !== (metadata.localRelays?.length ?? 0) || (dm?.length ?? 0) !== (metadata.dmRelays?.length ?? 0); if (!changed) return null; return { ...metadata, relays, ...(metadata.localRelays ? { localRelays: local } : {}), ...(metadata.dmRelays ? { dmRelays: dm } : {}), }; } export interface ResolvedRelays { /** NIP-65 relays with blocked ones removed (read/write flags kept). */ configured: { url: string; read: boolean; write: boolean }[]; /** App-local relays (blocked ones removed). */ local: string[]; /** The blocklist, lower-cased. */ blocked: string[]; /** URLs to read from: local first, then read-enabled configured. */ read: string[]; /** URLs to write to: write-enabled configured, then local. */ write: string[]; } /** * Resolve the effective relay sets from config: blocked relays are removed * everywhere, and local relays are added to both reads and writes. */ export function resolveRelays(metadata: RelayMetadata): ResolvedRelays { const blocked = new Set((metadata.blockedRelays ?? []).map((u) => u.toLowerCase())); const local = (metadata.localRelays ?? []).filter( (u) => !blocked.has(u.toLowerCase()) && !isRetiredRelay(u), ); const configured = metadata.relays.filter( (r) => !blocked.has(r.url.toLowerCase()) && !isRetiredRelay(r.url), ); return { configured, local, blocked: [...blocked], read: [...local, ...configured.filter((r) => r.read).map((r) => r.url)], write: [...configured.filter((r) => r.write).map((r) => r.url), ...local], }; } /** React wrapper around {@link resolveRelays} for components/hooks. */ export function useRelays(): ResolvedRelays { const { config } = useAppContext(); return useMemo(() => resolveRelays(config.relayMetadata), [config.relayMetadata]); }