Browse Source

turn off outboxes and hints

imwald
Silberengel 3 weeks ago
parent
commit
e455a73f0a
  1. 39
      src/components/MetadataRelaysOnlySetting/index.tsx
  2. 3
      src/components/Relay/index.tsx
  3. 2
      src/constants.ts
  4. 1
      src/hooks/index.tsx
  5. 13
      src/hooks/useBypassMetadataRelaysOnlyPolicy.ts
  6. 5
      src/i18n/locales/de.ts
  7. 3
      src/i18n/locales/en.ts
  8. 10
      src/lib/metadata-policy-curated-relays.test.ts
  9. 56
      src/lib/metadata-policy-curated-relays.ts
  10. 52
      src/lib/read-only-relay-personal.test.ts
  11. 114
      src/lib/read-only-relay-personal.ts
  12. 2
      src/lib/relay-list-sanitize.test.ts
  13. 7
      src/lib/viewer-relay-defaults.ts
  14. 2
      src/pages/primary/ExplorePage/index.tsx
  15. 2
      src/pages/primary/SearchPage/index.tsx
  16. 2
      src/pages/secondary/RelayReviewsPage/index.tsx
  17. 4
      src/pages/secondary/RelaySettingsPage/index.tsx
  18. 2
      src/pages/secondary/SearchPage/index.tsx
  19. 11
      src/services/client-query.service.ts
  20. 35
      src/services/client.service.ts
  21. 26
      src/services/local-storage.service.ts

39
src/components/MetadataRelaysOnlySetting/index.tsx

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import client from '@/services/client.service'
import storage from '@/services/local-storage.service'
import { setRestrictConnectionsToMetadataRelaysOnly } from '@/lib/read-only-relay-personal'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function MetadataRelaysOnlySetting() {
const { t } = useTranslation()
const [enabled, setEnabled] = useState(false)
useEffect(() => {
const on = storage.getRestrictRelaysToMetadataLists()
setEnabled(on)
setRestrictConnectionsToMetadataRelaysOnly(on)
}, [])
const onChange = (checked: boolean) => {
setEnabled(checked)
storage.setRestrictRelaysToMetadataLists(checked)
setRestrictConnectionsToMetadataRelaysOnly(checked)
client.interruptBackgroundQueries({ closePooledRelayConnections: true })
}
return (
<div className="space-y-2 rounded-lg border border-border p-4">
<div className="flex items-center space-x-2">
<Label htmlFor="metadata-relays-only">{t('Only my relay lists')}</Label>
<Switch id="metadata-relays-only" checked={enabled} onCheckedChange={onChange} />
</div>
<div className="text-muted-foreground text-xs max-w-xl">
{t(
'When on, the app only connects to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. It will not open background connections to public mirrors, author outboxes, or other suggested relays.'
)}
</div>
</div>
)
}

3
src/components/Relay/index.tsx

@ -2,7 +2,7 @@ import NormalFeed from '@/components/NormalFeed' @@ -2,7 +2,7 @@ import NormalFeed from '@/components/NormalFeed'
import type { TNoteListRef } from '@/components/NoteList'
import RelayInfo from '@/components/RelayInfo'
import SearchInput from '@/components/SearchInput'
import { useFetchRelayInfo } from '@/hooks'
import { useBypassMetadataRelaysOnlyPolicy, useFetchRelayInfo } from '@/hooks'
import type { TPrimaryPageName } from '@/PageManager'
import { SINGLE_RELAY_KINDLESS_REQ_LIMIT } from '@/constants'
import { canonicalRelaySessionKey, isLocalNetworkUrl, normalizeRelayUrlForPage } from '@/lib/url'
@ -31,6 +31,7 @@ const Relay = forwardRef< @@ -31,6 +31,7 @@ const Relay = forwardRef<
ref
) {
const { t } = useTranslation()
useBypassMetadataRelaysOnlyPolicy()
const { addRelayUrls, removeRelayUrls } = useCurrentRelays()
const { showKinds } = useKindFilterOrDefaults()
const normalizedUrl = useMemo(() => (url ? normalizeRelayUrlForPage(url) : undefined), [url])

2
src/constants.ts

@ -369,6 +369,8 @@ export const StorageKey = { @@ -369,6 +369,8 @@ export const StorageKey = {
SHOW_RSS_FEED: 'showRssFeed',
PANE_MODE: 'paneMode',
ADD_RANDOM_RELAYS_TO_PUBLISH: 'addRandomRelaysToPublish',
/** When `'true'`, only connect to relays on the viewer's NIP-65 / favorites / cache / HTTP lists. */
RESTRICT_RELAYS_TO_METADATA_LISTS: 'restrictRelaysToMetadataLists',
/** When not `'false'`, show green Sonner toasts after successful publishes (default on). */
SHOW_PUBLISH_SUCCESS_TOASTS: 'showPublishSuccessToasts',
/** When not `'false'`, show NIP-53 live activity banner (default on). */

1
src/hooks/index.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
export * from './useBypassMetadataRelaysOnlyPolicy'
export * from './useNearViewport'
export * from './useFetchCalendarRsvps'
export * from './useFetchEvent'

13
src/hooks/useBypassMetadataRelaysOnlyPolicy.ts

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
import {
enterMetadataRelaysOnlyBypass,
leaveMetadataRelaysOnlyBypass
} from '@/lib/read-only-relay-personal'
import { useEffect } from 'react'
/** Disable “only my relay lists” while mounted (relay explore, search, relay directory). */
export function useBypassMetadataRelaysOnlyPolicy(): void {
useEffect(() => {
enterMetadataRelaysOnlyBypass()
return () => leaveMetadataRelaysOnlyBypass()
}, [])
}

5
src/i18n/locales/de.ts

@ -106,7 +106,10 @@ export default { @@ -106,7 +106,10 @@ export default {
"Please log in to view notifications.": "Please log in to view notifications.",
"Follows you": "Folgt dir",
"Relay Settings": "Relay-Einstellungen",
"Relays and Storage Settings": "Relays and Storage Settings",
"Relays and Storage Settings": "Relays und Speicher",
"Only my relay lists": "Nur meine Relay-Listen",
"When on, the app only connects to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. It will not open background connections to public mirrors, author outboxes, or other suggested relays.":
"Wenn aktiv, werden Feeds nicht mehr auf generische öffentliche Leserelays (FAST_READ) oder zufällige Autoren-/Hinweis-Relays erweitert. Deine Relay-Listen, Profil- und Suchindex-Relays, Dokument-Relays und aggr.nostr.land (mit Nostr Land) bleiben aktiv. Relay-Entdecken und Suche sind ausgenommen.",
"Relay set name": "Relay-Set Name",
"Add a new relay set": "Neues Relay-Set hinzufügen",
Add: "Hinzufügen",

3
src/i18n/locales/en.ts

@ -112,6 +112,9 @@ export default { @@ -112,6 +112,9 @@ export default {
"Follows you": "Follows you",
"Relay Settings": "Relays and Storage Settings",
"Relays and Storage Settings": "Relays and Storage Settings",
"Only my relay lists": "Only my relay lists",
"When on, the app only connects to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. It will not open background connections to public mirrors, author outboxes, or other suggested relays.":
"When on, the app stops widening feeds to generic public read relays (FAST_READ) and random author or hint relays. Your relay lists, profile and search index relays, document relays, and aggr.nostr.land (with Nostr Land) still work. Relay explore and Search pages are exempt.",
"Relay set name": "Relay set name",
"Add a new relay set": "Add a new relay set",
Add: "Add",

10
src/lib/metadata-policy-curated-relays.test.ts

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
import { PROFILE_RELAY_URLS } from '@/constants'
import { describe, expect, it } from 'vitest'
import { isMetadataPolicyCuratedRelay } from './metadata-policy-curated-relays'
describe('metadata-policy-curated-relays', () => {
it('recognizes profile relay constants', () => {
expect(isMetadataPolicyCuratedRelay(PROFILE_RELAY_URLS[0]!)).toBe(true)
expect(isMetadataPolicyCuratedRelay('wss://nostr.wirednet.jp/')).toBe(false)
})
})

56
src/lib/metadata-policy-curated-relays.ts

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
import {
BOOKSTR_RELAY_URLS,
DOCUMENT_RELAY_URLS,
FOLLOWS_HISTORY_RELAY_URLS,
GIF_RELAY_URLS,
NIP42_POOL_AUTOMATIC_AUTH_RELAY_URLS,
NIP66_DISCOVERY_RELAY_URLS,
PROFILE_RELAY_URLS,
READ_ONLY_RELAY_URLS,
SEARCHABLE_RELAY_URLS
} from '@/constants'
import { normalizeAnyRelayUrl } from '@/lib/url'
/** App-defined relay stacks (metadata, search, documents, …) — not ad-hoc FAST_READ widening. */
const METADATA_POLICY_CURATED_RELAY_LISTS: readonly (readonly string[])[] = [
PROFILE_RELAY_URLS,
READ_ONLY_RELAY_URLS,
SEARCHABLE_RELAY_URLS,
DOCUMENT_RELAY_URLS,
GIF_RELAY_URLS,
BOOKSTR_RELAY_URLS,
NIP66_DISCOVERY_RELAY_URLS,
FOLLOWS_HISTORY_RELAY_URLS,
NIP42_POOL_AUTOMATIC_AUTH_RELAY_URLS
]
let curatedRelayKeySet: ReadonlySet<string> | null = null
function relayKeyForCuratedSet(url: string): string {
return (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase()
}
function getCuratedRelayKeySet(): ReadonlySet<string> {
if (!curatedRelayKeySet) {
const out = new Set<string>()
for (const list of METADATA_POLICY_CURATED_RELAY_LISTS) {
for (const u of list) {
const key = relayKeyForCuratedSet(u)
if (key) out.add(key)
}
}
curatedRelayKeySet = out
}
return curatedRelayKeySet
}
/** True for relays from specialized constants (profile fetch, read-only indexers, NIP-50, …). */
export function isMetadataPolicyCuratedRelay(url: string): boolean {
const key = relayKeyForCuratedSet(url)
return key.length > 0 && getCuratedRelayKeySet().has(key)
}
/** For tests: reset lazy-built key set after constant changes. */
export function resetMetadataPolicyCuratedRelayKeysForTests(): void {
curatedRelayKeySet = null
}

52
src/lib/read-only-relay-personal.test.ts

@ -5,19 +5,26 @@ import { @@ -5,19 +5,26 @@ import {
buildPersonalRelayKeySet,
filterReadOnlyRelaysUnlessPersonal,
isPersonalListRequiredReadOnlyRelay,
isRelayConnectionAllowedForViewer,
sanitizeRelayUrlsForFetch,
enterMetadataRelaysOnlyBypass,
leaveMetadataRelaysOnlyBypass,
setRestrictConnectionsToMetadataRelaysOnly,
setViewerPersonalRelayKeys
} from './read-only-relay-personal'
import { setViewerBlockedRelayUrls } from './viewer-blocked-relays'
describe('read-only-relay-personal', () => {
beforeEach(() => {
setViewerPersonalRelayKeys(new Set())
setRestrictConnectionsToMetadataRelaysOnly(false)
setViewerPersonalRelayKeys(new Set(), { viewerActive: false })
setViewerBlockedRelayUrls([])
syncViewerRelayStackNostrLandAggrEligible([])
})
afterEach(() => {
setRestrictConnectionsToMetadataRelaysOnly(false)
leaveMetadataRelaysOnlyBypass()
setViewerBlockedRelayUrls([])
syncViewerRelayStackNostrLandAggrEligible([])
})
@ -61,8 +68,49 @@ describe('read-only-relay-personal', () => { @@ -61,8 +68,49 @@ describe('read-only-relay-personal', () => {
})
it('keeps filter.nostr.wine when on the viewer personal list', () => {
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://filter.nostr.wine/']))
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://filter.nostr.wine/']), { viewerActive: true })
const urls = ['wss://relay.damus.io/', 'wss://filter.nostr.wine/']
expect(filterReadOnlyRelaysUnlessPersonal(urls)).toEqual(urls)
})
it('metadata-only policy blocks ad-hoc and FAST_READ bootstrap relays, keeps profile relays', () => {
setRestrictConnectionsToMetadataRelaysOnly(true)
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://relay.example.com/']), { viewerActive: true })
const urls = [
'wss://relay.example.com/',
'wss://profiles.nostr1.com/',
'wss://theforest.nostr1.com/',
'wss://nostr.wirednet.jp/'
]
expect(sanitizeRelayUrlsForFetch(urls)).toEqual([
'wss://relay.example.com/',
'wss://profiles.nostr1.com/'
])
expect(isRelayConnectionAllowedForViewer('wss://profiles.nostr1.com/')).toBe(true)
expect(isRelayConnectionAllowedForViewer('wss://theforest.nostr1.com/')).toBe(false)
expect(isRelayConnectionAllowedForViewer('wss://nostr.wirednet.jp/')).toBe(false)
})
it('metadata-only policy still allows aggr when viewer lists wss://nostr.land', () => {
setRestrictConnectionsToMetadataRelaysOnly(true)
syncViewerRelayStackNostrLandAggrEligible(['wss://nostr.land/'])
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://nostr.land/']), { viewerActive: true })
const urls = ['wss://nostr.land/', AGGR_NOSTR_LAND_WSS, 'wss://nostr.wirednet.jp/']
expect(sanitizeRelayUrlsForFetch(urls).map((u) => u.replace(/\/$/, ''))).toEqual([
'wss://nostr.land',
'wss://aggr.nostr.land',
])
expect(isRelayConnectionAllowedForViewer(AGGR_NOSTR_LAND_WSS)).toBe(true)
expect(isRelayConnectionAllowedForViewer('wss://nostr.wirednet.jp/')).toBe(false)
})
it('metadata-only bypass allows relays outside personal lists', () => {
setRestrictConnectionsToMetadataRelaysOnly(true)
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://nostr.land/']), { viewerActive: true })
enterMetadataRelaysOnlyBypass()
const urls = ['wss://nostr.land/', 'wss://relay.damus.io/']
expect(sanitizeRelayUrlsForFetch(urls)).toEqual(urls)
expect(isRelayConnectionAllowedForViewer('wss://relay.damus.io/')).toBe(true)
leaveMetadataRelaysOnlyBypass()
})
})

114
src/lib/read-only-relay-personal.ts

@ -1,5 +1,15 @@ @@ -1,5 +1,15 @@
import { READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS } from '@/constants'
import { filterAggrNostrLandUnlessViewerEligible } from '@/lib/nostr-land-relay-eligibility'
import {
DEFAULT_FAVORITE_RELAYS,
FAST_READ_RELAY_URLS,
FAST_WRITE_RELAY_URLS,
READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS
} from '@/constants'
import { isMetadataPolicyCuratedRelay } from '@/lib/metadata-policy-curated-relays'
import {
filterAggrNostrLandUnlessViewerEligible,
getViewerRelayStackNostrLandAggrEligible,
relayUrlIsAggrNostrLand
} from '@/lib/nostr-land-relay-eligibility'
import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize'
import { filterViewerBlockedRelaysForFetch } from '@/lib/viewer-blocked-relays'
import { normalizeAnyRelayUrl } from '@/lib/url'
@ -11,6 +21,90 @@ const personalListRequiredKeySet = new Set( @@ -11,6 +21,90 @@ const personalListRequiredKeySet = new Set(
)
let viewerPersonalRelayKeys = new Set<string>()
/** True after a logged-in viewer's personal relay keys were synced (including empty lists). */
let viewerMetadataRelaysPolicyActive = false
let restrictConnectionsToMetadataRelaysOnly = false
/** Relay explore / search UI: metadata-only policy must not narrow relays on those pages. */
let metadataRelaysOnlyBypassDepth = 0
export function setRestrictConnectionsToMetadataRelaysOnly(enabled: boolean): void {
restrictConnectionsToMetadataRelaysOnly = enabled
}
export function isRestrictConnectionsToMetadataRelaysOnly(): boolean {
return restrictConnectionsToMetadataRelaysOnly
}
export function enterMetadataRelaysOnlyBypass(): void {
metadataRelaysOnlyBypassDepth++
}
export function leaveMetadataRelaysOnlyBypass(): void {
metadataRelaysOnlyBypassDepth = Math.max(0, metadataRelaysOnlyBypassDepth - 1)
}
export function isMetadataRelaysOnlyBypassActive(): boolean {
return metadataRelaysOnlyBypassDepth > 0
}
let metadataPolicyBootstrapBlockedKeys: ReadonlySet<string> | null = null
function getMetadataPolicyBootstrapBlockedKeys(): ReadonlySet<string> {
if (!metadataPolicyBootstrapBlockedKeys) {
const out = new Set<string>()
for (const list of [FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, DEFAULT_FAVORITE_RELAYS]) {
for (const u of list) {
const key = relayUrlKey(u)
if (key) out.add(key)
}
}
metadataPolicyBootstrapBlockedKeys = out
}
return metadataPolicyBootstrapBlockedKeys
}
/** True when URL is only a generic bootstrap mirror (FAST_READ / FAST_WRITE / default favorites). */
export function isMetadataPolicyBootstrapRelay(url: string): boolean {
const key = relayUrlKey(url)
return key.length > 0 && getMetadataPolicyBootstrapBlockedKeys().has(key)
}
/** Logged-in viewer with metadata-only mode: block FAST_READ widening, keep curated stacks. */
export function isMetadataRelaysOnlyPolicyActive(): boolean {
return (
restrictConnectionsToMetadataRelaysOnly &&
viewerMetadataRelaysPolicyActive &&
!isMetadataRelaysOnlyBypassActive()
)
}
export function isRelayUrlInViewerMetadataLists(url: string): boolean {
const key = relayUrlKey(url)
return key.length > 0 && viewerPersonalRelayKeys.has(key)
}
/**
* Under metadata-only policy: viewer lists, Nostr Land aggr, and {@link isMetadataPolicyCuratedRelay}
* (profile / read-only / searchable / document stacks). Blocks ad-hoc relays and FAST_READ bootstrap only.
*/
export function isRelayAllowedUnderMetadataOnlyPolicy(url: string): boolean {
if (isRelayUrlInViewerMetadataLists(url)) return true
if (getViewerRelayStackNostrLandAggrEligible() && relayUrlIsAggrNostrLand(url)) return true
if (isMetadataPolicyCuratedRelay(url)) return true
if (isMetadataPolicyBootstrapRelay(url)) return false
return false
}
/** Block WebSocket (and other) pool connects when metadata-only policy is on. */
export function isRelayConnectionAllowedForViewer(url: string): boolean {
if (!isMetadataRelaysOnlyPolicyActive()) return true
return isRelayAllowedUnderMetadataOnlyPolicy(url)
}
function filterToViewerMetadataRelaysOnly(urls: readonly string[]): string[] {
if (!isMetadataRelaysOnlyPolicyActive()) return [...urls]
return urls.filter((u) => isRelayAllowedUnderMetadataOnlyPolicy(u))
}
export function relayUrlKey(url: string): string {
return (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase()
@ -37,8 +131,14 @@ export function buildPersonalRelayKeySet(urls: readonly string[]): Set<string> { @@ -37,8 +131,14 @@ export function buildPersonalRelayKeySet(urls: readonly string[]): Set<string> {
}
/** Updated when the logged-in viewer's NIP-65 / favorites / cache relays hydrate. */
export function setViewerPersonalRelayKeys(keys: ReadonlySet<string>): void {
export function setViewerPersonalRelayKeys(
keys: ReadonlySet<string>,
policy?: { viewerActive?: boolean }
): void {
viewerPersonalRelayKeys = new Set(keys)
if (policy?.viewerActive !== undefined) {
viewerMetadataRelaysPolicyActive = policy.viewerActive
}
}
export function getViewerPersonalRelayKeys(): ReadonlySet<string> {
@ -83,9 +183,11 @@ export function sanitizeRelayUrlsForFetch( @@ -83,9 +183,11 @@ export function sanitizeRelayUrlsForFetch(
const key = relayUrlKey(u)
return key.length > 0 && keys.has(key)
})
return filterViewerBlockedRelaysForFetch(
filterAggrNostrLandUnlessViewerEligible(
filterReadOnlyRelaysUnlessPersonal(withoutThirdPartyLocals, keys)
return filterToViewerMetadataRelaysOnly(
filterViewerBlockedRelaysForFetch(
filterAggrNostrLandUnlessViewerEligible(
filterReadOnlyRelaysUnlessPersonal(withoutThirdPartyLocals, keys)
)
)
)
}

2
src/lib/relay-list-sanitize.test.ts

@ -23,7 +23,7 @@ describe('stripLocalRelaysFromThirdPartyHints', () => { @@ -23,7 +23,7 @@ describe('stripLocalRelaysFromThirdPartyHints', () => {
describe('sanitizeRelayUrlsForFetch', () => {
it('strips third-party locals and unlisted filter.nostr.wine', () => {
setViewerPersonalRelayKeys(new Set())
setViewerPersonalRelayKeys(new Set(), { viewerActive: false })
const urls = ['wss://relay.example.com/', 'ws://127.0.0.1:7777/', 'wss://filter.nostr.wine/']
expect(sanitizeRelayUrlsForFetch(urls)).toEqual(['wss://relay.example.com/'])
})

7
src/lib/viewer-relay-defaults.ts

@ -3,6 +3,7 @@ import { @@ -3,6 +3,7 @@ import {
FAST_READ_RELAY_URLS,
PROFILE_RELAY_URLS
} from '@/constants'
import { isMetadataRelaysOnlyPolicyActive } from '@/lib/read-only-relay-personal'
import { normalizeUrl } from '@/lib/url'
export type ViewerRelayListLike = {
@ -16,11 +17,17 @@ export type ViewerRelayListLike = { @@ -16,11 +17,17 @@ export type ViewerRelayListLike = {
* the user is not signed in, or when they are signed in but have configured neither favorite relays nor a NIP-65
* (kind 10002 / HTTP index) relay list. Otherwise REQ/publish stacks should stay on their own relays.
*/
/** Public read mirrors used when relay lists are empty; empty when metadata-only policy is on. */
export function publicReadRelayFallbackUrls(): readonly string[] {
return isMetadataRelaysOnlyPolicyActive() ? [] : FAST_READ_RELAY_URLS
}
export function viewerUsesGlobalRelayDefaults(args: {
viewerPubkey: string | null | undefined
favoriteRelayUrls: readonly string[]
relayList: ViewerRelayListLike
}): boolean {
if (isMetadataRelaysOnlyPolicyActive()) return false
if (!args.viewerPubkey?.trim()) return true
const hasFavorites = args.favoriteRelayUrls.some((u) => typeof u === 'string' && u.trim().length > 0)
const rl = args.relayList

2
src/pages/primary/ExplorePage/index.tsx

@ -3,6 +3,7 @@ import { buildExplorePopularRelayUrls } from '@/lib/explore-popular-relays' @@ -3,6 +3,7 @@ import { buildExplorePopularRelayUrls } from '@/lib/explore-popular-relays'
import { RefreshButton } from '@/components/RefreshButton'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { syncUserDeletionTombstones } from '@/lib/sync-user-deletions'
import { useBypassMetadataRelaysOnlyPolicy } from '@/hooks/useBypassMetadataRelaysOnlyPolicy'
import { useSmartRelayNavigation } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
@ -61,6 +62,7 @@ function filterMonitoringRelaySuggestions(urls: string[], rawQuery: string): str @@ -61,6 +62,7 @@ function filterMonitoringRelaySuggestions(urls: string[], rawQuery: string): str
}
const ExplorePage = forwardRef<TPageRef>((_, ref) => {
useBypassMetadataRelaysOnlyPolicy()
const { pubkey, relayList } = useNostr()
const layoutRef = useRef<TPageRef>(null)
const [contentRefreshKey, setContentRefreshKey] = useState(0)

2
src/pages/primary/SearchPage/index.tsx

@ -8,10 +8,12 @@ import { useNostr } from '@/providers/NostrProvider' @@ -8,10 +8,12 @@ import { useNostr } from '@/providers/NostrProvider'
import { TPageRef, TSearchParams } from '@/types'
import { BookOpen, Search } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useBypassMetadataRelaysOnlyPolicy } from '@/hooks/useBypassMetadataRelaysOnlyPolicy'
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
const SearchPage = forwardRef<TPageRef>((_props, ref) => {
useBypassMetadataRelaysOnlyPolicy()
const { t } = useTranslation()
const { current, display } = usePrimaryPage()
const { pubkey, relayList } = useNostr()

2
src/pages/secondary/RelayReviewsPage/index.tsx

@ -13,11 +13,13 @@ import { normalizeRelayUrlForPage, simplifyUrl } from '@/lib/url' @@ -13,11 +13,13 @@ import { normalizeRelayUrlForPage, simplifyUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import type { TFeedSubRequest } from '@/types'
import { useBypassMetadataRelaysOnlyPolicy } from '@/hooks/useBypassMetadataRelaysOnlyPolicy'
import { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import NotFoundPage from '../NotFoundPage'
const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url?: string; index?: number; hideTitlebar?: boolean }, ref) => {
useBypassMetadataRelaysOnlyPolicy()
const { t } = useTranslation()
const { registerPrimaryPanelRefresh } = usePrimaryNoteView()
const feedRef = useRef<TNoteListRef>(null)

4
src/pages/secondary/RelaySettingsPage/index.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import CacheRelaysSetting from '@/components/CacheRelaysSetting'
import MetadataRelaysOnlySetting from '@/components/MetadataRelaysOnlySetting'
import HttpRelaysSetting from '@/components/HttpRelaysSetting'
import JsonViewDialog from '@/components/JsonViewDialog'
import MailboxSetting from '@/components/MailboxSetting'
@ -120,6 +121,9 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: @@ -120,6 +121,9 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?:
}
>
<JsonViewDialog value={jsonPayload} isOpen={jsonOpen} onClose={() => setJsonOpen(false)} />
<div className="px-4 pt-3">
<MetadataRelaysOnlySetting />
</div>
<Tabs key={contentKey} value={tabValue} onValueChange={setTabValue} className="px-4 py-3 space-y-4">
<TabsList className="flex-col sm:flex-row h-auto sm:h-9">
<TabsTrigger value="favorite-relays" className="w-full sm:w-auto">{t('Favorite Relays')}</TabsTrigger>

2
src/pages/secondary/SearchPage/index.tsx

@ -11,10 +11,12 @@ import { useNostr } from '@/providers/NostrProvider' @@ -11,10 +11,12 @@ import { useNostr } from '@/providers/NostrProvider'
import { BookOpen } from 'lucide-react'
import { TSearchParams } from '@/types'
import { Button } from '@/components/ui/button'
import { useBypassMetadataRelaysOnlyPolicy } from '@/hooks/useBypassMetadataRelaysOnlyPolicy'
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
useBypassMetadataRelaysOnlyPolicy()
const { t } = useTranslation()
const { registerPrimaryPanelRefresh } = usePrimaryNoteView()
const { push } = useSecondaryPage()

11
src/services/client-query.service.ts

@ -42,6 +42,7 @@ import type { Filter, Event as NEvent } from 'nostr-tools' @@ -42,6 +42,7 @@ import type { Filter, Event as NEvent } from 'nostr-tools'
import { SimplePool, EventTemplate, VerifiedEvent, nip19 } from 'nostr-tools'
import type { AbstractRelay } from 'nostr-tools/abstract-relay'
import { sanitizeRelayUrlsForFetch } from '@/lib/read-only-relay-personal'
import { publicReadRelayFallbackUrls } from '@/lib/viewer-relay-defaults'
import nip66Service from './nip66.service'
import type { ISigner, TSignerType } from '@/types'
@ -803,7 +804,7 @@ export class QueryService { @@ -803,7 +804,7 @@ export class QueryService {
if (relayFiltersUseCapitalLetterTagKeys(filters)) {
relays = relayUrlsStripExtendedTagReqBlocked(relays)
if (relays.length === 0) {
relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS])
relays = relayUrlsStripExtendedTagReqBlocked([...publicReadRelayFallbackUrls()])
}
}
// WebSocket REQ only — drop https URLs (index relays use HTTP polling elsewhere).
@ -1071,7 +1072,7 @@ export class QueryService { @@ -1071,7 +1072,7 @@ export class QueryService {
const originalDedupedRelays = Array.from(new Set(urls))
let relays = originalDedupedRelays
if (relays.length === 0) {
relays = [...FAST_READ_RELAY_URLS]
relays = [...publicReadRelayFallbackUrls()]
}
const filters = Array.isArray(filter) ? filter : [filter]
const stripSocialBlockedRelays =
@ -1082,16 +1083,16 @@ export class QueryService { @@ -1082,16 +1083,16 @@ export class QueryService {
const stripped = relays.filter((url) => !socialKindBlockedSet.has(normalizeUrl(url) || url))
relays = relaysAfterSocialKindBlockedStrip(originalDedupedRelays, stripped)
if (relays.length === 0) {
const fallback = [...FAST_READ_RELAY_URLS].filter(
const fallback = [...publicReadRelayFallbackUrls()].filter(
(url) => !socialKindBlockedSet.has(normalizeUrl(url) || url)
)
relays = fallback.length > 0 ? fallback : [...FAST_READ_RELAY_URLS]
relays = fallback.length > 0 ? fallback : [...publicReadRelayFallbackUrls()]
}
}
if (relayFiltersUseCapitalLetterTagKeys(filters)) {
relays = relayUrlsStripExtendedTagReqBlocked(relays)
if (relays.length === 0) {
relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS])
relays = relayUrlsStripExtendedTagReqBlocked([...publicReadRelayFallbackUrls()])
}
}
const { onevent, ...queryOpts } = options ?? {}

35
src/services/client.service.ts

@ -41,9 +41,15 @@ import { @@ -41,9 +41,15 @@ import {
sanitizeRelayUrlsForFetch,
isReadOnlyIndexerRelay,
isReadOnlyRelayAllowedForViewer,
isRelayConnectionAllowedForViewer,
isMetadataRelaysOnlyPolicyActive,
setViewerPersonalRelayKeys
} from '@/lib/read-only-relay-personal'
import { profileFetchRelayUrlsWithoutFastReadLayer, viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import {
profileFetchRelayUrlsWithoutFastReadLayer,
publicReadRelayFallbackUrls,
viewerUsesGlobalRelayDefaults
} from '@/lib/viewer-relay-defaults'
import {
parseBlockedRelayUrlsFromEvent,
setViewerBlockedRelayUrls
@ -440,6 +446,9 @@ class ClientService extends EventTarget { @@ -440,6 +446,9 @@ class ClientService extends EventTarget {
if (!navigator.onLine && !isLocalNetworkUrl(url)) {
throw new Error(`[offline] skipping non-local relay ${url}`)
}
if (!isRelayConnectionAllowedForViewer(url)) {
throw new Error(`[metadata-relays-only] skipping relay ${url}`)
}
if (!isWebsocketUrl(url) && isKind10243HttpRelayTagUrl(url)) {
throw new Error(`[http-index-relay] ${url} uses the HTTPS index API, not WebSocket`)
}
@ -618,7 +627,7 @@ class ClientService extends EventTarget { @@ -618,7 +627,7 @@ class ClientService extends EventTarget {
const pk = pubkey?.trim() || this.pubkey?.trim()
if (!pk) {
this.viewerHttpIndexRelayBases = []
setViewerPersonalRelayKeys(new Set())
setViewerPersonalRelayKeys(new Set(), { viewerActive: false })
syncViewerRelayStackNostrLandAggrEligible([])
setViewerBlockedRelayUrls([])
return
@ -655,7 +664,7 @@ class ClientService extends EventTarget { @@ -655,7 +664,7 @@ class ClientService extends EventTarget {
} catch {
// ignore
}
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(urls))
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(urls), { viewerActive: true })
syncViewerRelayStackNostrLandAggrEligible(urls)
}
@ -2060,7 +2069,7 @@ class ClientService extends EventTarget { @@ -2060,7 +2069,7 @@ class ClientService extends EventTarget {
if (relayFiltersUseCapitalLetterTagKeys(filter as Filter)) {
relays = relayUrlsStripExtendedTagReqBlocked(relays)
if (relays.length === 0 && navigator.onLine) {
relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS])
relays = relayUrlsStripExtendedTagReqBlocked([...publicReadRelayFallbackUrls()])
}
}
const key = this.generateTimelineKey(relays, filter as Filter)
@ -2490,7 +2499,7 @@ class ClientService extends EventTarget { @@ -2490,7 +2499,7 @@ class ClientService extends EventTarget {
if (relayFiltersUseCapitalLetterTagKeys(filters)) {
relays = relayUrlsStripExtendedTagReqBlocked(relays)
if (relays.length === 0) {
relays = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS])
relays = relayUrlsStripExtendedTagReqBlocked([...publicReadRelayFallbackUrls()])
}
}
relays = Array.from(new Set(relays))
@ -2861,7 +2870,7 @@ class ClientService extends EventTarget { @@ -2861,7 +2870,7 @@ class ClientService extends EventTarget {
if (relayFiltersUseCapitalLetterTagKeys(filter as Filter)) {
wsRelayUrls = relayUrlsStripExtendedTagReqBlocked(wsRelayUrls)
if (wsRelayUrls.length === 0 && navigator.onLine && !relayAuthoritativeTimeline) {
wsRelayUrls = relayUrlsStripExtendedTagReqBlocked([...FAST_READ_RELAY_URLS])
wsRelayUrls = relayUrlsStripExtendedTagReqBlocked([...publicReadRelayFallbackUrls()])
}
}
const timelineUrls = originalDedupedRelays
@ -3376,7 +3385,7 @@ class ClientService extends EventTarget { @@ -3376,7 +3385,7 @@ class ClientService extends EventTarget {
)
let relays = [...wsOriginal]
if (relays.length === 0 && httpRelayBases.length === 0) {
relays = [...FAST_READ_RELAY_URLS]
relays = [...publicReadRelayFallbackUrls()]
}
const filters = Array.isArray(filter) ? filter : [filter]
relays = withDocumentRelayUrlsForFilters(relays, filters)
@ -3392,7 +3401,7 @@ class ClientService extends EventTarget { @@ -3392,7 +3401,7 @@ class ClientService extends EventTarget {
let queryRelays = dedupeNormalizeRelayUrlsOrdered([...relays, ...httpRelayBases])
/** If every candidate was filtered away, still hit public read mirrors so REQ does not no-op. */
if (queryRelays.length === 0) {
queryRelays = dedupeNormalizeRelayUrlsOrdered([...FAST_READ_RELAY_URLS])
queryRelays = dedupeNormalizeRelayUrlsOrdered([...publicReadRelayFallbackUrls()])
}
const events = await this.queryService.query(queryRelays, filter, onevent, {
eoseTimeout,
@ -4507,8 +4516,8 @@ class ClientService extends EventTarget { @@ -4507,8 +4516,8 @@ class ClientService extends EventTarget {
write =
stripped.write.length > 0 ? stripped.write : write.filter(urlIsNonLocalForRemoteViewer)
if (read.length === 0 && write.length === 0) {
read = [...FAST_READ_RELAY_URLS]
write = [...FAST_WRITE_RELAY_URLS]
read = [...publicReadRelayFallbackUrls()]
write = isMetadataRelaysOnlyPolicyActive() ? [] : [...FAST_WRITE_RELAY_URLS]
}
}
return mergeKind10243({
@ -4944,10 +4953,12 @@ class ClientService extends EventTarget { @@ -4944,10 +4953,12 @@ class ClientService extends EventTarget {
// If many websocket connections are initiated simultaneously, it will be
// very slow on Safari (for unknown reason)
if (isSafari()) {
let urls = FAST_READ_RELAY_URLS
let urls = [...publicReadRelayFallbackUrls()]
if (myPubkey) {
const relayList = await this.fetchRelayList(myPubkey)
urls = relayList.read.concat(FAST_READ_RELAY_URLS).slice(0, 5)
urls = isMetadataRelaysOnlyPolicyActive()
? relayList.read.slice(0, 5)
: relayList.read.concat([...publicReadRelayFallbackUrls()]).slice(0, 5)
}
return [{ urls, filter: { authors: pubkeys } }]
}

26
src/services/local-storage.service.ts

@ -8,6 +8,7 @@ import { @@ -8,6 +8,7 @@ import {
} from '@/constants'
import { kinds } from 'nostr-tools'
import { isSameAccount } from '@/lib/account'
import { setRestrictConnectionsToMetadataRelaysOnly } from '@/lib/read-only-relay-personal'
import { randomString } from '@/lib/random'
import {
TAccount,
@ -82,7 +83,8 @@ const SETTINGS_KEYS = [ @@ -82,7 +83,8 @@ const SETTINGS_KEYS = [
StorageKey.RESPECT_QUIET_TAGS,
StorageKey.GLOBAL_QUIET_MODE,
StorageKey.SHOW_RSS_FEED,
StorageKey.PANE_MODE
StorageKey.PANE_MODE,
StorageKey.RESTRICT_RELAYS_TO_METADATA_LISTS
] as const
class LocalStorageService {
@ -131,6 +133,7 @@ class LocalStorageService { @@ -131,6 +133,7 @@ class LocalStorageService {
private addRandomRelaysToPublish: boolean = false
private showPublishSuccessToasts: boolean = true
private showLiveActivitiesBanner: boolean = true
private restrictRelaysToMetadataLists: boolean = false
constructor() {
if (!LocalStorageService.instance) {
@ -460,6 +463,12 @@ class LocalStorageService { @@ -460,6 +463,12 @@ class LocalStorageService {
const showLiveActivitiesStr = window.localStorage.getItem(StorageKey.SHOW_LIVE_ACTIVITIES_BANNER)
this.showLiveActivitiesBanner = showLiveActivitiesStr !== 'false'
const restrictMetadataRelaysStr = window.localStorage.getItem(
StorageKey.RESTRICT_RELAYS_TO_METADATA_LISTS
)
this.restrictRelaysToMetadataLists = restrictMetadataRelaysStr === 'true'
setRestrictConnectionsToMetadataRelaysOnly(this.restrictRelaysToMetadataLists)
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@ -660,6 +669,11 @@ class LocalStorageService { @@ -660,6 +669,11 @@ class LocalStorageService {
if (showRssStr != null) this.showRssFeed = showRssStr === 'true'
const paneStr = get(StorageKey.PANE_MODE)
if (paneStr === 'single' || paneStr === 'double') this.panelMode = paneStr
const restrictMetadataRelaysStr = get(StorageKey.RESTRICT_RELAYS_TO_METADATA_LISTS)
if (restrictMetadataRelaysStr != null) {
this.restrictRelaysToMetadataLists = restrictMetadataRelaysStr === 'true'
setRestrictConnectionsToMetadataRelaysOnly(this.restrictRelaysToMetadataLists)
}
}
getRelaySets() {
@ -1128,6 +1142,16 @@ class LocalStorageService { @@ -1128,6 +1142,16 @@ class LocalStorageService {
this.persistSetting(StorageKey.PANE_MODE, mode)
}
getRestrictRelaysToMetadataLists(): boolean {
return this.restrictRelaysToMetadataLists
}
setRestrictRelaysToMetadataLists(restrict: boolean) {
this.restrictRelaysToMetadataLists = restrict
setRestrictConnectionsToMetadataRelaysOnly(restrict)
this.persistSetting(StorageKey.RESTRICT_RELAYS_TO_METADATA_LISTS, restrict.toString())
}
getAccountNetworkHydrateAt(pubkey: string): number | undefined {
try {
const raw = window.localStorage.getItem(StorageKey.ACCOUNT_NETWORK_HYDRATE_AT_MAP)

Loading…
Cancel
Save