feat: add option to disable filtering for onion relays

This commit is contained in:
codytseng
2025-11-15 13:58:20 +08:00
parent 606f9af1ba
commit 5ba5c26fcd
25 changed files with 98 additions and 36 deletions

View File

@@ -129,6 +129,13 @@ export default function Settings() {
{copiedNcryptsec ? <Check /> : <Copy />}
</SettingItem>
)}
<SettingItem className="clickable" onClick={() => push(toSystemSettings())}>
<div className="flex items-center gap-4">
<Cog />
<div>{t('System')}</div>
</div>
<ChevronRight />
</SettingItem>
<AboutInfoDialog>
<SettingItem className="clickable">
<div className="flex items-center gap-4">
@@ -143,13 +150,6 @@ export default function Settings() {
</div>
</SettingItem>
</AboutInfoDialog>
<SettingItem className="clickable" onClick={() => push(toSystemSettings())}>
<div className="flex items-center gap-4">
<Cog />
<div>{t('System')}</div>
</div>
<ChevronRight />
</SettingItem>
<div className="px-4 mt-4">
<Donation />
</div>

View File

@@ -40,6 +40,7 @@ export const StorageKey = {
PRIMARY_COLOR: 'primaryColor',
ENABLE_SINGLE_COLUMN_LAYOUT: 'enableSingleColumnLayout',
FAVICON_URL_TEMPLATE: 'faviconUrlTemplate',
FILTER_OUT_ONION_RELAYS: 'filterOutOnionRelays',
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated

View File

@@ -532,6 +532,7 @@ export default {
'Failed to get invite code from relay': 'فشل الحصول على رمز الدعوة من المرحل',
'Failed to get invite code': 'فشل الحصول على رمز الدعوة',
'Invite code copied to clipboard': 'تم نسخ رمز الدعوة إلى الحافظة',
'Favicon URL': 'رابط الأيقونة المفضلة'
'Favicon URL': 'رابط الأيقونة المفضلة',
'Filter out onion relays': 'تصفية مرحلات onion'
}
}

View File

@@ -548,6 +548,7 @@ export default {
'Failed to get invite code from relay': 'Fehler beim Abrufen des Einladungscodes vom Relay',
'Failed to get invite code': 'Fehler beim Abrufen des Einladungscodes',
'Invite code copied to clipboard': 'Einladungscode in die Zwischenablage kopiert',
'Favicon URL': 'Favicon-URL'
'Favicon URL': 'Favicon-URL',
'Filter out onion relays': 'Onion-Relays herausfiltern'
}
}

View File

@@ -533,6 +533,7 @@ export default {
'Failed to get invite code from relay': 'Failed to get invite code from relay',
'Failed to get invite code': 'Failed to get invite code',
'Invite code copied to clipboard': 'Invite code copied to clipboard',
'Favicon URL': 'Favicon URL'
'Favicon URL': 'Favicon URL',
'Filter out onion relays': 'Filter out onion relays'
}
}

View File

@@ -542,6 +542,7 @@ export default {
'Failed to get invite code from relay': 'Error al obtener código de invitación del relay',
'Failed to get invite code': 'Error al obtener código de invitación',
'Invite code copied to clipboard': 'Código de invitación copiado al portapapeles',
'Favicon URL': 'URL del Favicon'
'Favicon URL': 'URL del Favicon',
'Filter out onion relays': 'Filtrar relés onion'
}
}

View File

@@ -537,6 +537,7 @@ export default {
'Failed to get invite code from relay': 'دریافت کد دعوت از رله ناموفق بود',
'Failed to get invite code': 'دریافت کد دعوت ناموفق بود',
'Invite code copied to clipboard': 'کد دعوت در کلیپ‌بورد کپی شد',
'Favicon URL': 'آدرس نماد سایت'
'Favicon URL': 'آدرس نماد سایت',
'Filter out onion relays': 'فیلتر کردن رله‌های onion'
}
}

View File

@@ -547,6 +547,7 @@ export default {
'Failed to get invite code from relay': "Échec de l'obtention du code d'invitation du relay",
'Failed to get invite code': "Échec de l'obtention du code d'invitation",
'Invite code copied to clipboard': "Code d'invitation copié dans le presse-papiers",
'Favicon URL': 'URL du Favicon'
'Favicon URL': 'URL du Favicon',
'Filter out onion relays': 'Filtrer les relais onion'
}
}

View File

@@ -539,6 +539,7 @@ export default {
'Failed to get invite code from relay': 'रिले से निमंत्रण कोड प्राप्त करने में विफल',
'Failed to get invite code': 'निमंत्रण कोड प्राप्त करने में विफल',
'Invite code copied to clipboard': 'निमंत्रण कोड क्लिपबोर्ड पर कॉपी किया गया',
'Favicon URL': 'फ़ेविकॉन URL'
'Favicon URL': 'फ़ेविकॉन URL',
'Filter out onion relays': 'ओनियन रिले फ़िल्टर करें'
}
}

View File

@@ -534,6 +534,7 @@ export default {
'Failed to get invite code from relay': 'Nem sikerült lekérni a meghívókódot a relay-től',
'Failed to get invite code': 'Nem sikerült lekérni a meghívókódot',
'Invite code copied to clipboard': 'Meghívókód vágólapra másolva',
'Favicon URL': 'Favicon URL'
'Favicon URL': 'Favicon URL',
'Filter out onion relays': 'Onion relay-ek kiszűrése'
}
}

View File

@@ -542,6 +542,7 @@ export default {
'Failed to get invite code from relay': 'Impossibile ottenere il codice di invito dal relay',
'Failed to get invite code': 'Impossibile ottenere il codice di invito',
'Invite code copied to clipboard': 'Codice di invito copiato negli appunti',
'Favicon URL': 'URL Favicon'
'Favicon URL': 'URL Favicon',
'Filter out onion relays': 'Filtra relay onion'
}
}

View File

@@ -536,6 +536,7 @@ export default {
'Failed to get invite code from relay': 'リレーから招待コードの取得に失敗しました',
'Failed to get invite code': '招待コードの取得に失敗しました',
'Invite code copied to clipboard': '招待コードをクリップボードにコピーしました',
'Favicon URL': 'ファビコンURL'
'Favicon URL': 'ファビコンURL',
'Filter out onion relays': 'Onionリレーを除外'
}
}

View File

@@ -536,6 +536,7 @@ export default {
'Failed to get invite code from relay': '릴레이에서 초대 코드 가져오기 실패',
'Failed to get invite code': '초대 코드 가져오기 실패',
'Invite code copied to clipboard': '초대 코드가 클립보드에 복사되었습니다',
'Favicon URL': '파비콘 URL'
'Favicon URL': '파비콘 URL',
'Filter out onion relays': '어니언 릴레이 필터링'
}
}

View File

@@ -542,6 +542,7 @@ export default {
'Failed to get invite code from relay': 'Nie udało się uzyskać kodu zaproszenia z przekaźnika',
'Failed to get invite code': 'Nie udało się uzyskać kodu zaproszenia',
'Invite code copied to clipboard': 'Kod zaproszenia skopiowany do schowka',
'Favicon URL': 'URL Favicon'
'Favicon URL': 'URL Favicon',
'Filter out onion relays': 'Filtruj przekaźniki onion'
}
}

View File

@@ -539,6 +539,7 @@ export default {
'Failed to get invite code from relay': 'Falha ao obter código de convite do relay',
'Failed to get invite code': 'Falha ao obter código de convite',
'Invite code copied to clipboard': 'Código de convite copiado para a área de transferência',
'Favicon URL': 'URL do Favicon'
'Favicon URL': 'URL do Favicon',
'Filter out onion relays': 'Filtrar relays onion'
}
}

View File

@@ -542,6 +542,7 @@ export default {
'Failed to get invite code from relay': 'Falha ao obter código de convite do relay',
'Failed to get invite code': 'Falha ao obter código de convite',
'Invite code copied to clipboard': 'Código de convite copiado para a área de transferência',
'Favicon URL': 'URL do Favicon'
'Favicon URL': 'URL do Favicon',
'Filter out onion relays': 'Filtrar relays onion'
}
}

View File

@@ -544,6 +544,7 @@ export default {
'Failed to get invite code from relay': 'Не удалось получить код приглашения от релея',
'Failed to get invite code': 'Не удалось получить код приглашения',
'Invite code copied to clipboard': 'Код приглашения скопирован в буфер обмена',
'Favicon URL': 'URL фавикона'
'Favicon URL': 'URL фавикона',
'Filter out onion relays': 'Фильтровать onion-релеи'
}
}

View File

@@ -530,6 +530,7 @@ export default {
'Failed to get invite code from relay': 'ไม่สามารถรับรหัสเชิญจากรีเลย์',
'Failed to get invite code': 'ไม่สามารถรับรหัสเชิญ',
'Invite code copied to clipboard': 'คัดลอกรหัสเชิญไปยังคลิปบอร์ดแล้ว',
'Favicon URL': 'URL ไอคอน'
'Favicon URL': 'URL ไอคอน',
'Filter out onion relays': 'กรองรีเลย์ onion'
}
}

View File

@@ -527,6 +527,7 @@ export default {
'Failed to get invite code from relay': '从中继器获取邀请码失败',
'Failed to get invite code': '获取邀请码失败',
'Invite code copied to clipboard': '邀请码已复制到剪贴板',
'Favicon URL': '网站图标 URL'
'Favicon URL': '网站图标 URL',
'Filter out onion relays': '过滤洋葱中继'
}
}

View File

@@ -5,16 +5,17 @@ import { buildATag } from './draft-event'
import { getReplaceableEventIdentifier } from './event'
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
import { formatPubkey, pubkeyToNpub } from './pubkey'
import { getEmojiInfosFromEmojiTags, generateBech32IdFromETag, tagNameEquals } from './tag'
import { isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url'
import { isTorBrowser } from './utils'
import { generateBech32IdFromETag, getEmojiInfosFromEmojiTags, tagNameEquals } from './tag'
import { isOnionUrl, isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url'
export function getRelayListFromEvent(event?: Event | null) {
export function getRelayListFromEvent(
event?: Event | null,
filterOutOnionRelays: boolean = true
): TRelayList {
if (!event) {
return { write: BIG_RELAY_URLS, read: BIG_RELAY_URLS, originalRelays: [] }
}
const torBrowserDetected = isTorBrowser()
const relayList = { write: [], read: [], originalRelays: [] } as TRelayList
event.tags.filter(tagNameEquals('r')).forEach(([, url, type]) => {
if (!url || !isWebsocketUrl(url)) return
@@ -25,8 +26,7 @@ export function getRelayListFromEvent(event?: Event | null) {
const scope = type === 'read' ? 'read' : type === 'write' ? 'write' : 'both'
relayList.originalRelays.push({ url: normalizedUrl, scope })
// Filter out .onion URLs if not using Tor browser
if (normalizedUrl.endsWith('.onion/') && !torBrowserDetected) return
if (filterOutOnionRelays && isOnionUrl(normalizedUrl)) return
if (type === 'write') {
relayList.write.push(normalizedUrl)

View File

@@ -2,6 +2,15 @@ export function isWebsocketUrl(url: string): boolean {
return /^wss?:\/\/.+$/.test(url)
}
export function isOnionUrl(url: string): boolean {
try {
const hostname = new URL(url).hostname
return hostname.endsWith('.onion')
} catch {
return false
}
}
// copy from nostr-tools/utils
export function normalizeUrl(url: string): string {
try {

View File

@@ -1,14 +1,19 @@
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { DEFAULT_FAVICON_URL_TEMPLATE } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { forwardRef } from 'react'
import storage from '@/services/local-storage.service'
import { forwardRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
const { faviconUrlTemplate, setFaviconUrlTemplate } = useContentPolicy()
const [filterOutOnionRelays, setFilterOutOnionRelays] = useState(
storage.getFilterOutOnionRelays()
)
return (
<SecondaryPageLayout ref={ref} index={index} title={t('System')}>
@@ -25,6 +30,19 @@ const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
placeholder={DEFAULT_FAVICON_URL_TEMPLATE}
/>
</div>
<div className="flex justify-between items-center px-4 min-h-9">
<Label htmlFor="filter-out-onion-relays" className="text-base font-normal">
{t('Filter out onion relays')}
</Label>
<Switch
id="filter-out-onion-relays"
checked={filterOutOnionRelays}
onCheckedChange={(checked) => {
storage.setFilterOutOnionRelays(checked)
setFilterOutOnionRelays(checked)
}}
/>
</div>
</div>
</SecondaryPageLayout>
)

View File

@@ -207,7 +207,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
indexedDb.getReplaceableEvent(account.pubkey, kinds.Pinlist)
])
if (storedRelayListEvent) {
setRelayList(getRelayListFromEvent(storedRelayListEvent))
setRelayList(getRelayListFromEvent(storedRelayListEvent, storage.getFilterOutOnionRelays()))
}
if (storedProfileEvent) {
setProfileEvent(storedProfileEvent)
@@ -237,7 +237,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
authors: [account.pubkey]
})
const relayListEvent = getLatestEvent(relayListEvents) ?? storedRelayListEvent
const relayList = getRelayListFromEvent(relayListEvent)
const relayList = getRelayListFromEvent(relayListEvent, storage.getFilterOutOnionRelays())
if (relayListEvent) {
client.updateRelayListCache(relayListEvent)
await indexedDb.putReplaceableEvent(relayListEvent)
@@ -705,7 +705,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const updateRelayListEvent = async (relayListEvent: Event) => {
const newRelayList = await client.updateRelayListCache(relayListEvent)
setRelayList(getRelayListFromEvent(newRelayList))
setRelayList(getRelayListFromEvent(newRelayList, storage.getFilterOutOnionRelays()))
}
const updateProfileEvent = async (profileEvent: Event) => {

View File

@@ -31,6 +31,7 @@ import {
} from 'nostr-tools'
import { AbstractRelay } from 'nostr-tools/abstract-relay'
import indexedDb from './indexed-db.service'
import storage from './local-storage.service'
type TTimelineRef = [string, number]
@@ -1131,7 +1132,7 @@ class ClientService extends EventTarget {
return relayEvents.map((event) => {
if (event) {
return getRelayListFromEvent(event)
return getRelayListFromEvent(event, storage.getFilterOutOnionRelays())
}
return {
write: BIG_RELAY_URLS,

View File

@@ -10,6 +10,7 @@ import {
} from '@/constants'
import { isSameAccount } from '@/lib/account'
import { randomString } from '@/lib/random'
import { isTorBrowser } from '@/lib/utils'
import {
TAccount,
TAccountPointer,
@@ -54,6 +55,7 @@ class LocalStorageService {
private primaryColor: TPrimaryColor = 'DEFAULT'
private enableSingleColumnLayout: boolean = true
private faviconUrlTemplate: string = DEFAULT_FAVICON_URL_TEMPLATE
private filterOutOnionRelays: boolean = !isTorBrowser()
constructor() {
if (!LocalStorageService.instance) {
@@ -210,6 +212,11 @@ class LocalStorageService {
this.faviconUrlTemplate =
window.localStorage.getItem(StorageKey.FAVICON_URL_TEMPLATE) ?? DEFAULT_FAVICON_URL_TEMPLATE
const filterOutOnionRelaysStr = window.localStorage.getItem(StorageKey.FILTER_OUT_ONION_RELAYS)
if (filterOutOnionRelaysStr) {
this.filterOutOnionRelays = filterOutOnionRelaysStr !== 'false'
}
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@@ -529,6 +536,15 @@ class LocalStorageService {
this.faviconUrlTemplate = template
window.localStorage.setItem(StorageKey.FAVICON_URL_TEMPLATE, template)
}
getFilterOutOnionRelays() {
return this.filterOutOnionRelays
}
setFilterOutOnionRelays(filterOut: boolean) {
this.filterOutOnionRelays = filterOut
window.localStorage.setItem(StorageKey.FILTER_OUT_ONION_RELAYS, filterOut.toString())
}
}
const instance = new LocalStorageService()