Collapse Nest summary by default; tap title to expand

Move the LIVE chip and listener count out of the header strip and into
the right side of the Stage card's title row (listeners first, then
chip). The remaining summary now hides by default and toggles when the
user taps the room title in the top bar, reclaiming vertical space for
the stage and chat.
This commit is contained in:
Claude
2026-04-28 19:55:40 +00:00
parent 24336cb940
commit 7fe88ac4be
2 changed files with 79 additions and 68 deletions
@@ -20,14 +20,12 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
@@ -39,7 +37,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -52,11 +49,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
@@ -86,11 +81,13 @@ import kotlinx.coroutines.launch
/**
* Full-screen layout for [com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity]. Vertically split into:
*
* 1. TopAppBar — room title + overflow menu (Share, host's Edit).
* 2. Header strip — LIVE chip, listener count, optional 1-line summary.
* 3. Stage — vertical adaptive grid of host/speakers (height-bounded
* so a 30-speaker room scrolls inside the strip and never pushes
* the chat below the fold).
* 1. TopAppBar — room title (tap to expand/collapse summary) + overflow
* menu (Share, host's Edit).
* 2. Optional summary strip — only visible when the user taps the title.
* 3. Stage — vertical adaptive grid of host/speakers, with the LIVE
* chip and listener count rendered in the stage header row
* (height-bounded so a 30-speaker room scrolls inside the strip
* and never pushes the chat below the fold).
* 4. Tabs — `Chat | Audience · N | Hands · N` (Hands host-only,
* shown only while there's at least one raised hand).
* 5. Tab content — fills the remaining vertical space:
@@ -122,6 +119,11 @@ internal fun NestFullScreen(
var showHostLeaveConfirm by rememberSaveable { mutableStateOf(false) }
var showReactionPicker by rememberSaveable { mutableStateOf(false) }
var hostMenuTarget by rememberSaveable { mutableStateOf<String?>(null) }
// Summary is collapsed by default; tapping the top-bar title
// toggles it so the user can preview the room description without
// opening a separate sheet, then collapse again to reclaim vertical
// room for the stage and chat.
var summaryExpanded by rememberSaveable { mutableStateOf(false) }
// Tab selection survives configuration changes and PIP transitions.
// Stored as ordinal so rememberSaveable can persist it without a
// custom Saver.
@@ -187,6 +189,7 @@ internal fun NestFullScreen(
onMinimize = onMinimize,
onMenuOpen = { showHostMenu = true },
onMenuDismiss = { showHostMenu = false },
onTitleClick = { summaryExpanded = !summaryExpanded },
onShare = {
showHostMenu = false
shareRoomNaddr(topBarContext, event)
@@ -223,10 +226,9 @@ internal fun NestFullScreen(
.fillMaxSize()
.padding(padding),
) {
RoomHeaderStrip(
summary = event.summary(),
listenerCount = presences.size,
)
if (summaryExpanded) {
RoomSummaryStrip(summary = event.summary())
}
// Self-cell tap toggles mic-mute when broadcasting; null
// when not broadcasting so the avatar falls back to the
// default no-op tap (rather than offering a button that
@@ -245,6 +247,7 @@ internal fun NestFullScreen(
onLongPressParticipant = onLongPressParticipant,
myPubkey = myPubkey,
onTapSelf = onTapSelf,
listenerCount = presences.size,
modifier = Modifier.padding(horizontal = 16.dp),
)
NestTabRow(
@@ -366,55 +369,23 @@ internal fun NestFullScreen(
}
/**
* Header strip rendered between the TopAppBar and the stage grid.
* Carries the LIVE chip + listener count and (when present) a single-
* line ellipsised summary. Lives at the screen level rather than in
* the TopAppBar so the chip + count have room to breathe and the
* summary's typography matches the body, not the title.
* Expanded summary strip shown between the TopAppBar and the stage grid
* when the user taps the title. The LIVE chip and listener count have
* moved to the stage card's header row, so this strip now carries only
* the room description.
*/
@Composable
private fun RoomHeaderStrip(
summary: String?,
listenerCount: Int,
) {
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
LiveChip()
Text(
text = pluralStringResource(R.plurals.nest_listener_count, listenerCount, listenerCount),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
if (!summary.isNullOrBlank()) {
Text(
text = summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(top = 4.dp),
)
}
}
}
@Composable
private fun LiveChip() {
Surface(
shape = RoundedCornerShape(50),
color = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer,
) {
Text(
text = stringRes(R.string.nest_live_chip),
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
)
}
private fun RoomSummaryStrip(summary: String?) {
if (summary.isNullOrBlank()) return
Text(
text = summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
)
}
/**
@@ -491,6 +462,7 @@ private fun NestTopAppBar(
onMinimize: () -> Unit,
onMenuOpen: () -> Unit,
onMenuDismiss: () -> Unit,
onTitleClick: () -> Unit,
onShare: () -> Unit,
onEdit: () -> Unit,
) {
@@ -500,6 +472,10 @@ private fun NestTopAppBar(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier
.fillMaxWidth()
.clickable(onClick = onTitleClick),
)
},
actions = {
@@ -34,6 +34,7 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
@@ -57,6 +58,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -137,6 +139,7 @@ internal fun StageGrid(
onLongPressParticipant: ((String) -> Unit)? = null,
myPubkey: String? = null,
onTapSelf: (() -> Unit)? = null,
listenerCount: Int = 0,
) {
// Float currently-speaking members to the top so the listener can
// see who they're hearing without scrolling. sortedBy is stable in
@@ -165,12 +168,29 @@ internal fun StageGrid(
vertical = STAGE_CARD_PADDING_VERTICAL,
),
) {
Text(
text = stringRes(R.string.nest_stage),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 8.dp),
)
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringRes(R.string.nest_stage),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f),
)
Text(
text =
pluralStringResource(
R.plurals.nest_listener_count,
listenerCount,
listenerCount,
),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(end = 8.dp),
)
LiveChip()
}
if (members.isEmpty()) {
EmptyStageHint()
return@Column
@@ -203,6 +223,21 @@ internal fun StageGrid(
}
}
@Composable
private fun LiveChip() {
Surface(
shape = RoundedCornerShape(50),
color = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer,
) {
Text(
text = stringRes(R.string.nest_live_chip),
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
)
}
}
/**
* Idle "Waiting for speakers…" placeholder for [StageGrid]. Pairs an
* hourglass glyph with the existing copy so the empty state reads as