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 b3bced4bbc..e2b79b3bd9 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 @@ -36,7 +36,7 @@ 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.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton @@ -50,6 +50,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -90,6 +91,8 @@ import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ROLE import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * In-app navigation lobby for a NIP-53 audio room (kind-30312). Sits @@ -101,11 +104,12 @@ import com.vitorpamplona.quartz.utils.TimeUtils * * 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. + * stay fresh, renders the kind-1311 transcript with the same + * `LazyColumn(reverseLayout = true)` list the in-room screen uses, and + * exposes the same composer (kind-1311 chat) — 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. */ @Composable fun NestLobbyScreen( @@ -142,6 +146,21 @@ private fun NestLobbyContent( nestScreenModel.init(accountViewModel) nestScreenModel.load(event) + val messages by produceState(initialValue = emptyList(), roomATag) { + val filter = + Filter( + kinds = listOf(LiveActivitiesChatMessageEvent.KIND), + tags = mapOf("a" to listOf(roomATag)), + ) + LocalCache.observeNotes(filter).collect { notes -> + value = notes.sortedByDescending { it.createdAt() ?: 0L }.take(LOBBY_CHAT_LIMIT) + } + } + + val routeForLastRead = remember(roomATag) { "NestLobbyChat/$roomATag" } + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + Scaffold( contentWindowInsets = WindowInsets(0), topBar = { @@ -177,15 +196,41 @@ private fun NestLobbyContent( .consumeWindowInsets(padding) .imePadding(), ) { + // Same chat list shape as NestFullScreen: a reverse-layout + // LazyColumn keyed by note id. The lobby uses cached + // LocalCache notes instead of an active NestViewModel.chat, + // and stacks the room metadata (header, listener count, + // "Recent chat" label) above the oldest message so the + // user can scroll up through history into the room info. LazyColumn( - modifier = Modifier.weight(1f), - state = rememberLazyListState(), + modifier = Modifier.weight(1f).fillMaxSize(), + state = listState, + reverseLayout = true, ) { - item("header") { - RoomHeader(event, accountViewModel, nav) + items( + items = messages, + key = { it.idHex }, + ) { note -> + ChatroomMessageCompose( + baseNote = note, + routeForLastRead = routeForLastRead, + accountViewModel = accountViewModel, + nav = nav, + onWantsToReply = nestScreenModel::reply, + onWantsToEditDraft = nestScreenModel::editFromDraft, + ) } - item("listeners") { - CachedListenerCount(roomATag) + if (messages.isEmpty()) { + item("chat-empty") { + Text( + text = stringRes(R.string.nest_chat_empty), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } item("chat-header") { Text( @@ -194,7 +239,12 @@ private fun NestLobbyContent( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), ) } - cachedChatItems(roomATag, accountViewModel, nav) + item("listeners") { + CachedListenerCount(roomATag) + } + item("header") { + RoomHeader(event, accountViewModel, nav) + } } // The composer sits above the 3-button nav when the keyboard @@ -209,7 +259,12 @@ private fun NestLobbyContent( NestEditFieldRow( nestScreenModel = nestScreenModel, accountViewModel = accountViewModel, - onSendNewMessage = {}, + onSendNewMessage = { + scope.launch { + delay(100) + listState.animateScrollToItem(0) + } + }, nav = nav, ) } @@ -401,61 +456,6 @@ private fun CachedListenerCount(roomATag: String) { ) } -private fun LazyListScope.cachedChatItems( - roomATag: String, - accountViewModel: AccountViewModel, - nav: INav, -) { - item("chat-list") { - CachedChatList(roomATag, accountViewModel, nav) - } -} - -@Composable -private fun CachedChatList( - roomATag: String, - accountViewModel: AccountViewModel, - nav: INav, -) { - val messages by produceState(initialValue = emptyList(), roomATag) { - val filter = - Filter( - kinds = listOf(LiveActivitiesChatMessageEvent.KIND), - tags = mapOf("a" to listOf(roomATag)), - ) - LocalCache.observeNotes(filter).collect { notes -> - value = notes.sortedByDescending { it.createdAt() ?: 0L }.take(LOBBY_CHAT_LIMIT) - } - } - - val routeForLastRead = remember(roomATag) { "NestLobbyChat/$roomATag" } - - if (messages.isEmpty()) { - Text( - text = stringRes(R.string.nest_chat_empty), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - return - } - - Column(modifier = Modifier.fillMaxWidth()) { - messages.forEach { note -> - ChatroomMessageCompose( - baseNote = note, - routeForLastRead = routeForLastRead, - accountViewModel = accountViewModel, - nav = nav, - onWantsToReply = {}, - onWantsToEditDraft = {}, - ) - } - } -} - private const val LOBBY_CHAT_LIMIT = 50 // 6 minutes — matches PRESENCE_STALE_THRESHOLD_SEC in NestRoomEventCollectors.