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:
+38
-62
@@ -20,14 +20,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.screen
|
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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.BadgedBox
|
import androidx.compose.material3.BadgedBox
|
||||||
@@ -39,7 +37,6 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.PrimaryTabRow
|
import androidx.compose.material3.PrimaryTabRow
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -52,11 +49,9 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
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:
|
* 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).
|
* 1. TopAppBar — room title (tap to expand/collapse summary) + overflow
|
||||||
* 2. Header strip — LIVE chip, listener count, optional 1-line summary.
|
* menu (Share, host's Edit).
|
||||||
* 3. Stage — vertical adaptive grid of host/speakers (height-bounded
|
* 2. Optional summary strip — only visible when the user taps the title.
|
||||||
* so a 30-speaker room scrolls inside the strip and never pushes
|
* 3. Stage — vertical adaptive grid of host/speakers, with the LIVE
|
||||||
* the chat below the fold).
|
* 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,
|
* 4. Tabs — `Chat | Audience · N | Hands · N` (Hands host-only,
|
||||||
* shown only while there's at least one raised hand).
|
* shown only while there's at least one raised hand).
|
||||||
* 5. Tab content — fills the remaining vertical space:
|
* 5. Tab content — fills the remaining vertical space:
|
||||||
@@ -122,6 +119,11 @@ internal fun NestFullScreen(
|
|||||||
var showHostLeaveConfirm by rememberSaveable { mutableStateOf(false) }
|
var showHostLeaveConfirm by rememberSaveable { mutableStateOf(false) }
|
||||||
var showReactionPicker by rememberSaveable { mutableStateOf(false) }
|
var showReactionPicker by rememberSaveable { mutableStateOf(false) }
|
||||||
var hostMenuTarget by rememberSaveable { mutableStateOf<String?>(null) }
|
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.
|
// Tab selection survives configuration changes and PIP transitions.
|
||||||
// Stored as ordinal so rememberSaveable can persist it without a
|
// Stored as ordinal so rememberSaveable can persist it without a
|
||||||
// custom Saver.
|
// custom Saver.
|
||||||
@@ -187,6 +189,7 @@ internal fun NestFullScreen(
|
|||||||
onMinimize = onMinimize,
|
onMinimize = onMinimize,
|
||||||
onMenuOpen = { showHostMenu = true },
|
onMenuOpen = { showHostMenu = true },
|
||||||
onMenuDismiss = { showHostMenu = false },
|
onMenuDismiss = { showHostMenu = false },
|
||||||
|
onTitleClick = { summaryExpanded = !summaryExpanded },
|
||||||
onShare = {
|
onShare = {
|
||||||
showHostMenu = false
|
showHostMenu = false
|
||||||
shareRoomNaddr(topBarContext, event)
|
shareRoomNaddr(topBarContext, event)
|
||||||
@@ -223,10 +226,9 @@ internal fun NestFullScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding),
|
.padding(padding),
|
||||||
) {
|
) {
|
||||||
RoomHeaderStrip(
|
if (summaryExpanded) {
|
||||||
summary = event.summary(),
|
RoomSummaryStrip(summary = event.summary())
|
||||||
listenerCount = presences.size,
|
}
|
||||||
)
|
|
||||||
// Self-cell tap toggles mic-mute when broadcasting; null
|
// Self-cell tap toggles mic-mute when broadcasting; null
|
||||||
// when not broadcasting so the avatar falls back to the
|
// when not broadcasting so the avatar falls back to the
|
||||||
// default no-op tap (rather than offering a button that
|
// default no-op tap (rather than offering a button that
|
||||||
@@ -245,6 +247,7 @@ internal fun NestFullScreen(
|
|||||||
onLongPressParticipant = onLongPressParticipant,
|
onLongPressParticipant = onLongPressParticipant,
|
||||||
myPubkey = myPubkey,
|
myPubkey = myPubkey,
|
||||||
onTapSelf = onTapSelf,
|
onTapSelf = onTapSelf,
|
||||||
|
listenerCount = presences.size,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
NestTabRow(
|
NestTabRow(
|
||||||
@@ -366,55 +369,23 @@ internal fun NestFullScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Header strip rendered between the TopAppBar and the stage grid.
|
* Expanded summary strip shown between the TopAppBar and the stage grid
|
||||||
* Carries the LIVE chip + listener count and (when present) a single-
|
* when the user taps the title. The LIVE chip and listener count have
|
||||||
* line ellipsised summary. Lives at the screen level rather than in
|
* moved to the stage card's header row, so this strip now carries only
|
||||||
* the TopAppBar so the chip + count have room to breathe and the
|
* the room description.
|
||||||
* summary's typography matches the body, not the title.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomHeaderStrip(
|
private fun RoomSummaryStrip(summary: String?) {
|
||||||
summary: String?,
|
if (summary.isNullOrBlank()) return
|
||||||
listenerCount: Int,
|
Text(
|
||||||
) {
|
text = summary,
|
||||||
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp)) {
|
style = MaterialTheme.typography.bodySmall,
|
||||||
Row(
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier =
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
Modifier
|
||||||
) {
|
.fillMaxWidth()
|
||||||
LiveChip()
|
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -491,6 +462,7 @@ private fun NestTopAppBar(
|
|||||||
onMinimize: () -> Unit,
|
onMinimize: () -> Unit,
|
||||||
onMenuOpen: () -> Unit,
|
onMenuOpen: () -> Unit,
|
||||||
onMenuDismiss: () -> Unit,
|
onMenuDismiss: () -> Unit,
|
||||||
|
onTitleClick: () -> Unit,
|
||||||
onShare: () -> Unit,
|
onShare: () -> Unit,
|
||||||
onEdit: () -> Unit,
|
onEdit: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -500,6 +472,10 @@ private fun NestTopAppBar(
|
|||||||
text = title,
|
text = title,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onTitleClick),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
|||||||
+41
-6
@@ -34,6 +34,7 @@ import androidx.compose.foundation.border
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
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.alpha
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -137,6 +139,7 @@ internal fun StageGrid(
|
|||||||
onLongPressParticipant: ((String) -> Unit)? = null,
|
onLongPressParticipant: ((String) -> Unit)? = null,
|
||||||
myPubkey: String? = null,
|
myPubkey: String? = null,
|
||||||
onTapSelf: (() -> Unit)? = null,
|
onTapSelf: (() -> Unit)? = null,
|
||||||
|
listenerCount: Int = 0,
|
||||||
) {
|
) {
|
||||||
// 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
|
||||||
@@ -165,12 +168,29 @@ internal fun StageGrid(
|
|||||||
vertical = STAGE_CARD_PADDING_VERTICAL,
|
vertical = STAGE_CARD_PADDING_VERTICAL,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
text = stringRes(R.string.nest_stage),
|
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
) {
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
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()) {
|
if (members.isEmpty()) {
|
||||||
EmptyStageHint()
|
EmptyStageHint()
|
||||||
return@Column
|
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
|
* Idle "Waiting for speakers…" placeholder for [StageGrid]. Pairs an
|
||||||
* hourglass glyph with the existing copy so the empty state reads as
|
* hourglass glyph with the existing copy so the empty state reads as
|
||||||
|
|||||||
Reference in New Issue
Block a user