feat: pull relay sets button

This commit is contained in:
codytseng
2025-04-06 16:21:44 +08:00
parent 27ae980f42
commit 864d8c617f
18 changed files with 273 additions and 9 deletions

View File

@@ -6,12 +6,12 @@ import { useTranslation } from 'react-i18next'
export default function AddNewRelaySet() {
const { t } = useTranslation()
const { addRelaySet } = useFavoriteRelays()
const { createRelaySet } = useFavoriteRelays()
const [newRelaySetName, setNewRelaySetName] = useState('')
const saveRelaySet = () => {
if (!newRelaySetName) return
addRelaySet(newRelaySetName)
createRelaySet(newRelaySetName)
setNewRelaySetName('')
}

View File

@@ -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>
)
}

View File

@@ -6,6 +6,7 @@ import { RelaySetsSettingComponentProvider } from './provider'
import RelayItem from './RelayItem'
import RelaySet from './RelaySet'
import TemporaryRelaySet from './TemporaryRelaySet'
import PullRelaySetsButton from './PullRelaySetsButton'
export default function FavoriteRelaysSetting() {
const { t } = useTranslation()
@@ -16,7 +17,12 @@ export default function FavoriteRelaysSetting() {
<div className="space-y-4">
<TemporaryRelaySet />
<div className="space-y-2">
<div className="text-muted-foreground font-semibold select-none">{t('Relay sets')}</div>
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="text-muted-foreground font-semibold select-nones shrink-0">
{t('Relay sets')}
</div>
<PullRelaySetsButton />
</div>
{relaySets.map((relaySet) => (
<RelaySet key={relaySet.id} relaySet={relaySet} />
))}

View File

@@ -18,7 +18,7 @@ export default function RelaySetCard({
return (
<div
className={`w-full border rounded-lg p-4 ${select ? 'border-highlight bg-highlight/5' : 'clickable'}`}
className={`w-full border rounded-lg p-4 clickable ${select ? 'border-highlight bg-highlight/5' : ''}`}
onClick={() => onSelectChange(!select)}
>
<div className="flex justify-between items-center">

View File

@@ -175,12 +175,12 @@ function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) {
function SaveToNewSet({ urls }: { urls: string[] }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { addRelaySet } = useFavoriteRelays()
const { createRelaySet } = useFavoriteRelays()
const handleSave = () => {
const newSetName = prompt(t('Enter a name for the new relay set'))
if (newSetName) {
addRelaySet(newSetName, urls)
createRelaySet(newSetName, urls)
}
}

View File

@@ -115,6 +115,11 @@ export default {
'R & W': 'قراءة وكتابة',
Read: 'قراءة',
Write: 'كتابة',
'Pull relay sets': 'سحب مجموعات الريلاي',
'Select the relay sets you want to pull': 'اختر مجموعات الريلاي التي تريد استلامها',
'No relay sets found': 'لم يتم العثور على مجموعات ريلاي',
'Pull n relay sets': 'سحب {{n}} مجموعات ريلاي',
Pull: 'سحب',
'Select all': 'اختر الكل',
'Relay Sets': 'مجموعات الريلاي',
'Read & Write Relays': 'ريلايات القراءة والكتابة',

View File

@@ -116,6 +116,11 @@ export default {
'R & W': 'R & W',
Read: 'Lesen',
Write: 'Schreiben',
'Pull relay sets': 'Relay-Sets abrufen',
'Select the relay sets you want to pull': 'Wähle die Relay-Sets, die du abrufen möchtest',
'No relay sets found': 'Keine Relay-Sets gefunden',
'Pull n relay sets': 'Hole {{n}} Relay-Sets',
Pull: 'Abrufen',
'Select all': 'Alle auswählen',
'Relay Sets': 'Relay-Sets',
'Read & Write Relays': 'Lese- & Schreib-Relays',

View File

@@ -115,6 +115,11 @@ export default {
'R & W': 'R & W',
Read: 'Read',
Write: 'Write',
'Pull relay sets': 'Pull relay sets',
'Select the relay sets you want to pull': 'Select the relay sets you want to pull',
'No relay sets found': 'No relay sets found',
'Pull n relay sets': 'Pull {{n}} relay sets',
Pull: 'Pull',
'Select all': 'Select all',
'Relay Sets': 'Relay Sets',
'Read & Write Relays': 'Read & Write Relays',

View File

@@ -116,6 +116,12 @@ export default {
'R & W': 'L y E',
Read: 'Leer',
Write: 'Escribir',
'Pull relay sets': 'Recibir conjuntos de relés',
'Select the relay sets you want to pull':
'Selecciona los conjuntos de relés que deseas recibir',
'No relay sets found': 'No se encontraron conjuntos de relés',
'Pull n relay sets': 'Recibir {{n}} conjuntos de relés',
Pull: 'Recibir',
'Select all': 'Seleccionar todo',
'Relay Sets': 'Conjuntos de relés',
'Read & Write Relays': 'Relés de lectura y escritura',

View File

@@ -116,6 +116,11 @@ export default {
'R & W': 'R & W',
Read: 'Lire',
Write: 'Écrire',
'Pull relay sets': 'Récupérer les groupes de relais',
'Select the relay sets you want to pull': 'Sélectionnez les groupes de relais à récupérer',
'No relay sets found': 'Aucun groupe de relais trouvé',
'Pull n relay sets': 'Récupérer {{n}} groupes de relais',
Pull: 'Récupérer',
'Select all': 'Tout sélectionner',
'Relay Sets': 'Groupes de relais',
'Read & Write Relays': 'Relais lecture & écriture',

View File

@@ -116,6 +116,11 @@ export default {
'R & W': 'L & S',
Read: 'Leggi',
Write: 'Scrivi',
'Pull relay sets': 'Ottieni set di relay',
'Select the relay sets you want to pull': 'Selezionare i set di relay che si desidera ottenere',
'No relay sets found': 'Nessun set di relay trovato',
'Pull n relay sets': 'Ottieni {{n}} set di relay',
Pull: 'Ottieni',
'Select all': 'Seleziona tutto',
'Relay Sets': 'Set di Relay',
'Read & Write Relays': 'Relay Leggi & Scrivi',

View File

@@ -116,6 +116,11 @@ export default {
'R & W': '読&書',
Read: '読む',
Write: '書く',
'Pull relay sets': 'リレイセットをプル',
'Select the relay sets you want to pull': 'プルするリレイセットを選択',
'No relay sets found': 'リレイセットが見つかりません',
'Pull n relay sets': '{{n}} 個のリレイセットをプル',
Pull: 'プル',
'Select all': 'すべて選択',
'Relay Sets': 'リレイセット',
'Read & Write Relays': '読み&書きリレイ',

View File

@@ -115,6 +115,11 @@ export default {
'R & W': 'O & Z',
Read: 'Odczyt',
Write: 'Zapis',
'Pull relay sets': 'Pobierz zestaw transmiterów',
'Select the relay sets you want to pull': 'Wybierz zestaw transmiterów do pobrania',
'No relay sets found': 'Nie znaleziono zestawu transmiterów',
'Pull n relay sets': 'Pobierz {{n}} zestawów transmiterów',
Pull: 'Pobierz',
'Select all': 'Wszystkie',
'Relay Sets': 'Grupy transmiterów',
'Read & Write Relays': 'Transmitery zapisu i odczytu',

View File

@@ -115,6 +115,11 @@ export default {
'R & W': 'Leitura & Escrita',
Read: 'Ler',
Write: 'Escrever',
'Pull relay sets': 'Receber conjuntos de relé',
'Select the relay sets you want to pull': 'Selecione os conjuntos de relé que deseja receber',
'No relay sets found': 'Nenhum conjunto de relé encontrado',
'Pull n relay sets': 'Receber {{n}} conjuntos de relé',
Pull: 'Receber',
'Select all': 'Selecionar todos',
'Relay Sets': 'Conjuntos de relé',
'Read & Write Relays': 'Relés de Leitura & Escrita',

View File

@@ -116,6 +116,11 @@ export default {
'R & W': 'Leitura & Escrita',
Read: 'Ler',
Write: 'Escrever',
'Pull relay sets': 'Receber conjuntos de relé',
'Select the relay sets you want to pull': 'Selecione os conjuntos de relé que deseja receber',
'No relay sets found': 'Nenhum conjunto de relé encontrado',
'Pull n relay sets': 'Receber {{n}} conjuntos de relé',
Pull: 'Receber',
'Select all': 'Selecionar todos',
'Relay Sets': 'Conjuntos de relé',
'Read & Write Relays': 'Relés de Leitura & Escrita',

View File

@@ -117,6 +117,11 @@ export default {
'R & W': 'Чтение & Запись',
Read: 'Читать',
Write: 'Писать',
'Pull relay sets': 'Получить наборы ретрансляторов',
'Select the relay sets you want to pull': 'Выберите наборы ретрансляторов для получения',
'No relay sets found': 'Наборы ретрансляторов не найдены',
'Pull n relay sets': 'Получить {{n}} наборов ретрансляторов',
Pull: 'Получить',
'Select all': 'Выбрать все',
'Relay Sets': 'Наборы ретрансляторов',
'Read & Write Relays': 'Ретрансляторы для чтения и записи',

View File

@@ -115,6 +115,11 @@ export default {
'R & W': '读写',
Read: '只读',
Write: '只写',
'Pull relay sets': '拉取服务器组',
'Select the relay sets you want to pull': '选择要拉取的服务器组',
'No relay sets found': '未找到服务器组',
'Pull n relay sets': '拉取 {{n}} 个服务器组',
Pull: '拉取',
'Select all': '全选',
'Relay Sets': '服务器组',
Mailbox: '邮箱',

View File

@@ -16,7 +16,8 @@ type TFavoriteRelaysContext = {
addFavoriteRelays: (relayUrls: string[]) => Promise<void>
deleteFavoriteRelays: (relayUrls: string[]) => Promise<void>
relaySets: TRelaySet[]
addRelaySet: (relaySetName: string, relayUrls?: string[]) => Promise<void>
createRelaySet: (relaySetName: string, relayUrls?: string[]) => Promise<void>
addRelaySets: (newRelaySetEvents: Event[]) => Promise<void>
deleteRelaySet: (id: string) => Promise<void>
updateRelaySet: (newSet: TRelaySet) => Promise<void>
}
@@ -149,7 +150,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
const addRelaySet = async (relaySetName: string, relayUrls: string[] = []) => {
const createRelaySet = async (relaySetName: string, relayUrls: string[] = []) => {
const normalizedUrls = relayUrls
.map((url) => normalizeUrl(url))
.filter((url) => isWebsocketUrl(url))
@@ -170,6 +171,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
const addRelaySets = async (newRelaySetEvents: Event[]) => {
const favoriteRelaysDraftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, [
...relaySetEvents,
...newRelaySetEvents
])
const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
const deleteRelaySet = async (id: string) => {
const newRelaySetEvents = relaySetEvents.filter((event) => {
return getReplaceableEventIdentifier(event) !== id
@@ -203,7 +213,8 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
addFavoriteRelays,
deleteFavoriteRelays,
relaySets,
addRelaySet,
createRelaySet,
addRelaySets,
deleteRelaySet,
updateRelaySet
}}