From ec11d53fac5bf2156f7c45259f2a684f8120471c Mon Sep 17 00:00:00 2001 From: codytseng Date: Sun, 21 Sep 2025 21:43:09 +0800 Subject: [PATCH] feat: add relay selector for posting --- src/components/NoteOptions/useMenuActions.tsx | 38 +--- src/components/PostEditor/PostContent.tsx | 58 ++--- .../PostEditor/PostRelaySelector.tsx | 209 ++++++++++++++++++ .../PostEditor/SendOnlyToSwitch.tsx | 79 ------- src/components/Relay/index.tsx | 11 + src/i18n/locales/ar.ts | 6 +- src/i18n/locales/de.ts | 6 +- src/i18n/locales/en.ts | 6 +- src/i18n/locales/es.ts | 6 +- src/i18n/locales/fa.ts | 6 +- src/i18n/locales/fr.ts | 6 +- src/i18n/locales/hi.ts | 6 +- src/i18n/locales/it.ts | 6 +- src/i18n/locales/ja.ts | 6 +- src/i18n/locales/ko.ts | 6 +- src/i18n/locales/pl.ts | 6 +- src/i18n/locales/pt-BR.ts | 6 +- src/i18n/locales/pt-PT.ts | 6 +- src/i18n/locales/ru.ts | 7 +- src/i18n/locales/th.ts | 6 +- src/i18n/locales/zh.ts | 6 +- src/pages/primary/NoteListPage/index.tsx | 11 + src/pages/primary/RelayPage/index.tsx | 10 +- src/providers/CurrentRelaysProvider.tsx | 44 ++-- src/services/client.service.ts | 62 +++--- 25 files changed, 418 insertions(+), 201 deletions(-) create mode 100644 src/components/PostEditor/PostRelaySelector.tsx delete mode 100644 src/components/PostEditor/SendOnlyToSwitch.tsx diff --git a/src/components/NoteOptions/useMenuActions.tsx b/src/components/NoteOptions/useMenuActions.tsx index 82cfe583..2dc2d655 100644 --- a/src/components/NoteOptions/useMenuActions.tsx +++ b/src/components/NoteOptions/useMenuActions.tsx @@ -2,22 +2,12 @@ import { getNoteBech32Id, isProtectedEvent } from '@/lib/event' import { toNjump } from '@/lib/link' import { pubkeyToNpub } from '@/lib/pubkey' import { simplifyUrl } from '@/lib/url' +import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useMuteList } from '@/providers/MuteListProvider' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' -import { - Bell, - BellOff, - Code, - Copy, - Link, - Mail, - SatelliteDish, - Server, - Trash2, - TriangleAlert -} from 'lucide-react' +import { Bell, BellOff, Code, Copy, Link, SatelliteDish, Trash2, TriangleAlert } from 'lucide-react' import { Event } from 'nostr-tools' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -59,7 +49,11 @@ export function useMenuActions({ }: UseMenuActionsProps) { const { t } = useTranslation() const { pubkey, attemptDelete } = useNostr() + const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays() const { relaySets, favoriteRelays } = useFavoriteRelays() + const relayUrls = useMemo(() => { + return Array.from(new Set(currentBrowsingRelayUrls.concat(favoriteRelays))) + }, [currentBrowsingRelayUrls, favoriteRelays]) const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList() const isMuted = useMemo(() => mutePubkeySet.has(event.pubkey), [mutePubkeySet, event]) @@ -67,12 +61,7 @@ export function useMenuActions({ const items = [] if (pubkey && event.pubkey === pubkey) { items.push({ - label: ( -
- -
{t('Suitable Relays')}
-
- ), + label:
{t('Write relays')}
, onClick: async () => { closeDrawer() const relays = await client.determineTargetRelays(event) @@ -97,12 +86,7 @@ export function useMenuActions({ ...relaySets .filter((set) => set.relayUrls.length) .map((set, index) => ({ - label: ( -
- -
{set.name}
-
- ), + label:
{set.name}
, onClick: async () => { closeDrawer() await client @@ -126,9 +110,9 @@ export function useMenuActions({ ) } - if (favoriteRelays.length) { + if (relayUrls.length) { items.push( - ...favoriteRelays.map((relay, index) => ({ + ...relayUrls.map((relay, index) => ({ label: (
@@ -159,7 +143,7 @@ export function useMenuActions({ } return items - }, [pubkey, favoriteRelays, relaySets]) + }, [pubkey, relayUrls, relaySets]) const menuActions: MenuAction[] = useMemo(() => { const actions: MenuAction[] = [ diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index 1d0c728d..410b4e48 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -14,15 +14,15 @@ import postEditorCache from '@/services/post-editor-cache.service' import { TPollCreateData } from '@/types' import { ImageUp, ListTodo, LoaderCircle, Settings, Smile, X } from 'lucide-react' import { Event, kinds } from 'nostr-tools' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import EmojiPickerDialog from '../EmojiPickerDialog' import Mentions from './Mentions' import PollEditor from './PollEditor' import PostOptions from './PostOptions' +import PostRelaySelector from './PostRelaySelector' import PostTextarea, { TPostTextareaHandle } from './PostTextarea' -import SendOnlyToSwitch from './SendOnlyToSwitch' import Uploader from './Uploader' export default function PostContent({ @@ -47,10 +47,11 @@ export default function PostContent({ >([]) const [showMoreOptions, setShowMoreOptions] = useState(false) const [addClientTag, setAddClientTag] = useState(false) - const [specifiedRelayUrls, setSpecifiedRelayUrls] = useState(undefined) const [mentions, setMentions] = useState([]) const [isNsfw, setIsNsfw] = useState(false) const [isPoll, setIsPoll] = useState(false) + const [isProtectedEvent, setIsProtectedEvent] = useState(false) + const [additionalRelayUrls, setAdditionalRelayUrls] = useState([]) const [pollCreateData, setPollCreateData] = useState({ isMultipleChoice: false, options: ['', ''], @@ -59,12 +60,25 @@ export default function PostContent({ }) const [minPow, setMinPow] = useState(0) const isFirstRender = useRef(true) - const canPost = - !!pubkey && - !!text && - !posting && - !uploadProgresses.length && - (!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) + const canPost = useMemo(() => { + return ( + !!pubkey && + !!text && + !posting && + !uploadProgresses.length && + (!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) && + (!isProtectedEvent || additionalRelayUrls.length > 0) + ) + }, [ + pubkey, + text, + posting, + uploadProgresses, + isPoll, + pollCreateData, + isProtectedEvent, + additionalRelayUrls + ]) useEffect(() => { if (isFirstRender.current) { @@ -97,15 +111,7 @@ export default function PostContent({ addClientTag } ) - }, [ - defaultContent, - parentEvent, - isNsfw, - isPoll, - pollCreateData, - specifiedRelayUrls, - addClientTag - ]) + }, [defaultContent, parentEvent, isNsfw, isPoll, pollCreateData, addClientTag]) const post = async (e?: React.MouseEvent) => { e?.stopPropagation() @@ -118,24 +124,24 @@ export default function PostContent({ parentEvent && parentEvent.kind !== kinds.ShortTextNote ? await createCommentDraftEvent(text, parentEvent, mentions, { addClientTag, - protectedEvent: !!specifiedRelayUrls, + protectedEvent: isProtectedEvent, isNsfw }) : isPoll - ? await createPollDraftEvent(pubkey, text, mentions, pollCreateData, { + ? await createPollDraftEvent(pubkey!, text, mentions, pollCreateData, { addClientTag, isNsfw }) : await createShortTextNoteDraftEvent(text, mentions, { parentEvent, addClientTag, - protectedEvent: !!specifiedRelayUrls, + protectedEvent: isProtectedEvent, isNsfw }) const newEvent = await publish(draftEvent, { - specifiedRelayUrls, - additionalRelayUrls: isPoll ? pollCreateData.relays : [], + specifiedRelayUrls: isProtectedEvent ? additionalRelayUrls : undefined, + additionalRelayUrls: isPoll ? pollCreateData.relays : additionalRelayUrls, minPow }) postEditorCache.clearPostCache({ defaultContent, parentEvent }) @@ -233,10 +239,10 @@ export default function PostContent({
))} {!isPoll && ( - )} diff --git a/src/components/PostEditor/PostRelaySelector.tsx b/src/components/PostEditor/PostRelaySelector.tsx new file mode 100644 index 00000000..38a3bc2a --- /dev/null +++ b/src/components/PostEditor/PostRelaySelector.tsx @@ -0,0 +1,209 @@ +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { isProtectedEvent } from '@/lib/event' +import { simplifyUrl } from '@/lib/url' +import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import client from '@/services/client.service' +import { NostrEvent } from 'nostr-tools' +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import RelayIcon from '../RelayIcon' + +type TPostTargetItem = + | { + type: 'writeRelays' + } + | { + type: 'relay' + url: string + } + | { + type: 'relaySet' + id: string + urls: string[] + } + +export default function PostRelaySelector({ + parentEvent, + openFrom, + setIsProtectedEvent, + setAdditionalRelayUrls +}: { + parentEvent?: NostrEvent + openFrom?: string[] + setIsProtectedEvent: Dispatch> + setAdditionalRelayUrls: Dispatch> +}) { + const { t } = useTranslation() + const { relayUrls } = useCurrentRelays() + const { relaySets, favoriteRelays } = useFavoriteRelays() + const [postTargetItems, setPostTargetItems] = useState([]) + const parentEventSeenOnRelays = useMemo(() => { + if (!parentEvent || !isProtectedEvent(parentEvent)) { + return [] + } + return client.getSeenEventRelayUrls(parentEvent.id) + }, [parentEvent]) + const selectableRelays = useMemo(() => { + return Array.from(new Set(parentEventSeenOnRelays.concat(relayUrls).concat(favoriteRelays))) + }, [parentEventSeenOnRelays, relayUrls, favoriteRelays]) + const description = useMemo(() => { + if (postTargetItems.length === 0) { + return t('No relays selected') + } + if (postTargetItems.length === 1) { + const item = postTargetItems[0] + if (item.type === 'writeRelays') { + return t('Write relays') + } + if (item.type === 'relay') { + return simplifyUrl(item.url) + } + if (item.type === 'relaySet') { + return item.urls.length > 1 + ? t('{{count}} relays', { count: item.urls.length }) + : simplifyUrl(item.urls[0]) + } + } + const hasWriteRelays = postTargetItems.some((item) => item.type === 'writeRelays') + const relayCount = postTargetItems.reduce((count, item) => { + if (item.type === 'relay') { + return count + 1 + } + if (item.type === 'relaySet') { + return count + item.urls.length + } + return count + }, 0) + if (hasWriteRelays) { + return t('Write relays and {{count}} other relays', { count: relayCount }) + } + return t('{{count}} relays', { count: relayCount }) + }, [postTargetItems]) + + useEffect(() => { + if (openFrom && openFrom.length) { + setPostTargetItems(Array.from(new Set(openFrom)).map((url) => ({ type: 'relay', url }))) + return + } + if (parentEventSeenOnRelays && parentEventSeenOnRelays.length) { + setPostTargetItems(parentEventSeenOnRelays.map((url) => ({ type: 'relay', url }))) + return + } + setPostTargetItems([{ type: 'writeRelays' }]) + }, [openFrom, parentEventSeenOnRelays]) + + useEffect(() => { + const isProtectedEvent = postTargetItems.every((item) => item.type !== 'writeRelays') + const relayUrls = postTargetItems.flatMap((item) => { + if (item.type === 'relay') { + return [item.url] + } + if (item.type === 'relaySet') { + return item.urls + } + return [] + }) + + setIsProtectedEvent(isProtectedEvent) + setAdditionalRelayUrls(relayUrls) + }, [postTargetItems]) + + const handleWriteRelaysCheckedChange = useCallback((checked: boolean) => { + if (checked) { + setPostTargetItems((prev) => [...prev, { type: 'writeRelays' }]) + } else { + setPostTargetItems((prev) => prev.filter((item) => item.type !== 'writeRelays')) + } + }, []) + + const handleRelayCheckedChange = useCallback((checked: boolean, url: string) => { + if (checked) { + setPostTargetItems((prev) => [...prev, { type: 'relay', url }]) + } else { + setPostTargetItems((prev) => + prev.filter((item) => !(item.type === 'relay' && item.url === url)) + ) + } + }, []) + + const handleRelaySetCheckedChange = useCallback( + (checked: boolean, id: string, urls: string[]) => { + if (checked) { + setPostTargetItems((prev) => [...prev, { type: 'relaySet', id, urls }]) + } else { + setPostTargetItems((prev) => + prev.filter((item) => !(item.type === 'relaySet' && item.id === id)) + ) + } + }, + [] + ) + + return ( + +
+ {t('Post to')} + + + +
+ + item.type === 'writeRelays')} + onSelect={(e) => e.preventDefault()} + onCheckedChange={handleWriteRelaysCheckedChange} + > + {t('Write relays')} + + {relaySets.length > 0 && ( + <> + + {relaySets + .filter(({ relayUrls }) => relayUrls.length) + .map(({ id, name, relayUrls }) => ( + item.type === 'relaySet' && item.id === id + )} + onSelect={(e) => e.preventDefault()} + onCheckedChange={(checked) => handleRelaySetCheckedChange(checked, id, relayUrls)} + > +
+ {name} ({relayUrls.length}) +
+
+ ))} + + )} + {selectableRelays.length > 0 && ( + <> + + {selectableRelays.map((url) => ( + item.type === 'relay' && item.url === url)} + onSelect={(e) => e.preventDefault()} + onCheckedChange={(checked) => handleRelayCheckedChange(checked, url)} + className="flex items-center gap-2" + > + +
{simplifyUrl(url)}
+
+ ))} + + )} +
+
+ ) +} diff --git a/src/components/PostEditor/SendOnlyToSwitch.tsx b/src/components/PostEditor/SendOnlyToSwitch.tsx deleted file mode 100644 index ddad7d1d..00000000 --- a/src/components/PostEditor/SendOnlyToSwitch.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Label } from '@/components/ui/label' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { Switch } from '@/components/ui/switch' -import { isProtectedEvent } from '@/lib/event' -import { simplifyUrl } from '@/lib/url' -import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' -import client from '@/services/client.service' -import { Info } from 'lucide-react' -import { Event } from 'nostr-tools' -import { Dispatch, SetStateAction, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' - -export default function SendOnlyToSwitch({ - parentEvent, - specifiedRelayUrls, - setSpecifiedRelayUrls, - openFrom -}: { - parentEvent?: Event - specifiedRelayUrls?: string[] - setSpecifiedRelayUrls: Dispatch> - openFrom?: string[] -}) { - const { t } = useTranslation() - const { currentRelayUrls } = useCurrentRelays() - const [urls, setUrls] = useState([]) - - useEffect(() => { - if (openFrom?.length) { - setUrls(openFrom) - setSpecifiedRelayUrls(openFrom) - return - } - if (!parentEvent) { - setUrls(currentRelayUrls) - return - } - const isProtected = isProtectedEvent(parentEvent) - const seenOn = client.getSeenEventRelayUrls(parentEvent.id) - if (isProtected && seenOn.length) { - setSpecifiedRelayUrls(seenOn) - setUrls(seenOn) - } else { - setUrls(currentRelayUrls) - } - }, [parentEvent, currentRelayUrls, openFrom]) - - if (!urls.length) return null - - return ( -
-
- - {urls.length > 1 && ( - - - - - - {urls.map((url) => ( -
{simplifyUrl(url)}
- ))} -
-
- )} -
- setSpecifiedRelayUrls(checked ? urls : undefined)} - /> -
- ) -} diff --git a/src/components/Relay/index.tsx b/src/components/Relay/index.tsx index 2fa9bb8f..9791367c 100644 --- a/src/components/Relay/index.tsx +++ b/src/components/Relay/index.tsx @@ -3,17 +3,28 @@ import RelayInfo from '@/components/RelayInfo' import SearchInput from '@/components/SearchInput' import { useFetchRelayInfo } from '@/hooks' import { normalizeUrl } from '@/lib/url' +import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import NotFound from '../NotFound' export default function Relay({ url, className }: { url?: string; className?: string }) { const { t } = useTranslation() + const { addRelayUrls, removeRelayUrls } = useCurrentRelays() const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url]) const { relayInfo } = useFetchRelayInfo(normalizedUrl) const [searchInput, setSearchInput] = useState('') const [debouncedInput, setDebouncedInput] = useState(searchInput) + useEffect(() => { + if (normalizedUrl) { + addRelayUrls([normalizedUrl]) + return () => { + removeRelayUrls([normalizedUrl]) + } + } + }, [normalizedUrl]) + useEffect(() => { const handler = setTimeout(() => { setDebouncedInput(searchInput) diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index f437369c..8b7bf875 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -415,6 +415,10 @@ export default { 'Failed to review': 'فشل في المراجعة', 'Write a review and pick a star rating': 'اكتب مراجعة واختر تقييماً بالنجوم', Submit: 'إرسال', - 'Reviews for {{relay}}': 'مراجعات لـ {{relay}}' + 'Reviews for {{relay}}': 'مراجعات لـ {{relay}}', + 'No relays selected': 'لم يتم اختيار أي مرحل', + 'Post to': 'نشر إلى', + 'Write relays and {{count}} other relays': 'مرحلات الكتابة و {{count}} مرحل آخر', + '{{count}} relays': '{{count}} ريلايات' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index c0a401a9..a18dfbb6 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -427,6 +427,10 @@ export default { 'Write a review and pick a star rating': 'Schreiben Sie eine Bewertung und wählen Sie eine Sternebewertung', Submit: 'Absenden', - 'Reviews for {{relay}}': 'Bewertungen für {{relay}}' + 'Reviews for {{relay}}': 'Bewertungen für {{relay}}', + 'No relays selected': 'Keine Relays ausgewählt', + 'Post to': 'Posten an', + 'Write relays and {{count}} other relays': 'Schreib-Relays und {{count}} andere Relays', + '{{count}} relays': '{{count}} Relays' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 49eead03..010237f7 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -414,6 +414,10 @@ export default { 'Failed to review': 'Failed to review', 'Write a review and pick a star rating': 'Write a review and pick a star rating', Submit: 'Submit', - 'Reviews for {{relay}}': 'Reviews for {{relay}}' + 'Reviews for {{relay}}': 'Reviews for {{relay}}', + 'No relays selected': 'No relays selected', + 'Post to': 'Post to', + 'Write relays and {{count}} other relays': 'Write relays and {{count}} other relays', + '{{count}} relays': '{{count}} relays' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 0c438b71..ff1882b8 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -422,6 +422,10 @@ export default { 'Write a review and pick a star rating': 'Escriba una reseña y elija una calificación de estrellas', Submit: 'Enviar', - 'Reviews for {{relay}}': 'Reseñas para {{relay}}' + 'Reviews for {{relay}}': 'Reseñas para {{relay}}', + 'No relays selected': 'No hay relés seleccionados', + 'Post to': 'Publicar en', + 'Write relays and {{count}} other relays': 'Relés de escritura y {{count}} otros relés', + '{{count}} relays': '{{count}} relés' } } diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index c0a98210..5bc57451 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -417,6 +417,10 @@ export default { 'Failed to review': 'نقد ناموفق', 'Write a review and pick a star rating': 'نقدی بنویسید و امتیاز ستاره‌ای انتخاب کنید', Submit: 'ارسال', - 'Reviews for {{relay}}': 'نقدها برای {{relay}}' + 'Reviews for {{relay}}': 'نقدها برای {{relay}}', + 'No relays selected': 'هیچ رله‌ای انتخاب نشده', + 'Post to': 'پست کردن به', + 'Write relays and {{count}} other relays': 'رله‌های نوشتن و {{count}} رله دیگر', + '{{count}} relays': '{{count}} رله' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index c6804405..36e7fe94 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -426,6 +426,10 @@ export default { 'Failed to review': 'Échec de l’avis', 'Write a review and pick a star rating': 'Écrivez un avis et choisissez une note en étoiles', Submit: 'Soumettre', - 'Reviews for {{relay}}': 'Avis pour {{relay}}' + 'Reviews for {{relay}}': 'Avis pour {{relay}}', + 'No relays selected': 'Aucun relais sélectionné', + 'Post to': 'Publier sur', + 'Write relays and {{count}} other relays': 'Relais d’écriture et {{count}} autres relais', + '{{count}} relays': '{{count}} relais' } } diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts index 4fb3f97b..618cf2cf 100644 --- a/src/i18n/locales/hi.ts +++ b/src/i18n/locales/hi.ts @@ -419,6 +419,10 @@ export default { 'Failed to review': 'समीक्षा असफल', 'Write a review and pick a star rating': 'एक समीक्षा लिखें और स्टार रेटिंग चुनें', Submit: 'सबमिट करें', - 'Reviews for {{relay}}': '{{relay}} के लिए समीक्षाएं' + 'Reviews for {{relay}}': '{{relay}} के लिए समीक्षाएं', + 'No relays selected': 'कोई रिले चयनित नहीं', + 'Post to': 'पोस्ट करें', + 'Write relays and {{count}} other relays': 'राइट रिले और {{count}} अन्य रिले', + '{{count}} relays': '{{count}} रिले' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index 9cc41460..81a99733 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -422,6 +422,10 @@ export default { 'Write a review and pick a star rating': 'Scrivi una recensione e scegli una valutazione a stelle', Submit: 'Invia', - 'Reviews for {{relay}}': 'Recensioni per {{relay}}' + 'Reviews for {{relay}}': 'Recensioni per {{relay}}', + 'No relays selected': 'Nessun relay selezionato', + 'Post to': 'Pubblica su', + 'Write relays and {{count}} other relays': 'Relay di scrittura e {{count}} altri relay', + '{{count}} relays': '{{count}} relay' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 4fde5c52..732ec188 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -418,6 +418,10 @@ export default { 'Failed to review': 'レビュー失敗', 'Write a review and pick a star rating': 'レビューを書いて星評価を選択してください', Submit: '送信', - 'Reviews for {{relay}}': '{{relay}} のレビュー' + 'Reviews for {{relay}}': '{{relay}} のレビュー', + 'No relays selected': 'リレーが選択されていません', + 'Post to': '投稿先', + 'Write relays and {{count}} other relays': '書き込みリレーと他の {{count}} 個のリレー', + '{{count}} relays': '{{count}} 個のリレー' } } diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 222d43c5..9c34bc77 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -418,6 +418,10 @@ export default { 'Failed to review': '리뷰 실패', 'Write a review and pick a star rating': '리뷰를 작성하고 별점을 선택하세요', Submit: '제출', - 'Reviews for {{relay}}': '{{relay}}에 대한 리뷰' + 'Reviews for {{relay}}': '{{relay}}에 대한 리뷰', + 'No relays selected': '선택된 릴레이가 없습니다', + 'Post to': '게시 대상', + 'Write relays and {{count}} other relays': '쓰기 릴레이 및 기타 {{count}}개 릴레이', + '{{count}} relays': '{{count}}개 릴레이' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 286ec7ae..d13c0f87 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -422,6 +422,10 @@ export default { 'Failed to review': 'Błąd opinii', 'Write a review and pick a star rating': 'Napisz opinię i wybierz ocenę gwiazdkową', Submit: 'Prześlij', - 'Reviews for {{relay}}': 'Opinie o {{relay}}' + 'Reviews for {{relay}}': 'Opinie o {{relay}}', + 'No relays selected': 'Nie wybrano przekaźników', + 'Post to': 'Opublikuj na', + 'Write relays and {{count}} other relays': 'Przekaźniki zapisu i {{count}} innych przekaźników', + '{{count}} relays': '{{count}} przekaźników' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 584139fd..0b5701c8 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -419,6 +419,10 @@ export default { 'Write a review and pick a star rating': 'Escreva uma avaliação e escolha uma classificação por estrelas', Submit: 'Enviar', - 'Reviews for {{relay}}': 'Avaliações para {{relay}}' + 'Reviews for {{relay}}': 'Avaliações para {{relay}}', + 'No relays selected': 'Nenhum relay selecionado', + 'Post to': 'Postar para', + 'Write relays and {{count}} other relays': 'Relays de escrita e {{count}} outros relays', + '{{count}} relays': '{{count}} relays' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index 6ebf6888..d37ea540 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -422,6 +422,10 @@ export default { 'Write a review and pick a star rating': 'Escreva uma avaliação e escolha uma classificação por estrelas', Submit: 'Enviar', - 'Reviews for {{relay}}': 'Avaliações para {{relay}}' + 'Reviews for {{relay}}': 'Avaliações para {{relay}}', + 'No relays selected': 'Nenhum relay selecionado', + 'Post to': 'Publicar para', + 'Write relays and {{count}} other relays': 'Relays de escrita e {{count}} outros relays', + '{{count}} relays': '{{count}} relays' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 33c260aa..d8938847 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -423,6 +423,11 @@ export default { 'Failed to review': 'Ошибка отзыва', 'Write a review and pick a star rating': 'Напишите отзыв и выберите звездный рейтинг', Submit: 'Отправить', - 'Reviews for {{relay}}': 'Отзывы для {{relay}}' + 'Reviews for {{relay}}': 'Отзывы для {{relay}}', + 'No relays selected': 'Ретрансляторы не выбраны', + 'Post to': 'Опубликовать в', + 'Write relays and {{count}} other relays': + 'Ретрансляторы записи и {{count}} других ретрансляторов', + '{{count}} relays': '{{count}} ретрансляторов' } } diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index 385dea52..b724fefb 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -413,6 +413,10 @@ export default { 'Failed to review': 'รีวิวล้มเหลว', 'Write a review and pick a star rating': 'เขียนรีวิวและเลือกคะแนนดาว', Submit: 'ส่ง', - 'Reviews for {{relay}}': 'รีวิวสำหรับ {{relay}}' + 'Reviews for {{relay}}': 'รีวิวสำหรับ {{relay}}', + 'No relays selected': 'ไม่ได้เลือกรีเลย์', + 'Post to': 'โพสต์ไปยัง', + 'Write relays and {{count}} other relays': 'รีเลย์เขียนและรีเลย์อื่น ๆ {{count}} ตัว', + '{{count}} relays': 'รีเลย์ {{count}} ตัว' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index e8d1c8c0..868ea62d 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -411,6 +411,10 @@ export default { 'Failed to review': '评价失败', 'Write a review and pick a star rating': '写下评价并选择星级评分', Submit: '提交', - 'Reviews for {{relay}}': '关于 {{relay}} 的评价' + 'Reviews for {{relay}}': '关于 {{relay}} 的评价', + 'No relays selected': '未选择服务器', + 'Post to': '发布到', + 'Write relays and {{count}} other relays': '写服务器和其他 {{count}} 个服务器', + '{{count}} relays': '{{count}} 个服务器' } } diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index 25d2a844..5af33a59 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -5,6 +5,7 @@ import RelayInfo from '@/components/RelayInfo' import { Button } from '@/components/ui/button' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import { toSearch } from '@/lib/link' +import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useFeed } from '@/providers/FeedProvider' import { useNostr } from '@/providers/NostrProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' @@ -26,6 +27,7 @@ import RelaysFeed from './RelaysFeed' const NoteListPage = forwardRef((_, ref) => { const { t } = useTranslation() + const { addRelayUrls, removeRelayUrls } = useCurrentRelays() const layoutRef = useRef(null) const { pubkey, checkLogin } = useNostr() const { feedInfo, relayUrls, isReady } = useFeed() @@ -38,6 +40,15 @@ const NoteListPage = forwardRef((_, ref) => { } }, [JSON.stringify(relayUrls), feedInfo]) + useEffect(() => { + if (relayUrls.length) { + addRelayUrls(relayUrls) + return () => { + removeRelayUrls(relayUrls) + } + } + }, [relayUrls]) + let content: React.ReactNode = null if (!isReady) { content =
{t('loading...')}
diff --git a/src/pages/primary/RelayPage/index.tsx b/src/pages/primary/RelayPage/index.tsx index cb14e349..523e7da5 100644 --- a/src/pages/primary/RelayPage/index.tsx +++ b/src/pages/primary/RelayPage/index.tsx @@ -1,20 +1,12 @@ import Relay from '@/components/Relay' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import { normalizeUrl, simplifyUrl } from '@/lib/url' -import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { Server } from 'lucide-react' -import { forwardRef, useEffect, useMemo } from 'react' +import { forwardRef, useMemo } from 'react' const RelayPage = forwardRef(({ url }: { url?: string }, ref) => { - const { setTemporaryRelayUrls } = useCurrentRelays() const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url]) - useEffect(() => { - if (normalizedUrl) { - setTemporaryRelayUrls([normalizedUrl]) - } - }, [normalizedUrl]) - return ( void + relayUrls: string[] + addRelayUrls: (urls: string[]) => void + removeRelayUrls: (urls: string[]) => void } const CurrentRelaysContext = createContext(undefined) @@ -18,17 +17,36 @@ export const useCurrentRelays = () => { } export function CurrentRelaysProvider({ children }: { children: React.ReactNode }) { - const { current } = usePrimaryPage() - const { relayUrls } = useFeed() - const [currentRelayUrls, setCurrentRelayUrls] = useState([]) - const [temporaryRelayUrls, setTemporaryRelayUrls] = useState([]) + const [relayRefCount, setRelayRefCount] = useState>({}) + const relayUrls = useMemo(() => Object.keys(relayRefCount), [relayRefCount]) - useEffect(() => { - setCurrentRelayUrls(current === 'relay' ? temporaryRelayUrls : relayUrls) - }, [temporaryRelayUrls, current, relayUrls]) + const addRelayUrls = useCallback((urls: string[]) => { + setRelayRefCount((prev) => { + const newCounts = { ...prev } + urls.forEach((url) => { + newCounts[url] = (newCounts[url] || 0) + 1 + }) + return newCounts + }) + }, []) + + const removeRelayUrls = useCallback((urls: string[]) => { + setRelayRefCount((prev) => { + const newCounts = { ...prev } + urls.forEach((url) => { + if (newCounts[url]) { + newCounts[url] -= 1 + if (newCounts[url] <= 0) { + delete newCounts[url] + } + } + }) + return newCounts + }) + }, []) return ( - + {children} ) diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 38608d42..ae268c48 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -94,41 +94,41 @@ class ClientService extends EventTarget { } } - const _additionalRelayUrls: string[] = additionalRelayUrls ?? [] - if (!specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist].includes(event.kind)) { - const mentions: string[] = [] - event.tags.forEach(([tagName, tagValue]) => { - if ( - ['p', 'P'].includes(tagName) && - !!tagValue && - isValidPubkey(tagValue) && - !mentions.includes(tagValue) - ) { - mentions.push(tagValue) - } - }) - if (mentions.length > 0) { - const relayLists = await this.fetchRelayLists(mentions) - relayLists.forEach((relayList) => { - _additionalRelayUrls.push(...relayList.read.slice(0, 4)) - }) - } - } - if ( - [ - kinds.RelayList, - kinds.Contacts, - ExtendedKind.FAVORITE_RELAYS, - ExtendedKind.BLOSSOM_SERVER_LIST - ].includes(event.kind) - ) { - _additionalRelayUrls.push(...BIG_RELAY_URLS) - } - let relays: string[] if (specifiedRelayUrls?.length) { relays = specifiedRelayUrls } else { + const _additionalRelayUrls: string[] = additionalRelayUrls ?? [] + if (!specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist].includes(event.kind)) { + const mentions: string[] = [] + event.tags.forEach(([tagName, tagValue]) => { + if ( + ['p', 'P'].includes(tagName) && + !!tagValue && + isValidPubkey(tagValue) && + !mentions.includes(tagValue) + ) { + mentions.push(tagValue) + } + }) + if (mentions.length > 0) { + const relayLists = await this.fetchRelayLists(mentions) + relayLists.forEach((relayList) => { + _additionalRelayUrls.push(...relayList.read.slice(0, 4)) + }) + } + } + if ( + [ + kinds.RelayList, + kinds.Contacts, + ExtendedKind.FAVORITE_RELAYS, + ExtendedKind.BLOSSOM_SERVER_LIST + ].includes(event.kind) + ) { + _additionalRelayUrls.push(...BIG_RELAY_URLS) + } + const relayList = await this.fetchRelayList(event.pubkey) relays = (relayList?.write.slice(0, 10) ?? []).concat( Array.from(new Set(_additionalRelayUrls)) ?? []