Browse Source

bug-fixes

imwald
Silberengel 4 weeks ago
parent
commit
aef8537c62
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 41
      src/components/PaytoDialog/index.tsx
  4. 1
      src/components/PaytoLink/index.tsx
  5. 25
      src/components/ZapDialog/index.tsx
  6. 1
      src/constants.ts
  7. 20
      src/features/feed/relay-policy.test.ts
  8. 10
      src/features/feed/relay-policy.ts
  9. 14
      src/lib/pre-publish-relay-cap.ts
  10. 41
      src/lib/relay-publish-filter.test.ts
  11. 65
      src/lib/relay-publish-filter.ts
  12. 3
      src/lib/relay-url-priority.test.ts
  13. 24
      src/lib/relay-url-priority.ts
  14. 9
      src/services/client.service.ts
  15. 23
      src/services/relay-selection.service.ts

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.13.0", "version": "23.13.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "imwald", "name": "imwald",
"version": "23.13.0", "version": "23.13.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.13.0", "version": "23.13.1",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true, "private": true,
"type": "module", "type": "module",

41
src/components/PaytoDialog/index.tsx

@ -1,3 +1,4 @@
import TipPublicMessagePrompt from '@/components/ZapDialog/TipPublicMessagePrompt'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -8,6 +9,7 @@ import {
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Copy, ExternalLink, Wallet, Zap } from 'lucide-react' import { Copy, ExternalLink, Wallet, Zap } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useRef, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { import {
filterPaytoPaymentOpenHandlersForDevice, filterPaytoPaymentOpenHandlersForDevice,
@ -15,6 +17,7 @@ import {
getPaytoTypeInfo getPaytoTypeInfo
} from '@/lib/payto' } from '@/lib/payto'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import LightningInvoiceSection from './LightningInvoiceSection' import LightningInvoiceSection from './LightningInvoiceSection'
export default function PaytoDialog({ export default function PaytoDialog({
@ -22,15 +25,21 @@ export default function PaytoDialog({
onOpenChange, onOpenChange,
type, type,
authority, authority,
paytoUri paytoUri,
recipientPubkey
}: { }: {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
type: string type: string
authority: string authority: string
paytoUri: string paytoUri: string
/** When set, closing the dialog offers a kind-24 tip notice to this pubkey. */
recipientPubkey?: string
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey: selfPubkey } = useNostr()
const [tipNoticeOpen, setTipNoticeOpen] = useState(false)
const skipTipNoticeOnCloseRef = useRef(false)
const info = getPaytoTypeInfo(type) const info = getPaytoTypeInfo(type)
const label = info?.label ?? type const label = info?.label ?? type
const isLightning = type.toLowerCase() === 'lightning' const isLightning = type.toLowerCase() === 'lightning'
@ -41,11 +50,29 @@ export default function PaytoDialog({
const handleCopy = (text: string, copyLabel?: string) => { const handleCopy = (text: string, copyLabel?: string) => {
navigator.clipboard.writeText(text) navigator.clipboard.writeText(text)
toast.success(copyLabel ? t('Copied {{label}} address', { label: copyLabel }) : t('Copied to clipboard')) toast.success(copyLabel ? t('Copied {{label}} address', { label: copyLabel }) : t('Copied to clipboard'))
onOpenChange(false) handleDialogOpenChange(false)
}
const maybeOfferTipNoticeOnClose = () => {
if (!recipientPubkey) return
if (skipTipNoticeOnCloseRef.current) return
if (selfPubkey && recipientPubkey === selfPubkey) return
setTipNoticeOpen(true)
}
const handleDialogOpenChange = (next: boolean) => {
if (!next) {
maybeOfferTipNoticeOnClose()
skipTipNoticeOnCloseRef.current = false
} else {
skipTipNoticeOnCloseRef.current = false
}
onOpenChange(next)
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <>
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
<DialogContent <DialogContent
className={cn( className={cn(
'left-[50%] top-[50%] flex w-[calc(100vw-1.25rem)] max-w-md translate-x-[-50%] translate-y-[-50%] flex-col gap-0', 'left-[50%] top-[50%] flex w-[calc(100vw-1.25rem)] max-w-md translate-x-[-50%] translate-y-[-50%] flex-col gap-0',
@ -133,5 +160,13 @@ export default function PaytoDialog({
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{recipientPubkey ? (
<TipPublicMessagePrompt
open={tipNoticeOpen}
onOpenChange={setTipNoticeOpen}
recipientPubkey={recipientPubkey}
/>
) : null}
</>
) )
} }

1
src/components/PaytoLink/index.tsx

@ -151,6 +151,7 @@ export default function PaytoLink({
type={type} type={type}
authority={authority} authority={authority}
paytoUri={raw} paytoUri={raw}
recipientPubkey={pubkey}
/> />
)} )}
</> </>

25
src/components/ZapDialog/index.tsx

@ -119,7 +119,6 @@ export default function ZapDialog({
: t('Send a payment to this user') : t('Send a payment to this user')
const maybeOfferTipNoticeOnClose = () => { const maybeOfferTipNoticeOnClose = () => {
if (paymentsOnly) return
if (skipTipNoticeOnCloseRef.current) return if (skipTipNoticeOnCloseRef.current) return
if (selfPubkey && pubkey === selfPubkey) return if (selfPubkey && pubkey === selfPubkey) return
setTipNoticeOpen(true) setTipNoticeOpen(true)
@ -198,13 +197,11 @@ export default function ZapDialog({
}} }}
/> />
</DrawerContent> </DrawerContent>
{!paymentsOnly && ( <TipPublicMessagePrompt
<TipPublicMessagePrompt open={tipNoticeOpen}
open={tipNoticeOpen} onOpenChange={setTipNoticeOpen}
onOpenChange={setTipNoticeOpen} recipientPubkey={pubkey}
recipientPubkey={pubkey} />
/>
)}
</Drawer> </Drawer>
) )
} }
@ -237,13 +234,11 @@ export default function ZapDialog({
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{!paymentsOnly && ( <TipPublicMessagePrompt
<TipPublicMessagePrompt open={tipNoticeOpen}
open={tipNoticeOpen} onOpenChange={setTipNoticeOpen}
onOpenChange={setTipNoticeOpen} recipientPubkey={pubkey}
recipientPubkey={pubkey} />
/>
)}
</> </>
) )
} }

1
src/constants.ts

@ -425,6 +425,7 @@ export const DOCUMENT_RELAY_URLS = [
*/ */
export const READ_ONLY_RELAY_URLS = [ export const READ_ONLY_RELAY_URLS = [
'wss://aggr.nostr.land', 'wss://aggr.nostr.land',
'wss://nostr.land',
'wss://relay.nostr.watch', 'wss://relay.nostr.watch',
'wss://relaypag.es', 'wss://relaypag.es',
'wss://relay.noswhere.com', 'wss://relay.noswhere.com',

20
src/features/feed/relay-policy.test.ts

@ -66,4 +66,24 @@ describe('applyFeedRelayPolicy', () => {
}) })
) )
}) })
it('excludes profile/index mirrors for kind 1 writes', () => {
const result = applyFeedRelayPolicy(
[
{
source: 'viewer-write',
urls: ['wss://profiles.nostrver.se/', 'wss://relay.example/']
}
],
{ operation: 'write', eventKind: 1, applySocialKindBlockedFilter: false }
)
expect(result.urls).toEqual(['wss://relay.example/'])
expect(result.dropped).toContainEqual(
expect.objectContaining({
normalizedUrl: 'wss://profiles.nostrver.se/',
reason: 'profile-index-for-write'
})
)
})
}) })

10
src/features/feed/relay-policy.ts

@ -3,6 +3,7 @@ import {
SOCIAL_KIND_BLOCKED_RELAY_URLS, SOCIAL_KIND_BLOCKED_RELAY_URLS,
relayFilterIncludesSocialKindBlockedKind relayFilterIncludesSocialKindBlockedKind
} from '@/constants' } from '@/constants'
import { relayAllowsPublishKind } from '@/lib/relay-publish-filter'
import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr' import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr'
import { getViewerRelayStackNostrLandAggrEligible } from '@/lib/nostr-land-relay-eligibility' import { getViewerRelayStackNostrLandAggrEligible } from '@/lib/nostr-land-relay-eligibility'
import { import {
@ -19,6 +20,7 @@ export type FeedRelayDropReason =
| 'duplicate' | 'duplicate'
| 'user-blocked' | 'user-blocked'
| 'read-only-for-write' | 'read-only-for-write'
| 'profile-index-for-write'
| 'social-kind-blocked' | 'social-kind-blocked'
| 'extended-tag-blocked' | 'extended-tag-blocked'
| 'third-party-local' | 'third-party-local'
@ -195,6 +197,14 @@ export function applyFeedRelayPolicy(
addDrop(dropped, normalized, layer.source, 'read-only-for-write') addDrop(dropped, normalized, layer.source, 'read-only-for-write')
continue continue
} }
if (
(context.operation === 'write' || context.operation === 'publish-picker') &&
context.eventKind !== undefined &&
!relayAllowsPublishKind(normalized, context.eventKind)
) {
addDrop(dropped, normalized, layer.source, 'profile-index-for-write')
continue
}
if ( if (
socialFilter && socialFilter &&
isSocialKindBlockedRelay(key) && isSocialKindBlockedRelay(key) &&

14
src/lib/pre-publish-relay-cap.ts

@ -1,9 +1,6 @@
import { import { isSocialKindBlockedKind, MAX_PUBLISH_RELAYS, SOCIAL_KIND_BLOCKED_RELAY_URLS } from '@/constants'
isSocialKindBlockedKind, import { kinds } from 'nostr-tools'
MAX_PUBLISH_RELAYS, import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter'
READ_ONLY_RELAY_URLS,
SOCIAL_KIND_BLOCKED_RELAY_URLS
} from '@/constants'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority' import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url' import { normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import type { NostrEvent } from 'nostr-tools' import type { NostrEvent } from 'nostr-tools'
@ -49,12 +46,11 @@ export function computePrePublishRelayCapPreview({
.map((u) => normalizeHttpRelayUrl(u) || u) .map((u) => normalizeHttpRelayUrl(u) || u)
.filter((u): u is string => !!u) .filter((u): u is string => !!u)
let outbox = dedupeNormalizeRelayUrlsOrdered([...httpOut, ...wsOut]) let outbox = dedupeNormalizeRelayUrlsOrdered([...httpOut, ...wsOut])
const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeAnyRelayUrl(u) || u)) const previewKind = kinds.ShortTextNote
const socialBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) const socialBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
outbox = dedupeNormalizeRelayUrlsOrdered( outbox = dedupeNormalizeRelayUrlsOrdered(
outbox.filter((url) => { filterRelaysForEventPublish(outbox, previewKind).filter((url) => {
const n = normalizeAnyRelayUrl(url) || url const n = normalizeAnyRelayUrl(url) || url
if (readOnlySet.has(n)) return false
if (applySocialOutboxFilter && socialBlockedSet.has(n)) return false if (applySocialOutboxFilter && socialBlockedSet.has(n)) return false
return true return true
}) })

41
src/lib/relay-publish-filter.test.ts

@ -0,0 +1,41 @@
import { kinds } from 'nostr-tools'
import { describe, expect, it } from 'vitest'
import {
filterContextAuthorReadRelaysForPublish,
filterRelaysForEventPublish,
relayAllowsPublishKind
} from './relay-publish-filter'
describe('relay-publish-filter', () => {
it('blocks profile/index mirrors for kind 1 and 7', () => {
expect(relayAllowsPublishKind('wss://profiles.nostr1.com/', kinds.ShortTextNote)).toBe(false)
expect(relayAllowsPublishKind('wss://purplepag.es/', kinds.Reaction)).toBe(false)
expect(relayAllowsPublishKind('wss://indexer.coracle.social/', kinds.ShortTextNote)).toBe(false)
})
it('allows profile/index mirrors for kind 0 and 10002', () => {
expect(relayAllowsPublishKind('wss://profiles.nostrver.se/', kinds.Metadata)).toBe(true)
expect(relayAllowsPublishKind('wss://indexer.coracle.social/', kinds.RelayList)).toBe(true)
})
it('strips read-only aggregators and profile mirrors from publish lists', () => {
const out = filterRelaysForEventPublish(
[
'wss://nostr.land/',
'wss://profiles.nostr1.com/',
'wss://relay.primal.net/',
'wss://aggr.nostr.land/'
],
kinds.ShortTextNote
)
expect(out).toEqual(['wss://relay.primal.net/'])
})
it('strips profile mirrors from author read hints', () => {
const out = filterContextAuthorReadRelaysForPublish([
'wss://profiles.nostrver.se/',
'wss://relay.example.com/'
])
expect(out).toEqual(['wss://relay.example.com/'])
})
})

65
src/lib/relay-publish-filter.ts

@ -0,0 +1,65 @@
import {
AUTHOR_PROFILE_VIEW_REPLACEABLE_KINDS,
READ_ONLY_RELAY_URLS
} from '@/constants'
import { normalizeAnyRelayUrl } from '@/lib/url'
/**
* Profile mirrors and indexers that reject notes, reactions, and other social kinds.
* Distinct from {@link READ_ONLY_RELAY_URLS} (search/index aggregators) and
* {@link SOCIAL_KIND_BLOCKED_RELAY_URLS} (subset also listed here for kind 1 / 1111 / 11).
*/
export const PROFILE_INDEX_ONLY_RELAY_URLS = [
'wss://profiles.nostr1.com',
'wss://purplepag.es',
'wss://profiles.nostrver.se/',
'wss://indexer.coracle.social/'
] as const
const profileIndexOnlyKeySet = new Set(
PROFILE_INDEX_ONLY_RELAY_URLS.map((u) => (normalizeAnyRelayUrl(u) || u).toLowerCase()).filter(Boolean)
)
const readOnlyKeySet = new Set(
READ_ONLY_RELAY_URLS.map((u) => (normalizeAnyRelayUrl(u) || u).toLowerCase()).filter(Boolean)
)
const profileIndexPublishKindSet = new Set<number>(AUTHOR_PROFILE_VIEW_REPLACEABLE_KINDS)
function relayKey(url: string): string {
return (normalizeAnyRelayUrl(url) || url.trim()).toLowerCase()
}
export function isProfileIndexOnlyRelay(url: string): boolean {
const key = relayKey(url)
return key.length > 0 && profileIndexOnlyKeySet.has(key)
}
export function isReadOnlyRelayUrl(url: string): boolean {
const key = relayKey(url)
return key.length > 0 && readOnlyKeySet.has(key)
}
/** True when this relay may receive an EVENT for `eventKind` (profile/list replaceables only on profile mirrors). */
export function relayAllowsPublishKind(url: string, eventKind: number): boolean {
if (!isProfileIndexOnlyRelay(url)) return true
return profileIndexPublishKindSet.has(eventKind)
}
export function filterRelaysForEventPublish(urls: readonly string[], eventKind: number): string[] {
return urls.filter((u) => relayAllowsPublishKind(u, eventKind) && !isReadOnlyRelayUrl(u))
}
/**
* Reply/mention author **read** hints used as publish targets: never LAN/Tor, read-only aggregators,
* or profile/index mirrors (those are not inboxes for notes or reactions).
*/
export function filterContextAuthorReadRelaysForPublish(urls: readonly string[]): string[] {
return urls.filter((u) => {
const key = relayKey(u)
if (!key) return false
if (isReadOnlyRelayUrl(u)) return false
if (isProfileIndexOnlyRelay(u)) return false
return true
})
}

3
src/lib/relay-url-priority.test.ts

@ -9,12 +9,13 @@ import { stripMailboxLocalUrlsForRemoteViewers } from '@/lib/relay-list-sanitize
import { syncViewerRelayStackNostrLandAggrEligible } from '@/lib/nostr-land-relay-eligibility' import { syncViewerRelayStackNostrLandAggrEligible } from '@/lib/nostr-land-relay-eligibility'
describe('filterContextAuthorReadRelaysForPublish', () => { describe('filterContextAuthorReadRelaysForPublish', () => {
it('drops loopback, LAN, and .onion; keeps public relays', () => { it('drops loopback, LAN, .onion, and profile/index mirrors; keeps public relays', () => {
const out = filterContextAuthorReadRelaysForPublish([ const out = filterContextAuthorReadRelaysForPublish([
'ws://localhost:4869/', 'ws://localhost:4869/',
'wss://127.0.0.1/', 'wss://127.0.0.1/',
'wss://192.168.0.5/', 'wss://192.168.0.5/',
'wss://abcdefghijklmnop.onion/', 'wss://abcdefghijklmnop.onion/',
'wss://profiles.nostrver.se/',
'wss://relay.example.com/' 'wss://relay.example.com/'
]) ])
expect(out).toEqual(['wss://relay.example.com/']) expect(out).toEqual(['wss://relay.example.com/'])

24
src/lib/relay-url-priority.ts

@ -21,12 +21,14 @@ export function dedupeNormalizeRelayUrlsOrdered(urls: string[]): string[] {
return out return out
} }
import { filterContextAuthorReadRelaysForPublish as stripNonInboxPublishHints } from '@/lib/relay-publish-filter'
/** /**
* NIP-65 **read** (inbox) hints from reply/mention context must never add LAN, loopback, or Tor-only * NIP-65 **read** (inbox) hints from reply/mention context must never add LAN, loopback, Tor-only,
* endpoints to the publish list those are the author's private reachability, not yours. * read-only aggregators, or profile/index mirrors to the publish list.
*/ */
export function filterContextAuthorReadRelaysForPublish(urls: string[]): string[] { export function filterContextAuthorReadRelaysForPublish(urls: string[]): string[] {
return dedupeNormalizeRelayUrlsOrdered(urls).filter((u) => { const reachable = dedupeNormalizeRelayUrlsOrdered(urls).filter((u) => {
const n = normalizeAnyRelayUrl(u) || u.trim() const n = normalizeAnyRelayUrl(u) || u.trim()
if (!n) return false if (!n) return false
if (isLocalNetworkUrl(u) || isLocalNetworkUrl(n)) return false if (isLocalNetworkUrl(u) || isLocalNetworkUrl(n)) return false
@ -38,6 +40,7 @@ export function filterContextAuthorReadRelaysForPublish(urls: string[]): string[
} }
return true return true
}) })
return dedupeNormalizeRelayUrlsOrdered(stripNonInboxPublishHints(reachable))
} }
/** LAN / local host relays first, then the rest; deduped. */ /** LAN / local host relays first, then the rest; deduped. */
@ -141,7 +144,7 @@ function buildWriteRelayPriorityLayers(opts: {
authorReadRelays?: string[] authorReadRelays?: string[]
favoriteRelays?: string[] favoriteRelays?: string[]
extraRelays?: string[] extraRelays?: string[]
/** When false, omit global FAST_WRITE and FAST_READ tails. Default true. */ /** When false, omit global FAST_WRITE tail. Default true. */
includeGlobalFastWriteReadTails?: boolean includeGlobalFastWriteReadTails?: boolean
}): string[][] { }): string[][] {
const tier1 = relayUrlsLocalsFirst(opts.userWriteRelays) const tier1 = relayUrlsLocalsFirst(opts.userWriteRelays)
@ -149,15 +152,15 @@ function buildWriteRelayPriorityLayers(opts: {
const tier3 = dedupeNormalizeRelayUrlsOrdered(opts.favoriteRelays ?? []) const tier3 = dedupeNormalizeRelayUrlsOrdered(opts.favoriteRelays ?? [])
const tier4 = dedupeNormalizeRelayUrlsOrdered(opts.extraRelays ?? []) const tier4 = dedupeNormalizeRelayUrlsOrdered(opts.extraRelays ?? [])
if (opts.includeGlobalFastWriteReadTails === false) { if (opts.includeGlobalFastWriteReadTails === false) {
return [tier1, tier2, tier3, tier4, [], []] return [tier1, tier2, tier3, tier4, []]
} }
const tier5 = normFastWrite() const tier5 = normFastWrite()
const tier6 = normFastRead() return [tier1, tier2, tier3, tier4, tier5]
return [tier1, tier2, tier3, tier4, tier5, tier6]
} }
/** /**
* Publish / write: user outboxes (locals first) target author inboxes favorites extras FAST_WRITE FAST_READ. * Publish / write: user outboxes (locals first) target author inboxes favorites extras FAST_WRITE.
* Read aggregators ({@link FAST_READ_RELAY_URLS}) are intentionally omitted they reject social writes.
*/ */
export function buildPrioritizedWriteRelayUrls(opts: { export function buildPrioritizedWriteRelayUrls(opts: {
userWriteRelays: string[] userWriteRelays: string[]
@ -168,7 +171,7 @@ export function buildPrioritizedWriteRelayUrls(opts: {
maxRelays?: number maxRelays?: number
/** When true, strip {@link SOCIAL_KIND_BLOCKED_RELAY_URLS} before capping (social kinds). */ /** When true, strip {@link SOCIAL_KIND_BLOCKED_RELAY_URLS} before capping (social kinds). */
applySocialKindBlockedFilter?: boolean applySocialKindBlockedFilter?: boolean
/** Default true: append FAST_WRITE then FAST_READ tiers. */ /** Default true: append FAST_WRITE tier. */
includeGlobalFastWriteReadTails?: boolean includeGlobalFastWriteReadTails?: boolean
}): string[] { }): string[] {
const max = opts.maxRelays ?? MAX_PUBLISH_RELAYS const max = opts.maxRelays ?? MAX_PUBLISH_RELAYS
@ -184,8 +187,7 @@ export function buildPrioritizedWriteRelayUrls(opts: {
{ source: 'author-read', urls: layers[1] ?? [] }, { source: 'author-read', urls: layers[1] ?? [] },
{ source: 'favorites', urls: layers[2] ?? [] }, { source: 'favorites', urls: layers[2] ?? [] },
{ source: 'explicit', urls: layers[3] ?? [] }, { source: 'explicit', urls: layers[3] ?? [] },
{ source: 'fast-write', urls: layers[4] ?? [] }, { source: 'fast-write', urls: layers[4] ?? [] }
{ source: 'fast-read', urls: layers[5] ?? [] }
], { ], {
operation: 'write', operation: 'write',
blockedRelays: opts.blockedRelays, blockedRelays: opts.blockedRelays,

9
src/services/client.service.ts

@ -129,6 +129,7 @@ import { hexPubkeysEqual, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/
import { collectNip05ValuesFromKind0 } from '@/lib/profile-metadata-search' import { collectNip05ValuesFromKind0 } from '@/lib/profile-metadata-search'
import { decodeProfileSearchQueryToPubkeyHex } from '@/lib/profile-search-query' import { decodeProfileSearchQueryToPubkeyHex } from '@/lib/profile-search-query'
import { getPubkeysFromPTags, tagNameEquals } from '@/lib/tag' import { getPubkeysFromPTags, tagNameEquals } from '@/lib/tag'
import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter'
import { import {
buildPrioritizedWriteRelayUrls, buildPrioritizedWriteRelayUrls,
dedupeNormalizeRelayUrlsOrdered, dedupeNormalizeRelayUrlsOrdered,
@ -710,12 +711,10 @@ class ClientService extends EventTarget {
* Normalize, dedupe, then cap at {@link MAX_PUBLISH_RELAYS}. * Normalize, dedupe, then cap at {@link MAX_PUBLISH_RELAYS}.
*/ */
private filterPublishingRelays(relays: string[], event: NEvent): string[] { private filterPublishingRelays(relays: string[], event: NEvent): string[] {
const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeAnyRelayUrl(u) || u))
const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
return dedupeNormalizeRelayUrlsOrdered( return dedupeNormalizeRelayUrlsOrdered(
relays.filter((url) => { filterRelaysForEventPublish(relays, event.kind).filter((url) => {
const n = normalizeAnyRelayUrl(url) || url const n = normalizeAnyRelayUrl(url) || url
if (readOnlySet.has(n)) return false
if (isSocialKindBlockedKind(event.kind) && socialKindBlockedSet.has(n)) return false if (isSocialKindBlockedKind(event.kind) && socialKindBlockedSet.has(n)) return false
return true return true
}) })
@ -1589,11 +1588,9 @@ class ClientService extends EventTarget {
: relayUrls : relayUrls
} }
const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeAnyRelayUrl(u) || u))
const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) const socialKindBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
let filtered = mergedRelayUrls.filter((url) => { let filtered = filterRelaysForEventPublish(mergedRelayUrls, event.kind).filter((url) => {
const n = normalizeAnyRelayUrl(url) || url const n = normalizeAnyRelayUrl(url) || url
if (readOnlySet.has(n)) return false
if (isSocialKindBlockedKind(event.kind) && socialKindBlockedSet.has(n)) return false if (isSocialKindBlockedKind(event.kind) && socialKindBlockedSet.has(n)) return false
return true return true
}) })

23
src/services/relay-selection.service.ts

@ -1,5 +1,6 @@
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT, READ_ONLY_RELAY_URLS } from '@/constants' import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT } from '@/constants'
import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns' import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns'
import client from '@/services/client.service' import client from '@/services/client.service'
@ -176,7 +177,7 @@ class RelaySelectionService {
} }
const deduplicatedRelays = order.map((o) => o.url) const deduplicatedRelays = order.map((o) => o.url)
const filtered = this.filterReadOnlyRelays( const filtered = this.filterPublishPickerRelays(
this.filterBlockedRelays(deduplicatedRelays, context.blockedRelays) this.filterBlockedRelays(deduplicatedRelays, context.blockedRelays)
) )
const relayTypes: Record<string, RelaySourceType> = {} const relayTypes: Record<string, RelaySourceType> = {}
@ -186,7 +187,7 @@ class RelaySelectionService {
return { return {
relays: filtered, relays: filtered,
relayTypes, relayTypes,
randomRelayUrls: this.filterReadOnlyRelays(randomRelayUrls) randomRelayUrls: this.filterPublishPickerRelays(randomRelayUrls)
} }
} }
@ -438,7 +439,7 @@ class RelaySelectionService {
selectedRelays = Array.from(new Set(selectedRelays)) selectedRelays = Array.from(new Set(selectedRelays))
} }
return this.filterReadOnlyRelays(this.filterBlockedRelays(selectedRelays, context.blockedRelays)) return this.filterPublishPickerRelays(this.filterBlockedRelays(selectedRelays, context.blockedRelays))
} }
/** /**
@ -808,17 +809,11 @@ class RelaySelectionService {
} }
/** /**
* Strip relays that never accept writes ({@link READ_ONLY_RELAY_URLS}) so they do not appear in the publish picker. * Strip read-only aggregators and profile/index mirrors from the post/reaction publish picker
* Same set as `ClientService` uses when filtering publish targets. * (notes and reactions are not kind 0 / NIP-65 list traffic).
*/ */
private filterReadOnlyRelays(relays: string[]): string[] { private filterPublishPickerRelays(relays: string[]): string[] {
const readOnlySet = new Set( return filterRelaysForEventPublish(relays, kinds.ShortTextNote)
READ_ONLY_RELAY_URLS.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean)
)
return relays.filter((relay) => {
const n = normalizeAnyRelayUrl(relay) || relay
return !readOnlySet.has(n)
})
} }
/** /**

Loading…
Cancel
Save