RelayUrl.tsx raw
1 import { Button } from '@/components/ui/button'
2 import { Input } from '@/components/ui/input'
3 import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
4 import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
5 import { CircleX } from 'lucide-react'
6 import { useMemo, useState } from 'react'
7 import { useTranslation } from 'react-i18next'
8 import RelayIcon from '../RelayIcon'
9
10 export default function RelayUrls({ relaySetId }: { relaySetId: string }) {
11 const { t } = useTranslation()
12 const { relaySets, updateRelaySet } = useFavoriteRelays()
13 const [newRelayUrl, setNewRelayUrl] = useState('')
14 const [newRelayUrlError, setNewRelayUrlError] = useState<string | null>(null)
15 const relaySet = useMemo(
16 () => relaySets.find((r) => r.id === relaySetId),
17 [relaySets, relaySetId]
18 )
19
20 if (!relaySet) return null
21
22 const removeRelayUrl = (url: string) => {
23 updateRelaySet({
24 ...relaySet,
25 relayUrls: relaySet.relayUrls.filter((u) => u !== url)
26 })
27 }
28
29 const saveNewRelayUrl = () => {
30 if (newRelayUrl === '') return
31 const normalizedUrl = normalizeUrl(newRelayUrl)
32 if (!normalizedUrl) {
33 return setNewRelayUrlError(t('Invalid relay URL'))
34 }
35 if (relaySet.relayUrls.includes(normalizedUrl)) {
36 return setNewRelayUrlError(t('Relay already exists'))
37 }
38 if (!isWebsocketUrl(normalizedUrl)) {
39 return setNewRelayUrlError(t('invalid relay URL'))
40 }
41 const newRelayUrls = [...relaySet.relayUrls, normalizedUrl]
42 updateRelaySet({ ...relaySet, relayUrls: newRelayUrls })
43 setNewRelayUrl('')
44 }
45
46 const handleRelayUrlInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
47 setNewRelayUrl(e.target.value)
48 setNewRelayUrlError(null)
49 }
50
51 const handleRelayUrlInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
52 if (event.key === 'Enter') {
53 event.preventDefault()
54 saveNewRelayUrl()
55 }
56 }
57
58 return (
59 <>
60 <div className="mt-1">
61 {relaySet.relayUrls.map((url, index) => (
62 <RelayUrl key={index} url={url} onRemove={() => removeRelayUrl(url)} />
63 ))}
64 </div>
65 <div className="mt-2 flex gap-2">
66 <Input
67 className={newRelayUrlError ? 'border-destructive' : ''}
68 placeholder={t('Add a new relay')}
69 value={newRelayUrl}
70 onKeyDown={handleRelayUrlInputKeyDown}
71 onChange={handleRelayUrlInputChange}
72 onBlur={saveNewRelayUrl}
73 />
74 <Button onClick={saveNewRelayUrl}>{t('Add')}</Button>
75 </div>
76 {newRelayUrlError && <div className="text-xs text-destructive mt-1">{newRelayUrlError}</div>}
77 </>
78 )
79 }
80
81 function RelayUrl({ url, onRemove }: { url: string; onRemove: () => void }) {
82 return (
83 <div className="flex items-center justify-between pl-1 pr-3">
84 <div className="flex gap-3 items-center flex-1 w-0">
85 <RelayIcon url={url} className="w-4 h-4" />
86 <div className="text-muted-foreground text-sm truncate">{url}</div>
87 </div>
88 <div className="shrink-0">
89 <CircleX
90 size={16}
91 onClick={onRemove}
92 className="text-muted-foreground hover:text-destructive cursor-pointer"
93 />
94 </div>
95 </div>
96 )
97 }
98