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