feat: add allow insecure connections toggle in system settings

Block ws:// relay connections and http:// resource loading by default
to prevent browser mixed content warnings. When blocked, resources
show clickable URL links instead of error placeholders so users can
open them manually.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
codytseng 2026-03-21 23:12:09 +08:00
parent c9bd7ca7d7
commit 3b4a2ba2d3
29 changed files with 156 additions and 24 deletions

View file

@ -1,6 +1,8 @@
import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import { isInsecureUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import storage from '@/services/local-storage.service'
import mediaManager from '@/services/media-manager.service'
import { Minimize2, Pause, Play, X } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
@ -26,7 +28,7 @@ export default function AudioPlayer({
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [error, setError] = useState(false)
const seekTimeoutRef = useRef<NodeJS.Timeout>()
const seekTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
const isSeeking = useRef(false)
const containerRef = useRef<HTMLDivElement>(null)
@ -121,7 +123,7 @@ export default function AudioPlayer({
}, 300)
}
if (error) {
if (error || (!storage.getAllowInsecureConnection() && isInsecureUrl(src))) {
return <ExternalLink url={src} />
}

View file

@ -1,4 +1,6 @@
import { isInsecureUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import storage from '@/services/local-storage.service'
import { TEmoji } from '@/types'
import { Heart } from 'lucide-react'
import { HTMLAttributes, useState } from 'react'
@ -23,7 +25,7 @@ export default function Emoji({
)
}
if (hasError) {
if (hasError || (!storage.getAllowInsecureConnection() && isInsecureUrl(emoji.url))) {
return (
<span className={cn('whitespace-nowrap', classNames?.text)}>{`:${emoji.shortcode}:`}</span>
)

View file

@ -1,11 +1,14 @@
import { Skeleton } from '@/components/ui/skeleton'
import { isInsecureUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import blossomService from '@/services/blossom.service'
import storage from '@/services/local-storage.service'
import { TImetaInfo } from '@/types'
import { decode } from 'blurhash'
import { ImageOff } from 'lucide-react'
import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react'
import { thumbHashToDataURL } from 'thumbhash'
import ExternalLink from '../ExternalLink'
export default function Image({
image: { url, blurHash, thumbHash, pubkey, dim },
@ -29,14 +32,22 @@ export default function Image({
const [isLoading, setIsLoading] = useState(true)
const [displaySkeleton, setDisplaySkeleton] = useState(true)
const [hasError, setHasError] = useState(false)
const [isBlocked, setIsBlocked] = useState(false)
const [imageUrl, setImageUrl] = useState<string>()
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
setIsLoading(true)
setHasError(false)
setIsBlocked(false)
setDisplaySkeleton(true)
if (!storage.getAllowInsecureConnection() && isInsecureUrl(url)) {
setIsBlocked(true)
setIsLoading(false)
return
}
if (pubkey) {
blossomService.getValidUrl(url, pubkey).then((validUrl) => {
setImageUrl(validUrl)
@ -53,6 +64,10 @@ export default function Image({
}
}, [url])
if (isBlocked) {
return <ExternalLink url={url} />
}
if (hideIfError && hasError) return null
const handleError = async () => {

View file

@ -1,6 +1,8 @@
import { isInsecureUrl } from '@/lib/url'
import { cn, isInViewport } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import storage from '@/services/local-storage.service'
import mediaManager from '@/services/media-manager.service'
import { useEffect, useRef, useState } from 'react'
import ExternalLink from '../ExternalLink'
@ -69,7 +71,7 @@ export default function VideoPlayer({ src, className }: { src: string; className
}
}, [muteMedia])
if (error) {
if (error || (!storage.getAllowInsecureConnection() && isInsecureUrl(src))) {
return <ExternalLink url={src} />
}

View file

@ -1,7 +1,9 @@
import { useFetchWebMetadata } from '@/hooks/useFetchWebMetadata'
import { isInsecureUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import storage from '@/services/local-storage.service'
import { useMemo } from 'react'
import Image from '../Image'
import ExternalLink from '../ExternalLink'
@ -27,6 +29,10 @@ export default function WebPreview({
}
}, [url])
if (!storage.getAllowInsecureConnection() && isInsecureUrl(url)) {
return null
}
if (!autoLoadMedia && !mustLoad) {
return null
}

View file

@ -40,6 +40,7 @@ export const StorageKey = {
ENABLE_SINGLE_COLUMN_LAYOUT: 'enableSingleColumnLayout',
FAVICON_URL_TEMPLATE: 'faviconUrlTemplate',
FILTER_OUT_ONION_RELAYS: 'filterOutOnionRelays',
ALLOW_INSECURE_CONNECTION: 'allowInsecureConnection',
QUICK_REACTION: 'quickReaction',
QUICK_REACTION_EMOJI: 'quickReactionEmoji',
NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy',

View file

@ -1,6 +1,8 @@
import { isInsecureUrl } from '@/lib/url'
import storage from '@/services/local-storage.service'
import webService from '@/services/web.service'
import { TWebMetadata } from '@/types'
import { useEffect, useState } from 'react'
import webService from '@/services/web.service'
export function useFetchWebMetadata(url: string) {
const [metadata, setMetadata] = useState<TWebMetadata>({})
@ -10,6 +12,8 @@ export function useFetchWebMetadata(url: string) {
}
useEffect(() => {
if (!storage.getAllowInsecureConnection() && isInsecureUrl(url)) return
webService.fetchWebMetadata(url).then((metadata) => setMetadata(metadata))
}, [url])

View file

@ -674,6 +674,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'الريلايات المستخدمة للبحث عن الملاحظات (NIP-50)',
'Protected event (NIP-70)': 'حدث محمي (NIP-70)',
'Protected': 'محمي',
'Protected event hint': 'الأحداث المحمية (NIP-70) لا يمكن نشرها إلا من قبل المؤلف. سترفض الخوادم هذه الأحداث من أطراف ثالثة، مما يمنع الآخرين من إعادة بث محتواك. ملاحظة: لا تدعم جميع الخوادم الأحداث المحمية.'
'Protected event hint': 'الأحداث المحمية (NIP-70) لا يمكن نشرها إلا من قبل المؤلف. سترفض الخوادم هذه الأحداث من أطراف ثالثة، مما يمنع الآخرين من إعادة بث محتواك. ملاحظة: لا تدعم جميع الخوادم الأحداث المحمية.',
'Allow insecure connections': 'السماح بالاتصالات غير الآمنة',
'Allow insecure connections description':
'السماح بتحميل موارد http:// والاتصال بمرحلات ws://. قد يؤدي إلى تحذيرات المحتوى المختلط في المتصفح.'
}
}

View file

@ -698,6 +698,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'Relays für die Notizsuche (NIP-50)',
'Protected event (NIP-70)': 'Geschütztes Ereignis (NIP-70)',
'Protected': 'Geschützt',
'Protected event hint': 'Geschützte Ereignisse (NIP-70) können nur vom Autor veröffentlicht werden. Relays lehnen diese Ereignisse von Dritten ab und verhindern so, dass andere Ihre Inhalte weiterverbreiten. Hinweis: Nicht alle Relays unterstützen geschützte Ereignisse.'
'Protected event hint': 'Geschützte Ereignisse (NIP-70) können nur vom Autor veröffentlicht werden. Relays lehnen diese Ereignisse von Dritten ab und verhindern so, dass andere Ihre Inhalte weiterverbreiten. Hinweis: Nicht alle Relays unterstützen geschützte Ereignisse.',
'Allow insecure connections': 'Unsichere Verbindungen zulassen',
'Allow insecure connections description':
'Laden von http://-Ressourcen und Verbindung zu ws://-Relays erlauben. Kann Browser-Warnungen zu gemischten Inhalten auslösen.'
}
}

View file

@ -681,6 +681,9 @@ export default {
'Protected event (NIP-70)': 'Protected event (NIP-70)',
'Protected': 'Protected',
'Protected event hint':
'Protected events (NIP-70) can only be published by the author. Relays will reject these events from third parties, preventing others from rebroadcasting your content. Note: not all relays support protected events.'
'Protected events (NIP-70) can only be published by the author. Relays will reject these events from third parties, preventing others from rebroadcasting your content. Note: not all relays support protected events.',
'Allow insecure connections': 'Allow insecure connections',
'Allow insecure connections description':
'Allow loading http:// resources and connecting to ws:// relays. May trigger browser mixed content warnings.'
}
}

View file

@ -691,6 +691,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'Relés utilizados para buscar notas (NIP-50)',
'Protected event (NIP-70)': 'Evento protegido (NIP-70)',
'Protected': 'Protegido',
'Protected event hint': 'Los eventos protegidos (NIP-70) solo pueden ser publicados por el autor. Los relés rechazarán estos eventos de terceros, evitando que otros redistribuyan tu contenido. Nota: no todos los relés admiten eventos protegidos.'
'Protected event hint': 'Los eventos protegidos (NIP-70) solo pueden ser publicados por el autor. Los relés rechazarán estos eventos de terceros, evitando que otros redistribuyan tu contenido. Nota: no todos los relés admiten eventos protegidos.',
'Allow insecure connections': 'Permitir conexiones inseguras',
'Allow insecure connections description':
'Permitir cargar recursos http:// y conectar a relays ws://. Puede activar advertencias de contenido mixto del navegador.'
}
}

View file

@ -686,6 +686,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'رله‌هایی که برای جستجوی یادداشت‌ها استفاده می‌شوند (NIP-50)',
'Protected event (NIP-70)': 'رویداد محافظت‌شده (NIP-70)',
'Protected': 'محافظت‌شده',
'Protected event hint': 'رویدادهای محافظت‌شده (NIP-70) فقط توسط نویسنده قابل انتشار هستند. رله‌ها این رویدادها را از اشخاص ثالث رد می‌کنند و از بازنشر محتوای شما توسط دیگران جلوگیری می‌کنند. توجه: همه رله‌ها از رویدادهای محافظت‌شده پشتیبانی نمی‌کنند.'
'Protected event hint': 'رویدادهای محافظت‌شده (NIP-70) فقط توسط نویسنده قابل انتشار هستند. رله‌ها این رویدادها را از اشخاص ثالث رد می‌کنند و از بازنشر محتوای شما توسط دیگران جلوگیری می‌کنند. توجه: همه رله‌ها از رویدادهای محافظت‌شده پشتیبانی نمی‌کنند.',
'Allow insecure connections': 'اجازه اتصالات ناامن',
'Allow insecure connections description':
'اجازه بارگذاری منابع http:// و اتصال به رله‌های ws://. ممکن است هشدارهای محتوای مختلط مرورگر را فعال کند.'
}
}

View file

@ -695,6 +695,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'Relais utilisés pour rechercher des notes (NIP-50)',
'Protected event (NIP-70)': 'Événement protégé (NIP-70)',
'Protected': 'Protégé',
'Protected event hint': 'Les événements protégés (NIP-70) ne peuvent être publiés que par l\'auteur. Les relais rejetteront ces événements provenant de tiers, empêchant les autres de rediffuser votre contenu. Remarque : tous les relais ne prennent pas en charge les événements protégés.'
'Protected event hint': 'Les événements protégés (NIP-70) ne peuvent être publiés que par l\'auteur. Les relais rejetteront ces événements provenant de tiers, empêchant les autres de rediffuser votre contenu. Remarque : tous les relais ne prennent pas en charge les événements protégés.',
'Allow insecure connections': 'Autoriser les connexions non sécurisées',
'Allow insecure connections description':
'Autoriser le chargement des ressources http:// et la connexion aux relais ws://. Peut déclencher des avertissements de contenu mixte du navigateur.'
}
}

View file

@ -686,6 +686,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'नोट्स खोजने के लिए उपयोग किए जाने वाले रिले (NIP-50)',
'Protected event (NIP-70)': 'संरक्षित इवेंट (NIP-70)',
'Protected': 'संरक्षित',
'Protected event hint': 'संरक्षित इवेंट (NIP-70) केवल लेखक द्वारा प्रकाशित किए जा सकते हैं। रिले तीसरे पक्ष से इन इवेंट को अस्वीकार कर देंगे, जिससे दूसरों को आपकी सामग्री को पुनः प्रसारित करने से रोका जा सके। नोट: सभी रिले संरक्षित इवेंट का समर्थन नहीं करते।'
'Protected event hint': 'संरक्षित इवेंट (NIP-70) केवल लेखक द्वारा प्रकाशित किए जा सकते हैं। रिले तीसरे पक्ष से इन इवेंट को अस्वीकार कर देंगे, जिससे दूसरों को आपकी सामग्री को पुनः प्रसारित करने से रोका जा सके। नोट: सभी रिले संरक्षित इवेंट का समर्थन नहीं करते।',
'Allow insecure connections': 'असुरक्षित कनेक्शन की अनुमति दें',
'Allow insecure connections description':
'http:// संसाधन लोड करने और ws:// रिले से कनेक्ट करने की अनुमति दें। ब्राउज़र मिश्रित सामग्री चेतावनियाँ ट्रिगर हो सकती हैं।'
}
}

View file

@ -680,6 +680,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'Jegyzetek kereséséhez használt csomópontok (NIP-50)',
'Protected event (NIP-70)': 'Védett esemény (NIP-70)',
'Protected': 'Védett',
'Protected event hint': 'A védett eseményeket (NIP-70) csak a szerző teheti közzé. A csomópontok elutasítják ezeket az eseményeket harmadik felektől, megakadályozva, hogy mások újraközvetítsék a tartalmadat. Megjegyzés: nem minden csomópont támogatja a védett eseményeket.'
'Protected event hint': 'A védett eseményeket (NIP-70) csak a szerző teheti közzé. A csomópontok elutasítják ezeket az eseményeket harmadik felektől, megakadályozva, hogy mások újraközvetítsék a tartalmadat. Megjegyzés: nem minden csomópont támogatja a védett eseményeket.',
'Allow insecure connections': 'Nem biztonságos kapcsolatok engedélyezése',
'Allow insecure connections description':
'http:// erőforrások betöltésének és ws:// relékhez való csatlakozás engedélyezése. Böngésző vegyes tartalom figyelmeztetéseket válthat ki.'
}
}

View file

@ -691,6 +691,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'Relay utilizzati per cercare le note (NIP-50)',
'Protected event (NIP-70)': 'Evento protetto (NIP-70)',
'Protected': 'Protetto',
'Protected event hint': 'Gli eventi protetti (NIP-70) possono essere pubblicati solo dall\'autore. I relay rifiuteranno questi eventi da terze parti, impedendo ad altri di ridiffondere i tuoi contenuti. Nota: non tutti i relay supportano gli eventi protetti.'
'Protected event hint': 'Gli eventi protetti (NIP-70) possono essere pubblicati solo dall\'autore. I relay rifiuteranno questi eventi da terze parti, impedendo ad altri di ridiffondere i tuoi contenuti. Nota: non tutti i relay supportano gli eventi protetti.',
'Allow insecure connections': 'Consenti connessioni non sicure',
'Allow insecure connections description':
'Consenti il caricamento di risorse http:// e la connessione a relay ws://. Potrebbe attivare avvisi di contenuto misto del browser.'
}
}

View file

@ -686,6 +686,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'ノート検索に使用するリレー (NIP-50)',
'Protected event (NIP-70)': '保護されたイベント (NIP-70)',
'Protected': '保護',
'Protected event hint': '保護されたイベントNIP-70は作成者のみが公開できます。リレーは第三者からのこれらのイベントを拒否し、他者によるコンテンツの再配信を防ぎます。 注意:すべてのリレーが保護されたイベントに対応しているわけではありません。'
'Protected event hint': '保護されたイベントNIP-70は作成者のみが公開できます。リレーは第三者からのこれらのイベントを拒否し、他者によるコンテンツの再配信を防ぎます。 注意:すべてのリレーが保護されたイベントに対応しているわけではありません。',
'Allow insecure connections': '安全でない接続を許可',
'Allow insecure connections description':
'http:// リソースの読み込みと ws:// リレーへの接続を許可します。ブラウザの混合コンテンツ警告が表示される場合があります。'
}
}

View file

@ -680,6 +680,9 @@ export default {
'Relays used for searching notes (NIP-50)': '노트 검색에 사용되는 릴레이 (NIP-50)',
'Protected event (NIP-70)': '보호된 이벤트 (NIP-70)',
'Protected': '보호됨',
'Protected event hint': '보호된 이벤트(NIP-70)는 작성자만 게시할 수 있습니다. 릴레이는 제3자의 이벤트를 거부하여 다른 사람이 콘텐츠를 재배포하는 것을 방지합니다. 참고: 모든 릴레이가 보호된 이벤트를 지원하는 것은 아닙니다.'
'Protected event hint': '보호된 이벤트(NIP-70)는 작성자만 게시할 수 있습니다. 릴레이는 제3자의 이벤트를 거부하여 다른 사람이 콘텐츠를 재배포하는 것을 방지합니다. 참고: 모든 릴레이가 보호된 이벤트를 지원하는 것은 아닙니다.',
'Allow insecure connections': '안전하지 않은 연결 허용',
'Allow insecure connections description':
'http:// 리소스 로드 및 ws:// 릴레이 연결을 허용합니다. 브라우저 혼합 콘텐츠 경고가 발생할 수 있습니다.'
}
}

View file

@ -692,6 +692,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'Przekaźniki używane do wyszukiwania notatek (NIP-50)',
'Protected event (NIP-70)': 'Chronione zdarzenie (NIP-70)',
'Protected': 'Chronione',
'Protected event hint': 'Chronione zdarzenia (NIP-70) mogą być publikowane tylko przez autora. Przekaźniki odrzucą te zdarzenia od osób trzecich, uniemożliwiając innym retransmisję Twoich treści. Uwaga: nie wszystkie przekaźniki obsługują chronione zdarzenia.'
'Protected event hint': 'Chronione zdarzenia (NIP-70) mogą być publikowane tylko przez autora. Przekaźniki odrzucą te zdarzenia od osób trzecich, uniemożliwiając innym retransmisję Twoich treści. Uwaga: nie wszystkie przekaźniki obsługują chronione zdarzenia.',
'Allow insecure connections': 'Zezwól na niezabezpieczone połączenia',
'Allow insecure connections description':
'Zezwól na ładowanie zasobów http:// i łączenie z przekaźnikami ws://. Może wywołać ostrzeżenia przeglądarki o mieszanej zawartości.'
}
}

View file

@ -689,6 +689,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'Relays usados para buscar notas (NIP-50)',
'Protected event (NIP-70)': 'Evento protegido (NIP-70)',
'Protected': 'Protegido',
'Protected event hint': 'Eventos protegidos (NIP-70) só podem ser publicados pelo autor. Os relays rejeitarão esses eventos de terceiros, impedindo que outros retransmitam seu conteúdo. Nota: nem todos os relays suportam eventos protegidos.'
'Protected event hint': 'Eventos protegidos (NIP-70) só podem ser publicados pelo autor. Os relays rejeitarão esses eventos de terceiros, impedindo que outros retransmitam seu conteúdo. Nota: nem todos os relays suportam eventos protegidos.',
'Allow insecure connections': 'Permitir conexões inseguras',
'Allow insecure connections description':
'Permitir carregar recursos http:// e conectar a relays ws://. Pode acionar avisos de conteúdo misto do navegador.'
}
}

View file

@ -692,6 +692,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'Relés usados para pesquisar notas (NIP-50)',
'Protected event (NIP-70)': 'Evento protegido (NIP-70)',
'Protected': 'Protegido',
'Protected event hint': 'Eventos protegidos (NIP-70) só podem ser publicados pelo autor. Os relés rejeitarão estes eventos de terceiros, impedindo que outros retransmitam o seu conteúdo. Nota: nem todos os relés suportam eventos protegidos.'
'Protected event hint': 'Eventos protegidos (NIP-70) só podem ser publicados pelo autor. Os relés rejeitarão estes eventos de terceiros, impedindo que outros retransmitam o seu conteúdo. Nota: nem todos os relés suportam eventos protegidos.',
'Allow insecure connections': 'Permitir ligações inseguras',
'Allow insecure connections description':
'Permitir carregar recursos http:// e ligar a relays ws://. Pode acionar avisos de conteúdo misto do navegador.'
}
}

View file

@ -691,6 +691,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'Ретрансляторы для поиска заметок (NIP-50)',
'Protected event (NIP-70)': 'Защищённое событие (NIP-70)',
'Protected': 'Защищённый',
'Protected event hint': 'Защищённые события (NIP-70) могут быть опубликованы только автором. Ретрансляторы отклонят эти события от третьих лиц, предотвращая повторную трансляцию вашего контента. Примечание: не все ретрансляторы поддерживают защищённые события.'
'Protected event hint': 'Защищённые события (NIP-70) могут быть опубликованы только автором. Ретрансляторы отклонят эти события от третьих лиц, предотвращая повторную трансляцию вашего контента. Примечание: не все ретрансляторы поддерживают защищённые события.',
'Allow insecure connections': 'Разрешить небезопасные соединения',
'Allow insecure connections description':
'Разрешить загрузку ресурсов http:// и подключение к реле ws://. Может вызвать предупреждения браузера о смешанном содержимом.'
}
}

View file

@ -676,6 +676,9 @@ export default {
'Relays used for searching notes (NIP-50)': 'รีเลย์ที่ใช้สำหรับค้นหาโน้ต (NIP-50)',
'Protected event (NIP-70)': 'เหตุการณ์ที่ได้รับการป้องกัน (NIP-70)',
'Protected': 'ป้องกัน',
'Protected event hint': 'เหตุการณ์ที่ได้รับการป้องกัน (NIP-70) สามารถเผยแพร่ได้โดยผู้เขียนเท่านั้น รีเลย์จะปฏิเสธเหตุการณ์เหล่านี้จากบุคคลที่สาม ป้องกันไม่ให้ผู้อื่นเผยแพร่เนื้อหาของคุณซ้ำ หมายเหตุ: รีเลย์บางแห่งไม่รองรับเหตุการณ์ที่ได้รับการป้องกัน'
'Protected event hint': 'เหตุการณ์ที่ได้รับการป้องกัน (NIP-70) สามารถเผยแพร่ได้โดยผู้เขียนเท่านั้น รีเลย์จะปฏิเสธเหตุการณ์เหล่านี้จากบุคคลที่สาม ป้องกันไม่ให้ผู้อื่นเผยแพร่เนื้อหาของคุณซ้ำ หมายเหตุ: รีเลย์บางแห่งไม่รองรับเหตุการณ์ที่ได้รับการป้องกัน',
'Allow insecure connections': 'อนุญาตการเชื่อมต่อที่ไม่ปลอดภัย',
'Allow insecure connections description':
'อนุญาตให้โหลดทรัพยากร http:// และเชื่อมต่อกับรีเลย์ ws:// อาจทำให้เบราว์เซอร์แสดงคำเตือนเนื้อหาแบบผสม'
}
}

View file

@ -658,6 +658,9 @@ export default {
'Relays used for searching notes (NIP-50)': '用於搜尋筆記的伺服器 (NIP-50)',
'Protected event (NIP-70)': '受保護的事件 (NIP-70)',
'Protected': '受保護',
'Protected event hint': '受保護的事件NIP-70只能由作者發布。伺服器將拒絕來自第三方的這些事件防止他人轉播你的內容。 注意:並非所有伺服器都支持受保護的事件。'
'Protected event hint': '受保護的事件NIP-70只能由作者發布。伺服器將拒絕來自第三方的這些事件防止他人轉播你的內容。 注意:並非所有伺服器都支持受保護的事件。',
'Allow insecure connections': '允許不安全的連線',
'Allow insecure connections description':
'允許載入 http:// 資源和連線 ws:// relay。可能會觸發瀏覽器混合內容警告。'
}
}

View file

@ -663,6 +663,9 @@ export default {
'Relays used for searching notes (NIP-50)': '用于搜索笔记的服务器 (NIP-50)',
'Protected event (NIP-70)': '受保护的事件 (NIP-70)',
'Protected': '受保护',
'Protected event hint': '受保护的事件NIP-70只能由作者发布。服务器将拒绝来自第三方的这些事件防止他人转播你的内容。 注意:并非所有服务器都支持受保护的事件。'
'Protected event hint': '受保护的事件NIP-70只能由作者发布。服务器将拒绝来自第三方的这些事件防止他人转播你的内容。 注意:并非所有服务器都支持受保护的事件。',
'Allow insecure connections': '允许不安全的连接',
'Allow insecure connections description':
'允许加载 http:// 资源和连接 ws:// relay。可能会触发浏览器混合内容警告。'
}
}

View file

@ -1,5 +1,7 @@
import storage from '@/services/local-storage.service'
import { SimplePool } from 'nostr-tools'
import { AbstractRelay } from 'nostr-tools/abstract-relay'
import { isInsecureUrl } from './url'
const DEFAULT_CONNECTION_TIMEOUT = 10 * 1000 // 10 seconds
const CLEANUP_THRESHOLD = 15 // number of relays to trigger cleanup
@ -17,6 +19,9 @@ export class SmartPool extends SimplePool {
}
ensureRelay(url: string): Promise<AbstractRelay> {
if (!storage.getAllowInsecureConnection() && isInsecureUrl(url)) {
return Promise.reject(new Error(`Insecure relay connection blocked: ${url}`))
}
// If relay is new and we have many relays, trigger cleanup
if (!this.relayIdleTracker.has(url) && this.relayIdleTracker.size > CLEANUP_THRESHOLD) {
this.cleanIdleRelays()

View file

@ -7,6 +7,15 @@ export function isWebsocketUrl(url: string): boolean {
}
}
export function isInsecureUrl(url: string): boolean {
try {
const protocol = new URL(url).protocol
return protocol === 'ws:' || protocol === 'http:'
} catch {
return false
}
}
export function isOnionUrl(url: string): boolean {
try {
const hostname = new URL(url).hostname

View file

@ -16,6 +16,9 @@ const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const [filterOutOnionRelays, setFilterOutOnionRelays] = useState(
storage.getFilterOutOnionRelays()
)
const [allowInsecureConnection, setAllowInsecureConnection] = useState(
storage.getAllowInsecureConnection()
)
return (
<SecondaryPageLayout ref={ref} index={index} title={t('System')}>
@ -45,6 +48,22 @@ const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
}}
/>
</div>
<div className="flex min-h-9 items-center justify-between px-4">
<Label htmlFor="allow-insecure-connection" className="text-base font-normal">
<div>{t('Allow insecure connections')}</div>
<div className="text-muted-foreground">
{t('Allow insecure connections description')}
</div>
</Label>
<Switch
id="allow-insecure-connection"
checked={allowInsecureConnection}
onCheckedChange={(checked) => {
storage.setAllowInsecureConnection(checked)
setAllowInsecureConnection(checked)
}}
/>
</div>
<div className="space-y-2 px-4">
<DefaultRelaysSetting />
</div>

View file

@ -63,6 +63,7 @@ class LocalStorageService {
private enableSingleColumnLayout: boolean = true
private faviconUrlTemplate: string = DEFAULT_FAVICON_URL_TEMPLATE
private filterOutOnionRelays: boolean = !isTorBrowser()
private allowInsecureConnection: boolean = false
private quickReaction: boolean = false
private quickReactionEmoji: string | TEmoji = '+'
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
@ -253,6 +254,9 @@ class LocalStorageService {
this.filterOutOnionRelays = filterOutOnionRelaysStr !== 'false'
}
this.allowInsecureConnection =
window.localStorage.getItem(StorageKey.ALLOW_INSECURE_CONNECTION) === 'true'
this.quickReaction = window.localStorage.getItem(StorageKey.QUICK_REACTION) === 'true'
const quickReactionEmojiStr =
window.localStorage.getItem(StorageKey.QUICK_REACTION_EMOJI) ?? '+'
@ -650,6 +654,15 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.FILTER_OUT_ONION_RELAYS, filterOut.toString())
}
getAllowInsecureConnection() {
return this.allowInsecureConnection
}
setAllowInsecureConnection(allow: boolean) {
this.allowInsecureConnection = allow
window.localStorage.setItem(StorageKey.ALLOW_INSECURE_CONNECTION, allow.toString())
}
getQuickReaction() {
return this.quickReaction
}