fix(nests): preserve relays tag on participant updates + sync action bar with stage grid

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<ParticipantTag>` (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
This commit is contained in:
Claude
2026-04-29 22:13:36 +00:00
parent 9628d17f5f
commit 8d228db440
2 changed files with 20 additions and 1 deletions
@@ -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)
}
}
@@ -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.