PullRelaySetsButton.tsx raw
1 import { Button } from '@/components/ui/button'
2 import {
3 Dialog,
4 DialogContent,
5 DialogDescription,
6 DialogHeader,
7 DialogTitle,
8 DialogTrigger
9 } from '@/components/ui/dialog'
10 import {
11 Drawer,
12 DrawerContent,
13 DrawerDescription,
14 DrawerHeader,
15 DrawerTitle,
16 DrawerTrigger
17 } from '@/components/ui/drawer'
18 import { getReplaceableEventIdentifier } from '@/lib/event'
19 import { tagNameEquals } from '@/lib/tag'
20 import { isWebsocketUrl, simplifyUrl } from '@/lib/url'
21 import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
22 import { useNostr } from '@/providers/NostrProvider'
23 import { useScreenSize } from '@/providers/ScreenSizeProvider'
24 import client from '@/services/client.service'
25 import { TRelaySet } from '@/types'
26 import { CloudDownload } from 'lucide-react'
27 import { Event, kinds } from 'nostr-tools'
28 import { useEffect, useState } from 'react'
29 import { useTranslation } from 'react-i18next'
30 import RelaySetCard from '../RelaySetCard'
31 import { buildATag } from '@/lib/draft-event'
32
33 export default function PullRelaySetsButton() {
34 const { t } = useTranslation()
35 const { pubkey } = useNostr()
36 const { isSmallScreen } = useScreenSize()
37 const [open, setOpen] = useState(false)
38
39 const trigger = (
40 <Button
41 variant="link"
42 className="text-muted-foreground hover:no-underline hover:text-foreground p-0 h-fit"
43 disabled={!pubkey}
44 >
45 <CloudDownload />
46 {t('Pull relay sets')}
47 </Button>
48 )
49
50 if (isSmallScreen) {
51 return (
52 <Drawer open={open} onOpenChange={setOpen}>
53 <DrawerTrigger asChild>{trigger}</DrawerTrigger>
54 <DrawerContent className="max-h-[90vh]">
55 <div className="flex flex-col p-4 gap-4 overflow-auto">
56 <DrawerHeader>
57 <DrawerTitle>{t('Select the relay sets you want to pull')}</DrawerTitle>
58 <DrawerDescription className="hidden" />
59 </DrawerHeader>
60 <RemoteRelaySets close={() => setOpen(false)} />
61 </div>
62 </DrawerContent>
63 </Drawer>
64 )
65 }
66
67 return (
68 <Dialog open={open} onOpenChange={setOpen}>
69 <DialogTrigger asChild>{trigger}</DialogTrigger>
70 <DialogContent className="max-h-[90vh] overflow-auto">
71 <DialogHeader>
72 <DialogTitle>{t('Select the relay sets you want to pull')}</DialogTitle>
73 <DialogDescription className="hidden" />
74 </DialogHeader>
75 <RemoteRelaySets close={() => setOpen(false)} />
76 </DialogContent>
77 </Dialog>
78 )
79 }
80
81 function RemoteRelaySets({ close }: { close?: () => void }) {
82 const { t } = useTranslation()
83 const { pubkey, relayList } = useNostr()
84 const { addRelaySets, relaySets: existingRelaySets } = useFavoriteRelays()
85 const [initialed, setInitialed] = useState(false)
86 const [relaySetEventMap, setRelaySetEventMap] = useState<Map<string, Event>>(new Map())
87 const [relaySets, setRelaySets] = useState<TRelaySet[]>([])
88 const [selectedRelaySetIds, setSelectedRelaySetIds] = useState<string[]>([])
89
90 useEffect(() => {
91 if (!pubkey) return
92
93 const init = async () => {
94 setInitialed(false)
95 const events = await client.fetchEvents(
96 (relayList?.write ?? []).concat(client.currentRelays).slice(0, 4),
97 {
98 kinds: [kinds.Relaysets],
99 authors: [pubkey],
100 limit: 50
101 }
102 )
103 events.sort((a, b) => b.created_at - a.created_at)
104
105 const relaySetIds = new Set<string>(existingRelaySets.map((r) => r.id))
106 const relaySets: TRelaySet[] = []
107 const relaySetEventMap = new Map<string, Event>()
108 events.forEach((evt) => {
109 const id = getReplaceableEventIdentifier(evt)
110 if (!id || relaySetIds.has(id)) return
111
112 relaySetIds.add(id)
113 const relayUrls = evt.tags
114 .filter(tagNameEquals('relay'))
115 .map((tag) => tag[1])
116 .filter((url) => url && isWebsocketUrl(url))
117 if (!relayUrls.length) return
118
119 let title = evt.tags.find(tagNameEquals('title'))?.[1]
120 if (!title) {
121 title = relayUrls.length === 1 ? simplifyUrl(relayUrls[0]) : id
122 }
123 relaySets.push({ id, name: title, relayUrls, aTag: buildATag(evt) })
124 relaySetEventMap.set(id, evt)
125 })
126
127 setRelaySets(relaySets)
128 setRelaySetEventMap(relaySetEventMap)
129 setInitialed(true)
130 }
131 init()
132 }, [pubkey])
133
134 if (!pubkey) return null
135 if (!initialed) return <div className="text-center text-muted-foreground">{t('loading...')}</div>
136 if (!relaySets.length) {
137 return <div className="text-center text-muted-foreground">{t('No relay sets found')}</div>
138 }
139
140 return (
141 <div className="space-y-4">
142 <div className="space-y-2">
143 {relaySets.map((relaySet) => (
144 <RelaySetCard
145 key={relaySet.id}
146 relaySet={relaySet}
147 select={selectedRelaySetIds.includes(relaySet.id)}
148 onSelectChange={(select) => {
149 if (select) {
150 setSelectedRelaySetIds([...selectedRelaySetIds, relaySet.id])
151 } else {
152 setSelectedRelaySetIds(selectedRelaySetIds.filter((id) => id !== relaySet.id))
153 }
154 }}
155 />
156 ))}
157 </div>
158 <div className="flex gap-2">
159 <Button
160 className="w-20 shrink-0"
161 variant="secondary"
162 onClick={() => setSelectedRelaySetIds(relaySets.map((r) => r.id))}
163 >
164 {t('Select all')}
165 </Button>
166 <Button
167 className="w-full"
168 disabled={!selectedRelaySetIds.length}
169 onClick={() => {
170 const selectedRelaySets = selectedRelaySetIds
171 .map((id) => relaySetEventMap.get(id))
172 .filter(Boolean) as Event[]
173 if (selectedRelaySets.length > 0) {
174 addRelaySets(selectedRelaySets)
175 close?.()
176 }
177 }}
178 >
179 {selectedRelaySetIds.length > 0
180 ? t('Pull n relay sets', { n: selectedRelaySetIds.length })
181 : t('Pull')}
182 </Button>
183 </div>
184 </div>
185 )
186 }
187