From ecc157360639b157dca50cf8ce3802acba9126b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 21:36:23 +0000 Subject: [PATCH 1/5] fix(nests): broadcast presence/chat to the linked room's full relay list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-raise toggles in Amethyst weren't appearing in nostrnests's UI. nostrnests's NestsUI v2 routes reads against a fixed five-relay list (NestsUI-v2/src/lib/const.ts: relay.snort.social, nos.lol, relay.damus.io, relay.ditto.pub, relay.primal.net) — no outbox model. Its presence query is just `kinds:[10312], "#a":[roomATag]` over those five relays. Commit 637174ef already taught computeRelayListToBroadcast to fan a kind 30312 *room* event out to its `relays` tag. But kind 10312 presence (and chat / reactions, etc.) is a different event type that *links* to the room via an `a` tag. For those, broadcast was only reaching: - the broadcaster's outbox relays - the single firstOrNull() hint baked into the `a` tag - the relay we happened to receive the room event from If none of those overlap with nostrnests's fixed five reads, the hand-raise update is silently dropped. Extend the `linkedAddressIds` block in computeRelayListToBroadcast: when the linked address resolves to a MeetingSpaceEvent / MeetingRoomEvent / LiveActivitiesEvent, also add that linked event's allRelayUrls(). This covers presence updates and any other room-scoped event that points at a Nest via `#a`. https://claude.ai/code/session_016G7oP5BotPjUBgvMYrrrxh --- .../com/vitorpamplona/amethyst/model/Account.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 981a776fa..fa65a3aa9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -195,6 +195,7 @@ 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.tags.AddressBookmark +import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip56Reports.ReportType @@ -1002,6 +1003,20 @@ class Account( linkedNote.event?.let { linkedEvent -> relayList.addAll(computeRelaysForChannels(linkedEvent)) + + // Audio rooms / live activities pin their relay set + // on the room event itself. Presence (kind 10312) and + // any other event whose `a` tag points at one of these + // must also fan out to that relay list — otherwise a + // hand-raise / mute / publishing update only reaches + // the broadcaster's outbox + the single firstOrNull() + // hint baked into the `a` tag, and a fixed-relay + // listener like nostrnests never sees it. + when (linkedEvent) { + is MeetingSpaceEvent -> relayList.addAll(linkedEvent.allRelayUrls()) + is MeetingRoomEvent -> relayList.addAll(linkedEvent.allRelayUrls()) + is LiveActivitiesEvent -> relayList.addAll(linkedEvent.allRelayUrls()) + } } } } From 9b583d99ffb2015d78542636e9a5eab1b9597eef Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 29 Apr 2026 17:44:11 -0400 Subject: [PATCH 2/5] Improves the relay list used for linked Notes to include their relay urls --- .../vitorpamplona/amethyst/model/Account.kt | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 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 fa65a3aa9..0a1e883bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -981,7 +981,7 @@ class Account( } linkedNote.event?.let { linkedEvent -> - relayList.addAll(computeRelaysForChannels(linkedEvent)) + relayList.addAll(computeRelayListToBroadcast(linkedEvent)) } } } @@ -1002,21 +1002,7 @@ class Account( } linkedNote.event?.let { linkedEvent -> - relayList.addAll(computeRelaysForChannels(linkedEvent)) - - // Audio rooms / live activities pin their relay set - // on the room event itself. Presence (kind 10312) and - // any other event whose `a` tag points at one of these - // must also fan out to that relay list — otherwise a - // hand-raise / mute / publishing update only reaches - // the broadcaster's outbox + the single firstOrNull() - // hint baked into the `a` tag, and a fixed-relay - // listener like nostrnests never sees it. - when (linkedEvent) { - is MeetingSpaceEvent -> relayList.addAll(linkedEvent.allRelayUrls()) - is MeetingRoomEvent -> relayList.addAll(linkedEvent.allRelayUrls()) - is LiveActivitiesEvent -> relayList.addAll(linkedEvent.allRelayUrls()) - } + relayList.addAll(computeRelayListToBroadcast(linkedEvent)) } } } @@ -1030,6 +1016,10 @@ class Account( relayList.addAll(event.allRelayUrls()) } + if (event is MeetingRoomEvent) { + relayList.addAll(event.allRelayUrls()) + } + if (event is LiveActivitiesEvent) { relayList.addAll(event.allRelayUrls()) } From b0e7c62bb51978df0265ebe84823a213ccaa26c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 21:36:23 +0000 Subject: [PATCH 3/5] fix(nests): broadcast presence/chat to the linked room's full relay list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-raise toggles in Amethyst weren't appearing in nostrnests's UI. nostrnests's NestsUI v2 routes reads against a fixed five-relay list (NestsUI-v2/src/lib/const.ts: relay.snort.social, nos.lol, relay.damus.io, relay.ditto.pub, relay.primal.net) — no outbox model. Its presence query is just `kinds:[10312], "#a":[roomATag]` over those five relays. Commit 637174ef already taught computeRelayListToBroadcast to fan a kind 30312 *room* event out to its `relays` tag. But kind 10312 presence (and chat / reactions, etc.) is a different event type that *links* to the room via an `a` tag. For those, broadcast was only reaching: - the broadcaster's outbox relays - the single firstOrNull() hint baked into the `a` tag - the relay we happened to receive the room event from If none of those overlap with nostrnests's fixed five reads, the hand-raise update is silently dropped. Extend the `linkedAddressIds` block in computeRelayListToBroadcast: when the linked address resolves to a MeetingSpaceEvent / MeetingRoomEvent / LiveActivitiesEvent, also add that linked event's allRelayUrls(). This covers presence updates and any other room-scoped event that points at a Nest via `#a`. https://claude.ai/code/session_016G7oP5BotPjUBgvMYrrrxh --- .../com/vitorpamplona/amethyst/model/Account.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 981a776fa..fa65a3aa9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -195,6 +195,7 @@ 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.tags.AddressBookmark +import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip56Reports.ReportType @@ -1002,6 +1003,20 @@ class Account( linkedNote.event?.let { linkedEvent -> relayList.addAll(computeRelaysForChannels(linkedEvent)) + + // Audio rooms / live activities pin their relay set + // on the room event itself. Presence (kind 10312) and + // any other event whose `a` tag points at one of these + // must also fan out to that relay list — otherwise a + // hand-raise / mute / publishing update only reaches + // the broadcaster's outbox + the single firstOrNull() + // hint baked into the `a` tag, and a fixed-relay + // listener like nostrnests never sees it. + when (linkedEvent) { + is MeetingSpaceEvent -> relayList.addAll(linkedEvent.allRelayUrls()) + is MeetingRoomEvent -> relayList.addAll(linkedEvent.allRelayUrls()) + is LiveActivitiesEvent -> relayList.addAll(linkedEvent.allRelayUrls()) + } } } } From 9628d17f5fae6ed87c207a7853fcc5b7ba049704 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 29 Apr 2026 17:44:11 -0400 Subject: [PATCH 4/5] Improves the relay list used for linked Notes to include their relay urls --- .../vitorpamplona/amethyst/model/Account.kt | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 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 fa65a3aa9..0a1e883bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -981,7 +981,7 @@ class Account( } linkedNote.event?.let { linkedEvent -> - relayList.addAll(computeRelaysForChannels(linkedEvent)) + relayList.addAll(computeRelayListToBroadcast(linkedEvent)) } } } @@ -1002,21 +1002,7 @@ class Account( } linkedNote.event?.let { linkedEvent -> - relayList.addAll(computeRelaysForChannels(linkedEvent)) - - // Audio rooms / live activities pin their relay set - // on the room event itself. Presence (kind 10312) and - // any other event whose `a` tag points at one of these - // must also fan out to that relay list — otherwise a - // hand-raise / mute / publishing update only reaches - // the broadcaster's outbox + the single firstOrNull() - // hint baked into the `a` tag, and a fixed-relay - // listener like nostrnests never sees it. - when (linkedEvent) { - is MeetingSpaceEvent -> relayList.addAll(linkedEvent.allRelayUrls()) - is MeetingRoomEvent -> relayList.addAll(linkedEvent.allRelayUrls()) - is LiveActivitiesEvent -> relayList.addAll(linkedEvent.allRelayUrls()) - } + relayList.addAll(computeRelayListToBroadcast(linkedEvent)) } } } @@ -1030,6 +1016,10 @@ class Account( relayList.addAll(event.allRelayUrls()) } + if (event is MeetingRoomEvent) { + relayList.addAll(event.allRelayUrls()) + } + if (event is LiveActivitiesEvent) { relayList.addAll(event.allRelayUrls()) } From 8d228db440bb564afef4e7d5e0a67b6848812afa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 22:13:36 +0000 Subject: [PATCH 5/5] fix(nests): preserve relays tag on participant updates + sync action bar with stage grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related bugs the user hit while testing against nostrnests: 1. Approving a hand-raise made the Hands tab disappear (the host's local app saw the role grant) but the audience member did not appear on stage anywhere else. Cause: RoomParticipantActions.rebuild() rebuilt the kind-30312 without the original room's `relays` tag. The recently-added broadcast fan-out path (Account.computeRelayListToBroadcast) keys off `event.allRelayUrls()` for kind 30312, so a republished room event with no relays tag only reaches the host's outbox — not nostrnests's fixed five reads or the audience member subscribing on those reads. The audience member never sees their SPEAKER tag and stays absent from the participant list. Fix: copy `original.relays()` into the rebuild so every republish (approve, demote, kick → re-broadcast) keeps the relay routing that lets the room reach its full audience. 2. Tapping "Leave Stage" as the host emptied the StageGrid ("Waiting for speakers…") but the action bar still showed the Talk + Leave Stage cluster. Cause: two different definitions of "on stage" were in play. StageGrid uses ParticipantGrid (role + presence.onstage flag), so flipping onStageNow=false drops the host out. The action bar gated on the role-only `onStage: List` (the host's HOST tag never goes away), so the cluster stayed. Fix: derive isOnStageMe from participantGrid.onStage so both surfaces share the same "stepped off" semantics. The host who taps Leave Stage now drops to the audience-style mute toggle and can rejoin (kind-10312 onstage flips back to 1) without the controls lying about their state. https://claude.ai/code/session_016G7oP5BotPjUBgvMYrrrxh --- .../room/participants/RoomParticipantActions.kt | 9 +++++++++ .../loggedIn/nests/room/screen/NestFullScreen.kt | 12 +++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/RoomParticipantActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/RoomParticipantActions.kt index 88311e720..bb00c422c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/RoomParticipantActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/RoomParticipantActions.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEv import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.endpoint import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.image import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.participants +import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.relays import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.summary import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag @@ -132,6 +133,14 @@ internal object RoomParticipantActions { original.endpoint()?.let { endpoint(it) } original.summary()?.takeIf { it.isNotBlank() }?.let { summary(it) } original.image()?.takeIf { it.isNotBlank() }?.let { image(it) } + // Preserve the room's `relays` tag — without it, the + // republished kind-30312 fans out only to the host's + // outbox, never reaching the room's own relay set + // (e.g. nostrnests's fixed five reads). The audience + // member would then never see their role grant and + // wouldn't appear on stage for any other participant + // listening on those relays. + original.relays().takeIf { it.isNotEmpty() }?.let { relays(it) } if (others.isNotEmpty()) participants(others) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestFullScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestFullScreen.kt index 1546caabd..8ed1ce968 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestFullScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestFullScreen.kt @@ -135,7 +135,6 @@ internal fun NestFullScreen( val isHost = accountViewModel.account.signer.pubKey == event.pubKey val myPubkey = accountViewModel.account.signer.pubKey - val isOnStageMe = remember(onStage, myPubkey) { onStage.any { it.pubKey == myPubkey } } val leaveScope = rememberCoroutineScope() val topBarContext = LocalContext.current @@ -152,6 +151,17 @@ internal fun NestFullScreen( presences = presences, ) } + // Tie the action bar's "am I on stage" gate to the same data + // source the StageGrid uses (role + presence.onstage flag), + // not just the kind-30312 role tag. Otherwise a host who taps + // "Leave Stage" flips presence to onstage=0 and disappears + // from the StageGrid, but the action bar keeps showing the + // Talk + Leave Stage cluster because it's still gated on + // role-only. + val isOnStageMe = + remember(participantGrid, myPubkey) { + participantGrid.onStage.any { it.pubkey == myPubkey } + } // Same logic HandRaiseQueueSection uses internally — duplicated // here so the tab label can show a count without coupling the // section to the screen.