Browse Source

fix relays

imwald
Silberengel 2 weeks ago
parent
commit
e50b27359b
  1. 6
      nip66-cron/index.mjs
  2. 5
      src/components/Embedded/EmbeddedNote.tsx
  3. 41
      src/components/MetadataRelaysOnlySetting/index.tsx
  4. 11
      src/constants.ts
  5. 1
      src/hooks/index.tsx
  6. 13
      src/hooks/useBypassMetadataRelaysOnlyPolicy.ts
  7. 15
      src/hooks/useRelayPageFeedPolicy.ts
  8. 3
      src/i18n/locales/cs.ts
  9. 3
      src/i18n/locales/de.ts
  10. 3
      src/i18n/locales/en.ts
  11. 3
      src/i18n/locales/es.ts
  12. 3
      src/i18n/locales/fr.ts
  13. 3
      src/i18n/locales/nl.ts
  14. 3
      src/i18n/locales/pl.ts
  15. 3
      src/i18n/locales/ru.ts
  16. 3
      src/i18n/locales/tr.ts
  17. 3
      src/i18n/locales/zh.ts
  18. 4
      src/lib/account-list-relay-urls.ts
  19. 2
      src/lib/favorites-feed-relays.ts
  20. 7
      src/lib/metadata-policy-curated-relays.test.ts
  21. 31
      src/lib/metadata-policy-curated-relays.ts
  22. 63
      src/lib/read-only-relay-personal.test.ts
  23. 68
      src/lib/read-only-relay-personal.ts
  24. 50
      src/lib/relay-pool-idle.test.ts
  25. 45
      src/lib/relay-pool-idle.ts
  26. 2
      src/pages/primary/ExplorePage/index.tsx
  27. 2
      src/pages/primary/SearchPage/index.tsx
  28. 2
      src/pages/secondary/RelayReviewsPage/index.tsx
  29. 4
      src/pages/secondary/RelaySettingsPage/index.tsx
  30. 2
      src/pages/secondary/SearchPage/index.tsx
  31. 5
      src/providers/FeedProvider.test.ts
  32. 11
      src/providers/FeedProvider.tsx
  33. 5
      src/providers/NostrProvider/index.tsx
  34. 19
      src/services/client-query.service.ts
  35. 78
      src/services/client.service.ts
  36. 27
      src/services/local-storage.service.ts

6
nip66-cron/index.mjs

@ -328,8 +328,9 @@ async function publishEvent (relayUrls, event) { @@ -328,8 +328,9 @@ async function publishEvent (relayUrls, event) {
let ok = 0
const conns = []
for (const url of relayUrls) {
let ws
try {
const ws = new WebSocket(url, { handshakeTimeout: 8000 })
ws = new WebSocket(url, { handshakeTimeout: 8000 })
await new Promise((resolve, reject) => {
let timeoutId
let resolved = false
@ -374,6 +375,9 @@ async function publishEvent (relayUrls, event) { @@ -374,6 +375,9 @@ async function publishEvent (relayUrls, event) {
})
} catch (err) {
log('Publish relay error', { url, err: err.message })
if (ws) {
try { ws.close() } catch (_) {}
}
}
}
for (const ws of conns) {

5
src/components/Embedded/EmbeddedNote.tsx

@ -23,8 +23,6 @@ import { @@ -23,8 +23,6 @@ import {
urlsForViewerNostrLandAggrEligibilitySync
} from '@/lib/nostr-land-relay-eligibility'
import {
enterMetadataRelaysOnlyBypass,
leaveMetadataRelaysOnlyBypass,
sanitizeRelayUrlsForFetch
} from '@/lib/read-only-relay-personal'
import { useFavoriteRelays } from '@/providers/favorite-relays-context'
@ -305,7 +303,6 @@ function EmbeddedNoteFetched({ @@ -305,7 +303,6 @@ function EmbeddedNoteFetched({
containingEventRef.current = containingEvent
useEffect(() => {
enterMetadataRelaysOnlyBypass()
let cancelled = false
const noteKey = noteId.trim()
embedNoteKeyRef.current = noteKey
@ -386,7 +383,6 @@ function EmbeddedNoteFetched({ @@ -386,7 +383,6 @@ function EmbeddedNoteFetched({
if (eventRef.current) {
return () => {
cancelled = true
leaveMetadataRelaysOnlyBypass()
if (retryIntervalRef.current) {
clearInterval(retryIntervalRef.current)
retryIntervalRef.current = null
@ -406,7 +402,6 @@ function EmbeddedNoteFetched({ @@ -406,7 +402,6 @@ function EmbeddedNoteFetched({
return () => {
cancelled = true
leaveMetadataRelaysOnlyBypass()
if (retryIntervalRef.current) {
clearInterval(retryIntervalRef.current)
retryIntervalRef.current = null

41
src/components/MetadataRelaysOnlySetting/index.tsx

@ -1,41 +0,0 @@ @@ -1,41 +0,0 @@
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { METADATA_RELAYS_ONLY_POLICY_CHANGED_EVENT, setRestrictConnectionsToMetadataRelaysOnly } from '@/lib/read-only-relay-personal'
import client from '@/services/client.service'
import storage from '@/services/local-storage.service'
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 })
client.closeMetadataPolicyDisallowedRelayConnections()
window.dispatchEvent(new CustomEvent(METADATA_RELAYS_ONLY_POLICY_CHANGED_EVENT))
}
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 (default), the app only opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists, plus hard-coded relays only while a query or subscription needs them. Publishing is unchanged. Relay explore and Search pages are exempt.'
)}
</div>
</div>
)
}

11
src/constants.ts

@ -388,7 +388,7 @@ export const StorageKey = { @@ -388,7 +388,7 @@ 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. */
/** @deprecated Removed — personal-relay read policy is always on when logged in. */
RESTRICT_RELAYS_TO_METADATA_LISTS: 'restrictRelaysToMetadataLists',
/** When `'true'`, show Sonner toasts after successful publishes (default off). */
SHOW_PUBLISH_SUCCESS_TOASTS: 'showPublishSuccessToasts',
@ -529,11 +529,8 @@ export const MONERO_NOSTR_RELAY_URLS = [ @@ -529,11 +529,8 @@ export const MONERO_NOSTR_RELAY_URLS = [
/** Relays used for NIP-94 file metadata (kind 1063) / GIF discovery and publish.
* Publish to all of these so GIFs are discoverable across clients; some may be temporarily down. */
export const GIF_RELAY_URLS = [
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://thecitadel.nostr1.com',
'wss://nos.lol',
'wss://nostr.mom'
'wss://gifbuddy.lol'
]
export const SEARCHABLE_RELAY_URLS = [
@ -552,8 +549,8 @@ export const SEARCH_QUERY_DEBOUNCE_MS = 550 @@ -552,8 +549,8 @@ export const SEARCH_QUERY_DEBOUNCE_MS = 550
export const PROFILE_RELAY_URLS = [
'wss://profiles.nostr1.com',
'wss://profiles.nostrver.se/',
'wss://indexer.coracle.social/',
'wss://thecitadel.nostr1.com'
'wss://thecitadel.nostr1.com',
'wss://indexer.coracle.social/'
]
export const FOLLOWS_HISTORY_RELAY_URLS = [

1
src/hooks/index.tsx

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

13
src/hooks/useBypassMetadataRelaysOnlyPolicy.ts

@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
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()
}, [])
}

15
src/hooks/useRelayPageFeedPolicy.ts

@ -1,19 +1,10 @@ @@ -1,19 +1,10 @@
import {
enterMetadataRelaysOnlyBypass,
enterSingleRelayExplicitBrowse,
leaveMetadataRelaysOnlyBypass,
leaveSingleRelayExplicitBrowse
} from '@/lib/read-only-relay-personal'
import { enterSingleRelayExplicitBrowse, leaveSingleRelayExplicitBrowse } from '@/lib/read-only-relay-personal'
import { useEffect } from 'react'
/** Relay detail feed: bypass metadata-only narrowing, user blocks, and session strikes for the page relay. */
/** Relay detail feed: connect to the page relay even if it is not on the viewer's personal lists. */
export function useRelayPageFeedPolicy(): void {
useEffect(() => {
enterMetadataRelaysOnlyBypass()
enterSingleRelayExplicitBrowse()
return () => {
leaveSingleRelayExplicitBrowse()
leaveMetadataRelaysOnlyBypass()
}
return () => leaveSingleRelayExplicitBrowse()
}, [])
}

3
src/i18n/locales/cs.ts

@ -115,9 +115,6 @@ export default { @@ -115,9 +115,6 @@ 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 opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. Publishing is unchanged. Relay explore and Search pages are exempt.':
'When on, read connections stay on your relay lists plus the built-in profile index relays (profiles.nostr1.com, relay.damus.io, etc.). Other relays used for feeds, threads, or search are not contacted unless listed. Publishing is unchanged. 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',

3
src/i18n/locales/de.ts

@ -116,9 +116,6 @@ export default { @@ -116,9 +116,6 @@ export default {
'Follows you': 'Folgt dir',
'Relay Settings': 'Relay-Einstellungen',
'Relays and Storage Settings': 'Relays und Speicher',
'Only my relay lists': 'Nur meine Relay-Listen',
'When on, the app only opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. Publishing is unchanged. Relay explore and Search pages are exempt.':
'Wenn aktiv, bleiben Lese-Verbindungen auf deinen Listen plus den eingebauten Profilindex-Relays (profiles.nostr1.com, relay.damus.io, …). Andere Relays für Feeds, Threads oder Suche werden nur bei Listeneintrag genutzt. Veröffentlichen bleibt unverändert. 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

@ -113,9 +113,6 @@ export default { @@ -113,9 +113,6 @@ 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 opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. Publishing is unchanged. Relay explore and Search pages are exempt.':
'When on, read connections stay on your relay lists plus the built-in profile index relays (profiles.nostr1.com, relay.damus.io, etc.). Other relays used for feeds, threads, or search are not contacted unless listed. Publishing is unchanged. 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',

3
src/i18n/locales/es.ts

@ -115,9 +115,6 @@ export default { @@ -115,9 +115,6 @@ export default {
'Follows you': 'Te sigue',
'Relay Settings': 'Configuración de relés',
'Relays and Storage Settings': 'Relays and Storage Settings',
'Only my relay lists': 'Only my relay lists',
'When on, the app only opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. Publishing is unchanged. Relay explore and Search pages are exempt.':
'When on, read connections stay on your relay lists plus the built-in profile index relays (profiles.nostr1.com, relay.damus.io, etc.). Other relays used for feeds, threads, or search are not contacted unless listed. Publishing is unchanged. Relay explore and Search pages are exempt.',
'Relay set name': 'Nombre del conjunto de relés',
'Add a new relay set': 'Agregar un nuevo conjunto de relés',
Add: 'Agregar',

3
src/i18n/locales/fr.ts

@ -115,9 +115,6 @@ export default { @@ -115,9 +115,6 @@ export default {
'Follows you': 'Vous suit',
'Relay Settings': 'Paramètres des relais',
'Relays and Storage Settings': 'Relays and Storage Settings',
'Only my relay lists': 'Only my relay lists',
'When on, the app only opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. Publishing is unchanged. Relay explore and Search pages are exempt.':
'When on, read connections stay on your relay lists plus the built-in profile index relays (profiles.nostr1.com, relay.damus.io, etc.). Other relays used for feeds, threads, or search are not contacted unless listed. Publishing is unchanged. Relay explore and Search pages are exempt.',
'Relay set name': 'Nom du groupe de relais',
'Add a new relay set': 'Ajouter un nouveau groupe de relais',
Add: 'Ajouter',

3
src/i18n/locales/nl.ts

@ -115,9 +115,6 @@ export default { @@ -115,9 +115,6 @@ 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 opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. Publishing is unchanged. Relay explore and Search pages are exempt.':
'When on, read connections stay on your relay lists plus the built-in profile index relays (profiles.nostr1.com, relay.damus.io, etc.). Other relays used for feeds, threads, or search are not contacted unless listed. Publishing is unchanged. 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',

3
src/i18n/locales/pl.ts

@ -115,9 +115,6 @@ export default { @@ -115,9 +115,6 @@ export default {
'Follows you': 'Obserwujący',
'Relay Settings': 'Ustawienia transmiterów',
'Relays and Storage Settings': 'Relays and Storage Settings',
'Only my relay lists': 'Only my relay lists',
'When on, the app only opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. Publishing is unchanged. Relay explore and Search pages are exempt.':
'When on, read connections stay on your relay lists plus the built-in profile index relays (profiles.nostr1.com, relay.damus.io, etc.). Other relays used for feeds, threads, or search are not contacted unless listed. Publishing is unchanged. Relay explore and Search pages are exempt.',
'Relay set name': 'Wpisz nazwę grupy',
'Add a new relay set': 'Utwórz grupę transmiterów',
Add: 'Dodaj',

3
src/i18n/locales/ru.ts

@ -115,9 +115,6 @@ export default { @@ -115,9 +115,6 @@ export default {
'Follows you': 'Подписан на вас',
'Relay Settings': 'Настройки ретрансляторов',
'Relays and Storage Settings': 'Relays and Storage Settings',
'Only my relay lists': 'Only my relay lists',
'When on, the app only opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. Publishing is unchanged. Relay explore and Search pages are exempt.':
'When on, read connections stay on your relay lists plus the built-in profile index relays (profiles.nostr1.com, relay.damus.io, etc.). Other relays used for feeds, threads, or search are not contacted unless listed. Publishing is unchanged. Relay explore and Search pages are exempt.',
'Relay set name': 'Имя набора ретрансляторов',
'Add a new relay set': 'Добавить новый набор ретрансляторов',
Add: 'Добавить',

3
src/i18n/locales/tr.ts

@ -115,9 +115,6 @@ export default { @@ -115,9 +115,6 @@ 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 opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. Publishing is unchanged. Relay explore and Search pages are exempt.':
'When on, read connections stay on your relay lists plus the built-in profile index relays (profiles.nostr1.com, relay.damus.io, etc.). Other relays used for feeds, threads, or search are not contacted unless listed. Publishing is unchanged. 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',

3
src/i18n/locales/zh.ts

@ -115,9 +115,6 @@ export default { @@ -115,9 +115,6 @@ export default {
'Follows you': '关注了你',
'Relay Settings': '服务器设置',
'Relays and Storage Settings': 'Relays and Storage Settings',
'Only my relay lists': 'Only my relay lists',
'When on, the app only opens read connections to relays on your Read & Write, Favorite, Cache, and HTTP relay lists. Publishing is unchanged. Relay explore and Search pages are exempt.':
'When on, read connections stay on your relay lists plus the built-in profile index relays (profiles.nostr1.com, relay.damus.io, etc.). Other relays used for feeds, threads, or search are not contacted unless listed. Publishing is unchanged. Relay explore and Search pages are exempt.',
'Relay set name': '服务器组名',
'Add a new relay set': '添加新的服务器组',
Add: '添加',

4
src/lib/account-list-relay-urls.ts

@ -4,6 +4,10 @@ import { normalizeRelayUrlByScheme } from '@/lib/url' @@ -4,6 +4,10 @@ import { normalizeRelayUrlByScheme } from '@/lib/url'
import { collectViewerReadInboxUrls } from '@/lib/viewer-read-inboxes'
import { collectViewerWriteOutboxUrls } from '@/lib/viewer-write-outboxes'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import {
viewerIncludeGlobalFastReadRelayLayer,
viewerIncludeGlobalFastWriteRelayLayer
} from '@/lib/read-only-relay-personal'
import client from '@/services/client.service'
/**

2
src/lib/favorites-feed-relays.ts

@ -20,7 +20,7 @@ import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay- @@ -20,7 +20,7 @@ import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay-
import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize'
import { relaySessionStrikes } from '@/lib/relay-strikes'
import { profileFetchRelayUrlsWithoutFastReadLayer } from '@/lib/viewer-relay-defaults'
import { viewerIncludeGlobalFastReadRelayLayer, viewerIncludeGlobalFastWriteRelayLayer } from '@/lib/read-only-relay-personal'
import { viewerIncludeGlobalFastReadRelayLayer } from '@/lib/read-only-relay-personal'
import { getCacheRelayUrlsFromEvent } from '@/lib/private-relays'
import { collectUserReadInboxUrls } from '@/lib/viewer-read-inboxes'
import { collectUserWriteOutboxUrls } from '@/lib/viewer-write-outboxes'

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

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { DOCUMENT_RELAY_URLS, FAST_READ_RELAY_URLS, PROFILE_RELAY_URLS } from '@/constants'
import { describe, expect, it } from 'vitest'
import {
isMetadataPolicyActiveReadGrantRelay,
isMetadataPolicyCuratedRelay,
isMetadataPolicyOperationScopedRelay
} from './metadata-policy-curated-relays'
@ -17,4 +18,10 @@ describe('metadata-policy-curated-relays', () => { @@ -17,4 +18,10 @@ describe('metadata-policy-curated-relays', () => {
expect(isMetadataPolicyOperationScopedRelay(FAST_READ_RELAY_URLS[0]!)).toBe(false)
expect(isMetadataPolicyOperationScopedRelay('wss://nostr.wirednet.jp/')).toBe(false)
})
it('active read grant includes search and discovery stacks', () => {
expect(isMetadataPolicyActiveReadGrantRelay('wss://search.nos.today/')).toBe(true)
expect(isMetadataPolicyActiveReadGrantRelay(FAST_READ_RELAY_URLS[0]!)).toBe(false)
expect(isMetadataPolicyActiveReadGrantRelay('wss://nostr.wirednet.jp/')).toBe(false)
})
})

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

@ -43,6 +43,30 @@ function relayKeyForCuratedSet(url: string): string { @@ -43,6 +43,30 @@ function relayKeyForCuratedSet(url: string): string {
return (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase()
}
/** Relays grantable for the duration of an active read query/subscribe (not general feed widening). */
const METADATA_POLICY_ACTIVE_READ_GRANT_RELAY_LISTS: readonly (readonly string[])[] = [
...METADATA_POLICY_OPERATION_SCOPED_RELAY_LISTS,
SEARCHABLE_RELAY_URLS,
READ_ONLY_RELAY_URLS,
NIP66_DISCOVERY_RELAY_URLS
]
let activeReadGrantRelayKeySet: ReadonlySet<string> | null = null
function getActiveReadGrantRelayKeySet(): ReadonlySet<string> {
if (!activeReadGrantRelayKeySet) {
const out = new Set<string>()
for (const list of METADATA_POLICY_ACTIVE_READ_GRANT_RELAY_LISTS) {
for (const u of list) {
const key = relayKeyForCuratedSet(u)
if (key) out.add(key)
}
}
activeReadGrantRelayKeySet = out
}
return activeReadGrantRelayKeySet
}
function getCuratedRelayKeySet(): ReadonlySet<string> {
if (!curatedRelayKeySet) {
const out = new Set<string>()
@ -83,6 +107,12 @@ export function isMetadataPolicyOperationScopedRelay(url: string): boolean { @@ -83,6 +107,12 @@ export function isMetadataPolicyOperationScopedRelay(url: string): boolean {
return key.length > 0 && getOperationScopedRelayKeySet().has(key)
}
/** Search / index / discovery stacks allowed only while an active read operation lists them. */
export function isMetadataPolicyActiveReadGrantRelay(url: string): boolean {
const key = relayKeyForCuratedSet(url)
return key.length > 0 && getActiveReadGrantRelayKeySet().has(key)
}
let profileRelayKeySet: ReadonlySet<string> | null = null
function getProfileRelayKeySet(): ReadonlySet<string> {
@ -107,5 +137,6 @@ export function isMetadataPolicyProfileRelay(url: string): boolean { @@ -107,5 +137,6 @@ export function isMetadataPolicyProfileRelay(url: string): boolean {
export function resetMetadataPolicyCuratedRelayKeysForTests(): void {
curatedRelayKeySet = null
operationScopedRelayKeySet = null
activeReadGrantRelayKeySet = null
profileRelayKeySet = null
}

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

@ -9,19 +9,15 @@ import { @@ -9,19 +9,15 @@ import {
isRelayConnectionAllowedForViewer,
resetRelayConnectionOperationScopeForTests,
sanitizeRelayUrlsForFetch,
enterMetadataRelaysOnlyBypass,
enterSingleRelayExplicitBrowse,
enterSingleRelayExplicitFetchScope,
leaveMetadataRelaysOnlyBypass,
leaveSingleRelayExplicitBrowse,
setRestrictConnectionsToMetadataRelaysOnly,
setViewerPersonalRelayKeys
} from './read-only-relay-personal'
import { setViewerBlockedRelayUrls } from './viewer-blocked-relays'
describe('read-only-relay-personal', () => {
beforeEach(() => {
setRestrictConnectionsToMetadataRelaysOnly(false)
setViewerPersonalRelayKeys(new Set(), { viewerActive: false })
setViewerBlockedRelayUrls([])
syncViewerRelayStackNostrLandAggrEligible([])
@ -29,9 +25,8 @@ describe('read-only-relay-personal', () => { @@ -29,9 +25,8 @@ describe('read-only-relay-personal', () => {
})
afterEach(() => {
setRestrictConnectionsToMetadataRelaysOnly(false)
leaveMetadataRelaysOnlyBypass()
leaveSingleRelayExplicitBrowse()
setViewerPersonalRelayKeys(new Set(), { viewerActive: false })
setViewerBlockedRelayUrls([])
syncViewerRelayStackNostrLandAggrEligible([])
resetRelayConnectionOperationScopeForTests()
@ -81,8 +76,7 @@ describe('read-only-relay-personal', () => { @@ -81,8 +76,7 @@ describe('read-only-relay-personal', () => {
expect(filterReadOnlyRelaysUnlessPersonal(urls)).toEqual(urls)
})
it('metadata-only policy blocks ad-hoc feed relays at connect time', () => {
setRestrictConnectionsToMetadataRelaysOnly(true)
it('personal-relay policy blocks ad-hoc feed relays at connect time', () => {
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://relay.example.com/']), { viewerActive: true })
const urls = [
'wss://relay.example.com/',
@ -90,16 +84,18 @@ describe('read-only-relay-personal', () => { @@ -90,16 +84,18 @@ describe('read-only-relay-personal', () => {
'wss://theforest.nostr1.com/',
'wss://nostr.wirednet.jp/'
]
expect(sanitizeRelayUrlsForFetch(urls)).toEqual(['wss://relay.example.com/'])
expect(isRelayConnectionAllowedForViewer('wss://profiles.nostr1.com/')).toBe(false)
expect(isRelayConnectionAllowedForViewer('wss://thecitadel.nostr1.com/')).toBe(false)
expect(sanitizeRelayUrlsForFetch(urls)).toEqual([
'wss://relay.example.com/',
'wss://profiles.nostr1.com/'
])
expect(isRelayConnectionAllowedForViewer('wss://profiles.nostr1.com/')).toBe(true)
expect(isRelayConnectionAllowedForViewer('wss://thecitadel.nostr1.com/')).toBe(true)
expect(isRelayConnectionAllowedForViewer('wss://relay.example.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)
it('personal-relay policy still allows aggr when viewer lists wss://nostr.land', () => {
syncViewerRelayStackNostrLandAggrEligible(['wss://nostr.land/'])
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://nostr.land/']), { viewerActive: true })
const urls = ['wss://nostr.land/', AGGR_NOSTR_LAND_WSS, 'wss://nostr.wirednet.jp/']
@ -112,33 +108,30 @@ describe('read-only-relay-personal', () => { @@ -112,33 +108,30 @@ describe('read-only-relay-personal', () => {
})
it('operation scope allows document and gif constant relays during fetch', () => {
setRestrictConnectionsToMetadataRelaysOnly(true)
setViewerPersonalRelayKeys(new Set(), { viewerActive: true })
expect(isRelayConnectionAllowedForViewer('wss://thecitadel.nostr1.com/')).toBe(false)
expect(isRelayConnectionAllowedForViewer('wss://nostr.wine/')).toBe(false)
expect(isRelayConnectionAllowedForViewer('wss://theforest.nostr1.com/')).toBe(false)
expect(isRelayConnectionAllowedForViewer('wss://nostr.wirednet.jp/')).toBe(false)
const revoke = grantRelayConnectionOperationScope([
'wss://thecitadel.nostr1.com/',
'wss://nostr.wine/',
'wss://nostr.wirednet.jp/',
'wss://essayist.decentnewsroom.com/'
])
expect(isRelayConnectionAllowedForViewer('wss://thecitadel.nostr1.com/')).toBe(true)
expect(isRelayConnectionAllowedForViewer('wss://nostr.wine/')).toBe(false)
expect(isRelayConnectionAllowedForViewer('wss://nostr.wirednet.jp/')).toBe(false)
expect(isRelayConnectionAllowedForViewer('wss://essayist.decentnewsroom.com/')).toBe(true)
revoke()
})
it('metadata-only policy allows curated relays only during an operation scope', () => {
setRestrictConnectionsToMetadataRelaysOnly(true)
it('personal-relay policy allows document relays only during an operation scope', () => {
setViewerPersonalRelayKeys(new Set(), { viewerActive: true })
expect(isRelayConnectionAllowedForViewer('wss://profiles.nostr1.com/')).toBe(false)
const revoke = grantRelayConnectionOperationScope(['wss://profiles.nostr1.com/'])
expect(isRelayConnectionAllowedForViewer('wss://profiles.nostr1.com/')).toBe(true)
expect(isRelayConnectionAllowedForViewer('wss://essayist.decentnewsroom.com/')).toBe(false)
const revoke = grantRelayConnectionOperationScope(['wss://essayist.decentnewsroom.com/'])
expect(isRelayConnectionAllowedForViewer('wss://essayist.decentnewsroom.com/')).toBe(true)
revoke()
expect(isRelayConnectionAllowedForViewer('wss://profiles.nostr1.com/')).toBe(false)
expect(isRelayConnectionAllowedForViewer('wss://essayist.decentnewsroom.com/')).toBe(false)
})
it('metadata-only policy allows viewer cache and HTTP index relays', () => {
setRestrictConnectionsToMetadataRelaysOnly(true)
it('personal-relay policy allows viewer cache and HTTP index relays', () => {
setViewerPersonalRelayKeys(
buildPersonalRelayKeySet([
'ws://localhost:4869/',
@ -152,18 +145,15 @@ describe('read-only-relay-personal', () => { @@ -152,18 +145,15 @@ describe('read-only-relay-personal', () => {
expect(isRelayConnectionAllowedForViewer('wss://theforest.nostr1.com/')).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()
it('personal-relay policy allows profile index relays without operation scope', () => {
setViewerPersonalRelayKeys(new Set(), { viewerActive: true })
expect(isRelayConnectionAllowedForViewer('wss://profiles.nostr1.com/')).toBe(true)
expect(sanitizeRelayUrlsForFetch(['wss://profiles.nostr1.com/', 'wss://nostr.wirednet.jp/'])).toEqual([
'wss://profiles.nostr1.com/'
])
})
it('explicit single-relay browse keeps user-blocked and non-list relays', () => {
setRestrictConnectionsToMetadataRelaysOnly(true)
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://nostr.land/']), { viewerActive: true })
setViewerBlockedRelayUrls(['wss://relay.layer.systems/'])
enterSingleRelayExplicitBrowse()
@ -173,8 +163,7 @@ describe('read-only-relay-personal', () => { @@ -173,8 +163,7 @@ describe('read-only-relay-personal', () => {
leaveSingleRelayExplicitBrowse()
})
it('operation scope grants an explicit single-relay target under metadata-only', () => {
setRestrictConnectionsToMetadataRelaysOnly(true)
it('operation scope grants an explicit single-relay target under personal-relay policy', () => {
setViewerPersonalRelayKeys(new Set(), { viewerActive: true })
const leaveFetchScope = enterSingleRelayExplicitFetchScope()
const target = 'wss://relay.layer.systems/'

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

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import {
READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS
} from '@/constants'
import { isMetadataPolicyOperationScopedRelay } from '@/lib/metadata-policy-curated-relays'
import { isMetadataPolicyActiveReadGrantRelay, isMetadataPolicyProfileRelay } from '@/lib/metadata-policy-curated-relays'
import {
filterAggrNostrLandUnlessViewerEligible,
getViewerRelayStackNostrLandAggrEligible,
@ -20,39 +20,13 @@ const personalListRequiredKeySet = new Set( @@ -20,39 +20,13 @@ 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
/** Relay detail page mounted: explicit single-relay browse must not be blocked by strikes / user blocks / list gates. */
let singleRelayExplicitBrowseDepth = 0
/** In-flight authoritative single-relay timeline REQ (see {@link enterSingleRelayExplicitFetchScope}). */
let singleRelayExplicitFetchDepth = 0
/** In-flight query/subscribe URLs (constants + caller stack) allowed to connect briefly under metadata-only policy. */
/** In-flight query/subscribe URLs allowed to connect briefly under the personal-relay read policy. */
const operationScopedRelayKeys = new Set<string>()
/** Dispatched when metadata-only relay policy toggles (feeds should rebuild relay URL lists). */
export const METADATA_RELAYS_ONLY_POLICY_CHANGED_EVENT = 'jumble:metadata-relays-only-changed'
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
}
export function enterSingleRelayExplicitBrowse(): void {
singleRelayExplicitBrowseDepth++
}
@ -91,13 +65,12 @@ function shouldPreserveExplicitSingleRelay( @@ -91,13 +65,12 @@ function shouldPreserveExplicitSingleRelay(
return isSingleRelayExplicitPolicyActive()
}
/** Logged-in viewer with metadata-only mode: only connect reads to the viewer's relay lists. */
/**
* Logged-in viewer: only connect reads to personal relay lists, aggr when eligible,
* operation-scoped URLs, and explicit single-relay browse.
*/
export function isMetadataRelaysOnlyPolicyActive(): boolean {
return (
restrictConnectionsToMetadataRelaysOnly &&
viewerMetadataRelaysPolicyActive &&
!isMetadataRelaysOnlyBypassActive()
)
return viewerMetadataRelaysPolicyActive
}
export function isRelayUrlInViewerMetadataLists(url: string): boolean {
@ -106,12 +79,13 @@ export function isRelayUrlInViewerMetadataLists(url: string): boolean { @@ -106,12 +79,13 @@ export function isRelayUrlInViewerMetadataLists(url: string): boolean {
}
/**
* Under metadata-only policy: viewer NIP-65 / favorites / cache / HTTP lists, plus aggr.nostr.land when
* wss://nostr.land is listed, plus relays in an active {@link grantRelayConnectionOperationScope}.
* Under personal-relay read policy: viewer NIP-65 / favorites / cache / HTTP lists, aggr.nostr.land when
* wss://nostr.land is listed, {@link PROFILE_RELAY_URLS}, plus relays in an active operation scope.
*/
export function isRelayAllowedUnderMetadataOnlyPolicy(url: string): boolean {
if (isRelayUrlInViewerMetadataLists(url)) return true
if (getViewerRelayStackNostrLandAggrEligible() && relayUrlIsAggrNostrLand(url)) return true
if (isMetadataPolicyProfileRelay(url)) return true
const key = relayUrlKey(url)
if (key.length > 0 && operationScopedRelayKeys.has(key)) return true
return false
@ -119,21 +93,17 @@ export function isRelayAllowedUnderMetadataOnlyPolicy(url: string): boolean { @@ -119,21 +93,17 @@ export function isRelayAllowedUnderMetadataOnlyPolicy(url: string): boolean {
/**
* Allow read connects to non-personal relays only for the lifetime of an in-flight query/subscribe.
* Under metadata-only policy, only {@link isMetadataPolicyOperationScopedRelay} URLs are granted
* (document / GIF / profile stacks not FAST_READ or feed widening).
* Call with the caller's intended URL list **before** {@link sanitizeRelayUrlsForFetch}.
*/
export function grantRelayConnectionOperationScope(urls: readonly string[]): () => void {
if (!isMetadataRelaysOnlyPolicyActive()) return () => {}
const singleExplicit = urls.length === 1 && isSingleRelayExplicitPolicyActive()
const added: string[] = []
for (const raw of urls) {
if (isRelayUrlInViewerMetadataLists(raw)) continue
if (getViewerRelayStackNostrLandAggrEligible() && relayUrlIsAggrNostrLand(raw)) continue
if (
!isMetadataPolicyOperationScopedRelay(raw) &&
!(urls.length === 1 && isSingleRelayExplicitPolicyActive())
) {
continue
}
if (isMetadataPolicyProfileRelay(raw)) continue
if (!isMetadataPolicyActiveReadGrantRelay(raw) && !singleExplicit) continue
const key = relayUrlKey(raw)
if (!key || operationScopedRelayKeys.has(key)) continue
operationScopedRelayKeys.add(key)
@ -149,7 +119,7 @@ export function resetRelayConnectionOperationScopeForTests(): void { @@ -149,7 +119,7 @@ export function resetRelayConnectionOperationScopeForTests(): void {
operationScopedRelayKeys.clear()
}
/** Block read-side pool connects / HTTP index fetches when metadata-only policy is on. */
/** Block read-side pool connects / HTTP index fetches when personal-relay policy is on. */
export function isRelayConnectionAllowedForViewer(url: string): boolean {
if (isSingleRelayExplicitPolicyActive()) return true
if (!isMetadataRelaysOnlyPolicyActive()) return true
@ -207,7 +177,7 @@ function isAllowedForKeys(url: string, personalKeys: ReadonlySet<string>): boole @@ -207,7 +177,7 @@ function isAllowedForKeys(url: string, personalKeys: ReadonlySet<string>): boole
return key.length > 0 && personalKeys.has(key)
}
/** Under metadata-only policy: viewer relay lists + aggr.nostr.land when nostr.land is listed. */
/** Under personal-relay policy: viewer lists + aggr + profile index + in-flight operation scope. */
function filterRelayUrlsToMetadataOnlyPersonalLists(
urls: readonly string[],
personalKeys: ReadonlySet<string>
@ -216,16 +186,18 @@ function filterRelayUrlsToMetadataOnlyPersonalLists( @@ -216,16 +186,18 @@ function filterRelayUrlsToMetadataOnlyPersonalLists(
const key = relayUrlKey(u)
if (key.length > 0 && personalKeys.has(key)) return true
if (getViewerRelayStackNostrLandAggrEligible() && relayUrlIsAggrNostrLand(u)) return true
if (isMetadataPolicyProfileRelay(u)) return true
if (key.length > 0 && operationScopedRelayKeys.has(key)) return true
return false
})
}
/** When metadata-only is on, omit global FAST_READ / trending widening from REQ stacks. */
/** Omit global FAST_READ / trending widening from REQ stacks under personal-relay policy. */
export function viewerIncludeGlobalFastReadRelayLayer(): boolean {
return !isMetadataRelaysOnlyPolicyActive()
}
/** When metadata-only is on, omit {@link FAST_WRITE_RELAY_URLS} from read-side merge/fetch stacks (publish unchanged). */
/** Omit {@link FAST_WRITE_RELAY_URLS} from read-side merge/fetch stacks (publish unchanged). */
export function viewerIncludeGlobalFastWriteRelayLayer(): boolean {
return !isMetadataRelaysOnlyPolicyActive()
}

50
src/lib/relay-pool-idle.test.ts

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
import { describe, expect, it, beforeEach, afterEach } from 'vitest'
import { PROFILE_RELAY_URLS } from '@/constants'
import { setViewerPersonalRelayKeys } from '@/lib/read-only-relay-personal'
import {
closePublishTransientRelaySockets,
initRelayPoolIdle,
resetRelayPoolIdleForTests
} from './relay-pool-idle'
describe('relay-pool-idle publish cleanup', () => {
const closed: string[] = []
const pool = {
listConnectionStatus: () =>
new Map([
['wss://author-outbox.example/', true],
['wss://relay.example.com/', true],
[PROFILE_RELAY_URLS[0]!, true]
]),
close: (urls: string[]) => {
closed.push(...urls)
}
}
beforeEach(() => {
closed.length = 0
setViewerPersonalRelayKeys(new Set(['wss://relay.example.com/']), { viewerActive: true })
initRelayPoolIdle(pool as never, () => false)
})
afterEach(() => {
resetRelayPoolIdleForTests()
setViewerPersonalRelayKeys(new Set(), { viewerActive: false })
})
it('closes author/random publish relays but keeps personal and profile index', () => {
closePublishTransientRelaySockets([
'wss://author-outbox.example/',
'wss://relay.example.com/',
PROFILE_RELAY_URLS[0]!
])
expect(closed).toEqual(['wss://author-outbox.example/'])
})
it('skips relays that still have active subscriptions', () => {
resetRelayPoolIdleForTests()
initRelayPoolIdle(pool as never, (key) => key.includes('author-outbox'))
closePublishTransientRelaySockets(['wss://author-outbox.example/'])
expect(closed).toEqual([])
})
})

45
src/lib/relay-pool-idle.ts

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
import { RELAY_POOL_IDLE_SWEEP_INTERVAL_MS, RELAY_POOL_SOCKET_IDLE_MS } from '@/constants'
import { isMetadataPolicyProfileRelay } from '@/lib/metadata-policy-curated-relays'
import { isRelayUrlInViewerMetadataLists } from '@/lib/read-only-relay-personal'
import logger from '@/lib/logger'
import { canonicalRelaySessionKey, normalizeAnyRelayUrl } from '@/lib/url'
import type { SimplePool } from 'nostr-tools'
@ -14,6 +16,10 @@ function canon(url: string): string { @@ -14,6 +16,10 @@ function canon(url: string): string {
return canonicalRelaySessionKey(normalizeAnyRelayUrl(url) || url.trim())
}
function shouldKeepProfileRelaySocketOpen(url: string): boolean {
return isMetadataPolicyProfileRelay(url)
}
/** Mark relay URL as recently used (connect, REQ, publish). */
export function touchRelayPoolActivity(url: string): void {
const key = canon(url)
@ -49,6 +55,7 @@ export function sweepIdleRelayPoolSockets(): void { @@ -49,6 +55,7 @@ export function sweepIdleRelayPoolSockets(): void {
if (!connected) continue
const key = canon(url)
if (!key) continue
if (shouldKeepProfileRelaySocketOpen(url)) continue
if (hasActiveSubs(key)) continue
const last = lastActivityMs.get(key) ?? 0
if (now - last < RELAY_POOL_SOCKET_IDLE_MS) continue
@ -81,6 +88,7 @@ export function closeRelayPoolSocketsIfIdle(urls: readonly string[]): void { @@ -81,6 +88,7 @@ export function closeRelayPoolSocketsIfIdle(urls: readonly string[]): void {
for (const raw of urls) {
const key = canon(raw)
if (!key || hasActiveSubs(key)) continue
if (shouldKeepProfileRelaySocketOpen(raw)) continue
const normalized = normalizeAnyRelayUrl(raw) || raw
const connected = [...status.entries()].some(
([u, ok]) => ok && canon(u) === key
@ -96,6 +104,43 @@ export function closeRelayPoolSocketsIfIdle(urls: readonly string[]): void { @@ -96,6 +104,43 @@ export function closeRelayPoolSocketsIfIdle(urls: readonly string[]): void {
}
}
/**
* After publish: drop sockets opened for author outboxes, random NIP-66 picks, and other non-personal
* targets. Keeps profile index relays and the viewer's own list relays connected.
*/
export function closePublishTransientRelaySockets(urls: readonly string[]): void {
if (!pool || !hasActiveSubs || urls.length === 0) return
let status: Map<string, boolean>
try {
status = pool.listConnectionStatus()
} catch {
return
}
const toClose: string[] = []
for (const raw of urls) {
if (isRelayUrlInViewerMetadataLists(raw)) continue
if (isMetadataPolicyProfileRelay(raw)) continue
const key = canon(raw)
if (!key || hasActiveSubs(key)) continue
const normalized = normalizeAnyRelayUrl(raw) || raw
const connected = [...status.entries()].some(
([u, ok]) => ok && canon(u) === key
)
if (connected) toClose.push(normalized)
}
if (toClose.length === 0) return
try {
pool.close(toClose)
logger.debug('[RelayPoolIdle] closed publish transient sockets', { relays: toClose })
} catch {
/* ignore */
}
for (const url of toClose) {
lastActivityMs.delete(canon(url))
}
}
export function resetRelayPoolIdleForTests(): void {
if (sweepTimer != null) {
clearInterval(sweepTimer)

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

@ -3,7 +3,6 @@ import { buildExplorePopularRelayUrls } from '@/lib/explore-popular-relays' @@ -3,7 +3,6 @@ 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'
@ -62,7 +61,6 @@ function filterMonitoringRelaySuggestions(urls: string[], rawQuery: string): str @@ -62,7 +61,6 @@ 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,12 +8,10 @@ import { useNostr } from '@/providers/NostrProvider' @@ -8,12 +8,10 @@ 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

@ -10,13 +10,11 @@ import { normalizeRelayUrlForPage, simplifyUrl } from '@/lib/url' @@ -10,13 +10,11 @@ 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,5 +1,4 @@ @@ -1,5 +1,4 @@
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'
@ -121,9 +120,6 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: @@ -121,9 +120,6 @@ 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,12 +11,10 @@ import { useNostr } from '@/providers/NostrProvider' @@ -11,12 +11,10 @@ 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()

5
src/providers/FeedProvider.test.ts

@ -4,7 +4,6 @@ import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr' @@ -4,7 +4,6 @@ import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr'
import { buildAllFavoritesFeedRelayUrls, stripNostrLandAggrFromRelayUrls } from '@/lib/home-feed-relays'
import { buildWispTrendingNotesRelayUrl, isWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import {
setRestrictConnectionsToMetadataRelaysOnly,
setViewerPersonalRelayKeys
} from '@/lib/read-only-relay-personal'
@ -43,14 +42,12 @@ describe('home feed relay policy', () => { @@ -43,14 +42,12 @@ describe('home feed relay policy', () => {
expect(merged).toContain('wss://inbox.example/')
})
it('metadata-only policy omits wisp trending from home feed relay list', () => {
setRestrictConnectionsToMetadataRelaysOnly(true)
it('personal-relay policy omits wisp trending from home feed relay list', () => {
setViewerPersonalRelayKeys(new Set(['wss://relay.example.com/']), { viewerActive: true })
const wisp = buildWispTrendingNotesRelayUrl()
const urls = buildAllFavoritesFeedRelayUrls(['wss://relay.example.com/'], [], [wisp])
expect(urls).toContain('wss://relay.example.com/')
expect(urls.some((u) => isWispTrendingNotesRelayUrl(u))).toBe(false)
setRestrictConnectionsToMetadataRelaysOnly(false)
setViewerPersonalRelayKeys(new Set(), { viewerActive: false })
})

11
src/providers/FeedProvider.tsx

@ -6,12 +6,11 @@ import { @@ -6,12 +6,11 @@ import {
syncViewerRelayStackNostrLandAggrEligible,
urlsForViewerNostrLandAggrEligibilitySync
} from '@/lib/nostr-land-relay-eligibility'
import { METADATA_RELAYS_ONLY_POLICY_CHANGED_EVENT } from '@/lib/read-only-relay-personal'
import { collectUserReadInboxUrls } from '@/lib/viewer-read-inboxes'
import { collectUserWriteOutboxUrls } from '@/lib/viewer-write-outboxes'
import { getCacheRelayUrlsFromEvent } from '@/lib/private-relays'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import { collectUserReadInboxUrls } from '@/lib/viewer-read-inboxes'
import { collectUserWriteOutboxUrls } from '@/lib/viewer-write-outboxes'
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
import type { Dispatch, ReactNode, SetStateAction } from 'react'
@ -265,12 +264,6 @@ export function FeedProvider({ children }: { children: ReactNode }) { @@ -265,12 +264,6 @@ export function FeedProvider({ children }: { children: ReactNode }) {
}
}, [isInitialized, favoriteRelaysIdentity, blockedRelaysIdentity, replyExtraRelaysIdentity, updateFeedRelayUrls])
useEffect(() => {
const onPolicyChange = () => updateFeedRelayUrls()
window.addEventListener(METADATA_RELAYS_ONLY_POLICY_CHANGED_EVENT, onPolicyChange)
return () => window.removeEventListener(METADATA_RELAYS_ONLY_POLICY_CHANGED_EVENT, onPolicyChange)
}, [updateFeedRelayUrls])
return (
<FeedContext.Provider
value={useMemo(

5
src/providers/NostrProvider/index.tsx

@ -1615,14 +1615,16 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1615,14 +1615,16 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
client.interruptBackgroundQueries()
noteStatsService.beginPublishPriority()
let publishRelayCandidates: string[] = []
try {
logger.debug('[Publish] Determining target relays...', { kind: event.kind, pubkey: event.pubkey?.substring(0, 8) })
const favoriteRelayUrls = favoriteRelayUrlsForPublish(favoriteRelaysEvent, account.pubkey, relayList)
const relays = await client.determineTargetRelays(event, {
publishRelayCandidates = await client.determineTargetRelays(event, {
...options,
favoriteRelayUrls,
blockedRelayUrls: options.blockedRelayUrls ?? blockedRelayUrlsFromEvent(blockedRelaysEvent)
})
const relays = publishRelayCandidates
logger.debug('[Publish] Target relays determined', { relayCount: relays.length, relays: relays.slice(0, 5) })
logger.debug('[Publish] Calling client.publishEvent()...', { relayCount: relays.length, eventId: event.id?.substring(0, 8) })
@ -1740,6 +1742,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1740,6 +1742,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
throw error
} finally {
noteStatsService.endPublishPriority()
client.closePublishTransientRelays(publishRelayCandidates)
}
}

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

@ -444,10 +444,18 @@ export class QueryService { @@ -444,10 +444,18 @@ export class QueryService {
onevent?: (evt: NEvent) => void,
options?: QueryOptions
): Promise<NEvent[]> {
urls = sanitizeRelayUrlsForFetch(urls)
const originalUrls = [...urls]
const revokeOperationScope = grantRelayConnectionOperationScope(originalUrls)
urls = sanitizeRelayUrlsForFetch(originalUrls)
const sanitizedFilters = sanitizeFiltersBeforeReq(filter)
if (sanitizedFilters.length === 0) return []
if (options?.signal?.aborted) return []
if (sanitizedFilters.length === 0) {
revokeOperationScope()
return []
}
if (options?.signal?.aborted) {
revokeOperationScope()
return []
}
const maxFilters = RELAY_REQ_MAX_FILTERS_PER_MESSAGE
if (sanitizedFilters.length > maxFilters) {
@ -535,7 +543,6 @@ export class QueryService { @@ -535,7 +543,6 @@ export class QueryService {
}
const resultPromise = new Promise<NEvent[]>((resolve) => {
const revokeOperationScope = grantRelayConnectionOperationScope(urls)
const events: NEvent[] = []
const cancelAbortRegistrations: Array<() => void> = []
const abortHttp = new AbortController()
@ -845,6 +852,7 @@ export class QueryService { @@ -845,6 +852,7 @@ export class QueryService {
return { close: () => {} }
}
const originalDedupedRelays = Array.from(new Set(urls))
const revokeOperationScope = grantRelayConnectionOperationScope(originalDedupedRelays)
let relays = sanitizeRelayUrlsForFetch(originalDedupedRelays)
const stripSocialBlockedRelays =
@ -870,12 +878,11 @@ export class QueryService { @@ -870,12 +878,11 @@ export class QueryService {
}
if (relays.length === 0) {
revokeOperationScope()
queueMicrotask(() => callbacks.oneose?.(true))
return { close: () => {} }
}
const revokeOperationScope = grantRelayConnectionOperationScope(relays)
const _knownIds = new Set<string>()
const grouped = new Map<string, Filter[]>()
for (const url of relays) {

78
src/services/client.service.ts

@ -47,6 +47,7 @@ import { @@ -47,6 +47,7 @@ import {
collectViewerWriteOutboxUrls,
collectWriteOutboxUrlsFromRelayList
} from '@/lib/viewer-write-outboxes'
import { isMetadataPolicyProfileRelay } from '@/lib/metadata-policy-curated-relays'
import {
buildPersonalRelayKeySet,
sanitizeRelayUrlsForFetch,
@ -54,7 +55,7 @@ import { @@ -54,7 +55,7 @@ import {
isReadOnlyRelayAllowedForViewer,
isRelayConnectionAllowedForViewer,
isMetadataRelaysOnlyPolicyActive,
isRestrictConnectionsToMetadataRelaysOnly,
isRelayUrlInViewerMetadataLists,
grantRelayConnectionOperationScope,
enterSingleRelayExplicitFetchScope,
isSingleRelayExplicitBrowseActive,
@ -200,7 +201,7 @@ import { @@ -200,7 +201,7 @@ import {
urlMatchesConfiguredHttpIndexRelay
} from '@/lib/url'
import { canonicalFeedFilter, canonicalRelayUrls } from '@/features/feed/descriptor'
import { initRelayPoolIdle, touchRelayPoolActivity, closeRelayPoolSocketsIfIdle } from '@/lib/relay-pool-idle'
import { initRelayPoolIdle, touchRelayPoolActivity, closePublishTransientRelaySockets, closeRelayPoolSocketsIfIdle } from '@/lib/relay-pool-idle'
import { relaySessionStrikes } from '@/lib/relay-strikes'
import { isSafari } from '@/lib/utils'
import {
@ -433,6 +434,8 @@ class ClientService extends EventTarget { @@ -433,6 +434,8 @@ class ClientService extends EventTarget {
/** Session-only: relay URL -> { successCount, sumLatencyMs } for preferring faster, proven relays when picking "random" relays. */
private sessionRelayPublishStats = new Map<string, { successCount: number; sumLatencyMs: number }>()
/** Author outbox / random publish targets to close after publish (not personal or profile index). */
private publishTransientRelayUrls = new Set<string>()
/**
* IndexedDB profile index + NIP-66 relay discovery run once per page session. When logged in,
@ -716,9 +719,7 @@ class ClientService extends EventTarget { @@ -716,9 +719,7 @@ class ClientService extends EventTarget {
/** IndexedDB-first: personal lists (incl. cache + HTTP) before policy or network so locals stay allowed. */
const storageUrls = await this.collectViewerPersonalRelayUrlsFromStorage(pk)
this.viewerHttpIndexRelayBases = storageUrls.httpIndexBases
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(storageUrls.all), {
viewerActive: isRestrictConnectionsToMetadataRelaysOnly()
})
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(storageUrls.all), { viewerActive: true })
syncViewerRelayStackNostrLandAggrEligible(storageUrls.all)
relaySessionStrikes.setSessionCacheRelayKeysFromKind10432(storageUrls.cacheRelayEvent)
this.closeMetadataPolicyDisallowedRelayConnections()
@ -1201,6 +1202,11 @@ class ClientService extends EventTarget { @@ -1201,6 +1202,11 @@ class ClientService extends EventTarget {
event: NEvent,
{ specifiedRelayUrls, additionalRelayUrls, favoriteRelayUrls, blockedRelayUrls }: TPublishOptions = {}
) {
this.publishTransientRelayUrls.clear()
const finish = (relays: string[]): string[] => {
this.stagePublishTransientRelays(relays)
return relays
}
const writeRelayPubOpts = {
blockedRelays: blockedRelayUrls,
applySocialKindBlockedFilter: isSocialKindBlockedKind(event.kind)
@ -1244,7 +1250,8 @@ class ClientService extends EventTarget { @@ -1244,7 +1250,8 @@ class ClientService extends EventTarget {
if (userWriteRelays.length === 0 && seenRelays.length === 0) {
if (!useGlobalRelayDefaults) {
return this.filterPublishingRelays(
return finish(
this.filterPublishingRelays(
buildPrioritizedWriteRelayUrls({
userWriteRelays: [],
favoriteRelays: favoriteRelayUrls ?? [],
@ -1254,8 +1261,10 @@ class ClientService extends EventTarget { @@ -1254,8 +1261,10 @@ class ClientService extends EventTarget {
}),
event
)
)
}
return this.filterPublishingRelays(
return finish(
this.filterPublishingRelays(
buildPrioritizedWriteRelayUrls({
userWriteRelays: [...FAST_WRITE_RELAY_URLS],
favoriteRelays: favoriteRelayUrls ?? [],
@ -1265,8 +1274,10 @@ class ClientService extends EventTarget { @@ -1265,8 +1274,10 @@ class ClientService extends EventTarget {
}),
event
)
)
}
return this.filterPublishingRelays(
return finish(
this.filterPublishingRelays(
buildPrioritizedWriteRelayUrls({
userWriteRelays: userWriteRelays,
authorReadRelays: [],
@ -1278,6 +1289,7 @@ class ClientService extends EventTarget { @@ -1278,6 +1289,7 @@ class ClientService extends EventTarget {
}),
event
)
)
}
// Public messages (kind 24) and calendar RSVPs (kind 31925): only author's outboxes + each recipient's
@ -1313,7 +1325,7 @@ class ClientService extends EventTarget { @@ -1313,7 +1325,7 @@ class ClientService extends EventTarget {
authorWriteCount: authorWrite.length,
recipientReadCount: recipientRead.length
})
return pubRelays
return finish(pubRelays)
}
// Payment attestations (9741): attester outbox + attester read inboxes (profile wall REQ) +
@ -1355,7 +1367,7 @@ class ClientService extends EventTarget { @@ -1355,7 +1367,7 @@ class ClientService extends EventTarget {
senderInboxCount: senderInboxes.length,
seenRelayCount: seenRelays.length
})
return attestationRelays
return finish(attestationRelays)
}
let relays: string[]
@ -1383,7 +1395,8 @@ class ClientService extends EventTarget { @@ -1383,7 +1395,8 @@ class ClientService extends EventTarget {
const n = normalizeRelayUrlByScheme(url) || url
return !readOnlySet.has(n)
})
return this.filterPublishingRelays(
return finish(
this.filterPublishingRelays(
buildPrioritizedWriteRelayUrls({
userWriteRelays:
spellWriteFiltered.length > 0
@ -1400,6 +1413,7 @@ class ClientService extends EventTarget { @@ -1400,6 +1413,7 @@ class ClientService extends EventTarget {
}),
event
)
)
}
const bootstrapExtras: string[] = [...(additionalRelayUrls ?? [])]
@ -1539,7 +1553,7 @@ class ClientService extends EventTarget { @@ -1539,7 +1553,7 @@ class ClientService extends EventTarget {
} else {
relays = dedupeNormalizeRelayUrlsOrdered(relays).slice(0, MAX_PUBLISH_RELAYS)
}
return relays
return finish(relays)
}
/** NOTICE handler: session strikes + rate-limit cooldown + debug log for fetch failures. */
@ -1621,6 +1635,34 @@ class ClientService extends EventTarget { @@ -1621,6 +1635,34 @@ class ClientService extends EventTarget {
relaySessionStrikes.clearKey(urlOrSessionKey)
}
/** Stage non-personal publish targets (e.g. from {@link determineTargetRelays}) for post-publish socket cleanup. */
stagePublishTransientRelays(urls: readonly string[]): void {
for (const raw of urls) {
if (!raw?.trim()) continue
if (isRelayUrlInViewerMetadataLists(raw)) continue
if (isMetadataPolicyProfileRelay(raw)) continue
this.publishTransientRelayUrls.add(normalizeUrl(raw) || raw.trim())
}
}
/** Close author outbox / random publish sockets; keeps profile index and viewer list relays up. */
closePublishTransientRelays(extraUrls?: readonly string[]): void {
const seen = new Set<string>()
const urls: string[] = []
const add = (list: readonly string[]) => {
for (const raw of list) {
const n = normalizeUrl(raw) || raw.trim()
if (!n || seen.has(n)) continue
seen.add(n)
urls.push(n)
}
}
add([...this.publishTransientRelayUrls])
if (extraUrls?.length) add(extraUrls)
this.publishTransientRelayUrls.clear()
closePublishTransientRelaySockets(urls)
}
/**
* From a list of candidate relay URLs (e.g. public lively), return up to `count` relays,
* preferring those that have succeeded and been fast this session. Excludes read-only relays.
@ -1788,7 +1830,10 @@ class ClientService extends EventTarget { @@ -1788,7 +1830,10 @@ class ClientService extends EventTarget {
publishOpBatch.record(idx, url, rs?.success === true, rs?.error)
})
publishOpBatch.logEnd(status)
queueMicrotask(() => closeRelayPoolSocketsIfIdle(publishTargetUrls))
queueMicrotask(() => {
closePublishTransientRelaySockets(publishTargetUrls)
client.closePublishTransientRelays()
})
}
/**
@ -2635,6 +2680,7 @@ class ClientService extends EventTarget { @@ -2635,6 +2680,7 @@ class ClientService extends EventTarget {
originalDedupedRelays.length === 1 &&
(singleRelayExplicit === true || isSingleRelayExplicitBrowseActive())
const revokeFetchScope = preserveExplicitSingleRelay ? enterSingleRelayExplicitFetchScope() : () => {}
const revokeOperationScope = grantRelayConnectionOperationScope(originalDedupedRelays)
const httpKeys = new Set(
httpIndexBasesForRelayQuery(originalDedupedRelays, this.viewerHttpIndexRelayBases).map((u) =>
canonicalRelaySessionKey(u)
@ -2661,6 +2707,8 @@ class ClientService extends EventTarget { @@ -2661,6 +2707,8 @@ class ClientService extends EventTarget {
oneose?.(true)
relayReqLog?.onBatchEnd?.([])
})
revokeOperationScope()
revokeFetchScope()
return {
close: () => {}
}
@ -2740,13 +2788,13 @@ class ClientService extends EventTarget { @@ -2740,13 +2788,13 @@ class ClientService extends EventTarget {
oneose?.(true)
relayReqLog?.onBatchEnd?.([])
})
revokeOperationScope()
revokeFetchScope()
return {
close: () => {}
}
}
const revokeOperationScope = grantRelayConnectionOperationScope(relays)
const reqGroupId =
relayReqLog?.groupId ??
`sub-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`

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

@ -11,7 +11,6 @@ import { isSameAccount } from '@/lib/account' @@ -11,7 +11,6 @@ import { isSameAccount } from '@/lib/account'
import { DEFAULT_ZAP_SATS } from '@/lib/lightning'
import { isPaytoCategory } from '@/lib/payto-category-display'
import type { PaytoCategory } from '@/lib/payto-registry'
import { setRestrictConnectionsToMetadataRelaysOnly } from '@/lib/read-only-relay-personal'
import { randomString } from '@/lib/random'
import {
TAccount,
@ -80,8 +79,7 @@ const SETTINGS_KEYS = [ @@ -80,8 +79,7 @@ const SETTINGS_KEYS = [
StorageKey.DEFAULT_EXPIRATION_ENABLED,
StorageKey.DEFAULT_EXPIRATION_MONTHS,
StorageKey.SHOW_RSS_FEED,
StorageKey.PANE_MODE,
StorageKey.RESTRICT_RELAYS_TO_METADATA_LISTS
StorageKey.PANE_MODE
] as const
class LocalStorageService {
@ -124,7 +122,6 @@ class LocalStorageService { @@ -124,7 +122,6 @@ class LocalStorageService {
private showPublishSuccessToasts: boolean = false
private showDetailedPublishToasts: boolean = true
private showLiveActivitiesBanner: boolean = true
private restrictRelaysToMetadataLists: boolean = true
constructor() {
if (!LocalStorageService.instance) {
@ -419,12 +416,6 @@ class LocalStorageService { @@ -419,12 +416,6 @@ 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 !== 'false'
setRestrictConnectionsToMetadataRelaysOnly(this.restrictRelaysToMetadataLists)
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@ -433,6 +424,7 @@ class LocalStorageService { @@ -433,6 +424,7 @@ class LocalStorageService {
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_DECRYPTED_TAGS_MAP)
window.localStorage.removeItem(StorageKey.ACTIVE_RELAY_SET_ID)
window.localStorage.removeItem(StorageKey.FEED_TYPE)
window.localStorage.removeItem(StorageKey.RESTRICT_RELAYS_TO_METADATA_LISTS)
}
/** Persist a setting. Keys in SETTINGS_KEYS go only to IndexedDB; others use localStorage. */
@ -615,11 +607,6 @@ class LocalStorageService { @@ -615,11 +607,6 @@ 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 !== 'false'
setRestrictConnectionsToMetadataRelaysOnly(this.restrictRelaysToMetadataLists)
}
}
getRelaySets() {
@ -1031,16 +1018,6 @@ class LocalStorageService { @@ -1031,16 +1018,6 @@ 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