Merge pull request #2669 from vitorpamplona/claude/update-nestlobby-chat-layout-JimqW

Add chat composer to nest lobby screen
This commit is contained in:
Vitor Pamplona
2026-04-30 16:04:02 -04:00
committed by GitHub
3 changed files with 122 additions and 82 deletions
@@ -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))
}
}
@@ -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,
+1 -1
View File
@@ -533,7 +533,7 @@
<string name="nest_notification_stop">Stop</string>
<string name="nest_join">Join nest</string>
<string name="nest_lobby_host_label">Host</string>
<string name="nest_lobby_open_room">Open room</string>
<string name="nest_lobby_open_action">Open</string>
<string name="nest_lobby_recent_chat">Recent chat</string>
<string name="nest_lobby_no_listeners">No one is in the room right now.</string>
<string name="nest_lobby_listeners_count">%1$d listening</string>