From aef8537c62c2988cba10e2f0304271fdd0916f09 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 20 May 2026 18:54:52 +0200 Subject: [PATCH] bug-fixes --- package-lock.json | 4 +- package.json | 2 +- src/components/PaytoDialog/index.tsx | 41 ++++++++++++++-- src/components/PaytoLink/index.tsx | 1 + src/components/ZapDialog/index.tsx | 25 ++++------ src/constants.ts | 1 + src/features/feed/relay-policy.test.ts | 20 ++++++++ src/features/feed/relay-policy.ts | 10 ++++ src/lib/pre-publish-relay-cap.ts | 14 ++---- src/lib/relay-publish-filter.test.ts | 41 ++++++++++++++++ src/lib/relay-publish-filter.ts | 65 +++++++++++++++++++++++++ src/lib/relay-url-priority.test.ts | 3 +- src/lib/relay-url-priority.ts | 24 ++++----- src/services/client.service.ts | 9 ++-- src/services/relay-selection.service.ts | 23 ++++----- 15 files changed, 221 insertions(+), 62 deletions(-) create mode 100644 src/lib/relay-publish-filter.test.ts create mode 100644 src/lib/relay-publish-filter.ts diff --git a/package-lock.json b/package-lock.json index 199b30f2..d89f34e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "imwald", - "version": "23.13.0", + "version": "23.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "imwald", - "version": "23.13.0", + "version": "23.13.1", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index 1a142f8a..66512a9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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", "private": true, "type": "module", diff --git a/src/components/PaytoDialog/index.tsx b/src/components/PaytoDialog/index.tsx index 4f99a3b8..dcdaed66 100644 --- a/src/components/PaytoDialog/index.tsx +++ b/src/components/PaytoDialog/index.tsx @@ -1,3 +1,4 @@ +import TipPublicMessagePrompt from '@/components/ZapDialog/TipPublicMessagePrompt' import { Dialog, DialogContent, @@ -8,6 +9,7 @@ import { import { Button } from '@/components/ui/button' import { Copy, ExternalLink, Wallet, Zap } from 'lucide-react' import { useTranslation } from 'react-i18next' +import { useRef, useState } from 'react' import { toast } from 'sonner' import { filterPaytoPaymentOpenHandlersForDevice, @@ -15,6 +17,7 @@ import { getPaytoTypeInfo } from '@/lib/payto' import { cn } from '@/lib/utils' +import { useNostr } from '@/providers/NostrProvider' import LightningInvoiceSection from './LightningInvoiceSection' export default function PaytoDialog({ @@ -22,15 +25,21 @@ export default function PaytoDialog({ onOpenChange, type, authority, - paytoUri + paytoUri, + recipientPubkey }: { open: boolean onOpenChange: (open: boolean) => void type: string authority: string paytoUri: string + /** When set, closing the dialog offers a kind-24 tip notice to this pubkey. */ + recipientPubkey?: string }) { const { t } = useTranslation() + const { pubkey: selfPubkey } = useNostr() + const [tipNoticeOpen, setTipNoticeOpen] = useState(false) + const skipTipNoticeOnCloseRef = useRef(false) const info = getPaytoTypeInfo(type) const label = info?.label ?? type const isLightning = type.toLowerCase() === 'lightning' @@ -41,11 +50,29 @@ export default function PaytoDialog({ const handleCopy = (text: string, copyLabel?: string) => { navigator.clipboard.writeText(text) 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 ( - + <> + + {recipientPubkey ? ( + + ) : null} + ) } diff --git a/src/components/PaytoLink/index.tsx b/src/components/PaytoLink/index.tsx index 9124f14b..efb4ecd2 100644 --- a/src/components/PaytoLink/index.tsx +++ b/src/components/PaytoLink/index.tsx @@ -151,6 +151,7 @@ export default function PaytoLink({ type={type} authority={authority} paytoUri={raw} + recipientPubkey={pubkey} /> )} diff --git a/src/components/ZapDialog/index.tsx b/src/components/ZapDialog/index.tsx index 137502b9..99e8f3aa 100644 --- a/src/components/ZapDialog/index.tsx +++ b/src/components/ZapDialog/index.tsx @@ -119,7 +119,6 @@ export default function ZapDialog({ : t('Send a payment to this user') const maybeOfferTipNoticeOnClose = () => { - if (paymentsOnly) return if (skipTipNoticeOnCloseRef.current) return if (selfPubkey && pubkey === selfPubkey) return setTipNoticeOpen(true) @@ -198,13 +197,11 @@ export default function ZapDialog({ }} /> - {!paymentsOnly && ( - - )} + ) } @@ -237,13 +234,11 @@ export default function ZapDialog({ /> - {!paymentsOnly && ( - - )} + ) } diff --git a/src/constants.ts b/src/constants.ts index 54dcf3b1..cb10fbef 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -425,6 +425,7 @@ export const DOCUMENT_RELAY_URLS = [ */ export const READ_ONLY_RELAY_URLS = [ 'wss://aggr.nostr.land', + 'wss://nostr.land', 'wss://relay.nostr.watch', 'wss://relaypag.es', 'wss://relay.noswhere.com', diff --git a/src/features/feed/relay-policy.test.ts b/src/features/feed/relay-policy.test.ts index 9ad6b0ea..140e2bb9 100644 --- a/src/features/feed/relay-policy.test.ts +++ b/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' + }) + ) + }) }) diff --git a/src/features/feed/relay-policy.ts b/src/features/feed/relay-policy.ts index 50ed3a8a..c0a3d398 100644 --- a/src/features/feed/relay-policy.ts +++ b/src/features/feed/relay-policy.ts @@ -3,6 +3,7 @@ import { SOCIAL_KIND_BLOCKED_RELAY_URLS, relayFilterIncludesSocialKindBlockedKind } from '@/constants' +import { relayAllowsPublishKind } from '@/lib/relay-publish-filter' import { AGGR_NOSTR_LAND_WSS } from '@/lib/nostr-land-aggr' import { getViewerRelayStackNostrLandAggrEligible } from '@/lib/nostr-land-relay-eligibility' import { @@ -19,6 +20,7 @@ export type FeedRelayDropReason = | 'duplicate' | 'user-blocked' | 'read-only-for-write' + | 'profile-index-for-write' | 'social-kind-blocked' | 'extended-tag-blocked' | 'third-party-local' @@ -195,6 +197,14 @@ export function applyFeedRelayPolicy( addDrop(dropped, normalized, layer.source, 'read-only-for-write') 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 ( socialFilter && isSocialKindBlockedRelay(key) && diff --git a/src/lib/pre-publish-relay-cap.ts b/src/lib/pre-publish-relay-cap.ts index 3eb02b51..d00f49a0 100644 --- a/src/lib/pre-publish-relay-cap.ts +++ b/src/lib/pre-publish-relay-cap.ts @@ -1,9 +1,6 @@ -import { - isSocialKindBlockedKind, - MAX_PUBLISH_RELAYS, - READ_ONLY_RELAY_URLS, - SOCIAL_KIND_BLOCKED_RELAY_URLS -} from '@/constants' +import { isSocialKindBlockedKind, MAX_PUBLISH_RELAYS, SOCIAL_KIND_BLOCKED_RELAY_URLS } from '@/constants' +import { kinds } from 'nostr-tools' +import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter' import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority' import { normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url' import type { NostrEvent } from 'nostr-tools' @@ -49,12 +46,11 @@ export function computePrePublishRelayCapPreview({ .map((u) => normalizeHttpRelayUrl(u) || u) .filter((u): u is string => !!u) 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)) outbox = dedupeNormalizeRelayUrlsOrdered( - outbox.filter((url) => { + filterRelaysForEventPublish(outbox, previewKind).filter((url) => { const n = normalizeAnyRelayUrl(url) || url - if (readOnlySet.has(n)) return false if (applySocialOutboxFilter && socialBlockedSet.has(n)) return false return true }) diff --git a/src/lib/relay-publish-filter.test.ts b/src/lib/relay-publish-filter.test.ts new file mode 100644 index 00000000..4a82718f --- /dev/null +++ b/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/']) + }) +}) diff --git a/src/lib/relay-publish-filter.ts b/src/lib/relay-publish-filter.ts new file mode 100644 index 00000000..ae79df16 --- /dev/null +++ b/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(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 + }) +} diff --git a/src/lib/relay-url-priority.test.ts b/src/lib/relay-url-priority.test.ts index 5974d3b4..8bb886cf 100644 --- a/src/lib/relay-url-priority.test.ts +++ b/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' 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([ 'ws://localhost:4869/', 'wss://127.0.0.1/', 'wss://192.168.0.5/', 'wss://abcdefghijklmnop.onion/', + 'wss://profiles.nostrver.se/', 'wss://relay.example.com/' ]) expect(out).toEqual(['wss://relay.example.com/']) diff --git a/src/lib/relay-url-priority.ts b/src/lib/relay-url-priority.ts index e0bda12e..90fceb6b 100644 --- a/src/lib/relay-url-priority.ts +++ b/src/lib/relay-url-priority.ts @@ -21,12 +21,14 @@ export function dedupeNormalizeRelayUrlsOrdered(urls: string[]): string[] { 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 - * endpoints to the publish list — those are the author's private reachability, not yours. + * NIP-65 **read** (inbox) hints from reply/mention context must never add LAN, loopback, Tor-only, + * read-only aggregators, or profile/index mirrors to the publish list. */ export function filterContextAuthorReadRelaysForPublish(urls: string[]): string[] { - return dedupeNormalizeRelayUrlsOrdered(urls).filter((u) => { + const reachable = dedupeNormalizeRelayUrlsOrdered(urls).filter((u) => { const n = normalizeAnyRelayUrl(u) || u.trim() if (!n) return false if (isLocalNetworkUrl(u) || isLocalNetworkUrl(n)) return false @@ -38,6 +40,7 @@ export function filterContextAuthorReadRelaysForPublish(urls: string[]): string[ } return true }) + return dedupeNormalizeRelayUrlsOrdered(stripNonInboxPublishHints(reachable)) } /** LAN / local host relays first, then the rest; deduped. */ @@ -141,7 +144,7 @@ function buildWriteRelayPriorityLayers(opts: { authorReadRelays?: string[] favoriteRelays?: 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 }): string[][] { const tier1 = relayUrlsLocalsFirst(opts.userWriteRelays) @@ -149,15 +152,15 @@ function buildWriteRelayPriorityLayers(opts: { const tier3 = dedupeNormalizeRelayUrlsOrdered(opts.favoriteRelays ?? []) const tier4 = dedupeNormalizeRelayUrlsOrdered(opts.extraRelays ?? []) if (opts.includeGlobalFastWriteReadTails === false) { - return [tier1, tier2, tier3, tier4, [], []] + return [tier1, tier2, tier3, tier4, []] } const tier5 = normFastWrite() - const tier6 = normFastRead() - return [tier1, tier2, tier3, tier4, tier5, tier6] + return [tier1, tier2, tier3, tier4, tier5] } /** - * 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: { userWriteRelays: string[] @@ -168,7 +171,7 @@ export function buildPrioritizedWriteRelayUrls(opts: { maxRelays?: number /** When true, strip {@link SOCIAL_KIND_BLOCKED_RELAY_URLS} before capping (social kinds). */ applySocialKindBlockedFilter?: boolean - /** Default true: append FAST_WRITE then FAST_READ tiers. */ + /** Default true: append FAST_WRITE tier. */ includeGlobalFastWriteReadTails?: boolean }): string[] { const max = opts.maxRelays ?? MAX_PUBLISH_RELAYS @@ -184,8 +187,7 @@ export function buildPrioritizedWriteRelayUrls(opts: { { source: 'author-read', urls: layers[1] ?? [] }, { source: 'favorites', urls: layers[2] ?? [] }, { source: 'explicit', urls: layers[3] ?? [] }, - { source: 'fast-write', urls: layers[4] ?? [] }, - { source: 'fast-read', urls: layers[5] ?? [] } + { source: 'fast-write', urls: layers[4] ?? [] } ], { operation: 'write', blockedRelays: opts.blockedRelays, diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 7d3436ce..7fd138d3 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -129,6 +129,7 @@ import { hexPubkeysEqual, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/ import { collectNip05ValuesFromKind0 } from '@/lib/profile-metadata-search' import { decodeProfileSearchQueryToPubkeyHex } from '@/lib/profile-search-query' import { getPubkeysFromPTags, tagNameEquals } from '@/lib/tag' +import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter' import { buildPrioritizedWriteRelayUrls, dedupeNormalizeRelayUrlsOrdered, @@ -710,12 +711,10 @@ class ClientService extends EventTarget { * Normalize, dedupe, then cap at {@link MAX_PUBLISH_RELAYS}. */ 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)) return dedupeNormalizeRelayUrlsOrdered( - relays.filter((url) => { + filterRelaysForEventPublish(relays, event.kind).filter((url) => { const n = normalizeAnyRelayUrl(url) || url - if (readOnlySet.has(n)) return false if (isSocialKindBlockedKind(event.kind) && socialKindBlockedSet.has(n)) return false return true }) @@ -1589,11 +1588,9 @@ class ClientService extends EventTarget { : 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)) - let filtered = mergedRelayUrls.filter((url) => { + let filtered = filterRelaysForEventPublish(mergedRelayUrls, event.kind).filter((url) => { const n = normalizeAnyRelayUrl(url) || url - if (readOnlySet.has(n)) return false if (isSocialKindBlockedKind(event.kind) && socialKindBlockedSet.has(n)) return false return true }) diff --git a/src/services/relay-selection.service.ts b/src/services/relay-selection.service.ts index 0d9dcba4..8e652828 100644 --- a/src/services/relay-selection.service.ts +++ b/src/services/relay-selection.service.ts @@ -1,5 +1,6 @@ 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 { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns' import client from '@/services/client.service' @@ -176,7 +177,7 @@ class RelaySelectionService { } const deduplicatedRelays = order.map((o) => o.url) - const filtered = this.filterReadOnlyRelays( + const filtered = this.filterPublishPickerRelays( this.filterBlockedRelays(deduplicatedRelays, context.blockedRelays) ) const relayTypes: Record = {} @@ -186,7 +187,7 @@ class RelaySelectionService { return { relays: filtered, relayTypes, - randomRelayUrls: this.filterReadOnlyRelays(randomRelayUrls) + randomRelayUrls: this.filterPublishPickerRelays(randomRelayUrls) } } @@ -438,7 +439,7 @@ class RelaySelectionService { 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. - * Same set as `ClientService` uses when filtering publish targets. + * Strip read-only aggregators and profile/index mirrors from the post/reaction publish picker + * (notes and reactions are not kind 0 / NIP-65 list traffic). */ - private filterReadOnlyRelays(relays: string[]): string[] { - const readOnlySet = new Set( - READ_ONLY_RELAY_URLS.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean) - ) - return relays.filter((relay) => { - const n = normalizeAnyRelayUrl(relay) || relay - return !readOnlySet.has(n) - }) + private filterPublishPickerRelays(relays: string[]): string[] { + return filterRelaysForEventPublish(relays, kinds.ShortTextNote) } /**