From 58efb3b1ecddbb78e27b0855dbf3b65bbffd675b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 02:53:40 +0000 Subject: [PATCH 1/3] fix(account): always include broadcasting relays in non-private sends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ShortNotePost compose path went through `signAndComputeBroadcast` → `computeRelayListToBroadcast(event)`, which only mixed in the user's broadcasting relays indirectly via `outboxRelays` — and only when `author == userProfile()`. Two real paths dropped the broadcast list entirely: * Anonymous posts (`signAnonymouslyAndBroadcast`) sign with a throwaway pubkey, so `author != userProfile()` and the branch that pulls in `outboxRelays.flow.value` is skipped. * `MetadataEvent` / `AdvertisedRelayListEvent` returned early with `followPlusAllMineWithIndex + availableRelays`, neither of which includes the broadcast list. Hoist `broadcastRelayList.flow.value` to the top of the function (after the `GiftWrapEvent` / `WrappedEvent` early returns that defines what "private" means) and seed it into every non-private return path. Sets dedupe so the regular path stays unchanged in the common case where broadcasting relays were already reachable through `outboxRelays`. --- .../com/vitorpamplona/amethyst/model/Account.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 30ca6da76..8fac99545 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -937,10 +937,6 @@ class Account( private fun computeRelaysForChannels(event: Event): Set = cache.getAnyChannel(event)?.relays() ?: emptySet() fun computeRelayListToBroadcast(event: Event): Set { - if (event is MetadataEvent || event is AdvertisedRelayListEvent) { - // everywhere - return followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value - } if (event is GiftWrapEvent) { val receiver = event.recipientPubKey() if (receiver != null) { @@ -959,7 +955,16 @@ class Account( return emptySet() } + // Non-private sends always go to the user's broadcasting relays. + val broadcastRelays = broadcastRelayList.flow.value + + if (event is MetadataEvent || event is AdvertisedRelayListEvent) { + // everywhere + return followPlusAllMineWithIndex.flow.value + client.availableRelaysFlow().value + broadcastRelays + } + val relayList = mutableSetOf() + relayList.addAll(broadcastRelays) val author = cache.getUserIfExists(event.pubKey) From 88fefc96b8ee5703dd6e515d7864b81f5a3cfcf8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 03:04:13 +0000 Subject: [PATCH 2/3] fix(account): keep broadcasting relays out of personal & channel sends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit unconditionally seeded broadcasting relays for every non-private send. That over-broadcasts events that exist only for the user (encrypted drafts, app-specific data, bookmark lists) and events that already declare their own relay scope (polls, live activities, meeting spaces, public chats, ephemeral chats — anything cached as a channel). Add `wantsBroadcastRelays(event)` and gate broadcast-list inclusion on it. When the event opts out, also rebuild the author=userProfile() outbox from `nip65 + privateStorage + local` directly, since `outboxRelays.flow.value` already merges broadcast in. Excluded event types: * Personal storage: DraftWrapEvent, AppSpecificDataEvent, BookmarkListEvent (+ OldBookmarkListEvent, LabeledBookmarkListEvent) * Channel/community: PollEvent, MeetingSpaceEvent, MeetingRoomEvent, LiveActivitiesEvent, plus any event LocalCache resolves to a channel (public-chat kinds, ephemeral chats, live-activity chat). --- .../vitorpamplona/amethyst/model/Account.kt | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 8fac99545..675dc3091 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -195,7 +195,9 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent @@ -234,6 +236,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.rules.CommunityRulesEvent import com.vitorpamplona.quartz.nip72ModCommunities.rules.tags.KindRuleTag import com.vitorpamplona.quartz.nip72ModCommunities.rules.tags.PubkeyRuleTag import com.vitorpamplona.quartz.nip72ModCommunities.rules.tags.WotTag +import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent @@ -936,6 +939,29 @@ class Account( private fun computeRelaysForChannels(event: Event): Set = cache.getAnyChannel(event)?.relays() ?: emptySet() + // Personal events the user stores just for themselves — drafts, app settings, bookmark + // lists — and channel/community events that already declare their own relay set should + // not be replicated to the user's broadcasting relays. + private fun wantsBroadcastRelays(event: Event): Boolean { + if (event is DraftWrapEvent || + event is AppSpecificDataEvent || + event is BookmarkListEvent || + event is OldBookmarkListEvent || + event is LabeledBookmarkListEvent + ) { + return false + } + if (event is PollEvent || + event is MeetingSpaceEvent || + event is MeetingRoomEvent || + event is LiveActivitiesEvent || + cache.getAnyChannel(event) != null + ) { + return false + } + return true + } + fun computeRelayListToBroadcast(event: Event): Set { if (event is GiftWrapEvent) { val receiver = event.recipientPubKey() @@ -955,8 +981,8 @@ class Account( return emptySet() } - // Non-private sends always go to the user's broadcasting relays. - val broadcastRelays = broadcastRelayList.flow.value + val includeBroadcast = wantsBroadcastRelays(event) + val broadcastRelays = if (includeBroadcast) broadcastRelayList.flow.value else emptySet() if (event is MetadataEvent || event is AdvertisedRelayListEvent) { // everywhere @@ -970,7 +996,15 @@ class Account( if (author != null) { if (author == userProfile()) { - relayList.addAll(outboxRelays.flow.value) + if (includeBroadcast) { + relayList.addAll(outboxRelays.flow.value) + } else { + // outboxRelays mixes in the broadcast list; for personal/channel events + // we want the user's NIP-65 / private / local outbox without it. + relayList.addAll(nip65RelayList.outboxFlow.value) + relayList.addAll(privateStorageRelayList.flow.value) + relayList.addAll(localRelayList.flow.value) + } } else { val relays = author.outboxRelays()?.ifEmpty { null } From 007161c7b45751c92b776ed7566aae048172d7cf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 03:15:02 +0000 Subject: [PATCH 3/3] fix(account): only skip broadcast for channel events that declare home relays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tighten the channel/community arm of wantsBroadcastRelays. The previous revision dropped broadcasting relays for every PollEvent / MeetingSpaceEvent / MeetingRoomEvent / LiveActivitiesEvent and for any event LocalCache resolves to a channel. That's wrong when the event itself doesn't carry a relay set — without broadcast there's no destination left. Only treat a channel/community event as "self-routing" when its own relays() / allRelayUrls() (or its channel's relays()) is non-empty. Otherwise fall through to the broadcasting list. --- .../vitorpamplona/amethyst/model/Account.kt | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 675dc3091..2138e02e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -940,8 +940,10 @@ class Account( private fun computeRelaysForChannels(event: Event): Set = cache.getAnyChannel(event)?.relays() ?: emptySet() // Personal events the user stores just for themselves — drafts, app settings, bookmark - // lists — and channel/community events that already declare their own relay set should - // not be replicated to the user's broadcasting relays. + // lists — and channel/community events that already declare their own home relays + // should not be replicated to the user's broadcasting relays. Channel/community events + // that don't define any home relays fall through to broadcast, since there's nowhere + // else for them to land. private fun wantsBroadcastRelays(event: Event): Boolean { if (event is DraftWrapEvent || event is AppSpecificDataEvent || @@ -951,14 +953,14 @@ class Account( ) { return false } - if (event is PollEvent || - event is MeetingSpaceEvent || - event is MeetingRoomEvent || - event is LiveActivitiesEvent || - cache.getAnyChannel(event) != null - ) { - return false - } + if (event is PollEvent && event.relays().isNotEmpty()) return false + if (event is MeetingSpaceEvent && event.allRelayUrls().isNotEmpty()) return false + if (event is MeetingRoomEvent && event.allRelayUrls().isNotEmpty()) return false + if (event is LiveActivitiesEvent && event.allRelayUrls().isNotEmpty()) return false + + val channelRelays = cache.getAnyChannel(event)?.relays() + if (channelRelays != null && channelRelays.isNotEmpty()) return false + return true }