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 =
modifier modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(top = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
@@ -246,18 +246,20 @@ internal fun NestFullScreen(
onTapSelf = onTapSelf, onTapSelf = onTapSelf,
listenerCount = presences.size, listenerCount = presences.size,
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
) // Speaker controls live inside the Stage card so they
// Speaker controls live attached to the Stage card so they // only render when the user is actually on stage. This
// only render when the user is actually on stage. This keeps // keeps the action bar narrow for audience and makes
// the action bar narrow for audience and makes the controls // the controls appearing/disappearing a deliberate
// appearing/disappearing a deliberate signal that the mic // signal that the mic is now (or no longer) available.
// is now (or no longer) available. bottomBar = {
StageControlsBar( StageControlsBar(
viewModel = viewModel, viewModel = viewModel,
ui = ui, ui = ui,
isOnStage = isOnStageMe, isOnStage = isOnStageMe,
canBroadcast = viewModel.canBroadcast, canBroadcast = viewModel.canBroadcast,
speakerPubkeyHex = myPubkey, speakerPubkeyHex = myPubkey,
)
},
) )
NestActionBar( NestActionBar(
viewModel = viewModel, viewModel = viewModel,
@@ -126,6 +126,10 @@ private val STAGE_MAX_HEIGHT = 320.dp
* *
* When [members] is empty, renders an "Waiting for speakers…" hint * When [members] is empty, renders an "Waiting for speakers…" hint
* instead so the strip stays visually anchored. * 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 @Composable
internal fun StageGrid( internal fun StageGrid(
@@ -140,6 +144,7 @@ internal fun StageGrid(
myPubkey: String? = null, myPubkey: String? = null,
onTapSelf: (() -> Unit)? = null, onTapSelf: (() -> Unit)? = null,
listenerCount: Int = 0, listenerCount: Int = 0,
bottomBar: (@Composable () -> Unit)? = null,
) { ) {
// Float currently-speaking members to the top so the listener can // Float currently-speaking members to the top so the listener can
// see who they're hearing without scrolling. sortedBy is stable in // see who they're hearing without scrolling. sortedBy is stable in
@@ -193,32 +198,33 @@ internal fun StageGrid(
} }
if (members.isEmpty()) { if (members.isEmpty()) {
EmptyStageHint() EmptyStageHint()
return@Column } else {
} LazyVerticalGrid(
LazyVerticalGrid( columns = GridCells.Adaptive(STAGE_CELL_MIN),
columns = GridCells.Adaptive(STAGE_CELL_MIN), modifier = Modifier.fillMaxWidth().heightIn(max = STAGE_MAX_HEIGHT),
modifier = Modifier.fillMaxWidth().heightIn(max = STAGE_MAX_HEIGHT), horizontalArrangement = Arrangement.spacedBy(GRID_SPACING),
horizontalArrangement = Arrangement.spacedBy(GRID_SPACING), verticalArrangement = Arrangement.spacedBy(GRID_SPACING),
verticalArrangement = Arrangement.spacedBy(GRID_SPACING), ) {
) { items(items = sortedMembers, key = { it.pubkey }) { member ->
items(items = sortedMembers, key = { it.pubkey }) { member -> val isSelf = myPubkey != null && member.pubkey == myPubkey
val isSelf = myPubkey != null && member.pubkey == myPubkey MemberCell(
MemberCell( member = member,
member = member, avatarSize = STAGE_AVATAR,
avatarSize = STAGE_AVATAR, isSpeaking = member.pubkey in speakingNow,
isSpeaking = member.pubkey in speakingNow, audioLevel = audioLevels[member.pubkey] ?: 0f,
audioLevel = audioLevels[member.pubkey] ?: 0f, isConnecting = member.pubkey in connectingSpeakers,
isConnecting = member.pubkey in connectingSpeakers, showMicBadge = true,
showMicBadge = true, reactions = reactionsByPubkey[member.pubkey].orEmpty(),
reactions = reactionsByPubkey[member.pubkey].orEmpty(), accountViewModel = accountViewModel,
accountViewModel = accountViewModel, onLongPressParticipant = onLongPressParticipant,
onLongPressParticipant = onLongPressParticipant, isSelf = isSelf,
isSelf = isSelf, onTapSelf = if (isSelf) onTapSelf else null,
onTapSelf = if (isSelf) onTapSelf else null, modifier = Modifier.animateItem(),
modifier = Modifier.animateItem(), )
) }
} }
} }
bottomBar?.invoke()
} }
} }
} }