Merge pull request #2629 from vitorpamplona/claude/expandable-nestfullscreen-summary-H38Ey

Refactor nest room UI: collapsible summary and stage header redesign
This commit is contained in:
Vitor Pamplona
2026-04-28 15:58:31 -04:00
committed by GitHub
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