import { useState, useEffect } from 'react'; import { Plus, X, Wifi, Settings } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { useAppContext } from '@/hooks/useAppContext'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useToast } from '@/hooks/useToast'; interface Relay { url: string; read: boolean; write: boolean; } export function RelayListManager() { const { config, updateConfig } = useAppContext(); const { user } = useCurrentUser(); const { mutate: publishEvent } = useNostrPublish(); const { toast } = useToast(); const [relays, setRelays] = useState(config.relayMetadata.relays); const [newRelayUrl, setNewRelayUrl] = useState(''); // Sync local state with config when it changes (e.g., from NostrProvider sync). // This is a legitimate "mirror external source of truth" pattern: the local // `relays` state is what the UI edits optimistically, and we need to reset // it when the authoritative config changes from outside. useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect setRelays(config.relayMetadata.relays); }, [config.relayMetadata.relays]); const normalizeRelayUrl = (url: string): string => { url = url.trim(); try { return new URL(url).toString(); } catch { try { return new URL(`wss://${url}`).toString(); } catch { return url; } } }; const isValidRelayUrl = (url: string): boolean => { const trimmed = url.trim(); if (!trimmed) return false; const normalized = normalizeRelayUrl(trimmed); try { new URL(normalized); return true; } catch { return false; } }; const handleAddRelay = () => { if (!isValidRelayUrl(newRelayUrl)) { toast({ title: 'Invalid relay URL', description: 'Please enter a valid relay URL (e.g., wss://relay.example.com)', variant: 'destructive', }); return; } const normalized = normalizeRelayUrl(newRelayUrl); if (relays.some(r => r.url === normalized)) { toast({ title: 'Relay already exists', description: 'This relay is already in your list.', variant: 'destructive', }); return; } const newRelays = [...relays, { url: normalized, read: true, write: true }]; setRelays(newRelays); setNewRelayUrl(''); saveRelays(newRelays); }; const handleRemoveRelay = (url: string) => { const newRelays = relays.filter(r => r.url !== url); setRelays(newRelays); saveRelays(newRelays); }; const handleToggleRead = (url: string) => { const newRelays = relays.map(r => r.url === url ? { ...r, read: !r.read } : r ); setRelays(newRelays); saveRelays(newRelays); }; const handleToggleWrite = (url: string) => { const newRelays = relays.map(r => r.url === url ? { ...r, write: !r.write } : r ); setRelays(newRelays); saveRelays(newRelays); }; const saveRelays = (newRelays: Relay[]) => { // Only called from event handlers, not during render, so Date.now() is safe. // eslint-disable-next-line react-hooks/purity const now = Math.floor(Date.now() / 1000); // Update local config updateConfig((current) => ({ ...current, relayMetadata: { relays: newRelays, updatedAt: now, }, })); // Publish to Nostr if user is logged in if (user) { publishNIP65RelayList(newRelays); } }; const publishNIP65RelayList = (relayList: Relay[]) => { const tags = relayList.map(relay => { if (relay.read && relay.write) { return ['r', relay.url]; } else if (relay.read) { return ['r', relay.url, 'read']; } else if (relay.write) { return ['r', relay.url, 'write']; } // If neither read nor write, don't include (shouldn't happen) return null; }).filter((tag): tag is string[] => tag !== null); publishEvent( { kind: 10002, content: '', tags, }, { onSuccess: () => { toast({ title: 'Relay list published', description: 'Your relay list has been published to Nostr.', }); }, onError: (error) => { console.error('Failed to publish relay list:', error); toast({ title: 'Failed to publish relay list', description: 'There was an error publishing your relay list to Nostr.', variant: 'destructive', }); }, } ); }; const renderRelayUrl = (url: string): string => { try { const parsed = new URL(url); if (parsed.protocol === 'wss:') { if (parsed.pathname === '/') { return parsed.host; } else { return parsed.host + parsed.pathname; } } else { return parsed.href; } } catch { return url; } } return (
{/* Relay List */}
{relays.map((relay) => (
{renderRelayUrl(relay.url)} {/* Settings Popover */}
handleToggleRead(relay.url)} className="data-[state=checked]:bg-green-500 scale-75" />
handleToggleWrite(relay.url)} className="data-[state=checked]:bg-blue-500 scale-75" />
{/* Remove Button */}
))}
{/* Add Relay Form */}
setNewRelayUrl(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { handleAddRelay(); } }} />
{!user && (

Log in to sync your relay list with Nostr

)}
); }