feat: add customizable search relays setting

Replace hardcoded SEARCHABLE_RELAY_URLS with user-configurable search
relays stored in localStorage. Add SearchRelaysSetting UI in System
settings page with add/remove/reset functionality.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
codytseng 2026-03-12 23:19:47 +08:00
parent 7be7b30d52
commit aae8fc2f17
27 changed files with 202 additions and 50 deletions

View file

@ -1,8 +1,8 @@
import KindFilter from '@/components/KindFilter'
import NoteList, { TNoteListRef } from '@/components/NoteList'
import Tabs from '@/components/Tabs'
import { MAX_PINNED_NOTES, SEARCHABLE_RELAY_URLS } from '@/constants'
import { getDefaultRelayUrls } from '@/lib/relay'
import { MAX_PINNED_NOTES } from '@/constants'
import { getDefaultRelayUrls, getSearchRelayUrls } from '@/lib/relay'
import { generateBech32IdFromETag } from '@/lib/tag'
import { isTouchDevice } from '@/lib/utils'
import { useKindFilter } from '@/providers/KindFilterProvider'
@ -125,7 +125,7 @@ export default function ProfileFeed({
)
setSubRequests([
{
urls: searchableRelays.concat(SEARCHABLE_RELAY_URLS).slice(0, 8),
urls: searchableRelays.concat(getSearchRelayUrls()).slice(0, 8),
filter: { authors: [pubkey], search }
}
])

View file

@ -0,0 +1,103 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { SEARCHABLE_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 RelayIcon from '../RelayIcon'
export default function SearchRelaysSetting() {
const { t } = useTranslation()
const [relayUrls, setRelayUrls] = useState<string[]>(storage.getSearchRelayUrls())
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.setSearchRelayUrls(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.setSearchRelayUrls(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(SEARCHABLE_RELAY_URLS)
storage.setSearchRelayUrls(SEARCHABLE_RELAY_URLS)
}
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-base font-normal">{t('Search relays')}</Label>
<Button variant="outline" size="sm" onClick={resetToDefault}>
{t('Reset to default')}
</Button>
</div>
<div className="text-xs text-muted-foreground">
{t('Relays used for searching notes (NIP-50)')}
</div>
<div className="mt-1">
{relayUrls.map((url, index) => (
<div key={index} className="flex items-center justify-between py-1 pl-1 pr-3">
<div className="flex w-0 flex-1 items-center gap-3">
<RelayIcon url={url} className="h-4 w-4" />
<div className="truncate text-sm text-muted-foreground">{url}</div>
</div>
<div className="shrink-0">
<CircleX
size={16}
onClick={() => removeRelayUrl(url)}
className="cursor-pointer text-muted-foreground hover:text-destructive"
/>
</div>
</div>
))}
</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="mt-1 text-xs text-destructive">{newRelayUrlError}</div>}
</div>
)
}

View file

@ -1,5 +1,5 @@
import { SEARCHABLE_RELAY_URLS, SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { getDefaultRelayUrls } from '@/lib/relay'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { getDefaultRelayUrls, getSearchRelayUrls } from '@/lib/relay'
import { TSearchParams } from '@/types'
import NormalFeed from '../NormalFeed'
import Profile from '../Profile'
@ -21,7 +21,7 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
return (
<NormalFeed
trustScoreFilterId={SPECIAL_TRUST_SCORE_FILTER_ID.SEARCH}
subRequests={[{ urls: SEARCHABLE_RELAY_URLS, filter: { search: searchParams.search } }]}
subRequests={[{ urls: getSearchRelayUrls(), filter: { search: searchParams.search } }]}
showRelayCloseReason
/>
)

View file

@ -47,6 +47,7 @@ export const StorageKey = {
MUTED_WORDS: 'mutedWords',
MIN_TRUST_SCORE: 'minTrustScore',
MIN_TRUST_SCORE_MAP: 'minTrustScoreMap',
SEARCH_RELAY_URLS: 'searchRelayUrls',
HIDE_INDIRECT_NOTIFICATIONS: 'hideIndirectNotifications',
ENABLE_LIVE_FEED: 'enableLiveFeed', // deprecated
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', // deprecated
@ -76,11 +77,7 @@ export const BIG_RELAY_URLS = [
'wss://offchain.pub/'
]
export const SEARCHABLE_RELAY_URLS = [
'wss://search.nos.today/',
'wss://relay.ditto.pub/',
'wss://relay.nostr.band/'
]
export const SEARCHABLE_RELAY_URLS = ['wss://search.nos.today/', 'wss://relay.nostr.band/']
export const TRENDING_NOTES_RELAY_URLS = ['wss://trending.relays.land/']

View file

@ -163,7 +163,7 @@ export default {
'Send only to r': 'إرسال فقط إلى {{r}}',
'Send only to these relays': 'إرسال فقط إلى هذه الريلايات',
Explore: 'استكشاف',
'Search relays': 'البحث في الريلايات',
'Search relays': 'ريلايات البحث',
relayInfoBadgeAuth: 'مصادقة',
relayInfoBadgeSearch: 'بحث',
relayInfoBadgePayment: 'دفع',
@ -670,6 +670,7 @@ export default {
'Hide indirect': 'إخفاء غير المباشرة',
'Copy note content': 'نسخ محتوى الملاحظة',
'Video loop': 'تكرار الفيديو',
'Automatically replay videos when they end': 'إعادة تشغيل مقاطع الفيديو تلقائيًا عند انتهائها'
'Automatically replay videos when they end': 'إعادة تشغيل مقاطع الفيديو تلقائيًا عند انتهائها',
'Relays used for searching notes (NIP-50)': 'الريلايات المستخدمة للبحث عن الملاحظات (NIP-50)'
}
}

View file

@ -167,7 +167,7 @@ export default {
'Send only to r': 'Nur an {{r}} senden',
'Send only to these relays': 'Nur an diese Relays senden',
Explore: 'Entdecken',
'Search relays': 'Relays suchen',
'Search relays': 'Such-Relays',
relayInfoBadgeAuth: 'Auth',
relayInfoBadgeSearch: 'Suche',
relayInfoBadgePayment: 'Zahlung',
@ -694,6 +694,7 @@ export default {
'Hide indirect': 'Indirekte ausblenden',
'Copy note content': 'Notizinhalt kopieren',
'Video loop': 'Video-Schleife',
'Automatically replay videos when they end': 'Videos automatisch wiederholen, wenn sie enden'
'Automatically replay videos when they end': 'Videos automatisch wiederholen, wenn sie enden',
'Relays used for searching notes (NIP-50)': 'Relays für die Notizsuche (NIP-50)'
}
}

View file

@ -676,6 +676,7 @@ export default {
'Hide indirect': 'Hide indirect',
'Copy note content': 'Copy note content',
'Video loop': 'Video loop',
'Automatically replay videos when they end': 'Automatically replay videos when they end'
'Automatically replay videos when they end': 'Automatically replay videos when they end',
'Relays used for searching notes (NIP-50)': 'Relays used for searching notes (NIP-50)'
}
}

View file

@ -167,7 +167,7 @@ export default {
'Send only to r': 'Enviar únicamente a {{r}}',
'Send only to these relays': 'Enviar únicamente a estos relés',
Explore: 'Explorar',
'Search relays': 'Buscar relés',
'Search relays': 'Relés de búsqueda',
relayInfoBadgeAuth: 'Autenticación',
relayInfoBadgeSearch: 'Búsqueda',
relayInfoBadgePayment: 'Pago',
@ -687,6 +687,7 @@ export default {
'Hide indirect': 'Ocultar indirectas',
'Copy note content': 'Copiar contenido de la nota',
'Video loop': 'Repetir video',
'Automatically replay videos when they end': 'Reproducir automáticamente los videos cuando terminen'
'Automatically replay videos when they end': 'Reproducir automáticamente los videos cuando terminen',
'Relays used for searching notes (NIP-50)': 'Relés utilizados para buscar notas (NIP-50)'
}
}

View file

@ -165,7 +165,7 @@ export default {
'Send only to r': 'فقط به {{r}} ارسال شود',
'Send only to these relays': 'فقط به این رله‌ها ارسال شود',
Explore: 'کاوش',
'Search relays': 'جستجو رله‌ها',
'Search relays': 'رله‌های جستجو',
relayInfoBadgeAuth: 'احراز هویت',
relayInfoBadgeSearch: 'جستجو',
relayInfoBadgePayment: 'پرداخت',
@ -682,6 +682,7 @@ export default {
'Hide indirect': 'پنهان کردن غیرمستقیم',
'Copy note content': 'کپی محتوای یادداشت',
'Video loop': 'تکرار ویدیو',
'Automatically replay videos when they end': 'پخش خودکار ویدیوها پس از پایان'
'Automatically replay videos when they end': 'پخش خودکار ویدیوها پس از پایان',
'Relays used for searching notes (NIP-50)': 'رله‌هایی که برای جستجوی یادداشت‌ها استفاده می‌شوند (NIP-50)'
}
}

View file

@ -166,7 +166,7 @@ export default {
'Send only to r': 'Envoyer uniquement à {{r}}',
'Send only to these relays': 'Envoyer uniquement à ces relais',
Explore: 'Explorer',
'Search relays': 'Rechercher des relais',
'Search relays': 'Relais de recherche',
relayInfoBadgeAuth: 'Auth',
relayInfoBadgeSearch: 'Recherche',
relayInfoBadgePayment: 'Paiement',
@ -691,6 +691,7 @@ export default {
'Hide indirect': 'Masquer indirects',
'Copy note content': 'Copier le contenu de la note',
'Video loop': 'Boucle vidéo',
'Automatically replay videos when they end': 'Rejouer automatiquement les vidéos à la fin'
'Automatically replay videos when they end': 'Rejouer automatiquement les vidéos à la fin',
'Relays used for searching notes (NIP-50)': 'Relais utilisés pour rechercher des notes (NIP-50)'
}
}

View file

@ -166,7 +166,7 @@ export default {
'Send only to r': 'केवल {{r}} को भेजें',
'Send only to these relays': 'केवल इन रिले को भेजें',
Explore: 'एक्सप्लोर करें',
'Search relays': 'रिले खोजें',
'Search relays': 'खोज रिले',
relayInfoBadgeAuth: 'प्रमाणीकरण',
relayInfoBadgeSearch: 'खोज',
relayInfoBadgePayment: 'भुगतान',
@ -682,6 +682,7 @@ export default {
'Hide indirect': 'अप्रत्यक्ष छुपाएं',
'Copy note content': 'नोट सामग्री कॉपी करें',
'Video loop': 'वीडियो लूप',
'Automatically replay videos when they end': 'वीडियो समाप्त होने पर स्वचालित रूप से दोबारा चलाएं'
'Automatically replay videos when they end': 'वीडियो समाप्त होने पर स्वचालित रूप से दोबारा चलाएं',
'Relays used for searching notes (NIP-50)': 'नोट्स खोजने के लिए उपयोग किए जाने वाले रिले (NIP-50)'
}
}

View file

@ -165,7 +165,7 @@ export default {
'Send only to r': 'Küldés csak a {{r}} csomópontra',
'Send only to these relays': 'Küldés csak ezekre a csomópontokra',
Explore: 'Felderítés',
'Search relays': 'Csomópontok kereséshez',
'Search relays': 'Keresési csomópontok',
relayInfoBadgeAuth: 'Auth',
relayInfoBadgeSearch: 'Keresés',
relayInfoBadgePayment: 'Fizetés',
@ -676,6 +676,7 @@ export default {
'Hide indirect': 'Közvetettek elrejtése',
'Copy note content': 'Jegyzet tartalmának másolása',
'Video loop': 'Videó ismétlése',
'Automatically replay videos when they end': 'Videók automatikus újrajátszása, amikor véget érnek'
'Automatically replay videos when they end': 'Videók automatikus újrajátszása, amikor véget érnek',
'Relays used for searching notes (NIP-50)': 'Jegyzetek kereséséhez használt csomópontok (NIP-50)'
}
}

View file

@ -166,7 +166,7 @@ export default {
'Send only to r': 'Invia solo a {{r}}',
'Send only to these relays': 'Invia solo a questi relay',
Explore: 'Esplora',
'Search relays': 'Ricerca relay',
'Search relays': 'Relay di ricerca',
relayInfoBadgeAuth: 'Autorizzazione',
relayInfoBadgeSearch: 'Ricerca',
relayInfoBadgePayment: 'Pagamento',
@ -687,6 +687,7 @@ export default {
'Hide indirect': 'Nascondi indirette',
'Copy note content': 'Copia contenuto della nota',
'Video loop': 'Ripetizione video',
'Automatically replay videos when they end': 'Riprodurre automaticamente i video quando terminano'
'Automatically replay videos when they end': 'Riprodurre automaticamente i video quando terminano',
'Relays used for searching notes (NIP-50)': 'Relay utilizzati per cercare le note (NIP-50)'
}
}

View file

@ -165,7 +165,7 @@ export default {
'Send only to r': '{{r}} にのみ送信',
'Send only to these relays': 'これらのリレイにのみ送信',
Explore: '探索',
'Search relays': 'リレイを検索',
'Search relays': '検索リレー',
relayInfoBadgeAuth: '認証',
relayInfoBadgeSearch: '検索',
relayInfoBadgePayment: '支払い',
@ -682,6 +682,7 @@ export default {
'Hide indirect': '間接通知を非表示',
'Copy note content': 'ノート内容をコピー',
'Video loop': 'ビデオループ',
'Automatically replay videos when they end': 'ビデオ終了時に自動的にリプレイする'
'Automatically replay videos when they end': 'ビデオ終了時に自動的にリプレイする',
'Relays used for searching notes (NIP-50)': 'ノート検索に使用するリレー (NIP-50)'
}
}

View file

@ -166,7 +166,7 @@ export default {
'Send only to r': '{{r}}에만 전송',
'Send only to these relays': '이 릴레이에만 전송',
Explore: '탐색',
'Search relays': '릴레이 검색',
'Search relays': '검색 릴레이',
relayInfoBadgeAuth: '로그인 필요',
relayInfoBadgeSearch: '검색 지원',
relayInfoBadgePayment: '유료',
@ -676,6 +676,7 @@ export default {
'Hide indirect': '간접 숨기기',
'Copy note content': '노트 내용 복사',
'Video loop': '비디오 반복',
'Automatically replay videos when they end': '비디오가 끝나면 자동으로 다시 재생'
'Automatically replay videos when they end': '비디오가 끝나면 자동으로 다시 재생',
'Relays used for searching notes (NIP-50)': '노트 검색에 사용되는 릴레이 (NIP-50)'
}
}

View file

@ -163,7 +163,7 @@ export default {
'Send only to r': 'Wyślij tylko do {{r}}',
'Send only to these relays': 'Wyślij tylko do tych transmiterów',
Explore: 'Transmitery',
'Search relays': 'Wyszukaj transmiter',
'Search relays': 'Przekaźniki wyszukiwania',
relayInfoBadgeAuth: '✔️',
relayInfoBadgeSearch: 'Wyszukiwarka',
relayInfoBadgePayment: 'Płatności',
@ -688,6 +688,7 @@ export default {
'Hide indirect': 'Ukryj pośrednie',
'Copy note content': 'Kopiuj treść notatki',
'Video loop': 'Zapętlanie wideo',
'Automatically replay videos when they end': 'Automatycznie powtarzaj filmy po zakończeniu'
'Automatically replay videos when they end': 'Automatycznie powtarzaj filmy po zakończeniu',
'Relays used for searching notes (NIP-50)': 'Przekaźniki używane do wyszukiwania notatek (NIP-50)'
}
}

View file

@ -166,7 +166,7 @@ export default {
'Send only to r': 'Enviar apenas para {{r}}',
'Send only to these relays': 'Enviar apenas para estes relays',
Explore: 'Explorar',
'Search relays': 'Pesquisar relays',
'Search relays': 'Relays de busca',
relayInfoBadgeAuth: 'Auth',
relayInfoBadgeSearch: 'Pesquisar',
relayInfoBadgePayment: 'Pagamento',
@ -685,6 +685,7 @@ export default {
'Hide indirect': 'Ocultar indiretas',
'Copy note content': 'Copiar conteúdo da nota',
'Video loop': 'Repetir vídeo',
'Automatically replay videos when they end': 'Reproduzir automaticamente os vídeos quando terminarem'
'Automatically replay videos when they end': 'Reproduzir automaticamente os vídeos quando terminarem',
'Relays used for searching notes (NIP-50)': 'Relays usados para buscar notas (NIP-50)'
}
}

View file

@ -166,7 +166,7 @@ export default {
'Send only to r': 'Enviar apenas para {{r}}',
'Send only to these relays': 'Enviar apenas para estes relés',
Explore: 'Explorar',
'Search relays': 'Pesquisar relés',
'Search relays': 'Relés de pesquisa',
relayInfoBadgeAuth: 'Auth',
relayInfoBadgeSearch: 'Pesquisar',
relayInfoBadgePayment: 'Pagamento',
@ -688,6 +688,7 @@ export default {
'Hide indirect': 'Ocultar indiretas',
'Copy note content': 'Copiar conteúdo da nota',
'Video loop': 'Repetir vídeo',
'Automatically replay videos when they end': 'Reproduzir automaticamente os vídeos quando terminarem'
'Automatically replay videos when they end': 'Reproduzir automaticamente os vídeos quando terminarem',
'Relays used for searching notes (NIP-50)': 'Relés usados para pesquisar notas (NIP-50)'
}
}

View file

@ -168,7 +168,7 @@ export default {
'Send only to r': 'Отправить только на {{r}}',
'Send only to these relays': 'Отправить только на эти ретрансляторы',
Explore: 'Обзор',
'Search relays': 'Поиск ретрансляторов',
'Search relays': 'Ретрансляторы для поиска',
relayInfoBadgeAuth: 'Авторизация',
relayInfoBadgeSearch: 'Поиск',
relayInfoBadgePayment: 'Платежи',
@ -687,6 +687,7 @@ export default {
'Hide indirect': 'Скрыть косвенные',
'Copy note content': 'Скопировать содержимое заметки',
'Video loop': 'Зацикливание видео',
'Automatically replay videos when they end': 'Автоматически воспроизводить видео заново после окончания'
'Automatically replay videos when they end': 'Автоматически воспроизводить видео заново после окончания',
'Relays used for searching notes (NIP-50)': 'Ретрансляторы для поиска заметок (NIP-50)'
}
}

View file

@ -163,7 +163,7 @@ export default {
'Send only to r': 'ส่งเฉพาะไปยัง {{r}}',
'Send only to these relays': 'ส่งเฉพาะไปยังรีเลย์เหล่านี้',
Explore: 'สำรวจ',
'Search relays': 'ค้นหารีเลย์',
'Search relays': 'รีเลย์สำหรับค้นหา',
relayInfoBadgeAuth: 'ยืนยันตัวตน',
relayInfoBadgeSearch: 'ค้นหา',
relayInfoBadgePayment: 'ชำระเงิน',
@ -672,6 +672,7 @@ export default {
'Hide indirect': 'ซ่อนทางอ้อม',
'Copy note content': 'คัดลอกเนื้อหาโน้ต',
'Video loop': 'เล่นวิดีโอซ้ำ',
'Automatically replay videos when they end': 'เล่นวิดีโอซ้ำอัตโนมัติเมื่อจบ'
'Automatically replay videos when they end': 'เล่นวิดีโอซ้ำอัตโนมัติเมื่อจบ',
'Relays used for searching notes (NIP-50)': 'รีเลย์ที่ใช้สำหรับค้นหาโน้ต (NIP-50)'
}
}

View file

@ -654,6 +654,7 @@ export default {
'Hide indirect': '隱藏間接通知',
'Copy note content': '複製筆記內容',
'Video loop': '影片循環',
'Automatically replay videos when they end': '影片播放結束後自動重新播放'
'Automatically replay videos when they end': '影片播放結束後自動重新播放',
'Relays used for searching notes (NIP-50)': '用於搜尋筆記的伺服器 (NIP-50)'
}
}

View file

@ -659,6 +659,7 @@ export default {
'Hide indirect': '隐藏间接通知',
'Copy note content': '复制笔记内容',
'Video loop': '视频循环',
'Automatically replay videos when they end': '视频播放结束后自动重新播放'
'Automatically replay videos when they end': '视频播放结束后自动重新播放',
'Relays used for searching notes (NIP-50)': '用于搜索笔记的服务器 (NIP-50)'
}
}

View file

@ -5,6 +5,10 @@ export function getDefaultRelayUrls() {
return storage.getDefaultRelayUrls()
}
export function getSearchRelayUrls() {
return storage.getSearchRelayUrls()
}
export function checkAlgoRelay(relayInfo: TRelayInfo | undefined) {
return relayInfo?.software === 'https://github.com/bitvora/algo-relay' // hardcode for now
}

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { ExtendedKind, SEARCHABLE_RELAY_URLS } from '@/constants'
import { ExtendedKind } from '@/constants'
import {
compareEvents,
getReplaceableCoordinate,
@ -7,7 +7,7 @@ import {
} from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey'
import { filterOutBigRelays, getDefaultRelayUrls } from '@/lib/relay'
import { filterOutBigRelays, getDefaultRelayUrls, getSearchRelayUrls } from '@/lib/relay'
import { SmartPool } from '@/lib/smart-pool'
import { getPubkeysFromPTags, getServersFromServerTags, tagNameEquals } from '@/lib/tag'
import { mergeTimelines } from '@/lib/timeline'
@ -159,7 +159,7 @@ class ClientService extends EventTarget {
async determineRelaysByFilter(filter: Filter) {
if (filter.search) {
return SEARCHABLE_RELAY_URLS
return getSearchRelayUrls()
} else if (filter.authors?.length) {
const relayLists = await this.fetchRelayLists(filter.authors)
return Array.from(new Set(relayLists.flatMap((list) => list.write.slice(0, 5))))

View file

@ -8,6 +8,7 @@ import {
NOTIFICATION_LIST_STYLE,
NSFW_DISPLAY_POLICY,
PROFILE_PICTURE_AUTO_LOAD_POLICY,
SEARCHABLE_RELAY_URLS,
StorageKey,
TPrimaryColor
} from '@/constants'
@ -66,6 +67,7 @@ class LocalStorageService {
private quickReactionEmoji: string | TEmoji = '+'
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
private defaultRelayUrls: string[] = BIG_RELAY_URLS
private searchRelayUrls: string[] = SEARCHABLE_RELAY_URLS
private mutedWords: string[] = []
private minTrustScore: number = 0
private minTrustScoreMap: Record<string, number> = {}
@ -310,6 +312,22 @@ class LocalStorageService {
}
}
const searchRelayUrlsStr = window.localStorage.getItem(StorageKey.SEARCH_RELAY_URLS)
if (searchRelayUrlsStr) {
try {
const urls = JSON.parse(searchRelayUrlsStr)
if (
Array.isArray(urls) &&
urls.length > 0 &&
urls.every((url) => typeof url === 'string')
) {
this.searchRelayUrls = urls
}
} catch {
// Invalid JSON, use default
}
}
const mutedWordsStr = window.localStorage.getItem(StorageKey.MUTED_WORDS)
if (mutedWordsStr) {
try {
@ -691,6 +709,15 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.DEFAULT_RELAY_URLS, JSON.stringify(urls))
}
getSearchRelayUrls() {
return this.searchRelayUrls
}
setSearchRelayUrls(urls: string[]) {
this.searchRelayUrls = urls
window.localStorage.setItem(StorageKey.SEARCH_RELAY_URLS, JSON.stringify(urls))
}
getMutedWords() {
return this.mutedWords
}