From 850d92de28f83e694d4098e3523e4d64814e9665 Mon Sep 17 00:00:00 2001 From: codytseng Date: Sun, 9 Nov 2025 00:26:16 +0800 Subject: [PATCH] feat: NIP-43 --- src/components/RelayInfo/index.tsx | 28 ++- .../InviteCodeDialog.tsx | 134 ++++++++++++++ .../RelayMembershipControl/JoinDialog.tsx | 141 +++++++++++++++ .../RelayMembershipControl/index.tsx | 166 ++++++++++++++++++ src/i18n/locales/ar.ts | 30 +++- src/i18n/locales/de.ts | 32 +++- src/i18n/locales/en.ts | 32 +++- src/i18n/locales/es.ts | 32 +++- src/i18n/locales/fa.ts | 32 +++- src/i18n/locales/fr.ts | 32 +++- src/i18n/locales/hi.ts | 32 +++- src/i18n/locales/hu.ts | 32 +++- src/i18n/locales/it.ts | 32 +++- src/i18n/locales/ja.ts | 30 +++- src/i18n/locales/ko.ts | 30 +++- src/i18n/locales/pl.ts | 32 +++- src/i18n/locales/pt-BR.ts | 32 +++- src/i18n/locales/pt-PT.ts | 32 +++- src/i18n/locales/ru.ts | 32 +++- src/i18n/locales/th.ts | 30 +++- src/i18n/locales/zh.ts | 30 +++- src/lib/draft-event.ts | 19 ++ src/lib/event.ts | 5 + src/lib/relay.ts | 4 + src/services/relay-membership.service.ts | 127 ++++++++++++++ 25 files changed, 1132 insertions(+), 26 deletions(-) create mode 100644 src/components/RelayMembershipControl/InviteCodeDialog.tsx create mode 100644 src/components/RelayMembershipControl/JoinDialog.tsx create mode 100644 src/components/RelayMembershipControl/index.tsx create mode 100644 src/services/relay-membership.service.ts diff --git a/src/components/RelayInfo/index.tsx b/src/components/RelayInfo/index.tsx index 9c1989d..2aeff0b 100644 --- a/src/components/RelayInfo/index.tsx +++ b/src/components/RelayInfo/index.tsx @@ -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?: - - + + {shouldShowPostButton && ( + <> + + + + )} diff --git a/src/components/RelayMembershipControl/InviteCodeDialog.tsx b/src/components/RelayMembershipControl/InviteCodeDialog.tsx new file mode 100644 index 0000000..b671893 --- /dev/null +++ b/src/components/RelayMembershipControl/InviteCodeDialog.tsx @@ -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 ? ( +
+
{t('Loading...')}
+
+ ) : inviteCode ? ( +
+ +
+ + +
+

+ {t('This invite code can be used by others to join the relay.')} +

+
+ ) : ( +
+ {t('No invite code available from this relay.')} +
+ ) + + if (isSmallScreen) { + return ( + + + + {t('Get Invite Code')} + + {t('Share this invite code with others to invite them to join this relay.')} + + +
{content}
+
+
+ ) + } + + return ( + + + + {t('Get Invite Code')} + + {t('Share this invite code with others to invite them to join this relay.')} + + + {content} + + + ) +} diff --git a/src/components/RelayMembershipControl/JoinDialog.tsx b/src/components/RelayMembershipControl/JoinDialog.tsx new file mode 100644 index 0000000..d7b276f --- /dev/null +++ b/src/components/RelayMembershipControl/JoinDialog.tsx @@ -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 = ( +
+ + setInviteCode(e.target.value)} + placeholder={t('Enter invite code')} + required + /> +

+ {t('You can get an invite code from a relay member.')} +

+
+ ) + + if (isSmallScreen) { + return ( + + + + {t('Request to Join Relay')} + + {t('Enter the invite code you received from a relay member.')} + + +
{content}
+ + + + + + +
+
+ ) + } + + return ( + + + + {t('Request to Join Relay')} + + {t('Enter the invite code you received from a relay member.')} + + + {content} + + + + + + + ) +} diff --git a/src/components/RelayMembershipControl/index.tsx b/src/components/RelayMembershipControl/index.tsx new file mode 100644 index 0000000..ecb233c --- /dev/null +++ b/src/components/RelayMembershipControl/index.tsx @@ -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 ? ( +
+ + +
+ ) : ( + + )} + + + + + + ) +} diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index ed4ce27..4517114 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -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': 'تم نسخ رمز الدعوة إلى الحافظة' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 56b7d4d..919314e 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -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' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 5ab6f4f..bd1d9e4 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -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' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index e79fcfd..e92161d 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -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' } } diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index 5ab2d7c..562666e 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -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': 'کد دعوت در کلیپ‌بورد کپی شد' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index f03e549..9867ef6 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -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" } } diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts index 9cb362f..2c8ff3e 100644 --- a/src/i18n/locales/hi.ts +++ b/src/i18n/locales/hi.ts @@ -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': 'निमंत्रण कोड क्लिपबोर्ड पर कॉपी किया गया' } } diff --git a/src/i18n/locales/hu.ts b/src/i18n/locales/hu.ts index d7bbe3d..1621b09 100644 --- a/src/i18n/locales/hu.ts +++ b/src/i18n/locales/hu.ts @@ -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' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index 9219b1b..a040524 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -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' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 04e863f..fd8b3ba 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -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': '招待コードをクリップボードにコピーしました' } } diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index d176f98..361218a 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -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': '초대 코드가 클립보드에 복사되었습니다' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index a68fa94..39fda20 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -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' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 15679d5..42695f0 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -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' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index 9ecf0cb..e962e12 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -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' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 5b2ab22..401059d 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -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': 'Код приглашения скопирован в буфер обмена' } } diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index 7a4291e..5724bd3 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -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': 'คัดลอกรหัสเชิญไปยังคลิปบอร์ดแล้ว' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index bde7b0c..b3925c2 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -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': '邀请码已复制到剪贴板' } } diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index c7c109f..9039f70 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -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) => { diff --git a/src/lib/event.ts b/src/lib/event.ts index b5c670f..6deae61 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -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)) +} diff --git a/src/lib/relay.ts b/src/lib/relay.ts index 4211bfc..04664ae 100644 --- a/src/lib/relay.ts +++ b/src/lib/relay.ts @@ -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)) } diff --git a/src/services/relay-membership.service.ts b/src/services/relay-membership.service.ts new file mode 100644 index 0000000..3718041 --- /dev/null +++ b/src/services/relay-membership.service.ts @@ -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>> = new Map() + private membershipListDataLoader = new DataLoader< + { url: string; pubkey: string }, + Set, + 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 { + 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> { + 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 { + 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