fix: render StageControlsBar inside the Stage card surface

The previous commit placed the controls strip as a sibling row right
after StageGrid. Visually it read as a separate band, not as part of
the stage card.

Adds a bottomBar slot to StageGrid that renders inside the same
Surface, below the grid, sharing the card's tonal background and
horizontal padding. StageControlsBar drops its own horizontal padding
since the card supplies it, and keeps a top inset so it has breathing
room from the avatars.
This commit is contained in:
Claude
2026-05-01 21:16:38 +00:00
parent 4af15a301f
commit c981058a9f
3 changed files with 45 additions and 37 deletions
@@ -154,7 +154,7 @@ internal fun StageControlsBar(
modifier =
modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
.padding(top = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
@@ -246,18 +246,20 @@ internal fun NestFullScreen(
onTapSelf = onTapSelf,
listenerCount = presences.size,
modifier = Modifier.padding(horizontal = 16.dp),
)
// 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,
// Speaker controls live inside 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.
bottomBar = {
StageControlsBar(
viewModel = viewModel,
ui = ui,
isOnStage = isOnStageMe,
canBroadcast = viewModel.canBroadcast,
speakerPubkeyHex = myPubkey,
)
},
)
NestActionBar(
viewModel = viewModel,
@@ -126,6 +126,10 @@ private val STAGE_MAX_HEIGHT = 320.dp
*
* When [members] is empty, renders an "Waiting for speakers…" hint
* instead so the strip stays visually anchored.
*
* [bottomBar] is rendered inside the same Surface, below the grid —
* used to anchor speaker controls to the stage card so they only
* appear when the local user is on stage.
*/
@Composable
internal fun StageGrid(
@@ -140,6 +144,7 @@ internal fun StageGrid(
myPubkey: String? = null,
onTapSelf: (() -> Unit)? = null,
listenerCount: Int = 0,
bottomBar: (@Composable () -> Unit)? = null,
) {
// Float currently-speaking members to the top so the listener can
// see who they're hearing without scrolling. sortedBy is stable in
@@ -193,32 +198,33 @@ internal fun StageGrid(
}
if (members.isEmpty()) {
EmptyStageHint()
return@Column
}
LazyVerticalGrid(
columns = GridCells.Adaptive(STAGE_CELL_MIN),
modifier = Modifier.fillMaxWidth().heightIn(max = STAGE_MAX_HEIGHT),
horizontalArrangement = Arrangement.spacedBy(GRID_SPACING),
verticalArrangement = Arrangement.spacedBy(GRID_SPACING),
) {
items(items = sortedMembers, key = { it.pubkey }) { member ->
val isSelf = myPubkey != null && member.pubkey == myPubkey
MemberCell(
member = member,
avatarSize = STAGE_AVATAR,
isSpeaking = member.pubkey in speakingNow,
audioLevel = audioLevels[member.pubkey] ?: 0f,
isConnecting = member.pubkey in connectingSpeakers,
showMicBadge = true,
reactions = reactionsByPubkey[member.pubkey].orEmpty(),
accountViewModel = accountViewModel,
onLongPressParticipant = onLongPressParticipant,
isSelf = isSelf,
onTapSelf = if (isSelf) onTapSelf else null,
modifier = Modifier.animateItem(),
)
} else {
LazyVerticalGrid(
columns = GridCells.Adaptive(STAGE_CELL_MIN),
modifier = Modifier.fillMaxWidth().heightIn(max = STAGE_MAX_HEIGHT),
horizontalArrangement = Arrangement.spacedBy(GRID_SPACING),
verticalArrangement = Arrangement.spacedBy(GRID_SPACING),
) {
items(items = sortedMembers, key = { it.pubkey }) { member ->
val isSelf = myPubkey != null && member.pubkey == myPubkey
MemberCell(
member = member,
avatarSize = STAGE_AVATAR,
isSpeaking = member.pubkey in speakingNow,
audioLevel = audioLevels[member.pubkey] ?: 0f,
isConnecting = member.pubkey in connectingSpeakers,
showMicBadge = true,
reactions = reactionsByPubkey[member.pubkey].orEmpty(),
accountViewModel = accountViewModel,
onLongPressParticipant = onLongPressParticipant,
isSelf = isSelf,
onTapSelf = if (isSelf) onTapSelf else null,
modifier = Modifier.animateItem(),
)
}
}
}
bottomBar?.invoke()
}
}
}