feat: add relay selector for posting

This commit is contained in:
codytseng
2025-09-21 21:43:09 +08:00
parent 2439150c6e
commit ec11d53fac
25 changed files with 418 additions and 201 deletions

View File

@@ -2,22 +2,12 @@ import { getNoteBech32Id, isProtectedEvent } from '@/lib/event'
import { toNjump } from '@/lib/link' import { toNjump } from '@/lib/link'
import { pubkeyToNpub } from '@/lib/pubkey' import { pubkeyToNpub } from '@/lib/pubkey'
import { simplifyUrl } from '@/lib/url' import { simplifyUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import { import { Bell, BellOff, Code, Copy, Link, SatelliteDish, Trash2, TriangleAlert } from 'lucide-react'
Bell,
BellOff,
Code,
Copy,
Link,
Mail,
SatelliteDish,
Server,
Trash2,
TriangleAlert
} from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -59,7 +49,11 @@ export function useMenuActions({
}: UseMenuActionsProps) { }: UseMenuActionsProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, attemptDelete } = useNostr() const { pubkey, attemptDelete } = useNostr()
const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays()
const { relaySets, favoriteRelays } = useFavoriteRelays() const { relaySets, favoriteRelays } = useFavoriteRelays()
const relayUrls = useMemo(() => {
return Array.from(new Set(currentBrowsingRelayUrls.concat(favoriteRelays)))
}, [currentBrowsingRelayUrls, favoriteRelays])
const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList() const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList()
const isMuted = useMemo(() => mutePubkeySet.has(event.pubkey), [mutePubkeySet, event]) const isMuted = useMemo(() => mutePubkeySet.has(event.pubkey), [mutePubkeySet, event])
@@ -67,12 +61,7 @@ export function useMenuActions({
const items = [] const items = []
if (pubkey && event.pubkey === pubkey) { if (pubkey && event.pubkey === pubkey) {
items.push({ items.push({
label: ( label: <div className="text-left"> {t('Write relays')}</div>,
<div className="flex items-center gap-2 w-full pl-1">
<Mail />
<div className="flex-1 truncate text-left">{t('Suitable Relays')}</div>
</div>
),
onClick: async () => { onClick: async () => {
closeDrawer() closeDrawer()
const relays = await client.determineTargetRelays(event) const relays = await client.determineTargetRelays(event)
@@ -97,12 +86,7 @@ export function useMenuActions({
...relaySets ...relaySets
.filter((set) => set.relayUrls.length) .filter((set) => set.relayUrls.length)
.map((set, index) => ({ .map((set, index) => ({
label: ( label: <div className="text-left truncate">{set.name}</div>,
<div className="flex items-center gap-2 w-full pl-1">
<Server />
<div className="flex-1 truncate text-left">{set.name}</div>
</div>
),
onClick: async () => { onClick: async () => {
closeDrawer() closeDrawer()
await client await client
@@ -126,9 +110,9 @@ export function useMenuActions({
) )
} }
if (favoriteRelays.length) { if (relayUrls.length) {
items.push( items.push(
...favoriteRelays.map((relay, index) => ({ ...relayUrls.map((relay, index) => ({
label: ( label: (
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<RelayIcon url={relay} /> <RelayIcon url={relay} />
@@ -159,7 +143,7 @@ export function useMenuActions({
} }
return items return items
}, [pubkey, favoriteRelays, relaySets]) }, [pubkey, relayUrls, relaySets])
const menuActions: MenuAction[] = useMemo(() => { const menuActions: MenuAction[] = useMemo(() => {
const actions: MenuAction[] = [ const actions: MenuAction[] = [

View File

@@ -14,15 +14,15 @@ import postEditorCache from '@/services/post-editor-cache.service'
import { TPollCreateData } from '@/types' import { TPollCreateData } from '@/types'
import { ImageUp, ListTodo, LoaderCircle, Settings, Smile, X } from 'lucide-react' import { ImageUp, ListTodo, LoaderCircle, Settings, Smile, X } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' 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 { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import EmojiPickerDialog from '../EmojiPickerDialog' import EmojiPickerDialog from '../EmojiPickerDialog'
import Mentions from './Mentions' import Mentions from './Mentions'
import PollEditor from './PollEditor' import PollEditor from './PollEditor'
import PostOptions from './PostOptions' import PostOptions from './PostOptions'
import PostRelaySelector from './PostRelaySelector'
import PostTextarea, { TPostTextareaHandle } from './PostTextarea' import PostTextarea, { TPostTextareaHandle } from './PostTextarea'
import SendOnlyToSwitch from './SendOnlyToSwitch'
import Uploader from './Uploader' import Uploader from './Uploader'
export default function PostContent({ export default function PostContent({
@@ -47,10 +47,11 @@ export default function PostContent({
>([]) >([])
const [showMoreOptions, setShowMoreOptions] = useState(false) const [showMoreOptions, setShowMoreOptions] = useState(false)
const [addClientTag, setAddClientTag] = useState(false) const [addClientTag, setAddClientTag] = useState(false)
const [specifiedRelayUrls, setSpecifiedRelayUrls] = useState<string[] | undefined>(undefined)
const [mentions, setMentions] = useState<string[]>([]) const [mentions, setMentions] = useState<string[]>([])
const [isNsfw, setIsNsfw] = useState(false) const [isNsfw, setIsNsfw] = useState(false)
const [isPoll, setIsPoll] = useState(false) const [isPoll, setIsPoll] = useState(false)
const [isProtectedEvent, setIsProtectedEvent] = useState(false)
const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>([])
const [pollCreateData, setPollCreateData] = useState<TPollCreateData>({ const [pollCreateData, setPollCreateData] = useState<TPollCreateData>({
isMultipleChoice: false, isMultipleChoice: false,
options: ['', ''], options: ['', ''],
@@ -59,12 +60,25 @@ export default function PostContent({
}) })
const [minPow, setMinPow] = useState(0) const [minPow, setMinPow] = useState(0)
const isFirstRender = useRef(true) const isFirstRender = useRef(true)
const canPost = const canPost = useMemo(() => {
return (
!!pubkey && !!pubkey &&
!!text && !!text &&
!posting && !posting &&
!uploadProgresses.length && !uploadProgresses.length &&
(!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) (!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) &&
(!isProtectedEvent || additionalRelayUrls.length > 0)
)
}, [
pubkey,
text,
posting,
uploadProgresses,
isPoll,
pollCreateData,
isProtectedEvent,
additionalRelayUrls
])
useEffect(() => { useEffect(() => {
if (isFirstRender.current) { if (isFirstRender.current) {
@@ -97,15 +111,7 @@ export default function PostContent({
addClientTag addClientTag
} }
) )
}, [ }, [defaultContent, parentEvent, isNsfw, isPoll, pollCreateData, addClientTag])
defaultContent,
parentEvent,
isNsfw,
isPoll,
pollCreateData,
specifiedRelayUrls,
addClientTag
])
const post = async (e?: React.MouseEvent) => { const post = async (e?: React.MouseEvent) => {
e?.stopPropagation() e?.stopPropagation()
@@ -118,24 +124,24 @@ export default function PostContent({
parentEvent && parentEvent.kind !== kinds.ShortTextNote parentEvent && parentEvent.kind !== kinds.ShortTextNote
? await createCommentDraftEvent(text, parentEvent, mentions, { ? await createCommentDraftEvent(text, parentEvent, mentions, {
addClientTag, addClientTag,
protectedEvent: !!specifiedRelayUrls, protectedEvent: isProtectedEvent,
isNsfw isNsfw
}) })
: isPoll : isPoll
? await createPollDraftEvent(pubkey, text, mentions, pollCreateData, { ? await createPollDraftEvent(pubkey!, text, mentions, pollCreateData, {
addClientTag, addClientTag,
isNsfw isNsfw
}) })
: await createShortTextNoteDraftEvent(text, mentions, { : await createShortTextNoteDraftEvent(text, mentions, {
parentEvent, parentEvent,
addClientTag, addClientTag,
protectedEvent: !!specifiedRelayUrls, protectedEvent: isProtectedEvent,
isNsfw isNsfw
}) })
const newEvent = await publish(draftEvent, { const newEvent = await publish(draftEvent, {
specifiedRelayUrls, specifiedRelayUrls: isProtectedEvent ? additionalRelayUrls : undefined,
additionalRelayUrls: isPoll ? pollCreateData.relays : [], additionalRelayUrls: isPoll ? pollCreateData.relays : additionalRelayUrls,
minPow minPow
}) })
postEditorCache.clearPostCache({ defaultContent, parentEvent }) postEditorCache.clearPostCache({ defaultContent, parentEvent })
@@ -233,10 +239,10 @@ export default function PostContent({
</div> </div>
))} ))}
{!isPoll && ( {!isPoll && (
<SendOnlyToSwitch <PostRelaySelector
setIsProtectedEvent={setIsProtectedEvent}
setAdditionalRelayUrls={setAdditionalRelayUrls}
parentEvent={parentEvent} parentEvent={parentEvent}
specifiedRelayUrls={specifiedRelayUrls}
setSpecifiedRelayUrls={setSpecifiedRelayUrls}
openFrom={openFrom} openFrom={openFrom}
/> />
)} )}

View File

@@ -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<SetStateAction<boolean>>
setAdditionalRelayUrls: Dispatch<SetStateAction<string[]>>
}) {
const { t } = useTranslation()
const { relayUrls } = useCurrentRelays()
const { relaySets, favoriteRelays } = useFavoriteRelays()
const [postTargetItems, setPostTargetItems] = useState<TPostTargetItem[]>([])
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 (
<DropdownMenu>
<div className="flex items-center gap-2">
{t('Post to')}
<DropdownMenuTrigger asChild>
<Button variant="outline" className="px-2">
{description}
</Button>
</DropdownMenuTrigger>
</div>
<DropdownMenuContent align="start" className="max-w-96">
<DropdownMenuCheckboxItem
checked={postTargetItems.some((item) => item.type === 'writeRelays')}
onSelect={(e) => e.preventDefault()}
onCheckedChange={handleWriteRelaysCheckedChange}
>
{t('Write relays')}
</DropdownMenuCheckboxItem>
{relaySets.length > 0 && (
<>
<DropdownMenuSeparator />
{relaySets
.filter(({ relayUrls }) => relayUrls.length)
.map(({ id, name, relayUrls }) => (
<DropdownMenuCheckboxItem
key={id}
checked={postTargetItems.some(
(item) => item.type === 'relaySet' && item.id === id
)}
onSelect={(e) => e.preventDefault()}
onCheckedChange={(checked) => handleRelaySetCheckedChange(checked, id, relayUrls)}
>
<div className="truncate">
{name} ({relayUrls.length})
</div>
</DropdownMenuCheckboxItem>
))}
</>
)}
{selectableRelays.length > 0 && (
<>
<DropdownMenuSeparator />
{selectableRelays.map((url) => (
<DropdownMenuCheckboxItem
key={url}
checked={postTargetItems.some((item) => item.type === 'relay' && item.url === url)}
onSelect={(e) => e.preventDefault()}
onCheckedChange={(checked) => handleRelayCheckedChange(checked, url)}
className="flex items-center gap-2"
>
<RelayIcon url={url} />
<div className="truncate">{simplifyUrl(url)}</div>
</DropdownMenuCheckboxItem>
))}
</>
)}
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -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<SetStateAction<string[] | undefined>>
openFrom?: string[]
}) {
const { t } = useTranslation()
const { currentRelayUrls } = useCurrentRelays()
const [urls, setUrls] = useState<string[]>([])
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 (
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 truncate">
<Label htmlFor="send-only-to-current-relays" className="truncate">
{urls.length === 1
? t('Send only to r', { r: simplifyUrl(urls[0]) })
: t('Send only to these relays')}
</Label>
{urls.length > 1 && (
<Popover>
<PopoverTrigger>
<Info size={14} />
</PopoverTrigger>
<PopoverContent className="w-fit text-sm">
{urls.map((url) => (
<div key={url}>{simplifyUrl(url)}</div>
))}
</PopoverContent>
</Popover>
)}
</div>
<Switch
className="shrink-0"
id="send-only-to-current-relays"
checked={!!specifiedRelayUrls}
onCheckedChange={(checked) => setSpecifiedRelayUrls(checked ? urls : undefined)}
/>
</div>
)
}

View File

@@ -3,17 +3,28 @@ import RelayInfo from '@/components/RelayInfo'
import SearchInput from '@/components/SearchInput' import SearchInput from '@/components/SearchInput'
import { useFetchRelayInfo } from '@/hooks' import { useFetchRelayInfo } from '@/hooks'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import NotFound from '../NotFound' import NotFound from '../NotFound'
export default function Relay({ url, className }: { url?: string; className?: string }) { export default function Relay({ url, className }: { url?: string; className?: string }) {
const { t } = useTranslation() const { t } = useTranslation()
const { addRelayUrls, removeRelayUrls } = useCurrentRelays()
const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url]) const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url])
const { relayInfo } = useFetchRelayInfo(normalizedUrl) const { relayInfo } = useFetchRelayInfo(normalizedUrl)
const [searchInput, setSearchInput] = useState('') const [searchInput, setSearchInput] = useState('')
const [debouncedInput, setDebouncedInput] = useState(searchInput) const [debouncedInput, setDebouncedInput] = useState(searchInput)
useEffect(() => {
if (normalizedUrl) {
addRelayUrls([normalizedUrl])
return () => {
removeRelayUrls([normalizedUrl])
}
}
}, [normalizedUrl])
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => { const handler = setTimeout(() => {
setDebouncedInput(searchInput) setDebouncedInput(searchInput)

View File

@@ -415,6 +415,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}}': 'مراجعات لـ {{relay}}' 'Reviews for {{relay}}': 'مراجعات لـ {{relay}}',
'No relays selected': 'لم يتم اختيار أي مرحل',
'Post to': 'نشر إلى',
'Write relays and {{count}} other relays': 'مرحلات الكتابة و {{count}} مرحل آخر',
'{{count}} relays': '{{count}} ريلايات'
} }
} }

View File

@@ -427,6 +427,10 @@ export default {
'Write a review and pick a star rating': 'Write a review and pick a star rating':
'Schreiben Sie eine Bewertung und wählen Sie eine Sternebewertung', 'Schreiben Sie eine Bewertung und wählen Sie eine Sternebewertung',
Submit: 'Absenden', 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'
} }
} }

View File

@@ -414,6 +414,10 @@ export default {
'Failed to review': 'Failed to review', 'Failed to review': 'Failed to review',
'Write a review and pick a star rating': 'Write a review and pick a star rating', 'Write a review and pick a star rating': 'Write a review and pick a star rating',
Submit: 'Submit', 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'
} }
} }

View File

@@ -422,6 +422,10 @@ export default {
'Write a review and pick a star rating': 'Write a review and pick a star rating':
'Escriba una reseña y elija una calificación de estrellas', 'Escriba una reseña y elija una calificación de estrellas',
Submit: 'Enviar', 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'
} }
} }

View File

@@ -417,6 +417,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}}': 'نقدها برای {{relay}}' 'Reviews for {{relay}}': 'نقدها برای {{relay}}',
'No relays selected': 'هیچ رله‌ای انتخاب نشده',
'Post to': 'پست کردن به',
'Write relays and {{count}} other relays': 'رله‌های نوشتن و {{count}} رله دیگر',
'{{count}} relays': '{{count}} رله'
} }
} }

View File

@@ -426,6 +426,10 @@ export default {
'Failed to review': 'Échec de lavis', 'Failed to review': 'Échec de lavis',
'Write a review and pick a star rating': 'Écrivez un avis et choisissez une note en étoiles', 'Write a review and pick a star rating': 'Écrivez un avis et choisissez une note en étoiles',
Submit: 'Soumettre', 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'
} }
} }

View File

@@ -419,6 +419,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}}': '{{relay}} के लिए समीक्षाएं' 'Reviews for {{relay}}': '{{relay}} के लिए समीक्षाएं',
'No relays selected': 'कोई रिले चयनित नहीं',
'Post to': 'पोस्ट करें',
'Write relays and {{count}} other relays': 'राइट रिले और {{count}} अन्य रिले',
'{{count}} relays': '{{count}} रिले'
} }
} }

View File

@@ -422,6 +422,10 @@ export default {
'Write a review and pick a star rating': 'Write a review and pick a star rating':
'Scrivi una recensione e scegli una valutazione a stelle', 'Scrivi una recensione e scegli una valutazione a stelle',
Submit: 'Invia', 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'
} }
} }

View File

@@ -418,6 +418,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}}': '{{relay}} のレビュー' 'Reviews for {{relay}}': '{{relay}} のレビュー',
'No relays selected': 'リレーが選択されていません',
'Post to': '投稿先',
'Write relays and {{count}} other relays': '書き込みリレーと他の {{count}} 個のリレー',
'{{count}} relays': '{{count}} 個のリレー'
} }
} }

View File

@@ -418,6 +418,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}}': '{{relay}}에 대한 리뷰' 'Reviews for {{relay}}': '{{relay}}에 대한 리뷰',
'No relays selected': '선택된 릴레이가 없습니다',
'Post to': '게시 대상',
'Write relays and {{count}} other relays': '쓰기 릴레이 및 기타 {{count}}개 릴레이',
'{{count}} relays': '{{count}}개 릴레이'
} }
} }

View File

@@ -422,6 +422,10 @@ export default {
'Failed to review': 'Błąd opinii', 'Failed to review': 'Błąd opinii',
'Write a review and pick a star rating': 'Napisz opinię i wybierz ocenę gwiazdkową', 'Write a review and pick a star rating': 'Napisz opinię i wybierz ocenę gwiazdkową',
Submit: 'Prześlij', 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'
} }
} }

View File

@@ -419,6 +419,10 @@ export default {
'Write a review and pick a star rating': 'Write a review and pick a star rating':
'Escreva uma avaliação e escolha uma classificação por estrelas', 'Escreva uma avaliação e escolha uma classificação por estrelas',
Submit: 'Enviar', 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'
} }
} }

View File

@@ -422,6 +422,10 @@ export default {
'Write a review and pick a star rating': 'Write a review and pick a star rating':
'Escreva uma avaliação e escolha uma classificação por estrelas', 'Escreva uma avaliação e escolha uma classificação por estrelas',
Submit: 'Enviar', 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'
} }
} }

View File

@@ -423,6 +423,11 @@ 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}}': 'Отзывы для {{relay}}' 'Reviews for {{relay}}': 'Отзывы для {{relay}}',
'No relays selected': 'Ретрансляторы не выбраны',
'Post to': 'Опубликовать в',
'Write relays and {{count}} other relays':
'Ретрансляторы записи и {{count}} других ретрансляторов',
'{{count}} relays': '{{count}} ретрансляторов'
} }
} }

View File

@@ -413,6 +413,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}}': 'รีวิวสำหรับ {{relay}}' 'Reviews for {{relay}}': 'รีวิวสำหรับ {{relay}}',
'No relays selected': 'ไม่ได้เลือกรีเลย์',
'Post to': 'โพสต์ไปยัง',
'Write relays and {{count}} other relays': 'รีเลย์เขียนและรีเลย์อื่น ๆ {{count}} ตัว',
'{{count}} relays': 'รีเลย์ {{count}} ตัว'
} }
} }

View File

@@ -411,6 +411,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}}': '关于 {{relay}} 的评价' 'Reviews for {{relay}}': '关于 {{relay}} 的评价',
'No relays selected': '未选择服务器',
'Post to': '发布到',
'Write relays and {{count}} other relays': '写服务器和其他 {{count}} 个服务器',
'{{count}} relays': '{{count}} 个服务器'
} }
} }

View File

@@ -5,6 +5,7 @@ import RelayInfo from '@/components/RelayInfo'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { toSearch } from '@/lib/link' import { toSearch } from '@/lib/link'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFeed } from '@/providers/FeedProvider' import { useFeed } from '@/providers/FeedProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
@@ -26,6 +27,7 @@ import RelaysFeed from './RelaysFeed'
const NoteListPage = forwardRef((_, ref) => { const NoteListPage = forwardRef((_, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { addRelayUrls, removeRelayUrls } = useCurrentRelays()
const layoutRef = useRef<TPageRef>(null) const layoutRef = useRef<TPageRef>(null)
const { pubkey, checkLogin } = useNostr() const { pubkey, checkLogin } = useNostr()
const { feedInfo, relayUrls, isReady } = useFeed() const { feedInfo, relayUrls, isReady } = useFeed()
@@ -38,6 +40,15 @@ const NoteListPage = forwardRef((_, ref) => {
} }
}, [JSON.stringify(relayUrls), feedInfo]) }, [JSON.stringify(relayUrls), feedInfo])
useEffect(() => {
if (relayUrls.length) {
addRelayUrls(relayUrls)
return () => {
removeRelayUrls(relayUrls)
}
}
}, [relayUrls])
let content: React.ReactNode = null let content: React.ReactNode = null
if (!isReady) { if (!isReady) {
content = <div className="text-center text-sm text-muted-foreground">{t('loading...')}</div> content = <div className="text-center text-sm text-muted-foreground">{t('loading...')}</div>

View File

@@ -1,20 +1,12 @@
import Relay from '@/components/Relay' import Relay from '@/components/Relay'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { normalizeUrl, simplifyUrl } from '@/lib/url' import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { Server } from 'lucide-react' import { Server } from 'lucide-react'
import { forwardRef, useEffect, useMemo } from 'react' import { forwardRef, useMemo } from 'react'
const RelayPage = forwardRef(({ url }: { url?: string }, ref) => { const RelayPage = forwardRef(({ url }: { url?: string }, ref) => {
const { setTemporaryRelayUrls } = useCurrentRelays()
const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url]) const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url])
useEffect(() => {
if (normalizedUrl) {
setTemporaryRelayUrls([normalizedUrl])
}
}, [normalizedUrl])
return ( return (
<PrimaryPageLayout <PrimaryPageLayout
pageName="relay" pageName="relay"

View File

@@ -1,10 +1,9 @@
import { usePrimaryPage } from '@/PageManager' import { createContext, useCallback, useContext, useMemo, useState } from 'react'
import { createContext, useContext, useEffect, useState } from 'react'
import { useFeed } from './FeedProvider'
type TCurrentRelaysContext = { type TCurrentRelaysContext = {
currentRelayUrls: string[] relayUrls: string[]
setTemporaryRelayUrls: (urls: string[]) => void addRelayUrls: (urls: string[]) => void
removeRelayUrls: (urls: string[]) => void
} }
const CurrentRelaysContext = createContext<TCurrentRelaysContext | undefined>(undefined) const CurrentRelaysContext = createContext<TCurrentRelaysContext | undefined>(undefined)
@@ -18,17 +17,36 @@ export const useCurrentRelays = () => {
} }
export function CurrentRelaysProvider({ children }: { children: React.ReactNode }) { export function CurrentRelaysProvider({ children }: { children: React.ReactNode }) {
const { current } = usePrimaryPage() const [relayRefCount, setRelayRefCount] = useState<Record<string, number>>({})
const { relayUrls } = useFeed() const relayUrls = useMemo(() => Object.keys(relayRefCount), [relayRefCount])
const [currentRelayUrls, setCurrentRelayUrls] = useState<string[]>([])
const [temporaryRelayUrls, setTemporaryRelayUrls] = useState<string[]>([])
useEffect(() => { const addRelayUrls = useCallback((urls: string[]) => {
setCurrentRelayUrls(current === 'relay' ? temporaryRelayUrls : relayUrls) setRelayRefCount((prev) => {
}, [temporaryRelayUrls, current, relayUrls]) 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 ( return (
<CurrentRelaysContext.Provider value={{ currentRelayUrls, setTemporaryRelayUrls }}> <CurrentRelaysContext.Provider value={{ relayUrls, addRelayUrls, removeRelayUrls }}>
{children} {children}
</CurrentRelaysContext.Provider> </CurrentRelaysContext.Provider>
) )

View File

@@ -94,6 +94,10 @@ class ClientService extends EventTarget {
} }
} }
let relays: string[]
if (specifiedRelayUrls?.length) {
relays = specifiedRelayUrls
} else {
const _additionalRelayUrls: string[] = additionalRelayUrls ?? [] const _additionalRelayUrls: string[] = additionalRelayUrls ?? []
if (!specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist].includes(event.kind)) { if (!specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist].includes(event.kind)) {
const mentions: string[] = [] const mentions: string[] = []
@@ -125,10 +129,6 @@ class ClientService extends EventTarget {
_additionalRelayUrls.push(...BIG_RELAY_URLS) _additionalRelayUrls.push(...BIG_RELAY_URLS)
} }
let relays: string[]
if (specifiedRelayUrls?.length) {
relays = specifiedRelayUrls
} else {
const relayList = await this.fetchRelayList(event.pubkey) const relayList = await this.fetchRelayList(event.pubkey)
relays = (relayList?.write.slice(0, 10) ?? []).concat( relays = (relayList?.write.slice(0, 10) ?? []).concat(
Array.from(new Set(_additionalRelayUrls)) ?? [] Array.from(new Set(_additionalRelayUrls)) ?? []