fix(nests): tighten action bar state coverage
Several gaps in the action bar were causing dead-end UI states or making non-actionable controls visible: * Hand-raise was rendered while disconnected / connecting / failed. Raising can't be delivered to the room until we're Connected, so hide it until the connection state agrees. Threaded `isConnected` into `EndCluster` from the root composable. * Audience listen-mute toggle (Volume Up/Off) removed. System volume keys cover local volume; the on-screen control was redundant and shared the same speaker glyph as the broadcasting mic-mute toggle, which made the two affordances easy to confuse. * On-stage user without `canBroadcast` had no way to step down without leaving the whole room. Show a "Leave the Stage" outlined button in that branch so the speaker slot can be released. * Broadcast `Connecting` and `Failed` substates also had no step-down affordance — forcing the user to either retry the mic or leave the room. Added "Leave the Stage" to both. `stopBroadcast()` cancels the in-flight `speakerConnectJob` (NestViewModel `teardownBroadcast`), so cancelling mid-handshake is safe. * `BroadcastUiState.Broadcasting.muteError` was tracked in state but never surfaced. Route it through `ActionBarStatusStrip`. * Permission denial pill said "Open settings"; renamed to "No permissions" so the label communicates the *state* (the tap still deep-links to the system settings page).
This commit is contained in:
+42
-28
@@ -126,6 +126,7 @@ internal fun NestActionBar(
|
|||||||
}
|
}
|
||||||
EndCluster(
|
EndCluster(
|
||||||
isOnStage = isOnStage,
|
isOnStage = isOnStage,
|
||||||
|
isConnected = ui.connection is ConnectionUiState.Connected,
|
||||||
handRaised = handRaised,
|
handRaised = handRaised,
|
||||||
onHandRaisedChange = onHandRaisedChange,
|
onHandRaisedChange = onHandRaisedChange,
|
||||||
onShowReactionPicker = onShowReactionPicker,
|
onShowReactionPicker = onShowReactionPicker,
|
||||||
@@ -150,6 +151,12 @@ private fun ActionBarStatusStrip(ui: NestUiState) {
|
|||||||
stringRes(R.string.nest_broadcast_failed, broadcast.reason)
|
stringRes(R.string.nest_broadcast_failed, broadcast.reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is BroadcastUiState.Broadcasting -> {
|
||||||
|
broadcast.muteError?.let {
|
||||||
|
stringRes(R.string.nest_mute_failed, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -201,33 +208,27 @@ private fun StartCluster(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is ConnectionUiState.Connected -> {
|
is ConnectionUiState.Connected -> {
|
||||||
if (canBroadcast && isOnStage) {
|
when {
|
||||||
OnStageControls(
|
canBroadcast && isOnStage -> {
|
||||||
viewModel = viewModel,
|
OnStageControls(
|
||||||
ui = ui,
|
viewModel = viewModel,
|
||||||
speakerPubkeyHex = speakerPubkeyHex,
|
ui = ui,
|
||||||
)
|
speakerPubkeyHex = speakerPubkeyHex,
|
||||||
} else {
|
|
||||||
// Audience: a single mute toggle for the inbound
|
|
||||||
// listener stream. Mirrors the old ConnectionRow.
|
|
||||||
FilledTonalIconToggleButton(
|
|
||||||
checked = ui.isMuted,
|
|
||||||
onCheckedChange = { viewModel.setMuted(it) },
|
|
||||||
modifier = Modifier.size(width = 40.dp, height = ButtonDefaults.MinHeight),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
symbol =
|
|
||||||
if (ui.isMuted) {
|
|
||||||
MaterialSymbols.AutoMirrored.VolumeOff
|
|
||||||
} else {
|
|
||||||
MaterialSymbols.AutoMirrored.VolumeUp
|
|
||||||
},
|
|
||||||
contentDescription =
|
|
||||||
stringRes(
|
|
||||||
if (ui.isMuted) R.string.nest_unmute else R.string.nest_mute,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isOnStage -> {
|
||||||
|
// On stage but cannot broadcast (no signer / no permission).
|
||||||
|
// Only sensible affordance is to step back down to audience.
|
||||||
|
OutlinedButton(onClick = { viewModel.setOnStage(false) }) {
|
||||||
|
Text(stringRes(R.string.nest_leave_stage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// Audience: nothing to do here. System volume keys cover
|
||||||
|
// local volume; the mute toggle was redundant.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +327,14 @@ private fun OnStageControls(
|
|||||||
enabled = false,
|
enabled = false,
|
||||||
label = { Text(stringRes(R.string.nest_broadcast_connecting)) },
|
label = { Text(stringRes(R.string.nest_broadcast_connecting)) },
|
||||||
)
|
)
|
||||||
|
// Always offer a way down. stopBroadcast() cancels the
|
||||||
|
// in-flight speakerConnectJob (NestViewModel.teardownBroadcast).
|
||||||
|
OutlinedButton(onClick = {
|
||||||
|
viewModel.stopBroadcast()
|
||||||
|
viewModel.setOnStage(false)
|
||||||
|
}) {
|
||||||
|
Text(stringRes(R.string.nest_leave_stage))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is BroadcastUiState.Broadcasting -> {
|
is BroadcastUiState.Broadcasting -> {
|
||||||
@@ -398,6 +407,9 @@ private fun OnStageControls(
|
|||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
OutlinedButton(onClick = { viewModel.setOnStage(false) }) {
|
||||||
|
Text(stringRes(R.string.nest_leave_stage))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,6 +417,7 @@ private fun OnStageControls(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun EndCluster(
|
private fun EndCluster(
|
||||||
isOnStage: Boolean,
|
isOnStage: Boolean,
|
||||||
|
isConnected: Boolean,
|
||||||
handRaised: Boolean,
|
handRaised: Boolean,
|
||||||
onHandRaisedChange: (Boolean) -> Unit,
|
onHandRaisedChange: (Boolean) -> Unit,
|
||||||
onShowReactionPicker: () -> Unit,
|
onShowReactionPicker: () -> Unit,
|
||||||
@@ -414,9 +427,10 @@ private fun EndCluster(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
// Hand-raise only makes sense for audience: if you're already
|
// Hand-raise only makes sense for audience that's actually
|
||||||
// on stage, you don't need to ask. Hidden when on-stage.
|
// connected — if you're already on stage you don't need to ask,
|
||||||
if (!isOnStage) {
|
// and raising while disconnected can't be delivered to the room.
|
||||||
|
if (!isOnStage && isConnected) {
|
||||||
FilledTonalIconToggleButton(
|
FilledTonalIconToggleButton(
|
||||||
checked = handRaised,
|
checked = handRaised,
|
||||||
onCheckedChange = onHandRaisedChange,
|
onCheckedChange = onHandRaisedChange,
|
||||||
|
|||||||
@@ -523,8 +523,9 @@
|
|||||||
<string name="nest_broadcast_connecting">Going live…</string>
|
<string name="nest_broadcast_connecting">Going live…</string>
|
||||||
<string name="nest_broadcasting">Live</string>
|
<string name="nest_broadcasting">Live</string>
|
||||||
<string name="nest_broadcast_failed">Broadcast failed: %1$s</string>
|
<string name="nest_broadcast_failed">Broadcast failed: %1$s</string>
|
||||||
|
<string name="nest_mute_failed">Mute failed: %1$s</string>
|
||||||
<string name="nest_record_permission_required">Microphone access is required to talk in this room.</string>
|
<string name="nest_record_permission_required">Microphone access is required to talk in this room.</string>
|
||||||
<string name="nest_open_settings">Open settings</string>
|
<string name="nest_open_settings">No permissions</string>
|
||||||
<string name="nest_notification_channel">Nests</string>
|
<string name="nest_notification_channel">Nests</string>
|
||||||
<string name="nest_notification_channel_description">Keeps audio playing while a room is open.</string>
|
<string name="nest_notification_channel_description">Keeps audio playing while a room is open.</string>
|
||||||
<string name="nest_notification_listening">Nest connected</string>
|
<string name="nest_notification_listening">Nest connected</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user