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 +
* Leave the Stage. Renders nothing when the local user isn't on stage.
*
* Visibility is gated on [isOnStage] alone — a stable signal driven
* by the kind-30312 role + presence `onstage` flag — so connection
* blips, broadcast state churn, mute toggles, and permission flows
* never make the bar appear or disappear. It only shows up on a real
* promote, and only goes away on a real demote (or the user tapping
* Leave the Stage).
* Visibility uses two signals AND'd together:
* - [isOnStage] — derived from `participantGrid.onStage` (kind-30312
* role + presence `onstage` flag); flips on a real promote/demote
* after the round-trip lands. Connection blips, broadcast state
* churn, mute toggles, and permission flows do NOT change it.
* - [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
internal fun StageControlsBar(
@@ -149,7 +157,7 @@ internal fun StageControlsBar(
speakerPubkeyHex: String,
modifier: Modifier = Modifier,
) {
if (!isOnStage) return
if (!isOnStage || !ui.onStageNow) return
Row(
modifier =
modifier