Adjusts the roudabout way of making the chat screen

This commit is contained in:
Vitor Pamplona
2026-04-28 15:11:17 -04:00
parent 2a7683a7c2
commit ee860b92a1
4 changed files with 40 additions and 35 deletions
@@ -45,13 +45,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel
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.BouncingIntentNav import com.vitorpamplona.amethyst.ui.navigation.navs.BouncingIntentNav
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.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import androidx.lifecycle.viewmodel.compose.viewModel as composeViewModel import androidx.lifecycle.viewmodel.compose.viewModel as composeViewModel
@@ -112,6 +110,7 @@ internal fun ColumnScope.NestChatPanel(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
onWantsToReply = nestScreenModel::reply, onWantsToReply = nestScreenModel::reply,
onWantsToEditDraft = nestScreenModel::editFromDraft,
) )
} }
} }
@@ -129,11 +128,12 @@ internal fun ColumnScope.NestChatPanel(
@Composable @Composable
private fun NestChatMessageList( private fun NestChatMessageList(
messages: List<LiveActivitiesChatMessageEvent>, messages: List<Note>,
routeForLastRead: String, routeForLastRead: String,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: BouncingIntentNav, nav: BouncingIntentNav,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
onWantsToEditDraft: (Note) -> Unit,
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
@@ -150,33 +150,23 @@ private fun NestChatMessageList(
} }
} }
// viewModel.chat is sorted ascending by created_at (oldest first).
// reverseLayout=true puts index 0 at the bottom, so reverse the
// list here to keep newest-at-bottom rendering — same behavior
// LiveStream chat ships in ChatFeedLoaded.
val reversed = remember(messages) { messages.asReversed() }
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState, state = listState,
reverseLayout = true, reverseLayout = true,
) { ) {
items( items(
items = reversed, items = messages,
key = { it.id }, key = { it.idHex },
contentType = { it.kind }, ) { note ->
) { event ->
val note = remember(event.id) { LocalCache.getOrCreateNote(event.id) }
ChatroomMessageCompose( ChatroomMessageCompose(
baseNote = note, baseNote = note,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
onWantsToReply = onWantsToReply, onWantsToReply = onWantsToReply,
onWantsToEditDraft = NEST_CHAT_NO_OP_NOTE, onWantsToEditDraft = onWantsToEditDraft,
) )
} }
} }
} }
private val NEST_CHAT_NO_OP_NOTE: (Note) -> Unit = {}
@@ -111,8 +111,8 @@ private fun ChatCollector(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND), kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
tags = mapOf("a" to listOf(roomATag)), tags = mapOf("a" to listOf(roomATag)),
) )
LocalCache.observeEvents<LiveActivitiesChatMessageEvent>(filter).collect { events -> LocalCache.observeNotes(filter).collect { notes ->
events.forEach { viewModel.onChatEvent(it) } notes.forEach { viewModel.onChatEvent(it) }
} }
} }
} }
@@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.model.Channel.Companion.DefaultFeedOrder
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.nestsclient.BroadcastHandle import com.vitorpamplona.nestsclient.BroadcastHandle
import com.vitorpamplona.nestsclient.NestsClient import com.vitorpamplona.nestsclient.NestsClient
import com.vitorpamplona.nestsclient.NestsListener import com.vitorpamplona.nestsclient.NestsListener
@@ -41,6 +43,7 @@ import com.vitorpamplona.nestsclient.connectReconnectingNestsSpeaker
import com.vitorpamplona.nestsclient.moq.SubscribeHandle import com.vitorpamplona.nestsclient.moq.SubscribeHandle
import com.vitorpamplona.nestsclient.transport.WebTransportFactory import com.vitorpamplona.nestsclient.transport.WebTransportFactory
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentSet import kotlinx.collections.immutable.toPersistentSet
@@ -136,7 +139,7 @@ class NestViewModel(
/** /**
* Chat ledger for the live-activities chat panel (#1) — every * Chat ledger for the live-activities chat panel (#1) — every
* kind-1311 ([com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent]) * kind-1311 ([LiveActivitiesChatMessageEvent])
* tagged with this room's `a`-pointer, ordered by `created_at` * tagged with this room's `a`-pointer, ordered by `created_at`
* ascending so the newest message is at the end (the panel * ascending so the newest message is at the end (the panel
* auto-scrolls to it). * auto-scrolls to it).
@@ -144,10 +147,10 @@ class NestViewModel(
* Dedupes by event id — a relay re-emit on reconnect can't * Dedupes by event id — a relay re-emit on reconnect can't
* produce a duplicate row. * produce a duplicate row.
*/ */
private val chatById = LinkedHashMap<String, com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent>() private val chatById = LinkedHashMap<String, Note>()
private val _chat = private val _chat =
MutableStateFlow<List<com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent>>(emptyList()) MutableStateFlow<List<Note>>(emptyList())
val chat: StateFlow<List<com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent>> = _chat.asStateFlow() val chat: StateFlow<List<Note>> = _chat.asStateFlow()
/** /**
* Recent kind-7 reactions for the floating speaker-avatar overlay * Recent kind-7 reactions for the floating speaker-avatar overlay
@@ -388,11 +391,11 @@ class NestViewModel(
* room before invoking this. Same-id re-emits are deduped; the * room before invoking this. Same-id re-emits are deduped; the
* resulting list is sorted by `created_at` ascending. * resulting list is sorted by `created_at` ascending.
*/ */
fun onChatEvent(event: com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent) { fun onChatEvent(chatNote: Note) {
if (closed) return if (closed) return
if (chatById.containsKey(event.id)) return if (chatById.containsKey(chatNote.idHex)) return
chatById[event.id] = event chatById[chatNote.idHex] = chatNote
_chat.value = chatById.values.sortedBy { it.createdAt } _chat.value = chatById.values.sortedWith(DefaultFeedOrder)
} }
/** /**
@@ -20,6 +20,8 @@
*/ */
package com.vitorpamplona.amethyst.commons.viewmodels package com.vitorpamplona.amethyst.commons.viewmodels
import com.sun.org.apache.xml.internal.serializer.utils.Utils.messages
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.nestsclient.NestsClient import com.vitorpamplona.nestsclient.NestsClient
import com.vitorpamplona.nestsclient.NestsException import com.vitorpamplona.nestsclient.NestsException
import com.vitorpamplona.nestsclient.NestsListener import com.vitorpamplona.nestsclient.NestsListener
@@ -32,6 +34,7 @@ import com.vitorpamplona.nestsclient.transport.WebTransportFactory
import com.vitorpamplona.nestsclient.transport.WebTransportSession import com.vitorpamplona.nestsclient.transport.WebTransportSession
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -224,7 +227,7 @@ class NestViewModelTest {
id: String, id: String,
createdAt: Long, createdAt: Long,
body: String, body: String,
) = com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent( ) = LiveActivitiesChatMessageEvent(
id = id, id = id,
pubKey = alice, pubKey = alice,
createdAt = createdAt, createdAt = createdAt,
@@ -233,15 +236,21 @@ class NestViewModelTest {
sig = "0".repeat(128), sig = "0".repeat(128),
) )
fun note(event: LiveActivitiesChatMessageEvent): Note {
val note = Note(event.id)
note.event = event
return note
}
// Out-of-order arrival on the relay must still produce a // Out-of-order arrival on the relay must still produce a
// chronological transcript on screen. // chronological transcript on screen.
vm.onChatEvent(chat(id = "1".repeat(64), createdAt = 200L, body = "second")) vm.onChatEvent(note(chat(id = "1".repeat(64), createdAt = 200L, body = "second")))
vm.onChatEvent(chat(id = "2".repeat(64), createdAt = 100L, body = "first")) vm.onChatEvent(note(chat(id = "2".repeat(64), createdAt = 100L, body = "first")))
val messages = vm.chat.value val messages = vm.chat.value
assertEquals(2, messages.size) assertEquals(2, messages.size)
assertEquals("first", messages[0].content) assertEquals("second", messages[0].event!!.content)
assertEquals("second", messages[1].content) assertEquals("first", messages[1].event!!.content)
} }
@Test @Test
@@ -250,7 +259,7 @@ class NestViewModelTest {
val vm = newViewModel { FakeNestsListener() } val vm = newViewModel { FakeNestsListener() }
val alice = "a".repeat(64) val alice = "a".repeat(64)
val msg = val msg =
com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent( LiveActivitiesChatMessageEvent(
id = "1".repeat(64), id = "1".repeat(64),
pubKey = alice, pubKey = alice,
createdAt = 100L, createdAt = 100L,
@@ -259,10 +268,13 @@ class NestViewModelTest {
sig = "0".repeat(128), sig = "0".repeat(128),
) )
val note = Note(msg.id)
note.event = msg
// Same id re-emitted by the relay on reconnect must not // Same id re-emitted by the relay on reconnect must not
// produce a duplicate row. // produce a duplicate row.
vm.onChatEvent(msg) vm.onChatEvent(note)
vm.onChatEvent(msg) vm.onChatEvent(note)
assertEquals(1, vm.chat.value.size) assertEquals(1, vm.chat.value.size)
} }