Browse Source

blocked relays more universally

imwald
Silberengel 3 weeks ago
parent
commit
3c1f03aae0
  1. 25
      src/components/Favicon/index.tsx
  2. 6
      src/hooks/useFetchNip05.tsx
  3. 43
      src/lib/nip05.ts
  4. 10
      src/lib/read-only-relay-personal.test.ts
  5. 7
      src/lib/read-only-relay-personal.ts
  6. 12
      src/lib/relay-list-builder.ts
  7. 36
      src/lib/viewer-blocked-relays.test.ts
  8. 32
      src/lib/viewer-blocked-relays.ts
  9. 5
      src/providers/FavoriteRelaysProvider.tsx
  10. 33
      src/providers/NostrProvider/index.tsx
  11. 11
      src/services/client.service.ts
  12. 9
      src/services/note-stats.service.ts

25
src/components/Favicon/index.tsx

@ -1,6 +1,6 @@
import { isFaviconLoadFailed, markFaviconLoadFailed, normalizeFaviconDomain } from '@/lib/favicon-fail-cache' import { isFaviconLoadFailed, markFaviconLoadFailed, normalizeFaviconDomain } from '@/lib/favicon-fail-cache'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useState } from 'react' import { useEffect, useRef, useState } from 'react'
export function Favicon({ export function Favicon({
domain, domain,
@ -12,9 +12,21 @@ export function Favicon({
fallback?: React.ReactNode fallback?: React.ReactNode
}) { }) {
const host = normalizeFaviconDomain(domain) const host = normalizeFaviconDomain(domain)
const knownFailed = host ? isFaviconLoadFailed(host) : true const [loading, setLoading] = useState(true)
const [loading, setLoading] = useState(!knownFailed) const [error, setError] = useState(false)
const [error, setError] = useState(knownFailed) const loadingRef = useRef(loading)
loadingRef.current = loading
useEffect(() => {
const knownFailed = !host || isFaviconLoadFailed(host)
setError(knownFailed)
setLoading(!knownFailed)
if (!host || knownFailed) return
return () => {
if (loadingRef.current) markFaviconLoadFailed(host)
}
}, [host])
if (error || !host) return fallback if (error || !host) return fallback
return ( return (
@ -28,7 +40,10 @@ export function Favicon({
markFaviconLoadFailed(host) markFaviconLoadFailed(host)
setError(true) setError(true)
}} }}
onLoad={() => setLoading(false)} onLoad={() => {
loadingRef.current = false
setLoading(false)
}}
/> />
</div> </div>
) )

6
src/hooks/useFetchNip05.tsx

@ -12,12 +12,18 @@ export function useFetchNip05(nip05?: string, pubkey?: string) {
setIsFetching(false) setIsFetching(false)
return return
} }
let cancelled = false
setIsFetching(true)
verifyNip05(nip05, pubkey).then(({ isVerified, nip05Name, nip05Domain }) => { verifyNip05(nip05, pubkey).then(({ isVerified, nip05Name, nip05Domain }) => {
if (cancelled) return
setNip05IsVerified(isVerified) setNip05IsVerified(isVerified)
setNip05Name(nip05Name) setNip05Name(nip05Name)
setNip05Domain(nip05Domain) setNip05Domain(nip05Domain)
setIsFetching(false) setIsFetching(false)
}) })
return () => {
cancelled = true
}
}, [nip05, pubkey]) }, [nip05, pubkey])
return { nip05IsVerified, nip05Name, nip05Domain, isFetching } return { nip05IsVerified, nip05Name, nip05Domain, isFetching }

43
src/lib/nip05.ts

@ -20,6 +20,14 @@ type TVerifyNip05Result = {
/** Bumps when verification rules change so LRU does not serve stale false negatives. */ /** Bumps when verification rules change so LRU does not serve stale false negatives. */
const VERIFY_CACHE_SCHEMA = 4 const VERIFY_CACHE_SCHEMA = 4
/** Per-domain `nostr.json` (or negative `null`) so feeds do not re-fetch every NIP-05 on the same host. */
const wellKnownJsonByDomain = new LRUCache<string, Record<string, unknown> | null>({ max: 512 })
const wellKnownDomainInFlight = new Map<string, Promise<Record<string, unknown> | null>>()
function normalizeNip05Domain(domain: string): string {
return domain.trim().toLowerCase().replace(/\.$/, '')
}
function asNip05LookupString(value: unknown): string { function asNip05LookupString(value: unknown): string {
if (typeof value === 'string') return value if (typeof value === 'string') return value
if (value == null) return '' if (value == null) return ''
@ -215,8 +223,11 @@ async function fetchWellKnownNostrJsonOnce(
} }
} }
/** Fetch `/.well-known/nostr.json` (with optional `?name=`). Retries without `name` if the entry is missing. */ /** Uncached network: optional `?name=` then full document. */
async function fetchWellKnownNostrJson(domain: string, name?: string): Promise<Record<string, unknown> | null> { async function fetchWellKnownNostrJsonNetwork(
domain: string,
name?: string
): Promise<Record<string, unknown> | null> {
const trimmedName = typeof name === 'string' ? name.trim() : '' const trimmedName = typeof name === 'string' ? name.trim() : ''
const withQuery = const withQuery =
trimmedName.length > 0 ? await fetchWellKnownNostrJsonOnce(domain, trimmedName) : null trimmedName.length > 0 ? await fetchWellKnownNostrJsonOnce(domain, trimmedName) : null
@ -234,6 +245,34 @@ async function fetchWellKnownNostrJson(domain: string, name?: string): Promise<R
return withQuery ?? full return withQuery ?? full
} }
async function getOrFetchWellKnownJsonForDomain(
domain: string,
nameHint?: string
): Promise<Record<string, unknown> | null> {
const key = normalizeNip05Domain(domain)
if (!key) return null
if (wellKnownJsonByDomain.has(key)) {
return wellKnownJsonByDomain.get(key) ?? null
}
let inflight = wellKnownDomainInFlight.get(key)
if (!inflight) {
inflight = fetchWellKnownNostrJsonNetwork(key, nameHint).then((json) => {
wellKnownJsonByDomain.set(key, json)
wellKnownDomainInFlight.delete(key)
return json
})
wellKnownDomainInFlight.set(key, inflight)
}
return inflight
}
/** Fetch `/.well-known/nostr.json` (with optional `?name=`). Retries without `name` if the entry is missing. */
async function fetchWellKnownNostrJson(domain: string, name?: string): Promise<Record<string, unknown> | null> {
const trimmedName = typeof name === 'string' ? name.trim() : ''
const hint = trimmedName.length > 0 ? trimmedName : undefined
return getOrFetchWellKnownJsonForDomain(domain, hint)
}
export async function fetchPubkeysFromDomain(domain: string): Promise<string[]> { export async function fetchPubkeysFromDomain(domain: string): Promise<string[]> {
try { try {
const json = await fetchWellKnownNostrJson(domain) const json = await fetchWellKnownNostrJson(domain)

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

@ -8,14 +8,17 @@ import {
sanitizeRelayUrlsForFetch, sanitizeRelayUrlsForFetch,
setViewerPersonalRelayKeys setViewerPersonalRelayKeys
} from './read-only-relay-personal' } from './read-only-relay-personal'
import { setViewerBlockedRelayUrls } from './viewer-blocked-relays'
describe('read-only-relay-personal', () => { describe('read-only-relay-personal', () => {
beforeEach(() => { beforeEach(() => {
setViewerPersonalRelayKeys(new Set()) setViewerPersonalRelayKeys(new Set())
setViewerBlockedRelayUrls([])
syncViewerRelayStackNostrLandAggrEligible([]) syncViewerRelayStackNostrLandAggrEligible([])
}) })
afterEach(() => { afterEach(() => {
setViewerBlockedRelayUrls([])
syncViewerRelayStackNostrLandAggrEligible([]) syncViewerRelayStackNostrLandAggrEligible([])
}) })
@ -50,6 +53,13 @@ describe('read-only-relay-personal', () => {
]) ])
}) })
it('sanitizeRelayUrlsForFetch drops user-blocked relays', () => {
setViewerBlockedRelayUrls(['wss://freelay.sovbit.host/'])
expect(
sanitizeRelayUrlsForFetch(['wss://relay.damus.io/', 'wss://freelay.sovbit.host/'])
).toEqual(['wss://relay.damus.io/'])
})
it('keeps filter.nostr.wine when on the viewer personal list', () => { it('keeps filter.nostr.wine when on the viewer personal list', () => {
setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://filter.nostr.wine/'])) setViewerPersonalRelayKeys(buildPersonalRelayKeySet(['wss://filter.nostr.wine/']))
const urls = ['wss://relay.damus.io/', 'wss://filter.nostr.wine/'] const urls = ['wss://relay.damus.io/', 'wss://filter.nostr.wine/']

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

@ -1,6 +1,7 @@
import { READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS } from '@/constants' import { READ_ONLY_PERSONAL_LIST_REQUIRED_RELAY_URLS } from '@/constants'
import { filterAggrNostrLandUnlessViewerEligible } from '@/lib/nostr-land-relay-eligibility' import { filterAggrNostrLandUnlessViewerEligible } from '@/lib/nostr-land-relay-eligibility'
import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize' import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize'
import { filterViewerBlockedRelaysForFetch } from '@/lib/viewer-blocked-relays'
import { normalizeAnyRelayUrl } from '@/lib/url' import { normalizeAnyRelayUrl } from '@/lib/url'
const personalListRequiredKeySet = new Set( const personalListRequiredKeySet = new Set(
@ -82,7 +83,9 @@ export function sanitizeRelayUrlsForFetch(
const key = relayUrlKey(u) const key = relayUrlKey(u)
return key.length > 0 && keys.has(key) return key.length > 0 && keys.has(key)
}) })
return filterAggrNostrLandUnlessViewerEligible( return filterViewerBlockedRelaysForFetch(
filterReadOnlyRelaysUnlessPersonal(withoutThirdPartyLocals, keys) filterAggrNostrLandUnlessViewerEligible(
filterReadOnlyRelaysUnlessPersonal(withoutThirdPartyLocals, keys)
)
) )
} }

12
src/lib/relay-list-builder.ts

@ -12,6 +12,7 @@
import { FAST_READ_RELAY_URLS, PROFILE_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants' import { FAST_READ_RELAY_URLS, PROFILE_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { feedRelayPolicyUrls } from '@/features/feed/relay-policy' import { feedRelayPolicyUrls } from '@/features/feed/relay-policy'
import { mergeRelayUrlLayers, userReadRelaysWithHttp } from '@/lib/favorites-feed-relays' import { mergeRelayUrlLayers, userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { isRelayBlockedByUser } from '@/lib/relay-blocked'
import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize' import { urlIsNonLocalForRemoteViewer } from '@/lib/relay-list-sanitize'
import { import {
canonicalRelaySessionKey, canonicalRelaySessionKey,
@ -165,20 +166,13 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
const trackPersonal = (url: string) => { const trackPersonal = (url: string) => {
personalRelayUrls.push(url) personalRelayUrls.push(url)
} }
const normalizedBlocked = new Set(
(blockedRelays || [])
.map((url) => (normalizeAnyRelayUrl(url) || url).toLowerCase())
.filter(Boolean)
)
const addRelay = (url: string | undefined) => { const addRelay = (url: string | undefined) => {
if (!url) return if (!url) return
// This builder feeds WebSocket REQ/publish lists; kind 10243 HTTP index relays use addHttpRelay. // This builder feeds WebSocket REQ/publish lists; kind 10243 HTTP index relays use addHttpRelay.
if (isKind10243HttpRelayTagUrl(url)) return if (isKind10243HttpRelayTagUrl(url)) return
const normalized = normalizeAnyRelayUrl(url) const normalized = normalizeAnyRelayUrl(url)
if (!normalized) return if (!normalized) return
// Filter blocked (case-insensitive comparison) if (isRelayBlockedByUser(normalized, blockedRelays)) return
if (normalizedBlocked.has(normalized.toLowerCase())) return
relayUrls.add(normalized) relayUrls.add(normalized)
} }
@ -191,7 +185,7 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
const addHttpRelay = (url: string | undefined) => { const addHttpRelay = (url: string | undefined) => {
if (!url) return if (!url) return
const normalized = normalizeHttpRelayUrl(url) const normalized = normalizeHttpRelayUrl(url)
if (!normalized || normalizedBlocked.has(normalized.toLowerCase())) return if (!normalized || isRelayBlockedByUser(normalized, blockedRelays)) return
if (httpRelayUrls.some((u) => relayKey(u) === relayKey(normalized))) return if (httpRelayUrls.some((u) => relayKey(u) === relayKey(normalized))) return
httpRelayUrls.push(normalized) httpRelayUrls.push(normalized)
} }

36
src/lib/viewer-blocked-relays.test.ts

@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest'
import {
filterViewerBlockedRelaysForFetch,
parseBlockedRelayUrlsFromEvent,
setViewerBlockedRelayUrls
} from './viewer-blocked-relays'
describe('viewer-blocked-relays', () => {
it('parseBlockedRelayUrlsFromEvent dedupes relay tags', () => {
setViewerBlockedRelayUrls([])
const urls = parseBlockedRelayUrlsFromEvent({
kind: 10006,
tags: [
['relay', 'wss://freelay.sovbit.host/'],
['relay', 'wss://freelay.sovbit.host']
],
content: '',
created_at: 1,
id: 'x',
pubkey: 'p',
sig: 's'
})
expect(urls).toEqual(['wss://freelay.sovbit.host/'])
})
it('filterViewerBlockedRelaysForFetch matches hostname across schemes', () => {
setViewerBlockedRelayUrls(['wss://freelay.sovbit.host/'])
expect(
filterViewerBlockedRelaysForFetch([
'wss://freelay.sovbit.host/',
'wss://relay.example.com/',
'https://freelay.sovbit.host/'
])
).toEqual(['wss://relay.example.com/'])
})
})

32
src/lib/viewer-blocked-relays.ts

@ -0,0 +1,32 @@
import type { Event } from 'nostr-tools'
import { isRelayBlockedByUser } from '@/lib/relay-blocked'
import { normalizeAnyRelayUrl } from '@/lib/url'
let viewerBlockedRelayUrls: readonly string[] = []
/** Kind 10006 `relay` tags → normalized URLs (deduped). */
export function parseBlockedRelayUrlsFromEvent(event: Event | null | undefined): string[] {
const out: string[] = []
if (!event) return out
event.tags.forEach(([tagName, tagValue]) => {
if (tagName !== 'relay' || !tagValue) return
const n = normalizeAnyRelayUrl(tagValue)
if (n && !out.includes(n)) out.push(n)
})
return out
}
/** Updated from IDB hydration and {@link FavoriteRelaysProvider} when the block list changes. */
export function setViewerBlockedRelayUrls(urls: readonly string[]): void {
viewerBlockedRelayUrls = urls.length ? [...urls] : []
}
export function getViewerBlockedRelayUrls(): readonly string[] {
return viewerBlockedRelayUrls
}
/** Drop user-blocked relays (hostname-aware) before any REQ / query / WebSocket connect. */
export function filterViewerBlockedRelaysForFetch(urls: readonly string[]): string[] {
if (!viewerBlockedRelayUrls.length) return [...urls]
return urls.filter((u) => !isRelayBlockedByUser(u, viewerBlockedRelayUrls))
}

5
src/providers/FavoriteRelaysProvider.tsx

@ -6,6 +6,7 @@ import { getReplaceableEventIdentifier } from '@/lib/event'
import { getRelaySetFromEvent } from '@/lib/event-metadata' import { getRelaySetFromEvent } from '@/lib/event-metadata'
import { randomString } from '@/lib/random' import { randomString } from '@/lib/random'
import { isWebsocketUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url' import { isWebsocketUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { setViewerBlockedRelayUrls } from '@/lib/viewer-blocked-relays'
import { queryService } from '@/services/client.service' import { queryService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import { TRelaySet } from '@/types' import { TRelaySet } from '@/types'
@ -156,6 +157,10 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
setBlockedRelays(relays) setBlockedRelays(relays)
}, [blockedRelaysEvent]) }, [blockedRelaysEvent])
useEffect(() => {
setViewerBlockedRelayUrls(blockedRelays)
}, [blockedRelays])
useEffect(() => { useEffect(() => {
setRelaySets( setRelaySets(
relaySetEvents.map((evt) => getRelaySetFromEvent(evt, blockedRelays)).filter(Boolean) as TRelaySet[] relaySetEvents.map((evt) => getRelaySetFromEvent(evt, blockedRelays)).filter(Boolean) as TRelaySet[]

33
src/providers/NostrProvider/index.tsx

@ -31,6 +31,10 @@ import {
} from '@/lib/event-metadata' } from '@/lib/event-metadata'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults' import { viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import {
parseBlockedRelayUrlsFromEvent,
setViewerBlockedRelayUrls
} from '@/lib/viewer-blocked-relays'
import { LoginRequiredError } from '@/lib/nostr-errors' import { LoginRequiredError } from '@/lib/nostr-errors'
import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url' import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey' import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
@ -106,15 +110,7 @@ function favoriteRelayUrlsForPublish(
} }
function blockedRelayUrlsFromEvent(blockedRelaysEvent: Event | null): string[] { function blockedRelayUrlsFromEvent(blockedRelaysEvent: Event | null): string[] {
const out: string[] = [] return parseBlockedRelayUrlsFromEvent(blockedRelaysEvent)
if (!blockedRelaysEvent) return out
blockedRelaysEvent.tags.forEach(([tagName, tagValue]) => {
if (tagName === 'relay' && tagValue) {
const n = normalizeAnyRelayUrl(tagValue)
if (n && !out.includes(n)) out.push(n)
}
})
return out
} }
const NIP07_SIGNER_PUBKEY_MISMATCH_MSG = 'Signer pubkey does not match current account' const NIP07_SIGNER_PUBKEY_MISMATCH_MSG = 'Signer pubkey does not match current account'
@ -325,20 +321,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.HTTP_RELAY_LIST) indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.HTTP_RELAY_LIST)
]) ])
// Extract blocked relays from event // Extract blocked relays from event (sync to fetch layer before feed REQs)
const blockedRelays: string[] = [] const blockedRelays = parseBlockedRelayUrlsFromEvent(storedBlockedRelaysEvent ?? null)
if (storedBlockedRelaysEvent) { setViewerBlockedRelayUrls(blockedRelays)
storedBlockedRelaysEvent.tags.forEach(([tagName, tagValue]) => { if (storedBlockedRelaysEvent && !userForcedAccountNetworkHydrate) {
if (tagName === 'relay' && tagValue) { setBlockedRelaysEvent(storedBlockedRelaysEvent)
const normalizedUrl = normalizeUrl(tagValue)
if (normalizedUrl && !blockedRelays.includes(normalizedUrl)) {
blockedRelays.push(normalizedUrl)
}
}
})
if (!userForcedAccountNetworkHydrate) {
setBlockedRelaysEvent(storedBlockedRelaysEvent)
}
} }
// Set initial relay list from stored events (will be updated with merged list later) // Set initial relay list from stored events (will be updated with merged list later)

11
src/services/client.service.ts

@ -44,6 +44,10 @@ import {
setViewerPersonalRelayKeys setViewerPersonalRelayKeys
} from '@/lib/read-only-relay-personal' } from '@/lib/read-only-relay-personal'
import { profileFetchRelayUrlsWithoutFastReadLayer, viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults' import { profileFetchRelayUrlsWithoutFastReadLayer, viewerUsesGlobalRelayDefaults } from '@/lib/viewer-relay-defaults'
import {
parseBlockedRelayUrlsFromEvent,
setViewerBlockedRelayUrls
} from '@/lib/viewer-blocked-relays'
/** NIP-01 filter keys only; NIP-50 adds `search` which non-searchable relays reject. */ /** NIP-01 filter keys only; NIP-50 adds `search` which non-searchable relays reject. */
function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter { function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter {
@ -616,8 +620,15 @@ class ClientService extends EventTarget {
this.viewerHttpIndexRelayBases = [] this.viewerHttpIndexRelayBases = []
setViewerPersonalRelayKeys(new Set()) setViewerPersonalRelayKeys(new Set())
syncViewerRelayStackNostrLandAggrEligible([]) syncViewerRelayStackNostrLandAggrEligible([])
setViewerBlockedRelayUrls([])
return return
} }
try {
const blockedEvt = await indexedDb.getReplaceableEvent(pk, ExtendedKind.BLOCKED_RELAYS)
setViewerBlockedRelayUrls(parseBlockedRelayUrlsFromEvent(blockedEvt ?? null))
} catch {
setViewerBlockedRelayUrls([])
}
const urls: string[] = [] const urls: string[] = []
try { try {
const rl = await this.peekRelayListFromStorage(pk) const rl = await this.peekRelayListFromStorage(pk)

9
src/services/note-stats.service.ts

@ -32,6 +32,8 @@ import {
getNip25ReactionTargetHexFromTags, getNip25ReactionTargetHexFromTags,
tagNameEquals tagNameEquals
} from '@/lib/tag' } from '@/lib/tag'
import { sanitizeRelayUrlsForFetch } from '@/lib/read-only-relay-personal'
import { getViewerBlockedRelayUrls } from '@/lib/viewer-blocked-relays'
import client, { eventService } from '@/services/client.service' import client, { eventService } from '@/services/client.service'
import { TEmoji } from '@/types' import { TEmoji } from '@/types'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@ -607,9 +609,11 @@ class NoteStatsService {
} }
} }
/** Stats REQs: dedupe, then prepend {@link AGGR_NOSTR_LAND_WSS} when the viewer lists `wss://nostr.land`. */ /** Stats REQs: dedupe, user-blocked strip, then prepend {@link AGGR_NOSTR_LAND_WSS} when eligible. */
private finalizeNoteStatsRelayUrls(urls: readonly string[]): string[] { private finalizeNoteStatsRelayUrls(urls: readonly string[]): string[] {
return prependAggrNostrLandIfViewerEligible(dedupeNormalizeRelayUrlsOrdered(urls)) return prependAggrNostrLandIfViewerEligible(
sanitizeRelayUrlsForFetch(dedupeNormalizeRelayUrlsOrdered(urls))
)
} }
/** {@link buildComprehensiveRelayList} for reactions/reposts/zaps on a note (thread hints, capped author NIP-65). */ /** {@link buildComprehensiveRelayList} for reactions/reposts/zaps on a note (thread hints, capped author NIP-65). */
@ -657,6 +661,7 @@ class NoteStatsService {
authorPubkey: event.pubkey, authorPubkey: event.pubkey,
userPubkey: me, userPubkey: me,
relayHints, relayHints,
blockedRelays: [...getViewerBlockedRelayUrls()],
includeUserOwnRelays: Boolean(me), includeUserOwnRelays: Boolean(me),
includeFavoriteRelays: Boolean(me), includeFavoriteRelays: Boolean(me),
includeFastReadRelays: useGlobal, includeFastReadRelays: useGlobal,

Loading…
Cancel
Save