feat(nests): wrap room screen in Scaffold; title + overflow → TopAppBar

The room name and the MoreVert overflow lived inline at the top of
the metadata Column, so they scrolled with the rest. Hoist them into
a Material 3 TopAppBar inside a Scaffold, matching every other major
screen in the app.

NestTopAppBar — new private composable:
  - Title is `event.room()`, single-line ellipsis.
  - Actions: Box wrapping the MoreVert IconButton + DropdownMenu
    (Share for everyone, Edit gated on `isHost`).
  - containerColor = Transparent so the themed background painted by
    NestThemedScope's Surface (and any optional `bg` image) shows
    through cleanly without a double-paint.

NestFullScreen body now:
  Scaffold(
    containerColor = Color.Transparent,
    topBar = { NestTopAppBar(...) },
  ) { padding ->
    Column(fillMaxSize.padding(padding)) {
      Column(weight(1, false), verticalScroll, hpad+top pad) {
        // summary, listener count, participants grid, host actions
        // sheet, hand-raise queue, connection row, talk row, action
        // row, reaction picker, host-leave confirm dialog
      }
      NestChatPanel(weight(1, true), hpad+bottom pad)
    }
  }

Side effects:
  - Drops the manual windowInsetsPadding(safeDrawing) — Scaffold's
    contentWindowInsets handles that and the TopAppBar's own
    windowInsets handles the top edge.
  - The EditNestSheet conditional moves to NestThemedScope's body
    (after the Scaffold). It's a ModalBottomSheet, so position in
    the tree doesn't change layout, but adjacency to the Scaffold
    keeps the sheet/dialog boundary obvious.

https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
This commit is contained in:
Claude
2026-04-27 20:36:28 +00:00
parent 84712f48f1
commit 8c5804063e
@@ -28,12 +28,9 @@ 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.Row
import androidx.compose.foundation.layout.WindowInsets
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.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
@@ -41,11 +38,18 @@ import androidx.compose.material3.AssistChip
import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.AssistChipDefaults
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalIconToggleButton import androidx.compose.material3.FilledTonalIconToggleButton
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -55,7 +59,9 @@ 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
@@ -98,87 +104,58 @@ internal fun NestFullScreen(
) { ) {
val roomTheme = androidx.compose.runtime.remember(event) { RoomTheme.from(event) } val roomTheme = androidx.compose.runtime.remember(event) { RoomTheme.from(event) }
NestThemedScope(theme = roomTheme, accountViewModel = accountViewModel) { NestThemedScope(theme = roomTheme, accountViewModel = accountViewModel) {
// Outer column owns the safeDrawing inset; the top metadata
// section and the chat panel are weighted siblings.
//
// The top section uses `weight(1f, fill = false)` so it never
// exceeds half the screen and scrolls internally if it would.
// The chat uses `weight(1f, fill = true)` so it always takes
// its full allocation — the user explicitly wanted chat to
// dominate the screen rather than ride a fixed-height box at
// the bottom of the metadata scroll.
Column(
modifier =
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing),
) {
var showEditSheet by rememberSaveable { mutableStateOf(false) } var showEditSheet by rememberSaveable { mutableStateOf(false) }
var showHostMenu by rememberSaveable { mutableStateOf(false) } var showHostMenu by rememberSaveable { mutableStateOf(false) }
var showHostLeaveConfirm by rememberSaveable { mutableStateOf(false) } var showHostLeaveConfirm by rememberSaveable { mutableStateOf(false) }
val isHost = accountViewModel.account.signer.pubKey == event.pubKey val isHost = accountViewModel.account.signer.pubKey == event.pubKey
val leaveScope = rememberCoroutineScope() val leaveScope = rememberCoroutineScope()
val topBarContext = LocalContext.current
// Scaffold owns safeDrawing insets via its `contentWindowInsets`
// default, so we don't manage them manually anymore. The
// container is transparent because NestThemedScope's outer
// Surface already paints the themed background (and overlays
// the optional `bg` image on top of it); painting again here
// would double up.
Scaffold(
modifier = Modifier.fillMaxSize(),
containerColor = Color.Transparent,
topBar = {
NestTopAppBar(
title = event.room().orEmpty(),
isHost = isHost,
showHostMenu = showHostMenu,
onMenuOpen = { showHostMenu = true },
onMenuDismiss = { showHostMenu = false },
onShare = {
showHostMenu = false
shareRoomNaddr(topBarContext, event)
},
onEdit = {
showHostMenu = false
showEditSheet = true
},
)
},
) { padding ->
// Inner column is just the two weighted siblings — top
// metadata (scrolls internally if overflow) and the chat
// panel (takes the rest). Title and overflow menu live in
// the TopAppBar above.
Column(
modifier =
Modifier
.fillMaxSize()
.padding(padding),
) {
Column( Column(
modifier = modifier =
Modifier Modifier
.weight(1f, fill = false) .weight(1f, fill = false)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(top = 16.dp), .padding(top = 8.dp),
) { ) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top,
) {
event.room()?.let {
Text(
text = it,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.weight(1f),
)
}
// Overflow menu — visible to everyone (Share); host-only
// rows (Edit) are gated inside.
Box {
androidx.compose.material3.IconButton(onClick = { showHostMenu = true }) {
Icon(
symbol = MaterialSymbols.MoreVert,
contentDescription = stringRes(R.string.nest_overflow_menu),
)
}
val context = androidx.compose.ui.platform.LocalContext.current
androidx.compose.material3.DropdownMenu(
expanded = showHostMenu,
onDismissRequest = { showHostMenu = false },
) {
androidx.compose.material3.DropdownMenuItem(
text = { Text(stringRes(R.string.nest_share_action)) },
onClick = {
showHostMenu = false
shareRoomNaddr(context, event)
},
)
if (isHost) {
androidx.compose.material3.DropdownMenuItem(
text = { Text(stringRes(R.string.nest_edit_title)) },
onClick = {
showHostMenu = false
showEditSheet = true
},
)
}
}
}
}
if (showEditSheet) {
EditNestSheet(
accountViewModel = accountViewModel,
event = event,
onDismiss = { showEditSheet = false },
)
}
event.summary()?.let { event.summary()?.let {
Text( Text(
text = it, text = it,
@@ -355,6 +332,79 @@ internal fun NestFullScreen(
) )
} }
} }
// EditNestSheet renders as a ModalBottomSheet — placement in
// the tree doesn't affect layout, but keeping it adjacent to
// the Scaffold makes the dialog/sheet boundary obvious.
if (showEditSheet) {
EditNestSheet(
accountViewModel = accountViewModel,
event = event,
onDismiss = { showEditSheet = false },
)
}
}
}
/**
* TopAppBar for the room screen — hosts the room name and the
* overflow menu (Share for everyone, Edit for the host). The menu
* state lives at the screen level so the EditNestSheet can be
* triggered from here and rendered alongside the Scaffold.
*
* Container color is transparent so the themed background painted
* by [NestThemedScope]'s Surface (and any optional `bg` image) shows
* through cleanly.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun NestTopAppBar(
title: String,
isHost: Boolean,
showHostMenu: Boolean,
onMenuOpen: () -> Unit,
onMenuDismiss: () -> Unit,
onShare: () -> Unit,
onEdit: () -> Unit,
) {
TopAppBar(
title = {
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
actions = {
Box {
IconButton(onClick = onMenuOpen) {
Icon(
symbol = MaterialSymbols.MoreVert,
contentDescription = stringRes(R.string.nest_overflow_menu),
)
}
DropdownMenu(
expanded = showHostMenu,
onDismissRequest = onMenuDismiss,
) {
DropdownMenuItem(
text = { Text(stringRes(R.string.nest_share_action)) },
onClick = onShare,
)
if (isHost) {
DropdownMenuItem(
text = { Text(stringRes(R.string.nest_edit_title)) },
onClick = onEdit,
)
}
}
}
},
colors =
TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
),
)
} }
@Composable @Composable