feat: allow changing default relays

This commit is contained in:
codytseng 2026-01-04 23:40:43 +08:00
parent 36959a1052
commit 53a67d8233
44 changed files with 356 additions and 92 deletions

View file

@ -0,0 +1,109 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { BIG_RELAY_URLS } from '@/constants'
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
import storage from '@/services/local-storage.service'
import { CircleX } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import InfoCard from '../InfoCard'
import RelayIcon from '../RelayIcon'
export default function DefaultRelaysSetting() {
const { t } = useTranslation()
const [relayUrls, setRelayUrls] = useState<string[]>(storage.getDefaultRelayUrls())
const [newRelayUrl, setNewRelayUrl] = useState('')
const [newRelayUrlError, setNewRelayUrlError] = useState<string | null>(null)
const removeRelayUrl = (url: string) => {
const normalizedUrl = normalizeUrl(url)
if (!normalizedUrl) return
const newUrls = relayUrls.filter((u) => u !== normalizedUrl)
setRelayUrls(newUrls)
storage.setDefaultRelayUrls(newUrls)
}
const saveNewRelayUrl = () => {
if (newRelayUrl === '') return
const normalizedUrl = normalizeUrl(newRelayUrl)
if (!normalizedUrl) {
return setNewRelayUrlError(t('Invalid relay URL'))
}
if (relayUrls.includes(normalizedUrl)) {
return setNewRelayUrlError(t('Relay already exists'))
}
if (!isWebsocketUrl(normalizedUrl)) {
return setNewRelayUrlError(t('invalid relay URL'))
}
const newUrls = [...relayUrls, normalizedUrl]
setRelayUrls(newUrls)
storage.setDefaultRelayUrls(newUrls)
setNewRelayUrl('')
}
const handleRelayUrlInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewRelayUrl(e.target.value)
setNewRelayUrlError(null)
}
const handleRelayUrlInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault()
saveNewRelayUrl()
}
}
const resetToDefault = () => {
setRelayUrls(BIG_RELAY_URLS)
storage.setDefaultRelayUrls(BIG_RELAY_URLS)
}
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-base font-normal">{t('Default relays')}</Label>
<Button variant="outline" size="sm" onClick={resetToDefault}>
{t('Reset to default')}
</Button>
</div>
<div className="text-xs text-muted-foreground">{t('Default relays description')}</div>
<InfoCard variant="alert" title={t('Default relays warning')} />
<div className="mt-1">
{relayUrls.map((url, index) => (
<RelayUrl key={index} url={url} onRemove={() => removeRelayUrl(url)} />
))}
</div>
<div className="mt-2 flex gap-2">
<Input
className={newRelayUrlError ? 'border-destructive' : ''}
placeholder={t('Add a new relay')}
value={newRelayUrl}
onKeyDown={handleRelayUrlInputKeyDown}
onChange={handleRelayUrlInputChange}
onBlur={saveNewRelayUrl}
/>
<Button onClick={saveNewRelayUrl}>{t('Add')}</Button>
</div>
{newRelayUrlError && <div className="text-xs text-destructive mt-1">{newRelayUrlError}</div>}
</div>
)
}
function RelayUrl({ url, onRemove }: { url: string; onRemove: () => void }) {
return (
<div className="flex items-center justify-between pl-1 pr-3 py-1">
<div className="flex gap-3 items-center flex-1 w-0">
<RelayIcon url={url} className="w-4 h-4" />
<div className="text-muted-foreground text-sm truncate">{url}</div>
</div>
<div className="shrink-0">
<CircleX
size={16}
onClick={onRemove}
className="text-muted-foreground hover:text-destructive cursor-pointer"
/>
</div>
</div>
)
}

View file

@ -15,8 +15,9 @@ import {
DrawerTitle, DrawerTitle,
DrawerTrigger DrawerTrigger
} from '@/components/ui/drawer' } from '@/components/ui/drawer'
import { BIG_RELAY_URLS } from '@/constants' import { buildATag } from '@/lib/draft-event'
import { getReplaceableEventIdentifier } from '@/lib/event' import { getReplaceableEventIdentifier } from '@/lib/event'
import { getDefaultRelayUrls } from '@/lib/relay'
import { tagNameEquals } from '@/lib/tag' import { tagNameEquals } from '@/lib/tag'
import { isWebsocketUrl, simplifyUrl } from '@/lib/url' import { isWebsocketUrl, simplifyUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
@ -29,7 +30,6 @@ import { Event, kinds } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import RelaySetCard from '../RelaySetCard' import RelaySetCard from '../RelaySetCard'
import { buildATag } from '@/lib/draft-event'
export default function PullRelaySetsButton() { export default function PullRelaySetsButton() {
const { t } = useTranslation() const { t } = useTranslation()
@ -94,7 +94,7 @@ function RemoteRelaySets({ close }: { close?: () => void }) {
const init = async () => { const init = async () => {
setInitialed(false) setInitialed(false)
const events = await client.fetchEvents( const events = await client.fetchEvents(
(relayList?.write ?? []).concat(BIG_RELAY_URLS).slice(0, 4), (relayList?.write ?? []).concat(getDefaultRelayUrls()).slice(0, 4),
{ {
kinds: [kinds.Relaysets], kinds: [kinds.Relaysets],
authors: [pubkey], authors: [pubkey],

View file

@ -1,6 +1,7 @@
import { BIG_RELAY_URLS, ExtendedKind, NOTIFICATION_LIST_STYLE } from '@/constants' import { ExtendedKind, NOTIFICATION_LIST_STYLE } from '@/constants'
import { useInfiniteScroll } from '@/hooks' import { useInfiniteScroll } from '@/hooks'
import { compareEvents } from '@/lib/event' import { compareEvents } from '@/lib/event'
import { getDefaultRelayUrls } from '@/lib/relay'
import { mergeTimelines } from '@/lib/timeline' import { mergeTimelines } from '@/lib/timeline'
import { isTouchDevice } from '@/lib/utils' import { isTouchDevice } from '@/lib/utils'
import { usePrimaryPage } from '@/PageManager' import { usePrimaryPage } from '@/PageManager'
@ -137,7 +138,7 @@ const NotificationList = forwardRef((_, ref) => {
const { closer, timelineKey } = await client.subscribeTimeline( const { closer, timelineKey } = await client.subscribeTimeline(
[ [
{ {
urls: relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS, urls: relayList.read.length > 0 ? relayList.read.slice(0, 5) : getDefaultRelayUrls(),
filter filter
} }
], ],

View file

@ -1,7 +1,6 @@
import Note from '@/components/Note' import Note from '@/components/Note'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { BIG_RELAY_URLS } from '@/constants'
import { import {
createCommentDraftEvent, createCommentDraftEvent,
createHighlightDraftEvent, createHighlightDraftEvent,
@ -9,6 +8,7 @@ import {
createShortTextNoteDraftEvent, createShortTextNoteDraftEvent,
deleteDraftEventCache deleteDraftEventCache
} from '@/lib/draft-event' } from '@/lib/draft-event'
import { getDefaultRelayUrls } from '@/lib/relay'
import { isTouchDevice } from '@/lib/utils' import { isTouchDevice } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import postEditorCache from '@/services/post-editor-cache.service' import postEditorCache from '@/services/post-editor-cache.service'
@ -146,7 +146,7 @@ export default function PostContent({
const _additionalRelayUrls = [...additionalRelayUrls] const _additionalRelayUrls = [...additionalRelayUrls]
if (parentStuff && typeof parentStuff === 'string') { if (parentStuff && typeof parentStuff === 'string') {
_additionalRelayUrls.push(...BIG_RELAY_URLS) _additionalRelayUrls.push(...getDefaultRelayUrls())
} }
const newEvent = await publish(draftEvent, { const newEvent = await publish(draftEvent, {

View file

@ -1,7 +1,8 @@
import KindFilter from '@/components/KindFilter' import KindFilter from '@/components/KindFilter'
import NoteList, { TNoteListRef } from '@/components/NoteList' import NoteList, { TNoteListRef } from '@/components/NoteList'
import Tabs from '@/components/Tabs' import Tabs from '@/components/Tabs'
import { BIG_RELAY_URLS, MAX_PINNED_NOTES, SEARCHABLE_RELAY_URLS } from '@/constants' import { MAX_PINNED_NOTES, SEARCHABLE_RELAY_URLS } from '@/constants'
import { getDefaultRelayUrls } from '@/lib/relay'
import { generateBech32IdFromETag } from '@/lib/tag' import { generateBech32IdFromETag } from '@/lib/tag'
import { isTouchDevice } from '@/lib/utils' import { isTouchDevice } from '@/lib/utils'
import { useKindFilter } from '@/providers/KindFilterProvider' import { useKindFilter } from '@/providers/KindFilterProvider'
@ -100,14 +101,14 @@ export default function ProfileFeed({
setSubRequests([ setSubRequests([
{ {
urls: myRelayList.write.concat(BIG_RELAY_URLS).slice(0, 5), urls: myRelayList.write.concat(getDefaultRelayUrls()).slice(0, 5),
filter: { filter: {
authors: [myPubkey], authors: [myPubkey],
'#p': [pubkey] '#p': [pubkey]
} }
}, },
{ {
urls: relayList.write.concat(BIG_RELAY_URLS).slice(0, 5), urls: relayList.write.concat(getDefaultRelayUrls()).slice(0, 5),
filter: { filter: {
authors: [pubkey], authors: [pubkey],
'#p': [myPubkey] '#p': [myPubkey]
@ -134,7 +135,7 @@ export default function ProfileFeed({
} else { } else {
setSubRequests([ setSubRequests([
{ {
urls: relayList.write.concat(BIG_RELAY_URLS).slice(0, 8), urls: relayList.write.concat(getDefaultRelayUrls()).slice(0, 8),
filter: { filter: {
authors: [pubkey] authors: [pubkey]
} }

View file

@ -1,6 +1,7 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { useStuff } from '@/hooks/useStuff' import { useStuff } from '@/hooks/useStuff'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { getDefaultRelayUrls } from '@/lib/relay'
import client from '@/services/client.service' import client from '@/services/client.service'
import { TFeedSubRequest } from '@/types' import { TFeedSubRequest } from '@/types'
import { Event, Filter, kinds } from 'nostr-tools' import { Event, Filter, kinds } from 'nostr-tools'
@ -13,7 +14,7 @@ export default function QuoteList({ stuff }: { stuff: Event | string }) {
useEffect(() => { useEffect(() => {
async function init() { async function init() {
const relaySet = new Set(BIG_RELAY_URLS) const relaySet = new Set(getDefaultRelayUrls())
const filters: Filter[] = [] const filters: Filter[] = []
if (event) { if (event) {
const relayList = await client.fetchRelayList(event.pubkey) const relayList = await client.fetchRelayList(event.pubkey)

View file

@ -7,10 +7,11 @@ import {
CarouselNext, CarouselNext,
CarouselPrevious CarouselPrevious
} from '@/components/ui/carousel' } from '@/components/ui/carousel'
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { compareEvents } from '@/lib/event' import { compareEvents } from '@/lib/event'
import { getStarsFromRelayReviewEvent } from '@/lib/event-metadata' import { getStarsFromRelayReviewEvent } from '@/lib/event-metadata'
import { toRelayReviews } from '@/lib/link' import { toRelayReviews } from '@/lib/link'
import { getDefaultRelayUrls } from '@/lib/relay'
import { cn, isTouchDevice } from '@/lib/utils' import { cn, isTouchDevice } from '@/lib/utils'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
@ -60,7 +61,7 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string })
if (pubkey) { if (pubkey) {
filters.push({ kinds: [ExtendedKind.RELAY_REVIEW], authors: [pubkey], '#d': [relayUrl] }) filters.push({ kinds: [ExtendedKind.RELAY_REVIEW], authors: [pubkey], '#d': [relayUrl] })
} }
const events = await client.fetchEvents([relayUrl, ...BIG_RELAY_URLS], filters, { const events = await client.fetchEvents([relayUrl, ...getDefaultRelayUrls()], filters, {
cache: true cache: true
}) })

View file

@ -1,4 +1,5 @@
import { BIG_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants' import { SEARCHABLE_RELAY_URLS } from '@/constants'
import { getDefaultRelayUrls } from '@/lib/relay'
import { TSearchParams } from '@/types' import { TSearchParams } from '@/types'
import NormalFeed from '../NormalFeed' import NormalFeed from '../NormalFeed'
import Profile from '../Profile' import Profile from '../Profile'
@ -27,7 +28,7 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
if (searchParams.type === 'hashtag') { if (searchParams.type === 'hashtag') {
return ( return (
<NormalFeed <NormalFeed
subRequests={[{ urls: BIG_RELAY_URLS, filter: { '#t': [searchParams.search] } }]} subRequests={[{ urls: getDefaultRelayUrls(), filter: { '#t': [searchParams.search] } }]}
showRelayCloseReason showRelayCloseReason
/> />
) )

View file

@ -1,12 +1,13 @@
import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer' import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer'
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover' import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
import { BIG_RELAY_URLS, LONG_PRESS_THRESHOLD } from '@/constants' import { LONG_PRESS_THRESHOLD } from '@/constants'
import { useStuff } from '@/hooks/useStuff' import { useStuff } from '@/hooks/useStuff'
import { useStuffStatsById } from '@/hooks/useStuffStatsById' import { useStuffStatsById } from '@/hooks/useStuffStatsById'
import { import {
createExternalContentReactionDraftEvent, createExternalContentReactionDraftEvent,
createReactionDraftEvent createReactionDraftEvent
} from '@/lib/draft-event' } from '@/lib/draft-event'
import { getDefaultRelayUrls } from '@/lib/relay'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
@ -79,7 +80,7 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
const reaction = event const reaction = event
? createReactionDraftEvent(event, emoji) ? createReactionDraftEvent(event, emoji)
: createExternalContentReactionDraftEvent(externalContent, emoji) : createExternalContentReactionDraftEvent(externalContent, emoji)
const seenOn = event ? client.getSeenEventRelayUrls(event.id) : BIG_RELAY_URLS const seenOn = event ? client.getSeenEventRelayUrls(event.id) : getDefaultRelayUrls()
const evt = await publish(reaction, { additionalRelayUrls: seenOn }) const evt = await publish(reaction, { additionalRelayUrls: seenOn })
stuffStatsService.updateStuffStatsByEvents([evt]) stuffStatsService.updateStuffStatsByEvents([evt])
} catch (error) { } catch (error) {

View file

@ -1,11 +1,11 @@
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { BIG_RELAY_URLS } from '@/constants'
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
import { useStuff } from '@/hooks/useStuff' import { useStuff } from '@/hooks/useStuff'
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
import { import {
createExternalContentReactionDraftEvent, createExternalContentReactionDraftEvent,
createReactionDraftEvent createReactionDraftEvent
} from '@/lib/draft-event' } from '@/lib/draft-event'
import { getDefaultRelayUrls } from '@/lib/relay'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
@ -53,7 +53,7 @@ export default function Likes({ stuff }: { stuff: Event | string }) {
const reaction = event const reaction = event
? createReactionDraftEvent(event, emoji) ? createReactionDraftEvent(event, emoji)
: createExternalContentReactionDraftEvent(externalContent, emoji) : createExternalContentReactionDraftEvent(externalContent, emoji)
const seenOn = event ? client.getSeenEventRelayUrls(event.id) : BIG_RELAY_URLS const seenOn = event ? client.getSeenEventRelayUrls(event.id) : getDefaultRelayUrls()
const evt = await publish(reaction, { additionalRelayUrls: seenOn }) const evt = await publish(reaction, { additionalRelayUrls: seenOn })
stuffStatsService.updateStuffStatsByEvents([evt]) stuffStatsService.updateStuffStatsByEvents([evt])
} catch (error) { } catch (error) {

View file

@ -43,6 +43,7 @@ export const StorageKey = {
NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy', NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy',
MIN_TRUST_SCORE: 'minTrustScore', MIN_TRUST_SCORE: 'minTrustScore',
ENABLE_LIVE_FEED: 'enableLiveFeed', ENABLE_LIVE_FEED: 'enableLiveFeed',
DEFAULT_RELAY_URLS: 'defaultRelayUrls',
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', // deprecated HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', // deprecated
HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', // deprecated HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', // deprecated
HIDE_UNTRUSTED_NOTIFICATIONS: 'hideUntrustedNotifications', // deprecated HIDE_UNTRUSTED_NOTIFICATIONS: 'hideUntrustedNotifications', // deprecated

View file

@ -653,6 +653,11 @@ export default {
'trust-filter.trust-score-description': 'محسوبة بناءً على سمعة المستخدم والنسبة المئوية للشبكة الاجتماعية', 'trust-filter.trust-score-description': 'محسوبة بناءً على سمعة المستخدم والنسبة المئوية للشبكة الاجتماعية',
'Auto-load profile pictures': 'تحميل صور الملف الشخصي تلقائيًا', 'Auto-load profile pictures': 'تحميل صور الملف الشخصي تلقائيًا',
'Disable live feed': 'تعطيل التغذية المباشرة', 'Disable live feed': 'تعطيل التغذية المباشرة',
'Enable live feed': 'تفعيل التغذية المباشرة' 'Enable live feed': 'تفعيل التغذية المباشرة',
'Default relays': 'المرحلات الافتراضية',
'Reset to default': 'إعادة تعيين إلى الافتراضي',
'Default relays description': 'تُستخدم للاستعلام عن تكوينات المرحلات للمستخدمين الآخرين وكبديل احتياطي عندما لا يكون لدى المستخدمين مرحلات مكوّنة.',
'Default relays warning': 'تحذير: يرجى عدم تعديل هذه الإعدادات بشكل عشوائي، فقد يؤثر ذلك على تجربتك الأساسية.',
'Invalid relay URL': 'عنوان URL للمرحل غير صالح'
} }
} }

View file

@ -675,6 +675,11 @@ export default {
'Berechnet basierend auf Benutzerreputation und sozialem Netzwerk-Perzentil', 'Berechnet basierend auf Benutzerreputation und sozialem Netzwerk-Perzentil',
'Auto-load profile pictures': 'Profilbilder automatisch laden', 'Auto-load profile pictures': 'Profilbilder automatisch laden',
'Disable live feed': 'Live-Feed deaktivieren', 'Disable live feed': 'Live-Feed deaktivieren',
'Enable live feed': 'Live-Feed aktivieren' 'Enable live feed': 'Live-Feed aktivieren',
'Default relays': 'Standard-Relays',
'Reset to default': 'Auf Standard zurücksetzen',
'Default relays description': 'Werden verwendet, um die Relay-Konfigurationen anderer Benutzer abzufragen und als Fallback, wenn Benutzer keine Relays konfiguriert haben.',
'Default relays warning': 'Warnung: Ändern Sie diese Einstellungen nicht leichtfertig, da dies Ihre grundlegende Erfahrung beeinträchtigen kann.',
'Invalid relay URL': 'Ungültige Relay-URL'
} }
} }

View file

@ -658,6 +658,11 @@ export default {
'trust-filter.trust-score-description': 'Calculated based on user reputation and social network percentile', 'trust-filter.trust-score-description': 'Calculated based on user reputation and social network percentile',
'Auto-load profile pictures': 'Auto-load profile pictures', 'Auto-load profile pictures': 'Auto-load profile pictures',
'Disable live feed': 'Disable live feed', 'Disable live feed': 'Disable live feed',
'Enable live feed': 'Enable live feed' 'Enable live feed': 'Enable live feed',
'Default relays': 'Default relays',
'Reset to default': 'Reset to default',
'Default relays description': 'Used to query other users\' relay configurations and as a fallback when users have no relays configured.',
'Default relays warning': 'Warning: Please do not modify these settings casually, as it may affect your basic experience.',
'Invalid relay URL': 'Invalid relay URL'
} }
} }

View file

@ -669,6 +669,11 @@ export default {
'Calculado según la reputación del usuario y el percentil de la red social', 'Calculado según la reputación del usuario y el percentil de la red social',
'Auto-load profile pictures': 'Cargar imágenes de perfil automáticamente', 'Auto-load profile pictures': 'Cargar imágenes de perfil automáticamente',
'Disable live feed': 'Desactivar feed en vivo', 'Disable live feed': 'Desactivar feed en vivo',
'Enable live feed': 'Activar feed en vivo' 'Enable live feed': 'Activar feed en vivo',
'Default relays': 'Relés predeterminados',
'Reset to default': 'Restablecer valores predeterminados',
'Default relays description': 'Se utilizan para consultar las configuraciones de relés de otros usuarios y como respaldo cuando los usuarios no tienen relés configurados.',
'Default relays warning': 'Advertencia: No modifiques estas configuraciones a la ligera, ya que puede afectar tu experiencia básica.',
'Invalid relay URL': 'URL de relé no válida'
} }
} }

View file

@ -665,6 +665,11 @@ export default {
'بر اساس شهرت کاربر و صدک شبکه اجتماعی محاسبه می‌شود', 'بر اساس شهرت کاربر و صدک شبکه اجتماعی محاسبه می‌شود',
'Auto-load profile pictures': 'بارگذاری خودکار تصاویر پروفایل', 'Auto-load profile pictures': 'بارگذاری خودکار تصاویر پروفایل',
'Disable live feed': 'غیرفعال کردن فید زنده', 'Disable live feed': 'غیرفعال کردن فید زنده',
'Enable live feed': 'فعال کردن فید زنده' 'Enable live feed': 'فعال کردن فید زنده',
'Default relays': 'رله‌های پیش‌فرض',
'Reset to default': 'بازنشانی به پیش‌فرض',
'Default relays description': 'برای پرس‌وجو از پیکربندی‌های رله کاربران دیگر و به عنوان جایگزین زمانی که کاربران رله پیکربندی نکرده‌اند استفاده می‌شود.',
'Default relays warning': 'هشدار: لطفاً این تنظیمات را به صورت تصادفی تغییر ندهید، ممکن است بر تجربه اولیه شما تأثیر بگذارد.',
'Invalid relay URL': 'آدرس URL رله نامعتبر است'
} }
} }

View file

@ -673,6 +673,11 @@ export default {
"Calculé en fonction de la réputation et du réseau social de l'utilisateur", "Calculé en fonction de la réputation et du réseau social de l'utilisateur",
'Auto-load profile pictures': 'Charger les images de profil automatiquement', 'Auto-load profile pictures': 'Charger les images de profil automatiquement',
'Disable live feed': 'Désactiver le flux en direct', 'Disable live feed': 'Désactiver le flux en direct',
'Enable live feed': 'Activer le flux en direct' 'Enable live feed': 'Activer le flux en direct',
'Default relays': 'Relais par défaut',
'Reset to default': 'Réinitialiser par défaut',
'Default relays description': 'Utilisés pour interroger les configurations de relais d\'autres utilisateurs et comme solution de secours lorsque les utilisateurs n\'ont pas de relais configurés.',
'Default relays warning': 'Attention : Ne modifiez pas ces paramètres à la légère, car cela pourrait affecter votre expérience de base.',
'Invalid relay URL': 'URL de relais non valide'
} }
} }

View file

@ -665,6 +665,11 @@ export default {
'उपयोगकर्ता की प्रतिष्ठा और सामाजिक नेटवर्क प्रतिशतक के आधार पर गणना की गई', 'उपयोगकर्ता की प्रतिष्ठा और सामाजिक नेटवर्क प्रतिशतक के आधार पर गणना की गई',
'Auto-load profile pictures': 'प्रोफ़ाइल चित्र स्वतः लोड करें', 'Auto-load profile pictures': 'प्रोफ़ाइल चित्र स्वतः लोड करें',
'Disable live feed': 'लाइव फ़ीड अक्षम करें', 'Disable live feed': 'लाइव फ़ीड अक्षम करें',
'Enable live feed': 'लाइव फ़ीड सक्षम करें' 'Enable live feed': 'लाइव फ़ीड सक्षम करें',
'Default relays': 'डिफ़ॉल्ट रिले',
'Reset to default': 'डिफ़ॉल्ट पर रीसेट करें',
'Default relays description': 'अन्य उपयोगकर्ताओं के रिले कॉन्फ़िगरेशन की जांच करने के लिए उपयोग किया जाता है और जब उपयोगकर्ताओं के पास रिले कॉन्फ़िगर नहीं है तो फ़ॉलबैक के रूप में।',
'Default relays warning': 'चेतावनी: कृपया इन सेटिंग्स को बेतरतीब ढंग से संशोधित न करें, क्योंकि यह आपके बुनियादी अनुभव को प्रभावित कर सकता है।',
'Invalid relay URL': 'अमान्य रिले URL'
} }
} }

View file

@ -658,6 +658,11 @@ export default {
'A felhasználó hírneve és a közösségi hálózat percentilise alapján számítva', 'A felhasználó hírneve és a közösségi hálózat percentilise alapján számítva',
'Auto-load profile pictures': 'Profilképek automatikus betöltése', 'Auto-load profile pictures': 'Profilképek automatikus betöltése',
'Disable live feed': 'Élő hírfolyam letiltása', 'Disable live feed': 'Élő hírfolyam letiltása',
'Enable live feed': 'Élő hírfolyam engedélyezése' 'Enable live feed': 'Élő hírfolyam engedélyezése',
'Default relays': 'Alapértelmezett továbbítók',
'Reset to default': 'Visszaállítás alapértelmezettre',
'Default relays description': 'Más felhasználók továbbító konfigurációinak lekérdezésére használatos, és tartalékként szolgál, ha a felhasználóknak nincsenek továbbítóik beállítva.',
'Default relays warning': 'Figyelmeztetés: Ne módosítsa ezeket a beállításokat meggondolatlanul, mert ez befolyásolhatja az alapvető élményt.',
'Invalid relay URL': 'Érvénytelen továbbító URL'
} }
} }

View file

@ -669,6 +669,11 @@ export default {
"Calcolato in base alla reputazione dell'utente e al percentile del social network", "Calcolato in base alla reputazione dell'utente e al percentile del social network",
'Auto-load profile pictures': 'Caricamento automatico immagini di profilo', 'Auto-load profile pictures': 'Caricamento automatico immagini di profilo',
'Disable live feed': 'Disattiva feed live', 'Disable live feed': 'Disattiva feed live',
'Enable live feed': 'Attiva feed live' 'Enable live feed': 'Attiva feed live',
'Default relays': 'Relay predefiniti',
'Reset to default': 'Ripristina predefiniti',
'Default relays description': 'Utilizzati per interrogare le configurazioni dei relay di altri utenti e come fallback quando gli utenti non hanno relay configurati.',
'Default relays warning': 'Attenzione: Non modificare queste impostazioni alla leggera, potrebbe influire sull\'esperienza di base.',
'Invalid relay URL': 'URL relay non valido'
} }
} }

View file

@ -663,6 +663,11 @@ export default {
'ユーザーの評判とソーシャルネットワークに基づいて信頼度パーセンタイルを計算', 'ユーザーの評判とソーシャルネットワークに基づいて信頼度パーセンタイルを計算',
'Auto-load profile pictures': 'プロフィール画像を自動読み込み', 'Auto-load profile pictures': 'プロフィール画像を自動読み込み',
'Disable live feed': 'ライブフィードを無効にする', 'Disable live feed': 'ライブフィードを無効にする',
'Enable live feed': 'ライブフィードを有効にする' 'Enable live feed': 'ライブフィードを有効にする',
'Default relays': 'デフォルトリレー',
'Reset to default': 'デフォルトにリセット',
'Default relays description': '他のユーザーのリレー設定を照会するために使用され、ユーザーがリレーを設定していない場合のフォールバックとして機能します。',
'Default relays warning': '警告:これらの設定を無闇に変更しないでください。基本的な体験に影響を与える可能性があります。',
'Invalid relay URL': '無効なリレーURL'
} }
} }

View file

@ -658,6 +658,11 @@ export default {
'trust-filter.trust-score-description': '사용자의 평판과 소셜 네트워크를 기반으로 신뢰도 백분위수 계산', 'trust-filter.trust-score-description': '사용자의 평판과 소셜 네트워크를 기반으로 신뢰도 백분위수 계산',
'Auto-load profile pictures': '프로필 사진 자동 로드', 'Auto-load profile pictures': '프로필 사진 자동 로드',
'Disable live feed': '라이브 피드 비활성화', 'Disable live feed': '라이브 피드 비활성화',
'Enable live feed': '라이브 피드 활성화' 'Enable live feed': '라이브 피드 활성화',
'Default relays': '기본 릴레이',
'Reset to default': '기본값으로 재설정',
'Default relays description': '다른 사용자의 릴레이 구성을 조회하는 데 사용되며, 사용자가 릴레이를 구성하지 않은 경우 대체 수단으로 사용됩니다.',
'Default relays warning': '경고: 이러한 설정을 임의로 수정하지 마십시오. 기본 경험에 영향을 줄 수 있습니다.',
'Invalid relay URL': '유효하지 않은 릴레이 URL'
} }
} }

View file

@ -670,6 +670,11 @@ export default {
'Obliczany na podstawie reputacji użytkownika i percentyla sieci społecznościowej', 'Obliczany na podstawie reputacji użytkownika i percentyla sieci społecznościowej',
'Auto-load profile pictures': 'Automatyczne ładowanie zdjęć profilowych', 'Auto-load profile pictures': 'Automatyczne ładowanie zdjęć profilowych',
'Disable live feed': 'Wyłącz kanał na żywo', 'Disable live feed': 'Wyłącz kanał na żywo',
'Enable live feed': 'Włącz kanał na żywo' 'Enable live feed': 'Włącz kanał na żywo',
'Default relays': 'Domyślne przekaźniki',
'Reset to default': 'Przywróć domyślne',
'Default relays description': 'Używane do odpytywania konfiguracji przekaźników innych użytkowników i jako rozwiązanie awaryjne, gdy użytkownicy nie mają skonfigurowanych przekaźników.',
'Default relays warning': 'Ostrzeżenie: Nie modyfikuj tych ustawień pochopnie, może to wpłynąć na podstawowe doświadczenie.',
'Invalid relay URL': 'Nieprawidłowy adres URL przekaźnika'
} }
} }

View file

@ -666,6 +666,11 @@ export default {
'Calculado com base na reputação do usuário e no percentil da rede social', 'Calculado com base na reputação do usuário e no percentil da rede social',
'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente', 'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente',
'Disable live feed': 'Desativar feed ao vivo', 'Disable live feed': 'Desativar feed ao vivo',
'Enable live feed': 'Ativar feed ao vivo' 'Enable live feed': 'Ativar feed ao vivo',
'Default relays': 'Relays padrão',
'Reset to default': 'Redefinir para padrão',
'Default relays description': 'Usados para consultar as configurações de relays de outros usuários e como alternativa quando os usuários não têm relays configurados.',
'Default relays warning': 'Aviso: Não modifique essas configurações casualmente, pois pode afetar sua experiência básica.',
'Invalid relay URL': 'URL de relay inválida'
} }
} }

View file

@ -669,6 +669,11 @@ export default {
'Calculado com base na reputação do utilizador e no percentil da rede social', 'Calculado com base na reputação do utilizador e no percentil da rede social',
'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente', 'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente',
'Disable live feed': 'Desativar feed ao vivo', 'Disable live feed': 'Desativar feed ao vivo',
'Enable live feed': 'Ativar feed ao vivo' 'Enable live feed': 'Ativar feed ao vivo',
'Default relays': 'Relays predefinidos',
'Reset to default': 'Repor predefinições',
'Default relays description': 'Utilizados para consultar as configurações de relays de outros utilizadores e como alternativa quando os utilizadores não têm relays configurados.',
'Default relays warning': 'Aviso: Não modifique estas configurações casualmente, pois pode afetar a sua experiência básica.',
'Invalid relay URL': 'URL de relay inválido'
} }
} }

View file

@ -669,6 +669,11 @@ export default {
'Рассчитывается на основе репутации пользователя и процентиля социальной сети', 'Рассчитывается на основе репутации пользователя и процентиля социальной сети',
'Auto-load profile pictures': 'Автозагрузка аватаров', 'Auto-load profile pictures': 'Автозагрузка аватаров',
'Disable live feed': 'Отключить прямую трансляцию', 'Disable live feed': 'Отключить прямую трансляцию',
'Enable live feed': 'Включить прямую трансляцию' 'Enable live feed': 'Включить прямую трансляцию',
'Default relays': 'Реле по умолчанию',
'Reset to default': 'Сбросить по умолчанию',
'Default relays description': 'Используются для запроса конфигураций реле других пользователей и в качестве резервного варианта, когда у пользователей не настроены реле.',
'Default relays warning': 'Предупреждение: Не изменяйте эти настройки без необходимости, это может повлиять на базовый опыт использования.',
'Invalid relay URL': 'Неверный URL реле'
} }
} }

View file

@ -655,6 +655,11 @@ export default {
'คำนวณจากชื่อเสียงของผู้ใช้และเปอร์เซ็นไทล์ของเครือข่ายสังคม', 'คำนวณจากชื่อเสียงของผู้ใช้และเปอร์เซ็นไทล์ของเครือข่ายสังคม',
'Auto-load profile pictures': 'โหลดรูปโปรไฟล์อัตโนมัติ', 'Auto-load profile pictures': 'โหลดรูปโปรไฟล์อัตโนมัติ',
'Disable live feed': 'ปิดฟีดสด', 'Disable live feed': 'ปิดฟีดสด',
'Enable live feed': 'เปิดฟีดสด' 'Enable live feed': 'เปิดฟีดสด',
'Default relays': 'รีเลย์เริ่มต้น',
'Reset to default': 'รีเซ็ตเป็นค่าเริ่มต้น',
'Default relays description': 'ใช้สำหรับสอบถามการกำหนดค่ารีเลย์ของผู้ใช้อื่นและเป็นทางเลือกสำรองเมื่อผู้ใช้ไม่ได้กำหนดค่ารีเลย์',
'Default relays warning': 'คำเตือน: กรุณาอย่าแก้ไขการตั้งค่าเหล่านี้โดยไม่ระมัดระวัง เพราะอาจส่งผลต่อประสบการณ์พื้นฐานของคุณ',
'Invalid relay URL': 'URL รีเลย์ไม่ถูกต้อง'
} }
} }

View file

@ -638,6 +638,11 @@ export default {
'trust-filter.trust-score-description': '基於使用者的聲譽和社交網路計算信任度百分位', 'trust-filter.trust-score-description': '基於使用者的聲譽和社交網路計算信任度百分位',
'Auto-load profile pictures': '自動載入大頭照', 'Auto-load profile pictures': '自動載入大頭照',
'Disable live feed': '停用即時推送', 'Disable live feed': '停用即時推送',
'Enable live feed': '啟用即時推送' 'Enable live feed': '啟用即時推送',
'Default relays': '預設中繼',
'Reset to default': '重置為預設',
'Default relays description': '用於查詢其他使用者的中繼配置,並在使用者沒有配置中繼時作為回退策略。',
'Default relays warning': '警告:請不要隨意修改這些設定,可能會影響基礎體驗。',
'Invalid relay URL': '無效的中繼地址'
} }
} }

View file

@ -643,6 +643,11 @@ export default {
'trust-filter.trust-score-description': '基于用户的声誉和社交网络计算信任度百分位', 'trust-filter.trust-score-description': '基于用户的声誉和社交网络计算信任度百分位',
'Auto-load profile pictures': '自动加载头像', 'Auto-load profile pictures': '自动加载头像',
'Disable live feed': '禁用实时推送', 'Disable live feed': '禁用实时推送',
'Enable live feed': '启用实时推送' 'Enable live feed': '启用实时推送',
'Default relays': '默认中继',
'Reset to default': '重置为默认',
'Default relays description': '用于查询其他用户的中继配置,并在用户没有配置中继时作为回退策略。',
'Default relays warning': '警告:请不要随意修改这些设置,可能会影响基础体验。',
'Invalid relay URL': '无效的中继地址'
} }
} }

View file

@ -1,10 +1,11 @@
import { BIG_RELAY_URLS, MAX_PINNED_NOTES, POLL_TYPE } from '@/constants' import { MAX_PINNED_NOTES, POLL_TYPE } from '@/constants'
import { TEmoji, TPollType, TRelayList, TRelaySet } from '@/types' import { TEmoji, TPollType, TRelayList, TRelaySet } from '@/types'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { buildATag } from './draft-event' import { buildATag } from './draft-event'
import { getReplaceableEventIdentifier } from './event' import { getReplaceableEventIdentifier } from './event'
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning' import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
import { formatPubkey, isValidPubkey, pubkeyToNpub } from './pubkey' import { formatPubkey, isValidPubkey, pubkeyToNpub } from './pubkey'
import { getDefaultRelayUrls } from './relay'
import { generateBech32IdFromETag, getEmojiInfosFromEmojiTags, tagNameEquals } from './tag' import { generateBech32IdFromETag, getEmojiInfosFromEmojiTags, tagNameEquals } from './tag'
import { isOnionUrl, isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url' import { isOnionUrl, isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url'
@ -12,8 +13,10 @@ export function getRelayListFromEvent(
event?: Event | null, event?: Event | null,
filterOutOnionRelays: boolean = true filterOutOnionRelays: boolean = true
): TRelayList { ): TRelayList {
const defaultRelays = getDefaultRelayUrls()
if (!event) { if (!event) {
return { write: BIG_RELAY_URLS, read: BIG_RELAY_URLS, originalRelays: [] } return { write: defaultRelays, read: defaultRelays, originalRelays: [] }
} }
const relayList = { write: [], read: [], originalRelays: [] } as TRelayList const relayList = { write: [], read: [], originalRelays: [] } as TRelayList
@ -38,11 +41,11 @@ export function getRelayListFromEvent(
} }
}) })
// If there are too many relays, use the default BIG_RELAY_URLS // If there are too many relays, use the default relays
// Because they don't know anything about relays, their settings cannot be trusted // Because they don't know anything about relays, their settings cannot be trusted
return { return {
write: relayList.write.length && relayList.write.length <= 8 ? relayList.write : BIG_RELAY_URLS, write: relayList.write.length && relayList.write.length <= 8 ? relayList.write : defaultRelays,
read: relayList.read.length && relayList.write.length <= 8 ? relayList.read : BIG_RELAY_URLS, read: relayList.read.length && relayList.write.length <= 8 ? relayList.read : defaultRelays,
originalRelays: relayList.originalRelays originalRelays: relayList.originalRelays
} }
} }

View file

@ -1,6 +1,10 @@
import { BIG_RELAY_URLS } from '@/constants' import storage from '@/services/local-storage.service'
import { TRelayInfo } from '@/types' import { TRelayInfo } from '@/types'
export function getDefaultRelayUrls() {
return storage.getDefaultRelayUrls()
}
export function checkAlgoRelay(relayInfo: TRelayInfo | undefined) { export function checkAlgoRelay(relayInfo: TRelayInfo | undefined) {
return relayInfo?.software === 'https://github.com/bitvora/algo-relay' // hardcode for now return relayInfo?.software === 'https://github.com/bitvora/algo-relay' // hardcode for now
} }
@ -14,7 +18,8 @@ export function checkNip43Support(relayInfo: TRelayInfo | undefined) {
} }
export function filterOutBigRelays(relayUrls: string[]) { export function filterOutBigRelays(relayUrls: string[]) {
return relayUrls.filter((url) => !BIG_RELAY_URLS.includes(url)) const defaultRelays = getDefaultRelayUrls()
return relayUrls.filter((url) => !defaultRelays.includes(url))
} }
export function recommendRelaysByLanguage(i18nLanguage: string) { export function recommendRelaysByLanguage(i18nLanguage: string) {

View file

@ -3,9 +3,10 @@ import FollowingFavoriteRelayList from '@/components/FollowingFavoriteRelayList'
import NoteList from '@/components/NoteList' import NoteList from '@/components/NoteList'
import Tabs from '@/components/Tabs' import Tabs from '@/components/Tabs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { getReplaceableEventIdentifier } from '@/lib/event' import { getReplaceableEventIdentifier } from '@/lib/event'
import { getDefaultRelayUrls } from '@/lib/relay'
import { isLocalNetworkUrl, isOnionUrl, isWebsocketUrl } from '@/lib/url' import { isLocalNetworkUrl, isOnionUrl, isWebsocketUrl } from '@/lib/url'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { TPageRef } from '@/types' import { TPageRef } from '@/types'
@ -42,7 +43,7 @@ const ExplorePage = forwardRef<TPageRef>((_, ref) => {
) : tab === 'reviews' ? ( ) : tab === 'reviews' ? (
<NoteList <NoteList
showKinds={[ExtendedKind.RELAY_REVIEW]} showKinds={[ExtendedKind.RELAY_REVIEW]}
subRequests={[{ urls: BIG_RELAY_URLS, filter: {} }]} subRequests={[{ urls: getDefaultRelayUrls(), filter: {} }]}
filterFn={relayReviewFilterFn} filterFn={relayReviewFilterFn}
filterMutedNotes filterMutedNotes
hideSpam hideSpam

View file

@ -1,8 +1,8 @@
import EmojiPackList from '@/components/EmojiPackList' import EmojiPackList from '@/components/EmojiPackList'
import NoteList from '@/components/NoteList' import NoteList from '@/components/NoteList'
import Tabs from '@/components/Tabs' import Tabs from '@/components/Tabs'
import { BIG_RELAY_URLS } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { getDefaultRelayUrls } from '@/lib/relay'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import { forwardRef, useState } from 'react' import { forwardRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -30,7 +30,7 @@ const EmojiPackSettingsPage = forwardRef(({ index }: { index?: number }, ref) =>
) : ( ) : (
<NoteList <NoteList
showKinds={[kinds.Emojisets]} showKinds={[kinds.Emojisets]}
subRequests={[{ urls: BIG_RELAY_URLS, filter: {} }]} subRequests={[{ urls: getDefaultRelayUrls(), filter: {} }]}
/> />
)} )}
</SecondaryPageLayout> </SecondaryPageLayout>

View file

@ -1,10 +1,11 @@
import { Favicon } from '@/components/Favicon' import { Favicon } from '@/components/Favicon'
import NormalFeed from '@/components/NormalFeed' import NormalFeed from '@/components/NormalFeed'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { BIG_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants' import { SEARCHABLE_RELAY_URLS } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { toProfileList } from '@/lib/link' import { toProfileList } from '@/lib/link'
import { fetchPubkeysFromDomain, getWellKnownNip05Url } from '@/lib/nip05' import { fetchPubkeysFromDomain, getWellKnownNip05Url } from '@/lib/nip05'
import { getDefaultRelayUrls } from '@/lib/relay'
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
@ -47,7 +48,7 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
setSubRequests([ setSubRequests([
{ {
filter: { '#t': [hashtag], ...(kinds.length > 0 ? { kinds } : {}) }, filter: { '#t': [hashtag], ...(kinds.length > 0 ? { kinds } : {}) },
urls: BIG_RELAY_URLS urls: getDefaultRelayUrls()
} }
]) ])
return return

View file

@ -1,6 +1,7 @@
import NoteList from '@/components/NoteList' import NoteList from '@/components/NoteList'
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { getDefaultRelayUrls } from '@/lib/relay'
import { normalizeUrl, simplifyUrl } from '@/lib/url' import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { forwardRef, useMemo } from 'react' import { forwardRef, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -24,7 +25,7 @@ const RelayReviewsPage = forwardRef(({ url, index }: { url?: string; index?: num
showKinds={[ExtendedKind.RELAY_REVIEW]} showKinds={[ExtendedKind.RELAY_REVIEW]}
subRequests={[ subRequests={[
{ {
urls: [normalizedUrl, ...BIG_RELAY_URLS], urls: [normalizedUrl, ...getDefaultRelayUrls()],
filter: { '#d': [normalizedUrl] } filter: { '#d': [normalizedUrl] }
} }
]} ]}

View file

@ -1,3 +1,4 @@
import DefaultRelaysSetting from '@/components/DefaultRelaysSetting'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
@ -43,6 +44,9 @@ const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
}} }}
/> />
</div> </div>
<div className="px-4 space-y-2">
<DefaultRelaysSetting />
</div>
</div> </div>
</SecondaryPageLayout> </SecondaryPageLayout>
) )

View file

@ -1,8 +1,8 @@
import { BIG_RELAY_URLS } from '@/constants'
import { createFavoriteRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event' import { createFavoriteRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event'
import { getReplaceableEventIdentifier } from '@/lib/event' import { getReplaceableEventIdentifier } from '@/lib/event'
import { getRelaySetFromEvent } from '@/lib/event-metadata' import { getRelaySetFromEvent } from '@/lib/event-metadata'
import { randomString } from '@/lib/random' import { randomString } from '@/lib/random'
import { getDefaultRelayUrls } from '@/lib/relay'
import { isWebsocketUrl, normalizeUrl } from '@/lib/url' import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
import client from '@/services/client.service' import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
@ -94,7 +94,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
setRelaySetEvents(storedRelaySetEvents.filter(Boolean) as Event[]) setRelaySetEvents(storedRelaySetEvents.filter(Boolean) as Event[])
const newRelaySetEvents = await client.fetchEvents( const newRelaySetEvents = await client.fetchEvents(
(relayList?.write ?? []).concat(BIG_RELAY_URLS).slice(0, 5), (relayList?.write ?? []).concat(getDefaultRelayUrls()).slice(0, 5),
{ {
kinds: [kinds.Relaysets], kinds: [kinds.Relaysets],
authors: [pubkey], authors: [pubkey],

View file

@ -1,6 +1,6 @@
import LoginDialog from '@/components/LoginDialog' import LoginDialog from '@/components/LoginDialog'
import PasswordInputDialog from '@/components/PasswordInputDialog' import PasswordInputDialog from '@/components/PasswordInputDialog'
import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { ApplicationDataKey, ExtendedKind } from '@/constants'
import { import {
createDeletionRequestDraftEvent, createDeletionRequestDraftEvent,
createFollowListDraftEvent, createFollowListDraftEvent,
@ -16,6 +16,7 @@ import {
} from '@/lib/event' } from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey' import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { getDefaultRelayUrls } from '@/lib/relay'
import client from '@/services/client.service' import client from '@/services/client.service'
import customEmojiService from '@/services/custom-emoji.service' import customEmojiService from '@/services/custom-emoji.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
@ -246,7 +247,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
setPinnedUsersEvent(storedPinnedUsersEvent) setPinnedUsersEvent(storedPinnedUsersEvent)
} }
const relayListEvents = await client.fetchEvents(BIG_RELAY_URLS, { const defaultRelays = getDefaultRelayUrls()
const relayListEvents = await client.fetchEvents(defaultRelays, {
kinds: [kinds.RelayList], kinds: [kinds.RelayList],
authors: [account.pubkey] authors: [account.pubkey]
}) })
@ -258,7 +260,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} }
setRelayList(relayList) setRelayList(relayList)
const events = await client.fetchEvents(relayList.write.concat(BIG_RELAY_URLS).slice(0, 4), [ const events = await client.fetchEvents(relayList.write.concat(defaultRelays).slice(0, 4), [
{ {
kinds: [ kinds: [
kinds.Metadata, kinds.Metadata,
@ -637,13 +639,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} }
const setupNewUser = async (signer: ISigner) => { const setupNewUser = async (signer: ISigner) => {
const defaultRelays = getDefaultRelayUrls()
await Promise.allSettled([ await Promise.allSettled([
client.publishEvent(BIG_RELAY_URLS, await signer.signEvent(createFollowListDraftEvent([]))), client.publishEvent(defaultRelays, await signer.signEvent(createFollowListDraftEvent([]))),
client.publishEvent(BIG_RELAY_URLS, await signer.signEvent(createMuteListDraftEvent([]))), client.publishEvent(defaultRelays, await signer.signEvent(createMuteListDraftEvent([]))),
client.publishEvent( client.publishEvent(
BIG_RELAY_URLS, defaultRelays,
await signer.signEvent( await signer.signEvent(
createRelayListDraftEvent(BIG_RELAY_URLS.map((url) => ({ url, scope: 'both' }))) createRelayListDraftEvent(defaultRelays.map((url) => ({ url, scope: 'both' })))
) )
) )
]) ])

View file

@ -1,6 +1,7 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { compareEvents } from '@/lib/event' import { compareEvents } from '@/lib/event'
import { notificationFilter } from '@/lib/notification' import { notificationFilter } from '@/lib/notification'
import { getDefaultRelayUrls } from '@/lib/relay'
import { usePrimaryPage } from '@/PageManager' import { usePrimaryPage } from '@/PageManager'
import client from '@/services/client.service' import client from '@/services/client.service'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
@ -103,7 +104,8 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
try { try {
let eosed = false let eosed = false
const relayList = await client.fetchRelayList(pubkey) const relayList = await client.fetchRelayList(pubkey)
const relays = relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS const relays =
relayList.read.length > 0 ? relayList.read.slice(0, 5) : getDefaultRelayUrls()
const subCloser = client.subscribe( const subCloser = client.subscribe(
relays, relays,
[ [

View file

@ -1,4 +1,4 @@
import { BIG_RELAY_URLS, ExtendedKind, SEARCHABLE_RELAY_URLS } from '@/constants' import { ExtendedKind, SEARCHABLE_RELAY_URLS } from '@/constants'
import { import {
compareEvents, compareEvents,
getReplaceableCoordinate, getReplaceableCoordinate,
@ -7,7 +7,7 @@ import {
} from '@/lib/event' } from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey' import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey'
import { filterOutBigRelays } from '@/lib/relay' import { filterOutBigRelays, getDefaultRelayUrls } from '@/lib/relay'
import { getPubkeysFromPTags, getServersFromServerTags, tagNameEquals } from '@/lib/tag' import { getPubkeysFromPTags, getServersFromServerTags, tagNameEquals } from '@/lib/tag'
import { mergeTimelines } from '@/lib/timeline' import { mergeTimelines } from '@/lib/timeline'
import { isLocalNetworkUrl, isWebsocketUrl, normalizeUrl } from '@/lib/url' import { isLocalNetworkUrl, isWebsocketUrl, normalizeUrl } from '@/lib/url'
@ -97,6 +97,7 @@ class ClientService extends EventTarget {
} }
} }
const defaultRelays = getDefaultRelayUrls()
const relaySet = new Set<string>() const relaySet = new Set<string>()
if (specifiedRelayUrls?.length) { if (specifiedRelayUrls?.length) {
specifiedRelayUrls.forEach((url) => relaySet.add(url)) specifiedRelayUrls.forEach((url) => relaySet.add(url))
@ -137,20 +138,20 @@ class ClientService extends EventTarget {
ExtendedKind.RELAY_REVIEW ExtendedKind.RELAY_REVIEW
].includes(event.kind) ].includes(event.kind)
) { ) {
BIG_RELAY_URLS.forEach((url) => relaySet.add(url)) defaultRelays.forEach((url) => relaySet.add(url))
} }
if (event.kind === ExtendedKind.COMMENT) { if (event.kind === ExtendedKind.COMMENT) {
const rootITag = event.tags.find(tagNameEquals('I')) const rootITag = event.tags.find(tagNameEquals('I'))
if (rootITag) { if (rootITag) {
// For external content comments, always publish to big relays // For external content comments, always publish to default relays
BIG_RELAY_URLS.forEach((url) => relaySet.add(url)) defaultRelays.forEach((url) => relaySet.add(url))
} }
} }
} }
if (!relaySet.size) { if (!relaySet.size) {
BIG_RELAY_URLS.forEach((url) => relaySet.add(url)) defaultRelays.forEach((url) => relaySet.add(url))
} }
return Array.from(relaySet) return Array.from(relaySet)
@ -166,7 +167,7 @@ class ClientService extends EventTarget {
const relayLists = await this.fetchRelayLists(filter['#p']) const relayLists = await this.fetchRelayLists(filter['#p'])
return Array.from(new Set(relayLists.flatMap((list) => list.read.slice(0, 5)))) return Array.from(new Set(relayLists.flatMap((list) => list.read.slice(0, 5))))
} }
return BIG_RELAY_URLS return getDefaultRelayUrls()
} }
async publishEvent(relayUrls: string[], event: NEvent) { async publishEvent(relayUrls: string[], event: NEvent) {
@ -807,7 +808,11 @@ class ClientService extends EventTarget {
} = {} } = {}
) { ) {
const relays = Array.from(new Set(urls)) const relays = Array.from(new Set(urls))
const events = await this.query(relays.length > 0 ? relays : BIG_RELAY_URLS, filter, onevent) const events = await this.query(
relays.length > 0 ? relays : getDefaultRelayUrls(),
filter,
onevent
)
if (cache) { if (cache) {
events.forEach((evt) => { events.forEach((evt) => {
this.addEventToCache(evt) this.addEventToCache(evt)
@ -935,7 +940,7 @@ class ClientService extends EventTarget {
} }
private async fetchEventsFromBigRelays(ids: readonly string[]) { private async fetchEventsFromBigRelays(ids: readonly string[]) {
const events = await this.query(BIG_RELAY_URLS, { const events = await this.query(getDefaultRelayUrls(), {
ids: Array.from(new Set(ids)), ids: Array.from(new Set(ids)),
limit: ids.length limit: ids.length
}) })
@ -961,7 +966,7 @@ class ClientService extends EventTarget {
private async _fetchFollowingFavoriteRelays(pubkey: string) { private async _fetchFollowingFavoriteRelays(pubkey: string) {
const fetchNewData = async () => { const fetchNewData = async () => {
const followings = await this.fetchFollowings(pubkey) const followings = await this.fetchFollowings(pubkey)
const events = await this.fetchEvents(BIG_RELAY_URLS, { const events = await this.fetchEvents(getDefaultRelayUrls(), {
authors: followings, authors: followings,
kinds: [ExtendedKind.FAVORITE_RELAYS, kinds.Relaysets], kinds: [ExtendedKind.FAVORITE_RELAYS, kinds.Relaysets],
limit: 1000 limit: 1000
@ -1182,9 +1187,10 @@ class ClientService extends EventTarget {
if (event) { if (event) {
return getRelayListFromEvent(event, storage.getFilterOutOnionRelays()) return getRelayListFromEvent(event, storage.getFilterOutOnionRelays())
} }
const defaultRelays = getDefaultRelayUrls()
return { return {
write: BIG_RELAY_URLS, write: defaultRelays,
read: BIG_RELAY_URLS, read: defaultRelays,
originalRelays: [] originalRelays: []
} }
}) })
@ -1224,7 +1230,7 @@ class ClientService extends EventTarget {
const eventsMap = new Map<string, NEvent>() const eventsMap = new Map<string, NEvent>()
await Promise.allSettled( await Promise.allSettled(
Array.from(groups.entries()).map(async ([kind, pubkeys]) => { Array.from(groups.entries()).map(async ([kind, pubkeys]) => {
const events = await this.query(BIG_RELAY_URLS, { const events = await this.query(getDefaultRelayUrls(), {
authors: pubkeys, authors: pubkeys,
kinds: [kind] kinds: [kind]
}) })
@ -1340,7 +1346,7 @@ class ClientService extends EventTarget {
: { authors: [pubkey], kinds: [kind] }) as Filter : { authors: [pubkey], kinds: [kind] }) as Filter
) )
const relayList = await this.fetchRelayList(pubkey) const relayList = await this.fetchRelayList(pubkey)
const relays = relayList.write.concat(BIG_RELAY_URLS).slice(0, 5) const relays = relayList.write.concat(getDefaultRelayUrls()).slice(0, 5)
const events = await this.query(relays, filters) const events = await this.query(relays, filters)
for (const event of events) { for (const event of events) {
@ -1471,10 +1477,10 @@ class ClientService extends EventTarget {
// If many websocket connections are initiated simultaneously, it will be // If many websocket connections are initiated simultaneously, it will be
// very slow on Safari (for unknown reason) // very slow on Safari (for unknown reason)
if (isSafari()) { if (isSafari()) {
let urls = BIG_RELAY_URLS let urls = getDefaultRelayUrls()
if (myPubkey) { if (myPubkey) {
const relayList = await this.fetchRelayList(myPubkey) const relayList = await this.fetchRelayList(myPubkey)
urls = relayList.read.concat(BIG_RELAY_URLS).slice(0, 5) urls = relayList.read.concat(getDefaultRelayUrls()).slice(0, 5)
} }
return [{ urls, filter: { authors: pubkeys } }] return [{ urls, filter: { authors: pubkeys } }]
} }

View file

@ -1,5 +1,6 @@
import { BIG_RELAY_URLS, CODY_PUBKEY, JUMBLE_PUBKEY } from '@/constants' import { CODY_PUBKEY, JUMBLE_PUBKEY } from '@/constants'
import { getZapInfoFromEvent } from '@/lib/event-metadata' import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { getDefaultRelayUrls } from '@/lib/relay'
import { TProfile } from '@/types' import { TProfile } from '@/types'
import { init, launchPaymentModal } from '@getalby/bitcoin-connect-react' import { init, launchPaymentModal } from '@getalby/bitcoin-connect-react'
import { Invoice } from '@getalby/lightning-tools' import { Invoice } from '@getalby/lightning-tools'
@ -52,7 +53,7 @@ class LightningService {
client.fetchRelayList(recipient), client.fetchRelayList(recipient),
sender sender
? client.fetchRelayList(sender) ? client.fetchRelayList(sender)
: Promise.resolve({ read: BIG_RELAY_URLS, write: BIG_RELAY_URLS }) : Promise.resolve({ read: getDefaultRelayUrls(), write: getDefaultRelayUrls() })
]) ])
if (!profile) { if (!profile) {
throw new Error('Recipient not found') throw new Error('Recipient not found')
@ -69,7 +70,7 @@ class LightningService {
relays: receiptRelayList.read relays: receiptRelayList.read
.slice(0, 4) .slice(0, 4)
.concat(senderRelayList.write.slice(0, 3)) .concat(senderRelayList.write.slice(0, 3))
.concat(BIG_RELAY_URLS), .concat(getDefaultRelayUrls()),
comment comment
}) })
const zapRequest = await client.signer.signEvent(zapRequestDraft) const zapRequest = await client.signer.signEvent(zapRequestDraft)
@ -134,7 +135,7 @@ class LightningService {
filter['#e'] = [event.id] filter['#e'] = [event.id]
} }
subCloser = client.subscribe( subCloser = client.subscribe(
senderRelayList.write.concat(BIG_RELAY_URLS).slice(0, 4), senderRelayList.write.concat(getDefaultRelayUrls()).slice(0, 4),
filter, filter,
{ {
onevent: (evt) => { onevent: (evt) => {

View file

@ -1,5 +1,6 @@
import { import {
ALLOWED_FILTER_KINDS, ALLOWED_FILTER_KINDS,
BIG_RELAY_URLS,
DEFAULT_FAVICON_URL_TEMPLATE, DEFAULT_FAVICON_URL_TEMPLATE,
DEFAULT_NIP_96_SERVICE, DEFAULT_NIP_96_SERVICE,
ExtendedKind, ExtendedKind,
@ -21,9 +22,9 @@ import {
TMediaAutoLoadPolicy, TMediaAutoLoadPolicy,
TMediaUploadServiceConfig, TMediaUploadServiceConfig,
TNoteListMode, TNoteListMode,
TProfilePictureAutoLoadPolicy,
TNsfwDisplayPolicy,
TNotificationStyle, TNotificationStyle,
TNsfwDisplayPolicy,
TProfilePictureAutoLoadPolicy,
TRelaySet, TRelaySet,
TThemeSetting, TThemeSetting,
TTranslationServiceConfig TTranslationServiceConfig
@ -65,6 +66,7 @@ class LocalStorageService {
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
private minTrustScore: number = 40 private minTrustScore: number = 40
private enableLiveFeed: boolean = false private enableLiveFeed: boolean = false
private defaultRelayUrls: string[] = BIG_RELAY_URLS
constructor() { constructor() {
if (!LocalStorageService.instance) { if (!LocalStorageService.instance) {
@ -278,6 +280,22 @@ class LocalStorageService {
this.enableLiveFeed = window.localStorage.getItem(StorageKey.ENABLE_LIVE_FEED) === 'true' this.enableLiveFeed = window.localStorage.getItem(StorageKey.ENABLE_LIVE_FEED) === 'true'
const defaultRelayUrlsStr = window.localStorage.getItem(StorageKey.DEFAULT_RELAY_URLS)
if (defaultRelayUrlsStr) {
try {
const urls = JSON.parse(defaultRelayUrlsStr)
if (
Array.isArray(urls) &&
urls.length > 0 &&
urls.every((url) => typeof url === 'string')
) {
this.defaultRelayUrls = urls
}
} catch {
// Invalid JSON, use default
}
}
// Clean up deprecated data // Clean up deprecated data
window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS) window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS)
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
@ -624,6 +642,15 @@ class LocalStorageService {
this.enableLiveFeed = enable this.enableLiveFeed = enable
window.localStorage.setItem(StorageKey.ENABLE_LIVE_FEED, enable.toString()) window.localStorage.setItem(StorageKey.ENABLE_LIVE_FEED, enable.toString())
} }
getDefaultRelayUrls() {
return this.defaultRelayUrls
}
setDefaultRelayUrls(urls: string[]) {
this.defaultRelayUrls = urls
window.localStorage.setItem(StorageKey.DEFAULT_RELAY_URLS, JSON.stringify(urls))
}
} }
const instance = new LocalStorageService() const instance = new LocalStorageService()

View file

@ -1,6 +1,7 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { getEventKey, getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { getEventKey, getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata' import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { getDefaultRelayUrls } from '@/lib/relay'
import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag' import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag'
import client from '@/services/client.service' import client from '@/services/client.service'
import { TEmoji } from '@/types' import { TEmoji } from '@/types'
@ -150,7 +151,9 @@ class StuffStatsService {
}) })
} }
const relays = relayList ? relayList.read.concat(BIG_RELAY_URLS).slice(0, 5) : BIG_RELAY_URLS const relays = relayList
? relayList.read.concat(getDefaultRelayUrls()).slice(0, 5)
: getDefaultRelayUrls()
const events: Event[] = [] const events: Event[] = []
await client.fetchEvents(relays, filters, { await client.fetchEvents(relays, filters, {

View file

@ -1,4 +1,4 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { import {
getEventKey, getEventKey,
getKeyFromTag, getKeyFromTag,
@ -9,6 +9,7 @@ import {
isReplaceableEvent, isReplaceableEvent,
isReplyNoteEvent isReplyNoteEvent
} from '@/lib/event' } from '@/lib/event'
import { getDefaultRelayUrls } from '@/lib/relay'
import { generateBech32IdFromETag } from '@/lib/tag' import { generateBech32IdFromETag } from '@/lib/tag'
import client from '@/services/client.service' import client from '@/services/client.service'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@ -69,7 +70,7 @@ class ThreadService {
const relayList = await client.fetchRelayList(rootPubkey) const relayList = await client.fetchRelayList(rootPubkey)
relayUrls = relayList.read relayUrls = relayList.read
} }
relayUrls = relayUrls.concat(BIG_RELAY_URLS).slice(0, 4) relayUrls = relayUrls.concat(getDefaultRelayUrls()).slice(0, 4)
// If current event is protected, we can assume its replies are also protected and stored on the same relays // If current event is protected, we can assume its replies are also protected and stored on the same relays
if (event && isProtectedEvent(event)) { if (event && isProtectedEvent(event)) {