SkateSpots/src/lib/relays.ts
SkateSpots e75c4a8355 feat(relays): Local + Blocked relay categories
Add a purposeful subset of Amethyst-style relay categories (not a full clone):
- App-local 'Local relays' (never published; added to reads+writes)
- App-local 'Blocked relays' (excluded from every read/write)
- src/lib/relays.ts: resolveRelays()/useRelays() + URL normalisation
- NostrProvider req/event routers go through resolveRelays (blocklist applied)
- RelayListManager: Local/Blocked editors; NIP-65 save now preserves the new
  lists instead of overwriting relayMetadata

Deliberately omitted (documented in AGENTS.md): KeyPackage (10051, no MLS
consumer) and proxy/broadcast/private-home (no such discovery model here).
Follow-up noted: a few hooks still read config.relayMetadata.relays directly.
2026-09-26 21:26:13 +07:00

65 lines
2.3 KiB
TypeScript

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;
}
}
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()));
const configured = metadata.relays.filter((r) => !blocked.has(r.url.toLowerCase()));
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]);
}