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