import { useState, useEffect } from 'react' import { configStorage } from '@/lib/configStorage' import type { RelayConfig } from '@/lib/configStorageTypes' import { t } from '@/lib/i18n' import { userConfirm } from '@/lib/userConfirm' import { relaySessionManager } from '@/lib/relaySessionManager' interface RelayManagerProps { onConfigChange?: () => void } export function RelayManager({ onConfigChange }: RelayManagerProps): React.ReactElement { const [relays, setRelays] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [editingId, setEditingId] = useState(null) const [newUrl, setNewUrl] = useState('') const [showAddForm, setShowAddForm] = useState(false) const [draggedId, setDraggedId] = useState(null) const [dragOverId, setDragOverId] = useState(null) useEffect(() => { void loadRelays() }, []) async function loadRelays(): Promise { try { setLoading(true) setError(null) const config = await configStorage.getConfig() // Get failed relays from session manager and disable them in config const failedRelays = relaySessionManager.getFailedRelays() if (failedRelays.length > 0) { let hasChanges = false for (const relayUrl of failedRelays) { // Find the relay config by URL const relayConfig = config.relays.find((r) => r.url === relayUrl) if (relayConfig?.enabled) { // Disable the failed relay await configStorage.updateRelay(relayConfig.id, { enabled: false }) hasChanges = true } } // Reload config if we made changes if (hasChanges) { const updatedConfig = await configStorage.getConfig() setRelays(updatedConfig.relays.sort((a, b) => a.priority - b.priority)) } else { setRelays(config.relays.sort((a, b) => a.priority - b.priority)) } } else { setRelays(config.relays.sort((a, b) => a.priority - b.priority)) } } catch (e) { const errorMessage = e instanceof Error ? e.message : t('settings.relay.error.loadFailed') setError(errorMessage) console.error('Error loading relays:', e) } finally { setLoading(false) } } async function handleToggleEnabled(id: string, enabled: boolean): Promise { try { await configStorage.updateRelay(id, { enabled }) await loadRelays() onConfigChange?.() } catch (e) { const errorMessage = e instanceof Error ? e.message : t('settings.relay.error.updateFailed') setError(errorMessage) console.error('Error updating relay:', e) } } async function handleUpdatePriorities(newOrder: RelayConfig[]): Promise { try { const updatePromises = newOrder.map((relay, index) => { const newPriority = index + 1 if (relay.priority !== newPriority) { return configStorage.updateRelay(relay.id, { priority: newPriority }) } return Promise.resolve() }) await Promise.all(updatePromises) await loadRelays() onConfigChange?.() } catch (e) { const errorMessage = e instanceof Error ? e.message : t('settings.relay.error.priorityFailed') setError(errorMessage) console.error('Error updating priorities:', e) } } function handleDragStart(e: React.DragEvent, id: string): void { setDraggedId(id) e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text/plain', id) } function handleDragOver(e: React.DragEvent, id: string): void { e.preventDefault() e.dataTransfer.dropEffect = 'move' setDragOverId(id) } function handleDragLeave(): void { setDragOverId(null) } function handleDrop(e: React.DragEvent, targetId: string): void { e.preventDefault() setDragOverId(null) if (!draggedId || draggedId === targetId) { setDraggedId(null) return } const draggedIndex = relays.findIndex((relay) => relay.id === draggedId) const targetIndex = relays.findIndex((relay) => relay.id === targetId) if (draggedIndex === -1 || targetIndex === -1) { setDraggedId(null) return } const newRelays = [...relays] const removed = newRelays[draggedIndex] if (!removed) { setDraggedId(null) return } newRelays.splice(draggedIndex, 1) newRelays.splice(targetIndex, 0, removed) setRelays(newRelays) setDraggedId(null) void handleUpdatePriorities(newRelays) } function handleDragEnd(): void { setDraggedId(null) } function DragHandle(): React.ReactElement { return (
) } async function handleUpdateUrl(id: string, url: string): Promise { try { await configStorage.updateRelay(id, { url }) await loadRelays() setEditingId(null) onConfigChange?.() } catch (e) { const errorMessage = e instanceof Error ? e.message : t('settings.relay.error.urlFailed') setError(errorMessage) console.error('Error updating URL:', e) } } async function handleAddRelay(): Promise { if (!newUrl.trim()) { setError(t('settings.relay.error.urlRequired')) return } try { // Normalize URL (add wss:// if missing) let normalizedUrl = newUrl.trim() if (!normalizedUrl.startsWith('ws://') && !normalizedUrl.startsWith('wss://')) { normalizedUrl = `wss://${normalizedUrl}` } // Validate URL format - throws if invalid void new URL(normalizedUrl) await configStorage.addRelay(normalizedUrl, true) setNewUrl('') setShowAddForm(false) await loadRelays() onConfigChange?.() } catch (e) { if (e instanceof TypeError && e.message.includes('Invalid URL')) { setError(t('settings.relay.error.invalidUrl')) } else { const errorMessage = e instanceof Error ? e.message : t('settings.relay.error.addFailed') setError(errorMessage) } console.error('Error adding relay:', e) } } async function handleRemoveRelay(id: string): Promise { if (!userConfirm(t('settings.relay.remove.confirm'))) { return } try { await configStorage.removeRelay(id) await loadRelays() onConfigChange?.() } catch (e) { const errorMessage = e instanceof Error ? e.message : t('settings.relay.error.removeFailed') setError(errorMessage) console.error('Error removing relay:', e) } } if (loading) { return (
{t('settings.relay.loading')}
) } return (
{error && (
{error}
)}

{t('settings.relay.title')}

{showAddForm && (
{ setNewUrl(e.target.value) }} placeholder={t('settings.relay.add.placeholder')} className="w-full px-4 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none" />
)}
{relays.length === 0 ? (
{t('settings.relay.empty')}
) : ( relays.map((relay) => (
{ handleDragOver(e, relay.id) }} onDragLeave={handleDragLeave} onDrop={(e) => { handleDrop(e, relay.id) }} className={`bg-cyber-dark border rounded p-4 space-y-3 transition-all ${ (() => { if (draggedId === relay.id) { return 'opacity-50 border-neon-cyan' } if (dragOverId === relay.id) { return 'border-neon-green shadow-lg' } return 'border-neon-cyan/30' })() }`} >
{ handleDragStart(e, relay.id) }} onDragEnd={handleDragEnd} onMouseDown={(e) => { e.stopPropagation() }} >
{editingId === relay.id ? (
{ if (e.target.value !== relay.url) { void handleUpdateUrl(relay.id, e.target.value) } else { setEditingId(null) } }} onKeyDown={(e) => { if (e.key === 'Enter') { e.currentTarget.blur() } else if (e.key === 'Escape') { setEditingId(null) } }} className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/50 rounded text-cyber-light focus:border-neon-cyan focus:outline-none" autoFocus />
) : (
{ setEditingId(relay.id) }} title={t('settings.relay.list.editUrl')} > {relay.url}
)}
{relay.lastSyncDate && (
{t('settings.relay.list.lastSync')}: {new Date(relay.lastSyncDate).toLocaleString()}
)}
)) )}

{t('settings.relay.note.title')} {t('settings.relay.note.priority')}

{t('settings.relay.note.rotation')}

) }