diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt
index 8a6bf0bad..7b764b5f5 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lobby
-import android.R.attr.textColor
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -42,7 +41,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
@@ -60,8 +58,6 @@ import com.vitorpamplona.amethyst.ui.note.types.MeetingSpaceOpenFlag
import com.vitorpamplona.amethyst.ui.note.types.MeetingSpacePlannedFlag
import com.vitorpamplona.amethyst.ui.note.types.MeetingSpacePrivateFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
-import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity
-import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestBridge
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
@@ -271,11 +267,11 @@ private fun NestJoinCardContent(
/**
* "Join nest" entry button — navigates to [NestLobbyScreen] rather
- * than launching the audio activity directly. The lobby is read-only
- * (cached chat, host, status), so a user who's just coming back to
- * check on an old room doesn't trigger a MoQ handshake or the host's
- * kind-30312 republish path. The actual room launch lives behind
- * [OpenNestRoomButton] inside the lobby.
+ * than launching the audio activity directly. The lobby exposes the
+ * cached chat plus an active composer, so a user who's just coming
+ * back to read or chime in doesn't trigger a MoQ handshake or the
+ * host's kind-30312 republish path. The actual room launch lives
+ * behind the lobby's top-bar "Open" action.
*
* Renders nothing for events without a service / endpoint / d-tag —
* those rooms can't be joined on the audio plane.
@@ -312,35 +308,3 @@ fun JoinNestButton(
Text(stringRes(R.string.nest_join))
}
}
-
-/**
- * Lobby's primary CTA. This is the only place that actually launches
- * [NestActivity] — meaning a host re-entering an old room only
- * triggers the audio session (and any rejoin-time event refresh)
- * when they explicitly opt in here.
- */
-@Composable
-fun OpenNestRoomButton(
- event: MeetingSpaceEvent,
- accountViewModel: AccountViewModel,
- modifier: Modifier = Modifier,
-) {
- val serviceBase = event.service()
- val endpoint = event.endpoint()
- val roomId = event.address().dTag
- if (serviceBase.isNullOrBlank() || endpoint.isNullOrBlank() || roomId.isBlank()) return
-
- val context = LocalContext.current
- Button(
- onClick = {
- NestBridge.set(accountViewModel)
- NestActivity.launch(
- context = context,
- addressValue = event.address().toValue(),
- )
- },
- modifier = modifier,
- ) {
- Text(stringRes(R.string.nest_lobby_open_room))
- }
-}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt
index b24902caa..b3bced4bb 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt
@@ -20,23 +20,30 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lobby
-import androidx.compose.foundation.background
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.Spacer
+import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -47,15 +54,18 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
-import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
+import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
+import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
@@ -66,6 +76,10 @@ import com.vitorpamplona.amethyst.ui.note.types.MeetingSpacePrivateFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestRoomFilterAssemblerSubscription
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestBridge
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat.NestEditFieldRow
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat.NestNewMessageViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.quartz.nip01Core.core.Address
@@ -82,12 +96,14 @@ import com.vitorpamplona.quartz.utils.TimeUtils
* between the feed / share-link entry points and [com.vitorpamplona.amethyst
* .ui.screen.loggedIn.nests.room.activity.NestActivity], so a user who's
* just coming back to read the chat or check who's hosting doesn't
- * trigger a MoQ handshake or a host-side kind-30312 republish. Only the
- * "Open room" button on this screen launches the audio activity.
+ * trigger a MoQ handshake or a host-side kind-30312 republish. The
+ * top-bar "Open" action launches the audio activity.
*
- * The lobby is read-only: it mounts the room's relay subscription
- * ([NestRoomFilterAssemblerSubscription]) so cached chat / presence
- * stay fresh, but it does NOT open a [com.vitorpamplona.amethyst.commons
+ * The lobby keeps the room's relay subscription
+ * ([NestRoomFilterAssemblerSubscription]) warm so cached chat / presence
+ * stay fresh, and exposes an active kind-1311 chat composer (same widget
+ * the in-room screen uses) so users can chime in without ever joining
+ * the audio plane. It does NOT open a [com.vitorpamplona.amethyst.commons
* .viewmodels.NestViewModel], does NOT publish kind-10312 presence,
* and does NOT touch the audio pipeline.
*/
@@ -105,6 +121,7 @@ fun NestLobbyScreen(
}
}
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun NestLobbyContent(
event: MeetingSpaceEvent,
@@ -114,57 +131,116 @@ private fun NestLobbyContent(
val roomATag = remember(event) { event.address().toValue() }
// Keep cached chat / presence warm while the user is on the lobby.
- // No MoQ session is opened — that's gated behind the Open Room button.
+ // No MoQ session is opened — that's gated behind the Open action.
NestRoomFilterAssemblerSubscription(roomATag, accountViewModel)
+ // Composer for kind-1311 chat. Same widget the in-room screen uses,
+ // so the user gets @-mention picker, file uploads, draft auto-save,
+ // emoji suggestions, and reply preview without joining audio.
+ val nestScreenModel: NestNewMessageViewModel =
+ viewModel(key = "NestLobby/$roomATag")
+ nestScreenModel.init(accountViewModel)
+ nestScreenModel.load(event)
+
Scaffold(
+ contentWindowInsets = WindowInsets(0),
topBar = {
- TopBarWithBackButton(
- caption = event.room().orEmpty(),
- nav = nav,
+ ShorterTopAppBar(
+ title = {
+ Text(
+ text = event.room().orEmpty(),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ },
+ navigationIcon = {
+ if (nav.canPop()) {
+ IconButton(nav::popBack) {
+ ArrowBackIcon()
+ }
+ }
+ },
+ actions = {
+ OpenNestRoomAction(
+ event = event,
+ accountViewModel = accountViewModel,
+ )
+ },
)
},
- bottomBar = {
+ ) { padding ->
+ Column(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .consumeWindowInsets(padding)
+ .imePadding(),
+ ) {
+ LazyColumn(
+ modifier = Modifier.weight(1f),
+ state = rememberLazyListState(),
+ ) {
+ item("header") {
+ RoomHeader(event, accountViewModel, nav)
+ }
+ item("listeners") {
+ CachedListenerCount(roomATag)
+ }
+ item("chat-header") {
+ Text(
+ text = stringRes(R.string.nest_lobby_recent_chat),
+ style = MaterialTheme.typography.titleSmall,
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
+ )
+ }
+ cachedChatItems(roomATag, accountViewModel, nav)
+ }
+
+ // The composer sits above the 3-button nav when the keyboard
+ // is closed and above the keyboard when it's open. The chat
+ // list above is free to scroll behind the navigation bar.
Box(
modifier =
Modifier
.fillMaxWidth()
- .background(MaterialTheme.colorScheme.surface)
- .padding(16.dp),
+ .windowInsetsPadding(WindowInsets.navigationBars),
) {
- OpenNestRoomButton(
- event = event,
+ NestEditFieldRow(
+ nestScreenModel = nestScreenModel,
accountViewModel = accountViewModel,
- modifier = Modifier.fillMaxWidth(),
+ onSendNewMessage = {},
+ nav = nav,
)
}
- },
- ) { padding ->
- LazyColumn(
- modifier =
- Modifier
- .fillMaxSize()
- .padding(padding),
- state = rememberLazyListState(),
- ) {
- item("header") {
- RoomHeader(event, accountViewModel, nav)
- }
- item("listeners") {
- CachedListenerCount(roomATag)
- }
- item("chat-header") {
- Text(
- text = stringRes(R.string.nest_lobby_recent_chat),
- style = MaterialTheme.typography.titleSmall,
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
- )
- }
- cachedChatItems(roomATag, accountViewModel, nav)
}
}
}
+@Composable
+private fun OpenNestRoomAction(
+ event: MeetingSpaceEvent,
+ accountViewModel: AccountViewModel,
+) {
+ val serviceBase = event.service()
+ val endpoint = event.endpoint()
+ val roomId = event.address().dTag
+ if (serviceBase.isNullOrBlank() || endpoint.isNullOrBlank() || roomId.isBlank()) return
+
+ val context = LocalContext.current
+ TextButton(
+ onClick = {
+ NestBridge.set(accountViewModel)
+ NestActivity.launch(
+ context = context,
+ addressValue = event.address().toValue(),
+ )
+ },
+ ) {
+ Text(stringRes(R.string.nest_lobby_open_action))
+ }
+}
+
@Composable
private fun RoomHeader(
event: MeetingSpaceEvent,
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index 2cc23756f..a30b9e49d 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -533,7 +533,7 @@
Stop
Join nest
Host
- Open room
+ Open
Recent chat
No one is in the room right now.
%1$d listening