From 9c26c51a1bc26c9655181b913ca629b445ec6428 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 May 2026 20:57:07 +0000 Subject: [PATCH] fix: move on-stage controls out of action bar onto stage card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NestActionBar held the speaker controls (Talk / MicMute / Leave the Stage) alongside the audience controls (hand-raise / react / leave room), making the bar uncomfortably wide on phones once a user was promoted. Splits responsibilities: - NestActionBar now only handles connection state in the start cluster (Connect / Connecting chip / Reconnecting chip / empty) plus the existing end cluster. - StageControlsBar is a new strip rendered under the Stage card, gated only on isOnStage, holding the full broadcast state machine. isOnStage is the single visibility signal — a stable derived value from the kind-30312 role + presence onstage flag — so connection blips, broadcast state churn, mute toggles, and permission flows never make the new bar appear or disappear. It only flips on a real promote/demote or the user tapping Leave the Stage. --- .../nests/room/screen/NestActionBar.kt | 99 +++++++++++-------- .../nests/room/screen/NestFullScreen.kt | 12 ++- 2 files changed, 68 insertions(+), 43 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt index 816dd75d6..c723c2491 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt @@ -74,18 +74,17 @@ import com.vitorpamplona.amethyst.ui.stringRes * Layout: `[ start cluster ] · · · [ end cluster ]` with an optional * red status strip above for connection / broadcast / mute failures. * - * Start cluster — driven by connection × broadcast × on-stage state: + * Start cluster — driven by connection state only: * - * | State | Start cluster contents | - * |---------------------------------------------|-----------------------------------------| - * | Idle / Closed / Failed connection | `[Connect]` | - * | Connecting / Reconnecting | status chip | - * | Connected, audience | empty (system volume keys are enough) | - * | Connected, on stage, !canBroadcast | `[Leave the Stage]` | - * | Connected, on stage, mic idle | `[Talk] [Leave the Stage]` (+ pill) | - * | Connected, on stage, going live | status chip + `[Leave the Stage]` | - * | Connected, on stage, broadcasting | `[MicMute] [Leave the Stage]` | - * | Connected, on stage, broadcast failed | `[Retry] [Leave the Stage]` | + * | State | Start cluster contents | + * |------------------------------------|------------------------| + * | Idle / Closed / Failed connection | `[Connect]` | + * | Connecting | status chip | + * | Reconnecting | status chip | + * | Connected | empty | + * + * On-stage controls (Talk / MicMute / Leave the Stage) live in + * [StageControlsBar], attached to the bottom of the Stage card. * * End cluster: hand-raise (audience + connected only), react, leave room. */ @@ -94,8 +93,6 @@ internal fun NestActionBar( viewModel: NestViewModel, ui: NestUiState, isOnStage: Boolean, - canBroadcast: Boolean, - speakerPubkeyHex: String, handRaised: Boolean, onHandRaisedChange: (Boolean) -> Unit, onShowReactionPicker: () -> Unit, @@ -116,13 +113,7 @@ internal fun NestActionBar( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Box(modifier = Modifier.weight(1f, fill = true)) { - StartCluster( - viewModel = viewModel, - ui = ui, - isOnStage = isOnStage, - canBroadcast = canBroadcast, - speakerPubkeyHex = speakerPubkeyHex, - ) + StartCluster(viewModel = viewModel, ui = ui) } EndCluster( isOnStage = isOnStage, @@ -137,6 +128,49 @@ internal fun NestActionBar( } } +/** + * Controls strip that attaches under the Stage card. Contains + * 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). + */ +@Composable +internal fun StageControlsBar( + viewModel: NestViewModel, + ui: NestUiState, + isOnStage: Boolean, + canBroadcast: Boolean, + speakerPubkeyHex: String, + modifier: Modifier = Modifier, +) { + if (!isOnStage) return + Row( + modifier = + modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (canBroadcast) { + OnStageControls( + viewModel = viewModel, + broadcast = ui.broadcast, + speakerPubkeyHex = speakerPubkeyHex, + ) + } else { + // No signing/permission — only thing we can do is step down. + LeaveStageButton(onClick = { viewModel.setOnStage(false) }) + } + } +} + /** Single-line red error strip. Surfaces the most relevant failure. */ @Composable private fun ActionBarStatusStrip(ui: NestUiState) { @@ -166,9 +200,6 @@ private fun NestUiState.statusStripText(): String? { private fun StartCluster( viewModel: NestViewModel, ui: NestUiState, - isOnStage: Boolean, - canBroadcast: Boolean, - speakerPubkeyHex: String, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -190,26 +221,10 @@ private fun StartCluster( StatusChip(label = stringRes(R.string.nest_reconnecting)) } + // On-stage controls live in [StageControlsBar]; audience + // has nothing to do here (system volume keys are enough). is ConnectionUiState.Connected -> { - when { - canBroadcast && isOnStage -> { - OnStageControls( - viewModel = viewModel, - broadcast = ui.broadcast, - speakerPubkeyHex = speakerPubkeyHex, - ) - } - - // On stage but no signing/permission — only thing we can do is step down. - isOnStage -> { - LeaveStageButton(onClick = { viewModel.setOnStage(false) }) - } - - // Audience: nothing here. Phone volume keys cover local volume. - else -> { - Unit - } - } + Unit } } } 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 c5ccc9bfd..32fb19579 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 @@ -247,12 +247,22 @@ internal fun NestFullScreen( listenerCount = presences.size, modifier = Modifier.padding(horizontal = 16.dp), ) - NestActionBar( + // Speaker controls live attached to the Stage card so they + // only render when the user is actually on stage. This keeps + // the action bar narrow for audience and makes the controls + // appearing/disappearing a deliberate signal that the mic + // is now (or no longer) available. + StageControlsBar( viewModel = viewModel, ui = ui, isOnStage = isOnStageMe, canBroadcast = viewModel.canBroadcast, speakerPubkeyHex = myPubkey, + ) + NestActionBar( + viewModel = viewModel, + ui = ui, + isOnStage = isOnStageMe, handRaised = handRaised, onHandRaisedChange = onHandRaisedChange, onShowReactionPicker = { showReactionPicker = true },