fix: move on-stage controls out of action bar onto stage card
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.
This commit is contained in:
+54
-39
@@ -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]` |
|
||||
* | 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,30 +221,14 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnStageControls(
|
||||
|
||||
+11
-1
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user