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
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -42,7 +41,6 @@ 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.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
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.MeetingSpacePlannedFlag
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.MeetingSpacePrivateFlag
|
import com.vitorpamplona.amethyst.ui.note.types.MeetingSpacePrivateFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||||
@@ -271,11 +267,11 @@ private fun NestJoinCardContent(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* "Join nest" entry button — navigates to [NestLobbyScreen] rather
|
* "Join nest" entry button — navigates to [NestLobbyScreen] rather
|
||||||
* than launching the audio activity directly. The lobby is read-only
|
* than launching the audio activity directly. The lobby exposes the
|
||||||
* (cached chat, host, status), so a user who's just coming back to
|
* cached chat plus an active composer, so a user who's just coming
|
||||||
* check on an old room doesn't trigger a MoQ handshake or the host's
|
* back to read or chime in doesn't trigger a MoQ handshake or the
|
||||||
* kind-30312 republish path. The actual room launch lives behind
|
* host's kind-30312 republish path. The actual room launch lives
|
||||||
* [OpenNestRoomButton] inside the lobby.
|
* behind the lobby's top-bar "Open" action.
|
||||||
*
|
*
|
||||||
* Renders nothing for events without a service / endpoint / d-tag —
|
* Renders nothing for events without a service / endpoint / d-tag —
|
||||||
* those rooms can't be joined on the audio plane.
|
* those rooms can't be joined on the audio plane.
|
||||||
@@ -312,35 +308,3 @@ fun JoinNestButton(
|
|||||||
Text(stringRes(R.string.nest_join))
|
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
|
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.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.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
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.height
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -47,15 +54,18 @@ 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.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
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.ClickableUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
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.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose
|
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.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.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
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
|
* between the feed / share-link entry points and [com.vitorpamplona.amethyst
|
||||||
* .ui.screen.loggedIn.nests.room.activity.NestActivity], so a user who's
|
* .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
|
* 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
|
* trigger a MoQ handshake or a host-side kind-30312 republish. The
|
||||||
* "Open room" button on this screen launches the audio activity.
|
* top-bar "Open" action launches the audio activity.
|
||||||
*
|
*
|
||||||
* The lobby is read-only: it mounts the room's relay subscription
|
* The lobby keeps the room's relay subscription
|
||||||
* ([NestRoomFilterAssemblerSubscription]) so cached chat / presence
|
* ([NestRoomFilterAssemblerSubscription]) warm so cached chat / presence
|
||||||
* stay fresh, but it does NOT open a [com.vitorpamplona.amethyst.commons
|
* 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,
|
* .viewmodels.NestViewModel], does NOT publish kind-10312 presence,
|
||||||
* and does NOT touch the audio pipeline.
|
* and does NOT touch the audio pipeline.
|
||||||
*/
|
*/
|
||||||
@@ -105,6 +121,7 @@ fun NestLobbyScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun NestLobbyContent(
|
private fun NestLobbyContent(
|
||||||
event: MeetingSpaceEvent,
|
event: MeetingSpaceEvent,
|
||||||
@@ -114,57 +131,116 @@ private fun NestLobbyContent(
|
|||||||
val roomATag = remember(event) { event.address().toValue() }
|
val roomATag = remember(event) { event.address().toValue() }
|
||||||
|
|
||||||
// Keep cached chat / presence warm while the user is on the lobby.
|
// 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)
|
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(
|
Scaffold(
|
||||||
|
contentWindowInsets = WindowInsets(0),
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBarWithBackButton(
|
ShorterTopAppBar(
|
||||||
caption = event.room().orEmpty(),
|
title = {
|
||||||
nav = nav,
|
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(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.windowInsetsPadding(WindowInsets.navigationBars),
|
||||||
.padding(16.dp),
|
|
||||||
) {
|
) {
|
||||||
OpenNestRoomButton(
|
NestEditFieldRow(
|
||||||
event = event,
|
nestScreenModel = nestScreenModel,
|
||||||
accountViewModel = accountViewModel,
|
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
|
@Composable
|
||||||
private fun RoomHeader(
|
private fun RoomHeader(
|
||||||
event: MeetingSpaceEvent,
|
event: MeetingSpaceEvent,
|
||||||
|
|||||||
@@ -533,7 +533,7 @@
|
|||||||
<string name="nest_notification_stop">Stop</string>
|
<string name="nest_notification_stop">Stop</string>
|
||||||
<string name="nest_join">Join nest</string>
|
<string name="nest_join">Join nest</string>
|
||||||
<string name="nest_lobby_host_label">Host</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_recent_chat">Recent chat</string>
|
||||||
<string name="nest_lobby_no_listeners">No one is in the room right now.</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>
|
<string name="nest_lobby_listeners_count">%1$d listening</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user