fix: hide StageControlsBar on local intent flag for responsive tap

After tapping Leave the Stage, the bar lingered because it was gated
solely on isOnStageMe — a value derived from the presence aggregator.
That aggregator only updates after a presence event signs, broadcasts
to relays, and loops back through LocalCache. With the recent
broadcast / mute traffic contending on the signer plus the 500 ms
mute debounce in NestPresencePublisher, the round-trip ran long
enough that the user perceived Leave the Stage as not firing.

AND the visibility gate with ui.onStageNow (the local intent flag,
which flips synchronously on setOnStage(false)) so the tap hides the
bar on the next frame. Host demotion still hides it via the existing
isOnStageMe path.
This commit is contained in:
Claude
2026-05-01 21:30:05 +00:00
parent c981058a9f
commit 6610683a20
@@ -133,12 +133,20 @@ internal fun NestActionBar(
* everything a speaker needs while on stage: Talk / MicMute / Retry + * everything a speaker needs while on stage: Talk / MicMute / Retry +
* Leave the Stage. Renders nothing when the local user isn't on stage. * Leave the Stage. Renders nothing when the local user isn't on stage.
* *
* Visibility is gated on [isOnStage] alone — a stable signal driven * Visibility uses two signals AND'd together:
* by the kind-30312 role + presence `onstage` flag — so connection * - [isOnStage] — derived from `participantGrid.onStage` (kind-30312
* blips, broadcast state churn, mute toggles, and permission flows * role + presence `onstage` flag); flips on a real promote/demote
* never make the bar appear or disappear. It only shows up on a real * after the round-trip lands. Connection blips, broadcast state
* promote, and only goes away on a real demote (or the user tapping * churn, mute toggles, and permission flows do NOT change it.
* Leave the Stage). * - [ui.onStageNow] — the LOCAL intent flag. Flips synchronously on
* `setOnStage(false)` so a Leave-the-Stage tap hides the bar on
* the next frame, instead of waiting for the presence event to
* sign, broadcast, and loop back through LocalCache (a delay made
* visible in scenario "broadcast → mute → unmute → leave", where
* the signer queues behind a pending mute frame + 500 ms debounce).
*
* AND-of-both also handles host demotion correctly: intent stays true
* but the aggregator drops us, so the bar still hides.
*/ */
@Composable @Composable
internal fun StageControlsBar( internal fun StageControlsBar(
@@ -149,7 +157,7 @@ internal fun StageControlsBar(
speakerPubkeyHex: String, speakerPubkeyHex: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (!isOnStage) return if (!isOnStage || !ui.onStageNow) return
Row( Row(
modifier = modifier =
modifier modifier