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