feat: NIP-43

This commit is contained in:
codytseng
2025-11-09 00:26:16 +08:00
parent 6614a615c4
commit 850d92de28
25 changed files with 1132 additions and 26 deletions

View File

@@ -2,15 +2,17 @@ import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { useFetchRelayInfo } from '@/hooks'
import { checkNip43Support } from '@/lib/relay'
import { normalizeHttpUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import { Check, Copy, GitBranch, Link, Mail, SquareCode } from 'lucide-react'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import PostEditor from '../PostEditor'
import RelayIcon from '../RelayIcon'
import RelayMembershipControl from '../RelayMembershipControl'
import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
@@ -21,6 +23,9 @@ export default function RelayInfo({ url, className }: { url: string; className?:
const { checkLogin } = useNostr()
const { relayInfo, isFetching } = useFetchRelayInfo(url)
const [open, setOpen] = useState(false)
const [isMember, setIsMember] = useState(false)
const supportsNip43 = useMemo(() => checkNip43Support(relayInfo), [relayInfo])
const shouldShowPostButton = useMemo(() => !supportsNip43 || isMember, [supportsNip43, isMember])
if (isFetching || !relayInfo) {
return null
@@ -105,14 +110,19 @@ export default function RelayInfo({ url, className }: { url: string; className?:
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
<Button
variant="secondary"
className="w-full"
onClick={() => checkLogin(() => setOpen(true))}
>
{t('Share something on this Relay')}
</Button>
<PostEditor open={open} setOpen={setOpen} openFrom={[relayInfo.url]} />
<RelayMembershipControl relayInfo={relayInfo} onMembershipStatusChange={setIsMember} />
{shouldShowPostButton && (
<>
<Button
variant="secondary"
className="w-full"
onClick={() => checkLogin(() => setOpen(true))}
>
{t('Share something on this Relay')}
</Button>
<PostEditor open={open} setOpen={setOpen} openFrom={[relayInfo.url]} />
</>
)}
</div>
<RelayReviewsPreview relayUrl={url} />
</div>

View File

@@ -0,0 +1,134 @@
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle
} from '@/components/ui/drawer'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import relayMembershipService from '@/services/relay-membership.service'
import { TRelayInfo } from '@/types'
import { Check, Copy } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
export default function InviteCodeDialog({
relayInfo,
showInviteCodeDialog,
setShowInviteCodeDialog
}: {
relayInfo: TRelayInfo
showInviteCodeDialog: boolean
setShowInviteCodeDialog: (open: boolean) => void
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const [isFetching, setIsFetching] = useState(false)
const [inviteCode, setInviteCode] = useState('')
const [copied, setCopied] = useState(false)
useEffect(() => {
if (!showInviteCodeDialog) {
setInviteCode('')
return
}
const getInviteCode = async () => {
setIsFetching(true)
try {
if (relayInfo.pubkey) {
const code = await relayMembershipService.requestInviteCode(
relayInfo.url,
relayInfo.pubkey
)
if (code) {
setInviteCode(code)
} else {
toast.error(t('Failed to get invite code from relay'))
}
}
} catch (error: any) {
toast.error(error.message || t('Failed to get invite code'))
} finally {
setIsFetching(false)
}
}
getInviteCode()
}, [showInviteCodeDialog])
const handleCopyInviteCode = () => {
if (!inviteCode) return
navigator.clipboard.writeText(inviteCode)
toast.success(t('Invite code copied to clipboard'))
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
}
const content = isFetching ? (
<div className="flex items-center justify-center py-8">
<div className="text-muted-foreground">{t('Loading...')}</div>
</div>
) : inviteCode ? (
<div className="space-y-2">
<Label htmlFor="fetched-invite-code">{t('Invite Code')}</Label>
<div className="flex gap-2">
<Input id="fetched-invite-code" value={inviteCode} readOnly className="font-mono" />
<Button onClick={handleCopyInviteCode} variant="outline">
{copied ? <Check /> : <Copy />}
</Button>
</div>
<p className="text-sm text-muted-foreground">
{t('This invite code can be used by others to join the relay.')}
</p>
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
{t('No invite code available from this relay.')}
</div>
)
if (isSmallScreen) {
return (
<Drawer open={showInviteCodeDialog} onOpenChange={setShowInviteCodeDialog}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>{t('Get Invite Code')}</DrawerTitle>
<DrawerDescription>
{t('Share this invite code with others to invite them to join this relay.')}
</DrawerDescription>
</DrawerHeader>
<div className="p-4">{content}</div>
</DrawerContent>
</Drawer>
)
}
return (
<Dialog open={showInviteCodeDialog} onOpenChange={setShowInviteCodeDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Get Invite Code')}</DialogTitle>
<DialogDescription>
{t('Share this invite code with others to invite them to join this relay.')}
</DialogDescription>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,141 @@
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle
} from '@/components/ui/drawer'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { createJoinDraftEvent } from '@/lib/draft-event'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import relayMembershipService from '@/services/relay-membership.service'
import { TRelayInfo } from '@/types'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
export default function JoinDialog({
relayInfo,
showJoinDialog,
setShowJoinDialog,
onMembershipStatusChange
}: {
relayInfo: TRelayInfo
showJoinDialog: boolean
setShowJoinDialog: (open: boolean) => void
onMembershipStatusChange: (status: boolean) => void
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { publish } = useNostr()
const [inviteCode, setInviteCode] = useState('')
const [isLoading, setIsLoading] = useState(false)
const handleJoinSubmit = async () => {
setIsLoading(true)
try {
const draftEvent = createJoinDraftEvent(inviteCode)
const joinRequestEvent = await publish(draftEvent, {
specifiedRelayUrls: [relayInfo.url]
})
toast.success(t('Join request sent successfully'))
await relayMembershipService.addNewMember(relayInfo.url, joinRequestEvent.pubkey)
onMembershipStatusChange(true)
setInviteCode('')
setShowJoinDialog(false)
} catch (error) {
const errors = error instanceof AggregateError ? error.errors : [error]
errors.forEach((err) => {
toast.error(
`${t('Failed to send join request')}: ${err instanceof Error ? err.message : String(err)}`,
{ duration: 10_000 }
)
console.error(err)
})
return
} finally {
setIsLoading(false)
}
}
const content = (
<div className="space-y-2">
<Label htmlFor="invite-code">{t('Invite Code')}</Label>
<Input
id="invite-code"
value={inviteCode}
onChange={(e) => setInviteCode(e.target.value)}
placeholder={t('Enter invite code')}
required
/>
<p className="text-sm text-muted-foreground">
{t('You can get an invite code from a relay member.')}
</p>
</div>
)
if (isSmallScreen) {
return (
<Drawer open={showJoinDialog} onOpenChange={setShowJoinDialog}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>{t('Request to Join Relay')}</DrawerTitle>
<DrawerDescription>
{t('Enter the invite code you received from a relay member.')}
</DrawerDescription>
</DrawerHeader>
<div className="p-4">{content}</div>
<DrawerFooter>
<Button onClick={handleJoinSubmit} disabled={isLoading || !inviteCode.trim()}>
{isLoading ? t('Sending...') : t('Send Request')}
</Button>
<DrawerClose asChild>
<Button variant="outline">{t('Cancel')}</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}
return (
<Dialog open={showJoinDialog} onOpenChange={setShowJoinDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Request to Join Relay')}</DialogTitle>
<DialogDescription>
{t('Enter the invite code you received from a relay member.')}
</DialogDescription>
</DialogHeader>
{content}
<DialogFooter>
<Button
variant="ghost"
onClick={() => {
setShowJoinDialog(false)
setInviteCode('')
}}
>
{t('Cancel')}
</Button>
<Button onClick={handleJoinSubmit} disabled={isLoading || !inviteCode.trim()}>
{isLoading ? t('Sending...') : t('Send Request')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,166 @@
import { Button } from '@/components/ui/button'
import { createJoinDraftEvent, createLeaveDraftEvent } from '@/lib/draft-event'
import { checkNip43Support } from '@/lib/relay'
import { useNostr } from '@/providers/NostrProvider'
import relayMembershipService from '@/services/relay-membership.service'
import { TRelayInfo } from '@/types'
import { LogIn, LogOut, Mail } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import InviteCodeDialog from './InviteCodeDialog'
import JoinDialog from './JoinDialog'
interface RelayMembershipControlProps {
relayInfo: TRelayInfo
onMembershipStatusChange?: (status: boolean) => void
}
export default function RelayMembershipControl({
relayInfo,
onMembershipStatusChange
}: RelayMembershipControlProps) {
const { t } = useTranslation()
const { pubkey, checkLogin, publish } = useNostr()
const [isMember, setIsMember] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [isChecking, setIsChecking] = useState(false)
const [showJoinDialog, setShowJoinDialog] = useState(false)
const [showInviteCodeDialog, setShowInviteCodeDialog] = useState(false)
const supportsNip43 = useMemo(() => checkNip43Support(relayInfo), [relayInfo])
useEffect(() => {
if (!supportsNip43 || !pubkey) {
setIsMember(false)
return
}
const checkMembership = async () => {
try {
setIsChecking(true)
const status = await relayMembershipService.checkMembership(
relayInfo.url,
pubkey,
relayInfo.pubkey
)
setIsMember(status)
} finally {
setIsChecking(false)
}
}
checkMembership()
}, [relayInfo.url, relayInfo.pubkey, pubkey, supportsNip43])
useEffect(() => {
if (onMembershipStatusChange) {
onMembershipStatusChange(isMember)
}
}, [isMember, onMembershipStatusChange])
if (!supportsNip43 || isChecking) {
return null
}
const submitJoinRequest = async () => {
setIsLoading(true)
try {
const draftEvent = createJoinDraftEvent('')
const joinRequestEvent = await publish(draftEvent, {
specifiedRelayUrls: [relayInfo.url]
})
toast.success(t('Join request sent successfully'))
await relayMembershipService.addNewMember(relayInfo.url, joinRequestEvent.pubkey)
onMembershipStatusChange?.(true)
} catch {
setShowJoinDialog(true)
} finally {
setIsLoading(false)
}
}
const handleGetInviteCodeClick = () => {
setShowInviteCodeDialog(true)
}
const handleLeaveClick = async () => {
if (!confirm(t('Are you sure you want to leave this relay?'))) {
return
}
setIsLoading(true)
try {
const draftEvent = createLeaveDraftEvent()
const leaveRequestEvent = await publish(draftEvent, {
specifiedRelayUrls: [relayInfo.url]
})
toast.success(t('Leave request sent successfully'))
await relayMembershipService.removeMember(relayInfo.url, leaveRequestEvent.pubkey)
setIsMember(false)
} catch (error: any) {
const errors = error instanceof AggregateError ? error.errors : [error]
errors.forEach((err) => {
toast.error(
`${t('Failed to send leave request')}: ${err instanceof Error ? err.message : String(err)}`,
{ duration: 10_000 }
)
console.error(err)
})
return
} finally {
setIsLoading(false)
}
}
return (
<>
{isMember ? (
<div className="grid grid-cols-2 gap-2">
<Button
variant="secondary"
className="w-full"
onClick={handleGetInviteCodeClick}
disabled={isLoading}
>
<Mail className="w-4 h-4 mr-2" />
{t('Get Invite Code')}
</Button>
<Button
variant="outline"
className="w-full"
onClick={handleLeaveClick}
disabled={isLoading}
>
<LogOut className="w-4 h-4 mr-2" />
{t('Leave')}
</Button>
</div>
) : (
<Button
variant="default"
className="w-full"
onClick={() => {
checkLogin(() => submitJoinRequest())
}}
disabled={isLoading}
>
<LogIn className="w-4 h-4 mr-2" />
{t('Request to Join Relay')}
</Button>
)}
<JoinDialog
relayInfo={relayInfo}
showJoinDialog={showJoinDialog}
setShowJoinDialog={setShowJoinDialog}
onMembershipStatusChange={setIsMember}
/>
<InviteCodeDialog
relayInfo={relayInfo}
showInviteCodeDialog={showInviteCodeDialog}
setShowInviteCodeDialog={setShowInviteCodeDialog}
/>
</>
)
}

View File

@@ -503,6 +503,34 @@ export default {
'My Packs': 'حزمي',
'Adding...': 'جاري الإضافة...',
'Removing...': 'جاري الإزالة...',
Reload: 'إعادة التحميل'
Reload: 'إعادة التحميل',
'Request to Join Relay': 'طلب الانضمام إلى المرحل',
'Leave Relay': 'مغادرة المرحل',
Leave: 'مغادرة',
'Are you sure you want to leave this relay?': 'هل أنت متأكد من أنك تريد مغادرة هذا المرحل؟',
'Join request sent successfully': 'تم إرسال طلب الانضمام بنجاح',
'Failed to send join request': 'فشل إرسال طلب الانضمام',
'Leave request sent successfully': 'تم إرسال طلب المغادرة بنجاح',
'Failed to send leave request': 'فشل إرسال طلب المغادرة',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'أدخل رمز الدعوة إذا كان لديك واحد. وإلا، اتركه فارغًا لإرسال طلب.',
'Invite Code (Optional)': 'رمز الدعوة (اختياري)',
'Enter invite code': 'أدخل رمز الدعوة',
'Sending...': 'جاري الإرسال...',
'Send Request': 'إرسال الطلب',
'You can get an invite code from a relay member.': 'يمكنك الحصول على رمز دعوة من عضو المرحل.',
'Enter the invite code you received from a relay member.': 'أدخل رمز الدعوة الذي تلقيته من عضو المرحل.',
'Get Invite Code': 'الحصول على رمز الدعوة',
'Share this invite code with others to invite them to join this relay.':
'شارك رمز الدعوة هذا مع الآخرين لدعوتهم للانضمام إلى هذا المرحل.',
'Invite Code': 'رمز الدعوة',
Copy: 'نسخ',
'This invite code can be used by others to join the relay.':
'يمكن للآخرين استخدام رمز الدعوة هذا للانضمام إلى المرحل.',
'No invite code available from this relay.': 'لا يوجد رمز دعوة متاح من هذا المرحل.',
Close: 'إغلاق',
'Failed to get invite code from relay': 'فشل الحصول على رمز الدعوة من المرحل',
'Failed to get invite code': 'فشل الحصول على رمز الدعوة',
'Invite code copied to clipboard': 'تم نسخ رمز الدعوة إلى الحافظة'
}
}

View File

@@ -517,6 +517,36 @@ export default {
'My Packs': 'Meine Pakete',
'Adding...': 'Wird hinzugefügt...',
'Removing...': 'Wird entfernt...',
Reload: 'Neu laden'
Reload: 'Neu laden',
'Request to Join Relay': 'Relay-Beitritt beantragen',
'Leave Relay': 'Relay verlassen',
Leave: 'Verlassen',
'Are you sure you want to leave this relay?': 'Möchten Sie dieses Relay wirklich verlassen?',
'Join request sent successfully': 'Beitrittsanfrage erfolgreich gesendet',
'Failed to send join request': 'Fehler beim Senden der Beitrittsanfrage',
'Leave request sent successfully': 'Austrittsanfrage erfolgreich gesendet',
'Failed to send leave request': 'Fehler beim Senden der Austrittsanfrage',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'Geben Sie einen Einladungscode ein, falls Sie einen haben. Andernfalls lassen Sie es leer, um eine Anfrage zu senden.',
'Invite Code (Optional)': 'Einladungscode (Optional)',
'Enter invite code': 'Einladungscode eingeben',
'Sending...': 'Wird gesendet...',
'Send Request': 'Anfrage senden',
'You can get an invite code from a relay member.':
'Sie können einen Einladungscode von einem Relay-Mitglied erhalten.',
'Enter the invite code you received from a relay member.':
'Geben Sie den Einladungscode ein, den Sie von einem Relay-Mitglied erhalten haben.',
'Get Invite Code': 'Einladungscode Erhalten',
'Share this invite code with others to invite them to join this relay.':
'Teilen Sie diesen Einladungscode mit anderen, um sie einzuladen, diesem Relay beizutreten.',
'Invite Code': 'Einladungscode',
Copy: 'Kopieren',
'This invite code can be used by others to join the relay.':
'Dieser Einladungscode kann von anderen verwendet werden, um dem Relay beizutreten.',
'No invite code available from this relay.': 'Kein Einladungscode von diesem Relay verfügbar.',
Close: 'Schließen',
'Failed to get invite code from relay': 'Fehler beim Abrufen des Einladungscodes vom Relay',
'Failed to get invite code': 'Fehler beim Abrufen des Einladungscodes',
'Invite code copied to clipboard': 'Einladungscode in die Zwischenablage kopiert'
}
}

View File

@@ -502,6 +502,36 @@ export default {
'My Packs': 'My Packs',
'Adding...': 'Adding...',
'Removing...': 'Removing...',
Reload: 'Reload'
Reload: 'Reload',
'Request to Join Relay': 'Request to Join Relay',
'Leave Relay': 'Leave Relay',
Leave: 'Leave',
'Are you sure you want to leave this relay?': 'Are you sure you want to leave this relay?',
'Join request sent successfully': 'Join request sent successfully',
'Failed to send join request': 'Failed to send join request',
'Leave request sent successfully': 'Leave request sent successfully',
'Failed to send leave request': 'Failed to send leave request',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.',
'Invite Code (Optional)': 'Invite Code (Optional)',
'Enter invite code': 'Enter invite code',
'Sending...': 'Sending...',
'Send Request': 'Send Request',
'You can get an invite code from a relay member.':
'You can get an invite code from a relay member.',
'Enter the invite code you received from a relay member.':
'Enter the invite code you received from a relay member.',
'Get Invite Code': 'Get Invite Code',
'Share this invite code with others to invite them to join this relay.':
'Share this invite code with others to invite them to join this relay.',
'Invite Code': 'Invite Code',
Copy: 'Copy',
'This invite code can be used by others to join the relay.':
'This invite code can be used by others to join the relay.',
'No invite code available from this relay.': 'No invite code available from this relay.',
Close: 'Close',
'Failed to get invite code from relay': 'Failed to get invite code from relay',
'Failed to get invite code': 'Failed to get invite code',
'Invite code copied to clipboard': 'Invite code copied to clipboard'
}
}

View File

@@ -511,6 +511,36 @@ export default {
'My Packs': 'Mis Paquetes',
'Adding...': 'Añadiendo...',
'Removing...': 'Eliminando...',
Reload: 'Recargar'
Reload: 'Recargar',
'Request to Join Relay': 'Solicitar unirse al Relay',
'Leave Relay': 'Salir del Relay',
Leave: 'Salir',
'Are you sure you want to leave this relay?': '¿Estás seguro de que quieres salir de este relay?',
'Join request sent successfully': 'Solicitud de unión enviada con éxito',
'Failed to send join request': 'Error al enviar solicitud de unión',
'Leave request sent successfully': 'Solicitud de salida enviada con éxito',
'Failed to send leave request': 'Error al enviar solicitud de salida',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'Ingresa un código de invitación si tienes uno. De lo contrario, déjalo en blanco para enviar una solicitud.',
'Invite Code (Optional)': 'Código de Invitación (Opcional)',
'Enter invite code': 'Ingresa el código de invitación',
'Sending...': 'Enviando...',
'Send Request': 'Enviar Solicitud',
'You can get an invite code from a relay member.':
'Puedes obtener un código de invitación de un miembro del relay.',
'Enter the invite code you received from a relay member.':
'Ingresa el código de invitación que recibiste de un miembro del relay.',
'Get Invite Code': 'Obtener Código de Invitación',
'Share this invite code with others to invite them to join this relay.':
'Comparte este código de invitación con otros para invitarlos a unirse a este relay.',
'Invite Code': 'Código de Invitación',
Copy: 'Copiar',
'This invite code can be used by others to join the relay.':
'Este código de invitación puede ser usado por otros para unirse al relay.',
'No invite code available from this relay.': 'No hay código de invitación disponible de este relay.',
Close: 'Cerrar',
'Failed to get invite code from relay': 'Error al obtener código de invitación del relay',
'Failed to get invite code': 'Error al obtener código de invitación',
'Invite code copied to clipboard': 'Código de invitación copiado al portapapeles'
}
}

View File

@@ -506,6 +506,36 @@ export default {
'My Packs': 'بسته‌های من',
'Adding...': 'در حال افزودن...',
'Removing...': 'در حال حذف...',
Reload: 'بازخوانی'
Reload: 'بازخوانی',
'Request to Join Relay': 'درخواست عضویت در رله',
'Leave Relay': 'خروج از رله',
Leave: 'خروج',
'Are you sure you want to leave this relay?': 'آیا مطمئن هستید که می‌خواهید از این رله خارج شوید؟',
'Join request sent successfully': 'درخواست عضویت با موفقیت ارسال شد',
'Failed to send join request': 'ارسال درخواست عضویت ناموفق بود',
'Leave request sent successfully': 'درخواست خروج با موفقیت ارسال شد',
'Failed to send leave request': 'ارسال درخواست خروج ناموفق بود',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'اگر کد دعوت دارید وارد کنید. در غیر این صورت، آن را خالی بگذارید تا درخواست ارسال شود.',
'Invite Code (Optional)': 'کد دعوت (اختیاری)',
'Enter invite code': 'کد دعوت را وارد کنید',
'Sending...': 'در حال ارسال...',
'Send Request': 'ارسال درخواست',
'You can get an invite code from a relay member.':
'می‌توانید کد دعوت را از یک عضو رله دریافت کنید.',
'Enter the invite code you received from a relay member.':
'کد دعوتی را که از یک عضو رله دریافت کرده‌اید وارد کنید.',
'Get Invite Code': 'دریافت کد دعوت',
'Share this invite code with others to invite them to join this relay.':
'این کد دعوت را با دیگران به اشتراک بگذارید تا آنها را به پیوستن به این رله دعوت کنید.',
'Invite Code': 'کد دعوت',
Copy: 'کپی',
'This invite code can be used by others to join the relay.':
'این کد دعوت می‌تواند توسط دیگران برای پیوستن به رله استفاده شود.',
'No invite code available from this relay.': 'هیچ کد دعوتی از این رله در دسترس نیست.',
Close: 'بستن',
'Failed to get invite code from relay': 'دریافت کد دعوت از رله ناموفق بود',
'Failed to get invite code': 'دریافت کد دعوت ناموفق بود',
'Invite code copied to clipboard': 'کد دعوت در کلیپ‌بورد کپی شد'
}
}

View File

@@ -516,6 +516,36 @@ export default {
'My Packs': 'Mes Packs',
'Adding...': 'Ajout...',
'Removing...': 'Suppression...',
Reload: 'Recharger'
Reload: 'Recharger',
'Request to Join Relay': 'Demander à rejoindre le Relay',
'Leave Relay': 'Quitter le Relay',
Leave: 'Quitter',
'Are you sure you want to leave this relay?': 'Êtes-vous sûr de vouloir quitter ce relay ?',
'Join request sent successfully': "Demande d'adhésion envoyée avec succès",
'Failed to send join request': "Échec de l'envoi de la demande d'adhésion",
'Leave request sent successfully': 'Demande de départ envoyée avec succès',
'Failed to send leave request': "Échec de l'envoi de la demande de départ",
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
"Entrez un code d'invitation si vous en avez un. Sinon, laissez-le vide pour envoyer une demande.",
'Invite Code (Optional)': "Code d'Invitation (Optionnel)",
'Enter invite code': "Entrez le code d'invitation",
'Sending...': 'Envoi...',
'Send Request': 'Envoyer la Demande',
'You can get an invite code from a relay member.':
"Vous pouvez obtenir un code d'invitation auprès d'un membre du relay.",
'Enter the invite code you received from a relay member.':
"Entrez le code d'invitation que vous avez reçu d'un membre du relay.",
'Get Invite Code': "Obtenir un Code d'Invitation",
'Share this invite code with others to invite them to join this relay.':
"Partagez ce code d'invitation avec d'autres pour les inviter à rejoindre ce relay.",
'Invite Code': "Code d'Invitation",
Copy: 'Copier',
'This invite code can be used by others to join the relay.':
"Ce code d'invitation peut être utilisé par d'autres pour rejoindre le relay.",
'No invite code available from this relay.': "Aucun code d'invitation disponible de ce relay.",
Close: 'Fermer',
'Failed to get invite code from relay': "Échec de l'obtention du code d'invitation du relay",
'Failed to get invite code': "Échec de l'obtention du code d'invitation",
'Invite code copied to clipboard': "Code d'invitation copié dans le presse-papiers"
}
}

View File

@@ -508,6 +508,36 @@ export default {
'My Packs': 'मेरे पैक',
'Adding...': 'जोड़ा जा रहा है...',
'Removing...': 'हटाया जा रहा है...',
Reload: 'रीलोड करें'
Reload: 'रीलोड करें',
'Request to Join Relay': 'रिले में शामिल होने का अनुरोध करें',
'Leave Relay': 'रिले छोड़ें',
Leave: 'छोड़ें',
'Are you sure you want to leave this relay?': 'क्या आप वाकई इस रिले को छोड़ना चाहते हैं?',
'Join request sent successfully': 'शामिल होने का अनुरोध सफलतापूर्वक भेजा गया',
'Failed to send join request': 'शामिल होने का अनुरोध भेजने में विफल',
'Leave request sent successfully': 'छोड़ने का अनुरोध सफलतापूर्वक भेजा गया',
'Failed to send leave request': 'छोड़ने का अनुरोध भेजने में विफल',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'यदि आपके पास निमंत्रण कोड है तो दर्ज करें। अन्यथा, अनुरोध भेजने के लिए इसे खाली छोड़ दें।',
'Invite Code (Optional)': 'निमंत्रण कोड (वैकल्पिक)',
'Enter invite code': 'निमंत्रण कोड दर्ज करें',
'Sending...': 'भेजा जा रहा है...',
'Send Request': 'अनुरोध भेजें',
'You can get an invite code from a relay member.':
'आप एक रिले सदस्य से निमंत्रण कोड प्राप्त कर सकते हैं।',
'Enter the invite code you received from a relay member.':
'रिले सदस्य से प्राप्त निमंत्रण कोड दर्ज करें।',
'Get Invite Code': 'निमंत्रण कोड प्राप्त करें',
'Share this invite code with others to invite them to join this relay.':
'इस रिले में शामिल होने के लिए दूसरों को आमंत्रित करने के लिए इस निमंत्रण कोड को साझा करें।',
'Invite Code': 'निमंत्रण कोड',
Copy: 'कॉपी करें',
'This invite code can be used by others to join the relay.':
'यह निमंत्रण कोड दूसरों द्वारा रिले में शामिल होने के लिए उपयोग किया जा सकता है।',
'No invite code available from this relay.': 'इस रिले से कोई निमंत्रण कोड उपलब्ध नहीं है।',
Close: 'बंद करें',
'Failed to get invite code from relay': 'रिले से निमंत्रण कोड प्राप्त करने में विफल',
'Failed to get invite code': 'निमंत्रण कोड प्राप्त करने में विफल',
'Invite code copied to clipboard': 'निमंत्रण कोड क्लिपबोर्ड पर कॉपी किया गया'
}
}

View File

@@ -503,6 +503,36 @@ export default {
'My Packs': 'Saját csomagjaim',
'Adding...': 'Hozzáadás...',
'Removing...': 'Eltávolítás...',
Reload: 'Újratöltés'
Reload: 'Újratöltés',
'Request to Join Relay': 'Csatlakozási kérelem küldése a relay-hez',
'Leave Relay': 'Relay elhagyása',
Leave: 'Kilépés',
'Are you sure you want to leave this relay?': 'Biztosan el szeretné hagyni ezt a relay-t?',
'Join request sent successfully': 'Csatlakozási kérelem sikeresen elküldve',
'Failed to send join request': 'Csatlakozási kérelem küldése sikertelen',
'Leave request sent successfully': 'Kilépési kérelem sikeresen elküldve',
'Failed to send leave request': 'Kilépési kérelem küldése sikertelen',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'Írjon be egy meghívókódot, ha van. Ellenkező esetben hagyja üresen a kérelem elküldéséhez.',
'Invite Code (Optional)': 'Meghívókód (opcionális)',
'Enter invite code': 'Írja be a meghívókódot',
'Sending...': 'Küldés...',
'Send Request': 'Kérelem küldése',
'You can get an invite code from a relay member.':
'Meghívókódot kaphat egy relay tagtól.',
'Enter the invite code you received from a relay member.':
'Írja be a relay tagtól kapott meghívókódot.',
'Get Invite Code': 'Meghívókód Lekérése',
'Share this invite code with others to invite them to join this relay.':
'Ossza meg ezt a meghívókódot másokkal, hogy meghívja őket ehhez a relay-hez.',
'Invite Code': 'Meghívókód',
Copy: 'Másolás',
'This invite code can be used by others to join the relay.':
'Ezt a meghívókódot mások használhatják a relay-hez való csatlakozáshoz.',
'No invite code available from this relay.': 'Nincs elérhető meghívókód ettől a relay-től.',
Close: 'Bezárás',
'Failed to get invite code from relay': 'Nem sikerült lekérni a meghívókódot a relay-től',
'Failed to get invite code': 'Nem sikerült lekérni a meghívókódot',
'Invite code copied to clipboard': 'Meghívókód vágólapra másolva'
}
}

View File

@@ -511,6 +511,36 @@ export default {
'My Packs': 'I Miei Pacchetti',
'Adding...': 'Aggiunta...',
'Removing...': 'Rimozione...',
Reload: 'Ricarica'
Reload: 'Ricarica',
'Request to Join Relay': 'Richiedi di unirti al Relay',
'Leave Relay': 'Lascia il Relay',
Leave: 'Esci',
'Are you sure you want to leave this relay?': 'Sei sicuro di voler lasciare questo relay?',
'Join request sent successfully': 'Richiesta di adesione inviata con successo',
'Failed to send join request': "Impossibile inviare la richiesta di adesione",
'Leave request sent successfully': 'Richiesta di uscita inviata con successo',
'Failed to send leave request': "Impossibile inviare la richiesta di uscita",
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
"Inserisci un codice di invito se ne hai uno. Altrimenti, lascialo vuoto per inviare una richiesta.",
'Invite Code (Optional)': 'Codice di Invito (Opzionale)',
'Enter invite code': 'Inserisci il codice di invito',
'Sending...': 'Invio...',
'Send Request': 'Invia Richiesta',
'You can get an invite code from a relay member.':
'Puoi ottenere un codice di invito da un membro del relay.',
'Enter the invite code you received from a relay member.':
'Inserisci il codice di invito che hai ricevuto da un membro del relay.',
'Get Invite Code': 'Ottieni Codice di Invito',
'Share this invite code with others to invite them to join this relay.':
'Condividi questo codice di invito con altri per invitarli a unirsi a questo relay.',
'Invite Code': 'Codice di Invito',
Copy: 'Copia',
'This invite code can be used by others to join the relay.':
'Questo codice di invito può essere utilizzato da altri per unirsi al relay.',
'No invite code available from this relay.': 'Nessun codice di invito disponibile da questo relay.',
Close: 'Chiudi',
'Failed to get invite code from relay': 'Impossibile ottenere il codice di invito dal relay',
'Failed to get invite code': 'Impossibile ottenere il codice di invito',
'Invite code copied to clipboard': 'Codice di invito copiato negli appunti'
}
}

View File

@@ -507,6 +507,34 @@ export default {
'My Packs': 'マイパック',
'Adding...': '追加中...',
'Removing...': '削除中...',
Reload: '再読み込み'
Reload: '再読み込み',
'Request to Join Relay': 'リレーへの参加をリクエスト',
'Leave Relay': 'リレーを退出',
Leave: '退出',
'Are you sure you want to leave this relay?': 'このリレーを退出してもよろしいですか?',
'Join request sent successfully': '参加リクエストを送信しました',
'Failed to send join request': '参加リクエストの送信に失敗しました',
'Leave request sent successfully': '退出リクエストを送信しました',
'Failed to send leave request': '退出リクエストの送信に失敗しました',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'招待コードをお持ちの場合は入力してください。それ以外の場合は空白のままリクエストを送信してください。',
'Invite Code (Optional)': '招待コード(オプション)',
'Enter invite code': '招待コードを入力',
'Sending...': '送信中...',
'Send Request': 'リクエストを送信',
'You can get an invite code from a relay member.': 'リレーメンバーから招待コードを取得できます。',
'Enter the invite code you received from a relay member.': 'リレーメンバーから受け取った招待コードを入力してください。',
'Get Invite Code': '招待コードを取得',
'Share this invite code with others to invite them to join this relay.':
'この招待コードを他の人と共有して、このリレーへの参加を招待してください。',
'Invite Code': '招待コード',
Copy: 'コピー',
'This invite code can be used by others to join the relay.':
'この招待コードは他の人がリレーに参加するために使用できます。',
'No invite code available from this relay.': 'このリレーから利用可能な招待コードはありません。',
Close: '閉じる',
'Failed to get invite code from relay': 'リレーから招待コードの取得に失敗しました',
'Failed to get invite code': '招待コードの取得に失敗しました',
'Invite code copied to clipboard': '招待コードをクリップボードにコピーしました'
}
}

View File

@@ -507,6 +507,34 @@ export default {
'My Packs': '내 팩',
'Adding...': '추가 중...',
'Removing...': '제거 중...',
Reload: '다시 불러오기'
Reload: '다시 불러오기',
'Request to Join Relay': '릴레이 가입 요청',
'Leave Relay': '릴레이 떠나기',
Leave: '나가기',
'Are you sure you want to leave this relay?': '이 릴레이를 떠나시겠습니까?',
'Join request sent successfully': '가입 요청을 성공적으로 보냈습니다',
'Failed to send join request': '가입 요청 전송 실패',
'Leave request sent successfully': '떠나기 요청을 성공적으로 보냈습니다',
'Failed to send leave request': '떠나기 요청 전송 실패',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'초대 코드가 있으면 입력하세요. 그렇지 않으면 비워두고 요청을 보내세요.',
'Invite Code (Optional)': '초대 코드 (선택 사항)',
'Enter invite code': '초대 코드 입력',
'Sending...': '전송 중...',
'Send Request': '요청 보내기',
'You can get an invite code from a relay member.': '릴레이 회원으로부터 초대 코드를 받을 수 있습니다.',
'Enter the invite code you received from a relay member.': '릴레이 회원으로부터 받은 초대 코드를 입력하세요.',
'Get Invite Code': '초대 코드 받기',
'Share this invite code with others to invite them to join this relay.':
'이 초대 코드를 다른 사람과 공유하여 이 릴레이에 초대하세요.',
'Invite Code': '초대 코드',
Copy: '복사',
'This invite code can be used by others to join the relay.':
'이 초대 코드는 다른 사람이 릴레이에 가입하는 데 사용할 수 있습니다.',
'No invite code available from this relay.': '이 릴레이에서 사용 가능한 초대 코드가 없습니다.',
Close: '닫기',
'Failed to get invite code from relay': '릴레이에서 초대 코드 가져오기 실패',
'Failed to get invite code': '초대 코드 가져오기 실패',
'Invite code copied to clipboard': '초대 코드가 클립보드에 복사되었습니다'
}
}

View File

@@ -511,6 +511,36 @@ export default {
'My Packs': 'Moje Pakiety',
'Adding...': 'Dodawanie...',
'Removing...': 'Usuwanie...',
Reload: 'Przeładuj'
Reload: 'Przeładuj',
'Request to Join Relay': 'Poproś o dołączenie do przekaźnika',
'Leave Relay': 'Opuść przekaźnik',
Leave: 'Opuść',
'Are you sure you want to leave this relay?': 'Czy na pewno chcesz opuścić ten przekaźnik?',
'Join request sent successfully': 'Prośba o dołączenie wysłana pomyślnie',
'Failed to send join request': 'Nie udało się wysłać prośby o dołączenie',
'Leave request sent successfully': 'Prośba o opuszczenie wysłana pomyślnie',
'Failed to send leave request': 'Nie udało się wysłać prośby o opuszczenie',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'Wprowadź kod zaproszenia, jeśli go masz. W przeciwnym razie pozostaw puste, aby wysłać prośbę.',
'Invite Code (Optional)': 'Kod zaproszenia (opcjonalnie)',
'Enter invite code': 'Wprowadź kod zaproszenia',
'Sending...': 'Wysyłanie...',
'Send Request': 'Wyślij prośbę',
'You can get an invite code from a relay member.':
'Możesz uzyskać kod zaproszenia od członka przekaźnika.',
'Enter the invite code you received from a relay member.':
'Wprowadź kod zaproszenia otrzymany od członka przekaźnika.',
'Get Invite Code': 'Uzyskaj Kod Zaproszenia',
'Share this invite code with others to invite them to join this relay.':
'Udostępnij ten kod zaproszenia innym, aby zaprosić ich do dołączenia do tego przekaźnika.',
'Invite Code': 'Kod Zaproszenia',
Copy: 'Kopiuj',
'This invite code can be used by others to join the relay.':
'Ten kod zaproszenia może być używany przez innych do dołączenia do przekaźnika.',
'No invite code available from this relay.': 'Brak dostępnego kodu zaproszenia z tego przekaźnika.',
Close: 'Zamknij',
'Failed to get invite code from relay': 'Nie udało się uzyskać kodu zaproszenia z przekaźnika',
'Failed to get invite code': 'Nie udało się uzyskać kodu zaproszenia',
'Invite code copied to clipboard': 'Kod zaproszenia skopiowany do schowka'
}
}

View File

@@ -508,6 +508,36 @@ export default {
'My Packs': 'Meus Pacotes',
'Adding...': 'Adicionando...',
'Removing...': 'Removendo...',
Reload: 'Recarregar'
Reload: 'Recarregar',
'Request to Join Relay': 'Solicitar entrada no Relay',
'Leave Relay': 'Sair do Relay',
Leave: 'Sair',
'Are you sure you want to leave this relay?': 'Tem certeza de que deseja sair deste relay?',
'Join request sent successfully': 'Solicitação de entrada enviada com sucesso',
'Failed to send join request': 'Falha ao enviar solicitação de entrada',
'Leave request sent successfully': 'Solicitação de saída enviada com sucesso',
'Failed to send leave request': 'Falha ao enviar solicitação de saída',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'Digite um código de convite se tiver um. Caso contrário, deixe em branco para enviar uma solicitação.',
'Invite Code (Optional)': 'Código de Convite (Opcional)',
'Enter invite code': 'Digite o código de convite',
'Sending...': 'Enviando...',
'Send Request': 'Enviar Solicitação',
'You can get an invite code from a relay member.':
'Você pode obter um código de convite de um membro do relay.',
'Enter the invite code you received from a relay member.':
'Digite o código de convite que você recebeu de um membro do relay.',
'Get Invite Code': 'Obter Código de Convite',
'Share this invite code with others to invite them to join this relay.':
'Compartilhe este código de convite com outros para convidá-los a participar deste relay.',
'Invite Code': 'Código de Convite',
Copy: 'Copiar',
'This invite code can be used by others to join the relay.':
'Este código de convite pode ser usado por outros para participar do relay.',
'No invite code available from this relay.': 'Nenhum código de convite disponível deste relay.',
Close: 'Fechar',
'Failed to get invite code from relay': 'Falha ao obter código de convite do relay',
'Failed to get invite code': 'Falha ao obter código de convite',
'Invite code copied to clipboard': 'Código de convite copiado para a área de transferência'
}
}

View File

@@ -511,6 +511,36 @@ export default {
'My Packs': 'Os Meus Pacotes',
'Adding...': 'A adicionar...',
'Removing...': 'A remover...',
Reload: 'Recarregar'
Reload: 'Recarregar',
'Request to Join Relay': 'Solicitar adesão ao Relay',
'Leave Relay': 'Sair do Relay',
Leave: 'Sair',
'Are you sure you want to leave this relay?': 'Tem a certeza de que deseja sair deste relay?',
'Join request sent successfully': 'Pedido de adesão enviado com sucesso',
'Failed to send join request': 'Falha ao enviar pedido de adesão',
'Leave request sent successfully': 'Pedido de saída enviado com sucesso',
'Failed to send leave request': 'Falha ao enviar pedido de saída',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'Introduza um código de convite se tiver um. Caso contrário, deixe em branco para enviar um pedido.',
'Invite Code (Optional)': 'Código de Convite (Opcional)',
'Enter invite code': 'Introduza o código de convite',
'Sending...': 'A enviar...',
'Send Request': 'Enviar Pedido',
'You can get an invite code from a relay member.':
'Pode obter um código de convite de um membro do relay.',
'Enter the invite code you received from a relay member.':
'Introduza o código de convite que recebeu de um membro do relay.',
'Get Invite Code': 'Obter Código de Convite',
'Share this invite code with others to invite them to join this relay.':
'Partilhe este código de convite com outros para os convidar a aderir a este relay.',
'Invite Code': 'Código de Convite',
Copy: 'Copiar',
'This invite code can be used by others to join the relay.':
'Este código de convite pode ser usado por outros para aderir ao relay.',
'No invite code available from this relay.': 'Nenhum código de convite disponível deste relay.',
Close: 'Fechar',
'Failed to get invite code from relay': 'Falha ao obter código de convite do relay',
'Failed to get invite code': 'Falha ao obter código de convite',
'Invite code copied to clipboard': 'Código de convite copiado para a área de transferência'
}
}

View File

@@ -513,6 +513,36 @@ export default {
'My Packs': 'Мои наборы',
'Adding...': 'Добавление...',
'Removing...': 'Удаление...',
Reload: 'Перезагрузить'
Reload: 'Перезагрузить',
'Request to Join Relay': 'Запросить присоединение к релею',
'Leave Relay': 'Покинуть релей',
Leave: 'Выйти',
'Are you sure you want to leave this relay?': 'Вы уверены, что хотите покинуть этот релей?',
'Join request sent successfully': 'Запрос на присоединение успешно отправлен',
'Failed to send join request': 'Не удалось отправить запрос на присоединение',
'Leave request sent successfully': 'Запрос на выход успешно отправлен',
'Failed to send leave request': 'Не удалось отправить запрос на выход',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'Введите код приглашения, если он у вас есть. В противном случае оставьте поле пустым для отправки запроса.',
'Invite Code (Optional)': 'Код приглашения (необязательно)',
'Enter invite code': 'Введите код приглашения',
'Sending...': 'Отправка...',
'Send Request': 'Отправить запрос',
'You can get an invite code from a relay member.':
'Вы можете получить код приглашения у члена релея.',
'Enter the invite code you received from a relay member.':
'Введите код приглашения, который вы получили от члена релея.',
'Get Invite Code': 'Получить Код Приглашения',
'Share this invite code with others to invite them to join this relay.':
'Поделитесь этим кодом приглашения с другими, чтобы пригласить их присоединиться к этому релею.',
'Invite Code': 'Код Приглашения',
Copy: 'Копировать',
'This invite code can be used by others to join the relay.':
'Этот код приглашения может быть использован другими для присоединения к релею.',
'No invite code available from this relay.': 'Нет доступного кода приглашения от этого релея.',
Close: 'Закрыть',
'Failed to get invite code from relay': 'Не удалось получить код приглашения от релея',
'Failed to get invite code': 'Не удалось получить код приглашения',
'Invite code copied to clipboard': 'Код приглашения скопирован в буфер обмена'
}
}

View File

@@ -501,6 +501,34 @@ export default {
'My Packs': 'แพ็คของฉัน',
'Adding...': 'กำลังเพิ่ม...',
'Removing...': 'กำลังลบ...',
Reload: 'โหลดใหม่'
Reload: 'โหลดใหม่',
'Request to Join Relay': 'ขอเข้าร่วมรีเลย์',
'Leave Relay': 'ออกจากรีเลย์',
Leave: 'ออก',
'Are you sure you want to leave this relay?': 'คุณแน่ใจหรือไม่ว่าต้องการออกจากรีเลย์นี้?',
'Join request sent successfully': 'ส่งคำขอเข้าร่วมสำเร็จแล้ว',
'Failed to send join request': 'การส่งคำขอเข้าร่วมล้มเหลว',
'Leave request sent successfully': 'ส่งคำขอออกสำเร็จแล้ว',
'Failed to send leave request': 'การส่งคำขอออกล้มเหลว',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'ป้อนรหัสเชิญหากคุณมี มิฉะนั้นให้เว้นว่างไว้เพื่อส่งคำขอ',
'Invite Code (Optional)': 'รหัสเชิญ (ไม่บังคับ)',
'Enter invite code': 'ป้อนรหัสเชิญ',
'Sending...': 'กำลังส่ง...',
'Send Request': 'ส่งคำขอ',
'You can get an invite code from a relay member.': 'คุณสามารถรับรหัสเชิญจากสมาชิกรีเลย์',
'Enter the invite code you received from a relay member.': 'ป้อนรหัสเชิญที่คุณได้รับจากสมาชิกรีเลย์',
'Get Invite Code': 'รับรหัสเชิญ',
'Share this invite code with others to invite them to join this relay.':
'แชร์รหัสเชิญนี้กับผู้อื่นเพื่อเชิญพวกเขาเข้าร่วมรีเลย์นี้',
'Invite Code': 'รหัสเชิญ',
Copy: 'คัดลอก',
'This invite code can be used by others to join the relay.':
'รหัสเชิญนี้สามารถใช้โดยผู้อื่นเพื่อเข้าร่วมรีเลย์',
'No invite code available from this relay.': 'ไม่มีรหัสเชิญที่ใช้ได้จากรีเลย์นี้',
Close: 'ปิด',
'Failed to get invite code from relay': 'ไม่สามารถรับรหัสเชิญจากรีเลย์',
'Failed to get invite code': 'ไม่สามารถรับรหัสเชิญ',
'Invite code copied to clipboard': 'คัดลอกรหัสเชิญไปยังคลิปบอร์ดแล้ว'
}
}

View File

@@ -498,6 +498,34 @@ export default {
'My Packs': '我的表情包',
'Adding...': '添加中...',
'Removing...': '移除中...',
Reload: '重新加载'
Reload: '重新加载',
'Request to Join Relay': '申请加入中继器',
'Leave Relay': '离开中继器',
Leave: '离开',
'Are you sure you want to leave this relay?': '您确定要离开此中继器吗?',
'Join request sent successfully': '加入请求已成功发送',
'Failed to send join request': '发送加入请求失败',
'Leave request sent successfully': '离开请求已成功发送',
'Failed to send leave request': '发送离开请求失败',
'Enter an invite code if you have one. Otherwise, leave it blank to send a request.':
'如果您有邀请码,请输入。否则,留空以发送请求。',
'Invite Code (Optional)': '邀请码(可选)',
'Enter invite code': '输入邀请码',
'Sending...': '发送中...',
'Send Request': '发送请求',
'You can get an invite code from a relay member.': '您可以从中继器成员获取邀请码。',
'Enter the invite code you received from a relay member.': '输入您从中继器成员处获得的邀请码。',
'Get Invite Code': '获取邀请码',
'Share this invite code with others to invite them to join this relay.':
'将此邀请码分享给他人以邀请他们加入此中继器。',
'Invite Code': '邀请码',
Copy: '复制',
'This invite code can be used by others to join the relay.':
'此邀请码可供他人用于加入中继器。',
'No invite code available from this relay.': '此中继器没有可用的邀请码。',
Close: '关闭',
'Failed to get invite code from relay': '从中继器获取邀请码失败',
'Failed to get invite code': '获取邀请码失败',
'Invite code copied to clipboard': '邀请码已复制到剪贴板'
}
}

View File

@@ -493,6 +493,25 @@ export function createRelayReviewDraftEvent(
}
}
// https://github.com/nostr-protocol/nips/blob/master/43.md
export function createJoinDraftEvent(inviteCode: string): TDraftEvent {
return {
kind: 28934,
created_at: Math.floor(Date.now() / 1000),
tags: [['claim', inviteCode], ['-']],
content: ''
}
}
export function createLeaveDraftEvent(): TDraftEvent {
return {
kind: 28936,
created_at: Math.floor(Date.now() / 1000),
tags: [['-']],
content: ''
}
}
function generateImetaTags(imageUrls: string[]) {
return imageUrls
.map((imageUrl) => {

View File

@@ -363,3 +363,8 @@ export function getRetainedEvent(a: Event, b: Event): Event {
}
return b
}
// Descending sort
export function sortEventsDesc(events: Event[]): Event[] {
return events.sort((a, b) => compareEvents(b, a))
}

View File

@@ -9,6 +9,10 @@ export function checkSearchRelay(relayInfo: TRelayInfo | undefined) {
return relayInfo?.supported_nips?.includes(50)
}
export function checkNip43Support(relayInfo: TRelayInfo | undefined) {
return relayInfo?.supported_nips?.includes(43) && !!relayInfo.pubkey
}
export function filterOutBigRelays(relayUrls: string[]) {
return relayUrls.filter((url) => !BIG_RELAY_URLS.includes(url))
}

View File

@@ -0,0 +1,127 @@
import { sortEventsDesc } from '@/lib/event'
import { isValidPubkey } from '@/lib/pubkey'
import client from '@/services/client.service'
import DataLoader from 'dataloader'
import { Filter } from 'nostr-tools'
/**
* NIP-43: Relay Access Metadata and Requests
* https://github.com/nostr-protocol/nips/blob/master/43.md
*/
class RelayMembershipService {
private static instance: RelayMembershipService
private membershipListCache: Map<string, Promise<Set<string>>> = new Map()
private membershipListDataLoader = new DataLoader<
{ url: string; pubkey: string },
Set<string>,
string
>(
async (params) => {
return Promise.all(params.map(({ url, pubkey }) => this.fetchMembershipList(url, pubkey)))
},
{ cacheKeyFn: (key) => key.url, cacheMap: this.membershipListCache }
)
public static getInstance(): RelayMembershipService {
if (!RelayMembershipService.instance) {
RelayMembershipService.instance = new RelayMembershipService()
}
return RelayMembershipService.instance
}
/**
* Check if a user is a member of a relay that supports NIP-43
* @param relayUrl The relay URL
* @param userPubkey The user's public key
* @param relayPubkey The relay's public key from NIP-11
* @returns Membership status
*/
async checkMembership(
relayUrl: string,
userPubkey: string,
relayPubkey?: string
): Promise<boolean> {
if (!relayPubkey) {
return false
}
const memberSet = await this.membershipListDataLoader.load({
url: relayUrl,
pubkey: relayPubkey
})
return memberSet.has(userPubkey)
}
private async fetchMembershipList(relayUrl: string, relayPubkey: string): Promise<Set<string>> {
try {
const filter: Filter = {
kinds: [13534],
authors: [relayPubkey],
limit: 1
}
const events = await client.fetchEvents([relayUrl], filter)
if (events.length === 0) {
return new Set()
}
const membershipEvent = sortEventsDesc(events)[0]
const members = membershipEvent.tags
.filter((tag) => tag[0] === 'member' && isValidPubkey(tag[1]))
.map((tag) => tag[1])
return new Set(members)
} catch (error) {
console.error('Error checking relay membership:', error)
return new Set()
}
}
/**
* Request an invite code from a relay (kind 28935)
* @param relayUrl The relay URL
* @param relayPubkey The relay's public key from NIP-11
* @returns Invite code or null
*/
async requestInviteCode(relayUrl: string, relayPubkey: string): Promise<string | null> {
try {
const filter: Filter = {
kinds: [28935],
authors: [relayPubkey],
limit: 1
}
const events = await client.fetchEvents([relayUrl], filter)
if (events.length === 0) {
return null
}
const inviteEvent = events[0]
const claimTag = inviteEvent.tags.find((tag) => tag[0] === 'claim')
return claimTag?.[1] ?? null
} catch (error) {
console.error('Error requesting invite code:', error)
return null
}
}
async addNewMember(relayUrl: string, newMemberPubkey: string) {
const cache = await this.membershipListCache.get(relayUrl)
if (cache) {
cache.add(newMemberPubkey)
}
}
async removeMember(relayUrl: string, memberPubkey: string) {
const cache = await this.membershipListCache.get(relayUrl)
if (cache) {
cache.delete(memberPubkey)
}
}
}
const instance = RelayMembershipService.getInstance()
export default instance