BlossomServerListSetting.tsx raw

   1  import { Badge } from '@/components/ui/badge'
   2  import { Button } from '@/components/ui/button'
   3  import { Input } from '@/components/ui/input'
   4  import { Separator } from '@/components/ui/separator'
   5  import { RECOMMENDED_BLOSSOM_SERVERS } from '@/constants'
   6  import { createBlossomServerListDraftEvent } from '@/lib/draft-event'
   7  import { getServersFromServerTags } from '@/lib/tag'
   8  import { normalizeHttpUrl } from '@/lib/url'
   9  import { cn } from '@/lib/utils'
  10  import { useNostr } from '@/providers/NostrProvider'
  11  import client from '@/services/client.service'
  12  import { AlertCircle, ArrowUpToLine, Loader, X } from 'lucide-react'
  13  import { Event } from 'nostr-tools'
  14  import { useEffect, useMemo, useState } from 'react'
  15  import { useTranslation } from 'react-i18next'
  16  
  17  export default function BlossomServerListSetting() {
  18    const { t } = useTranslation()
  19    const { pubkey, publish } = useNostr()
  20    const [blossomServerListEvent, setBlossomServerListEvent] = useState<Event | null>(null)
  21    const serverUrls = useMemo(() => {
  22      return getServersFromServerTags(blossomServerListEvent ? blossomServerListEvent.tags : [])
  23    }, [blossomServerListEvent])
  24    const [url, setUrl] = useState('')
  25    const [removingIndex, setRemovingIndex] = useState(-1)
  26    const [movingIndex, setMovingIndex] = useState(-1)
  27    const [adding, setAdding] = useState(false)
  28  
  29    useEffect(() => {
  30      const init = async () => {
  31        if (!pubkey) {
  32          setBlossomServerListEvent(null)
  33          return
  34        }
  35        const event = await client.fetchBlossomServerListEvent(pubkey)
  36        setBlossomServerListEvent(event)
  37      }
  38      init()
  39    }, [pubkey])
  40  
  41    const addBlossomUrl = async (url: string) => {
  42      if (!url || adding || removingIndex >= 0 || movingIndex >= 0) return
  43      setAdding(true)
  44      try {
  45        const draftEvent = createBlossomServerListDraftEvent([...serverUrls, url])
  46        const newEvent = await publish(draftEvent)
  47        await client.updateBlossomServerListEventCache(newEvent)
  48        setBlossomServerListEvent(newEvent)
  49        setUrl('')
  50      } catch (error) {
  51        console.error('Failed to add Blossom URL:', error)
  52      } finally {
  53        setAdding(false)
  54      }
  55    }
  56  
  57    const handleUrlInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
  58      if (event.key === 'Enter') {
  59        event.preventDefault()
  60        const normalizedUrl = normalizeHttpUrl(url.trim())
  61        if (!normalizedUrl) return
  62        addBlossomUrl(normalizedUrl)
  63      }
  64    }
  65  
  66    const removeBlossomUrl = async (idx: number) => {
  67      if (removingIndex >= 0 || adding || movingIndex >= 0) return
  68      setRemovingIndex(idx)
  69      try {
  70        const draftEvent = createBlossomServerListDraftEvent(serverUrls.filter((_, i) => i !== idx))
  71        const newEvent = await publish(draftEvent)
  72        await client.updateBlossomServerListEventCache(newEvent)
  73        setBlossomServerListEvent(newEvent)
  74      } catch (error) {
  75        console.error('Failed to remove Blossom URL:', error)
  76      } finally {
  77        setRemovingIndex(-1)
  78      }
  79    }
  80  
  81    const moveToTop = async (idx: number) => {
  82      if (removingIndex >= 0 || adding || movingIndex >= 0 || idx === 0) return
  83      setMovingIndex(idx)
  84      try {
  85        const newUrls = [serverUrls[idx], ...serverUrls.filter((_, i) => i !== idx)]
  86        const draftEvent = createBlossomServerListDraftEvent(newUrls)
  87        const newEvent = await publish(draftEvent)
  88        await client.updateBlossomServerListEventCache(newEvent)
  89        setBlossomServerListEvent(newEvent)
  90      } catch (error) {
  91        console.error('Failed to move Blossom URL to top:', error)
  92      } finally {
  93        setMovingIndex(-1)
  94      }
  95    }
  96  
  97    return (
  98      <div className="space-y-2">
  99        <div className="text-sm font-medium">{t('Blossom server URLs')}</div>
 100        {serverUrls.length === 0 && (
 101          <div className="flex flex-col gap-1 text-sm border rounded-lg p-2 bg-muted text-muted-foreground">
 102            <div className="font-medium flex gap-2 items-center">
 103              <AlertCircle className="size-4" />
 104              {t('You need to add at least one media server in order to upload media files.')}
 105            </div>
 106            <Separator className="bg-muted-foreground my-2" />
 107            <div className="font-medium">{t('Recommended blossom servers')}:</div>
 108            <div className="flex flex-col">
 109              {RECOMMENDED_BLOSSOM_SERVERS.map((recommendedUrl) => (
 110                <Button
 111                  variant="link"
 112                  key={recommendedUrl}
 113                  onClick={() => addBlossomUrl(recommendedUrl)}
 114                  disabled={removingIndex >= 0 || adding || movingIndex >= 0}
 115                  className="w-fit p-0 text-muted-foreground hover:text-foreground h-fit"
 116                >
 117                  {recommendedUrl}
 118                </Button>
 119              ))}
 120            </div>
 121          </div>
 122        )}
 123        {serverUrls.map((url, idx) => (
 124          <div
 125            key={url}
 126            className={cn(
 127              'flex items-center justify-between gap-2 pl-3 pr-1 py-1 border rounded-lg',
 128              idx === 0 && 'border-primary'
 129            )}
 130          >
 131            <a
 132              href={url}
 133              target="_blank"
 134              rel="noopener noreferrer"
 135              className="truncate hover:underline"
 136            >
 137              {url}
 138            </a>
 139            <div className="flex items-center gap-2">
 140              {idx > 0 ? (
 141                <Button
 142                  variant="ghost"
 143                  size="icon"
 144                  onClick={() => moveToTop(idx)}
 145                  title={t('Move to top')}
 146                  disabled={removingIndex >= 0 || adding || movingIndex >= 0}
 147                  className="text-muted-foreground"
 148                >
 149                  {movingIndex === idx ? <Loader className="animate-spin" /> : <ArrowUpToLine />}
 150                </Button>
 151              ) : (
 152                <Badge>{t('Preferred')}</Badge>
 153              )}
 154              <Button
 155                variant="ghost-destructive"
 156                size="icon"
 157                onClick={() => removeBlossomUrl(idx)}
 158                title={t('Remove')}
 159                disabled={removingIndex >= 0 || adding || movingIndex >= 0}
 160              >
 161                {removingIndex === idx ? <Loader className="animate-spin" /> : <X />}
 162              </Button>
 163            </div>
 164          </div>
 165        ))}
 166        <div className="flex items-center gap-2">
 167          <Input
 168            value={url}
 169            onChange={(e) => setUrl(e.target.value)}
 170            placeholder={t('Enter Blossom server URL')}
 171            onKeyDown={handleUrlInputKeyDown}
 172          />
 173          <Button
 174            type="button"
 175            onClick={() => {
 176              const normalizedUrl = normalizeHttpUrl(url.trim())
 177              if (!normalizedUrl) return
 178              addBlossomUrl(normalizedUrl)
 179            }}
 180            title={t('Add')}
 181          >
 182            {adding && <Loader className="animate-spin" />}
 183            {t('Add')}
 184          </Button>
 185        </div>
 186      </div>
 187    )
 188  }
 189