Merge pull request #2669 from vitorpamplona/claude/update-nestlobby-chat-layout-JimqW
Add chat composer to nest lobby screen
This commit is contained in:
+5
-41
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
+116
-40
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user