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