2026-01-06 20:59:59 +01:00

399 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'
interface RelayManagerProps {
onConfigChange?: () => void
}
export function RelayManager({ onConfigChange }: RelayManagerProps): React.ReactElement {
const [relays, setRelays] = useState<RelayConfig[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [editingId, setEditingId] = useState<string | null>(null)
const [newUrl, setNewUrl] = useState('')
const [showAddForm, setShowAddForm] = useState(false)
const [draggedId, setDraggedId] = useState<string | null>(null)
const [dragOverId, setDragOverId] = useState<string | null>(null)
useEffect(() => {
void loadRelays()
}, [])
async function loadRelays(): Promise<void> {
try {
setLoading(true)
setError(null)
const config = await configStorage.getConfig()
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<void> {
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<void> {
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<HTMLDivElement>, id: string): void {
setDraggedId(id)
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', id)
}
function handleDragOver(e: React.DragEvent<HTMLDivElement>, id: string): void {
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
setDragOverId(id)
}
function handleDragLeave(): void {
setDragOverId(null)
}
function handleDrop(e: React.DragEvent<HTMLDivElement>, 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 (
<div className="flex flex-col gap-1 cursor-grab active:cursor-grabbing text-cyber-accent/50 hover:text-neon-cyan transition-colors">
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
<circle cx="2" cy="2" r="1.5" />
<circle cx="6" cy="2" r="1.5" />
<circle cx="10" cy="2" r="1.5" />
<circle cx="2" cy="6" r="1.5" />
<circle cx="6" cy="6" r="1.5" />
<circle cx="10" cy="6" r="1.5" />
<circle cx="2" cy="10" r="1.5" />
<circle cx="6" cy="10" r="1.5" />
<circle cx="10" cy="10" r="1.5" />
</svg>
</div>
)
}
async function handleUpdateUrl(id: string, url: string): Promise<void> {
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<void> {
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
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<void> {
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 (
<div className="text-center py-8 text-neon-cyan">
<div>{t('settings.relay.loading')}</div>
</div>
)
}
return (
<div className="space-y-6">
{error && (
<div className="bg-red-900/30 border border-red-500/50 rounded p-4 text-red-300">
{error}
<button
onClick={() => {
setError(null)
}}
className="ml-4 text-red-400 hover:text-red-200"
>
×
</button>
</div>
)}
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-neon-cyan">{t('settings.relay.title')}</h2>
<button
onClick={() => {
setShowAddForm(!showAddForm)
}}
className="px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan border border-neon-cyan/50 rounded transition-colors"
>
{showAddForm ? t('settings.relay.add.cancel') : t('settings.relay.addButton')}
</button>
</div>
{showAddForm && (
<div className="bg-cyber-dark border border-neon-cyan/30 rounded p-4 space-y-4">
<div>
<label className="block text-sm font-medium text-cyber-accent mb-2">
{t('settings.relay.add.url')}
</label>
<input
type="text"
value={newUrl}
onChange={(e) => {
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"
/>
</div>
<div className="flex gap-2">
<button
onClick={() => {
void handleAddRelay()
}}
className="px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan border border-neon-cyan/50 rounded transition-colors"
>
{t('settings.relay.add.add')}
</button>
<button
onClick={() => {
setShowAddForm(false)
setNewUrl('')
setError(null)
}}
className="px-4 py-2 bg-cyber-accent/20 hover:bg-cyber-accent/30 text-cyber-accent border border-cyber-accent/50 rounded transition-colors"
>
{t('settings.relay.add.cancel')}
</button>
</div>
</div>
)}
<div className="space-y-4">
{relays.length === 0 ? (
<div className="text-center py-8 text-cyber-accent">
{t('settings.relay.empty')}
</div>
) : (
relays.map((relay, index) => (
<div
key={relay.id}
onDragOver={(e) => {
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 ${
draggedId === relay.id
? 'opacity-50 border-neon-cyan'
: dragOverId === relay.id
? 'border-neon-green shadow-lg'
: 'border-neon-cyan/30'
}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3 flex-1">
<div
className="drag-handle cursor-grab active:cursor-grabbing"
draggable
onDragStart={(e) => {
handleDragStart(e, relay.id)
}}
onDragEnd={handleDragEnd}
onMouseDown={(e) => {
e.stopPropagation()
}}
>
<DragHandle />
</div>
<div className="flex-1">
{editingId === relay.id ? (
<div className="space-y-2">
<input
type="text"
defaultValue={relay.url}
onBlur={(e) => {
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
/>
</div>
) : (
<div
className="text-neon-cyan cursor-pointer hover:text-neon-green transition-colors"
onClick={() => {
setEditingId(relay.id)
}}
title={t('settings.relay.list.editUrl')}
>
{relay.url}
</div>
)}
</div>
</div>
<div className="flex items-center gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={relay.enabled}
onChange={(e) => {
void handleToggleEnabled(relay.id, e.target.checked)
}}
className="w-4 h-4 text-neon-cyan bg-cyber-darker border-cyber-accent rounded focus:ring-neon-cyan"
/>
<span className="text-sm text-cyber-accent">
{relay.enabled ? t('settings.relay.list.enabled') : t('settings.relay.list.disabled')}
</span>
</label>
<button
onClick={() => {
void handleRemoveRelay(relay.id)
}}
className="px-3 py-1 text-sm bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-500/50 rounded transition-colors"
title={t('settings.relay.list.remove')}
>
{t('settings.relay.list.remove')}
</button>
</div>
</div>
<div className="flex items-center gap-2 text-xs text-cyber-accent/70">
<span>
{t('settings.relay.list.priorityLabel', { priority: index + 1, id: relay.id })}
</span>
</div>
</div>
))
)}
</div>
<div className="text-sm text-cyber-accent space-y-2">
<p>
<strong>{t('settings.relay.note.title')}</strong> {t('settings.relay.note.priority')}
</p>
<p>
{t('settings.relay.note.rotation')}
</p>
</div>
</div>
)
}