18 changed files with 273 additions and 9 deletions
@ -0,0 +1,186 @@ |
|||||||
|
import { Button } from '@/components/ui/button' |
||||||
|
import { |
||||||
|
Dialog, |
||||||
|
DialogContent, |
||||||
|
DialogDescription, |
||||||
|
DialogHeader, |
||||||
|
DialogTitle, |
||||||
|
DialogTrigger |
||||||
|
} from '@/components/ui/dialog' |
||||||
|
import { |
||||||
|
Drawer, |
||||||
|
DrawerContent, |
||||||
|
DrawerDescription, |
||||||
|
DrawerHeader, |
||||||
|
DrawerTitle, |
||||||
|
DrawerTrigger |
||||||
|
} from '@/components/ui/drawer' |
||||||
|
import { BIG_RELAY_URLS } from '@/constants' |
||||||
|
import { getReplaceableEventIdentifier } from '@/lib/event' |
||||||
|
import { tagNameEquals } from '@/lib/tag' |
||||||
|
import { isWebsocketUrl, simplifyUrl } from '@/lib/url' |
||||||
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' |
||||||
|
import { useNostr } from '@/providers/NostrProvider' |
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider' |
||||||
|
import client from '@/services/client.service' |
||||||
|
import { TRelaySet } from '@/types' |
||||||
|
import { CloudDownload } from 'lucide-react' |
||||||
|
import { Event, kinds } from 'nostr-tools' |
||||||
|
import { useEffect, useState } from 'react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
import RelaySetCard from '../RelaySetCard' |
||||||
|
|
||||||
|
export default function PullRelaySetsButton() { |
||||||
|
const { t } = useTranslation() |
||||||
|
const { pubkey } = useNostr() |
||||||
|
const { isSmallScreen } = useScreenSize() |
||||||
|
const [open, setOpen] = useState(false) |
||||||
|
|
||||||
|
const trigger = ( |
||||||
|
<Button |
||||||
|
variant="link" |
||||||
|
className="text-muted-foreground hover:no-underline hover:text-foreground p-0 h-fit" |
||||||
|
disabled={!pubkey} |
||||||
|
> |
||||||
|
<CloudDownload /> |
||||||
|
{t('Pull relay sets')} |
||||||
|
</Button> |
||||||
|
) |
||||||
|
|
||||||
|
if (isSmallScreen) { |
||||||
|
return ( |
||||||
|
<Drawer open={open} onOpenChange={setOpen}> |
||||||
|
<DrawerTrigger asChild>{trigger}</DrawerTrigger> |
||||||
|
<DrawerContent className="max-h-[90vh]"> |
||||||
|
<div className="flex flex-col p-4 gap-4 overflow-auto"> |
||||||
|
<DrawerHeader> |
||||||
|
<DrawerTitle>{t('Select the relay sets you want to pull')}</DrawerTitle> |
||||||
|
<DrawerDescription className="hidden" /> |
||||||
|
</DrawerHeader> |
||||||
|
<RemoteRelaySets close={() => setOpen(false)} /> |
||||||
|
</div> |
||||||
|
</DrawerContent> |
||||||
|
</Drawer> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Dialog open={open} onOpenChange={setOpen}> |
||||||
|
<DialogTrigger asChild>{trigger}</DialogTrigger> |
||||||
|
<DialogContent className="max-h-[90vh] overflow-auto"> |
||||||
|
<DialogHeader> |
||||||
|
<DialogTitle>{t('Select the relay sets you want to pull')}</DialogTitle> |
||||||
|
<DialogDescription className="hidden" /> |
||||||
|
</DialogHeader> |
||||||
|
<RemoteRelaySets close={() => setOpen(false)} /> |
||||||
|
</DialogContent> |
||||||
|
</Dialog> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
function RemoteRelaySets({ close }: { close?: () => void }) { |
||||||
|
const { t } = useTranslation() |
||||||
|
const { pubkey, relayList } = useNostr() |
||||||
|
const { addRelaySets, relaySets: existingRelaySets } = useFavoriteRelays() |
||||||
|
const [initialed, setInitialed] = useState(false) |
||||||
|
const [relaySetEventMap, setRelaySetEventMap] = useState<Map<string, Event>>(new Map()) |
||||||
|
const [relaySets, setRelaySets] = useState<TRelaySet[]>([]) |
||||||
|
const [selectedRelaySetIds, setSelectedRelaySetIds] = useState<string[]>([]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!pubkey) return |
||||||
|
|
||||||
|
const init = async () => { |
||||||
|
setInitialed(false) |
||||||
|
const events = await client.fetchEvents( |
||||||
|
(relayList?.write ?? []).concat(BIG_RELAY_URLS).slice(0, 4), |
||||||
|
{ |
||||||
|
kinds: [kinds.Relaysets], |
||||||
|
authors: [pubkey], |
||||||
|
limit: 50 |
||||||
|
} |
||||||
|
) |
||||||
|
events.sort((a, b) => b.created_at - a.created_at) |
||||||
|
|
||||||
|
const relaySetIds = new Set<string>(existingRelaySets.map((r) => r.id)) |
||||||
|
const relaySets: TRelaySet[] = [] |
||||||
|
const relaySetEventMap = new Map<string, Event>() |
||||||
|
events.forEach((evt) => { |
||||||
|
const id = getReplaceableEventIdentifier(evt) |
||||||
|
if (!id || relaySetIds.has(id)) return |
||||||
|
|
||||||
|
relaySetIds.add(id) |
||||||
|
const relayUrls = evt.tags |
||||||
|
.filter(tagNameEquals('relay')) |
||||||
|
.map((tag) => tag[1]) |
||||||
|
.filter((url) => url && isWebsocketUrl(url)) |
||||||
|
if (!relayUrls.length) return |
||||||
|
|
||||||
|
let title = evt.tags.find(tagNameEquals('title'))?.[1] |
||||||
|
if (!title) { |
||||||
|
title = relayUrls.length === 1 ? simplifyUrl(relayUrls[0]) : id |
||||||
|
} |
||||||
|
relaySets.push({ id, name: title, relayUrls }) |
||||||
|
relaySetEventMap.set(id, evt) |
||||||
|
}) |
||||||
|
|
||||||
|
setRelaySets(relaySets) |
||||||
|
setRelaySetEventMap(relaySetEventMap) |
||||||
|
setInitialed(true) |
||||||
|
} |
||||||
|
init() |
||||||
|
}, [pubkey]) |
||||||
|
|
||||||
|
if (!pubkey) return null |
||||||
|
if (!initialed) return <div className="text-center text-muted-foreground">{t('loading...')}</div> |
||||||
|
if (!relaySets.length) { |
||||||
|
return <div className="text-center text-muted-foreground">{t('No relay sets found')}</div> |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="space-y-4"> |
||||||
|
<div className="space-y-2"> |
||||||
|
{relaySets.map((relaySet) => ( |
||||||
|
<RelaySetCard |
||||||
|
key={relaySet.id} |
||||||
|
relaySet={relaySet} |
||||||
|
select={selectedRelaySetIds.includes(relaySet.id)} |
||||||
|
onSelectChange={(select) => { |
||||||
|
if (select) { |
||||||
|
setSelectedRelaySetIds([...selectedRelaySetIds, relaySet.id]) |
||||||
|
} else { |
||||||
|
setSelectedRelaySetIds(selectedRelaySetIds.filter((id) => id !== relaySet.id)) |
||||||
|
} |
||||||
|
}} |
||||||
|
/> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
<div className="flex gap-2"> |
||||||
|
<Button |
||||||
|
className="w-20 shrink-0" |
||||||
|
variant="secondary" |
||||||
|
onClick={() => setSelectedRelaySetIds(relaySets.map((r) => r.id))} |
||||||
|
> |
||||||
|
{t('Select all')} |
||||||
|
</Button> |
||||||
|
<Button |
||||||
|
className="w-full" |
||||||
|
disabled={!selectedRelaySetIds.length} |
||||||
|
onClick={() => { |
||||||
|
const selectedRelaySets = selectedRelaySetIds |
||||||
|
.map((id) => relaySetEventMap.get(id)) |
||||||
|
.filter(Boolean) as Event[] |
||||||
|
if (selectedRelaySets.length > 0) { |
||||||
|
addRelaySets(selectedRelaySets) |
||||||
|
close?.() |
||||||
|
} |
||||||
|
}} |
||||||
|
> |
||||||
|
{selectedRelaySetIds.length > 0 |
||||||
|
? t('Pull n relay sets', { n: selectedRelaySetIds.length }) |
||||||
|
: t('Pull')} |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
Loading…
Reference in new issue