diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt index 4abda15b9..41d04a58b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestChatPanel.kt @@ -45,13 +45,11 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.BouncingIntentNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import androidx.lifecycle.viewmodel.compose.viewModel as composeViewModel @@ -112,6 +110,7 @@ internal fun ColumnScope.NestChatPanel( accountViewModel = accountViewModel, nav = nav, onWantsToReply = nestScreenModel::reply, + onWantsToEditDraft = nestScreenModel::editFromDraft, ) } } @@ -129,11 +128,12 @@ internal fun ColumnScope.NestChatPanel( @Composable private fun NestChatMessageList( - messages: List, + messages: List, routeForLastRead: String, accountViewModel: AccountViewModel, nav: BouncingIntentNav, onWantsToReply: (Note) -> Unit, + onWantsToEditDraft: (Note) -> Unit, ) { 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( modifier = Modifier.fillMaxSize(), state = listState, reverseLayout = true, ) { items( - items = reversed, - key = { it.id }, - contentType = { it.kind }, - ) { event -> - val note = remember(event.id) { LocalCache.getOrCreateNote(event.id) } + items = messages, + key = { it.idHex }, + ) { note -> ChatroomMessageCompose( baseNote = note, routeForLastRead = routeForLastRead, accountViewModel = accountViewModel, nav = nav, onWantsToReply = onWantsToReply, - onWantsToEditDraft = NEST_CHAT_NO_OP_NOTE, + onWantsToEditDraft = onWantsToEditDraft, ) } } } - -private val NEST_CHAT_NO_OP_NOTE: (Note) -> Unit = {} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt index 01d3c3543..d4d243912 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt @@ -111,8 +111,8 @@ private fun ChatCollector( kinds = listOf(LiveActivitiesChatMessageEvent.KIND), tags = mapOf("a" to listOf(roomATag)), ) - LocalCache.observeEvents(filter).collect { events -> - events.forEach { viewModel.onChatEvent(it) } + LocalCache.observeNotes(filter).collect { notes -> + notes.forEach { viewModel.onChatEvent(it) } } } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt index e42838cf9..da52af697 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel 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.NestsClient import com.vitorpamplona.nestsclient.NestsListener @@ -41,6 +43,7 @@ import com.vitorpamplona.nestsclient.connectReconnectingNestsSpeaker import com.vitorpamplona.nestsclient.moq.SubscribeHandle import com.vitorpamplona.nestsclient.transport.WebTransportFactory import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentSet @@ -136,7 +139,7 @@ class NestViewModel( /** * 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` * ascending so the newest message is at the end (the panel * auto-scrolls to it). @@ -144,10 +147,10 @@ class NestViewModel( * Dedupes by event id — a relay re-emit on reconnect can't * produce a duplicate row. */ - private val chatById = LinkedHashMap() + private val chatById = LinkedHashMap() private val _chat = - MutableStateFlow>(emptyList()) - val chat: StateFlow> = _chat.asStateFlow() + MutableStateFlow>(emptyList()) + val chat: StateFlow> = _chat.asStateFlow() /** * 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 * 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 (chatById.containsKey(event.id)) return - chatById[event.id] = event - _chat.value = chatById.values.sortedBy { it.createdAt } + if (chatById.containsKey(chatNote.idHex)) return + chatById[chatNote.idHex] = chatNote + _chat.value = chatById.values.sortedWith(DefaultFeedOrder) } /** diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt index fd1798ba9..8783edcfd 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt @@ -20,6 +20,8 @@ */ 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.NestsException import com.vitorpamplona.nestsclient.NestsListener @@ -32,6 +34,7 @@ import com.vitorpamplona.nestsclient.transport.WebTransportFactory import com.vitorpamplona.nestsclient.transport.WebTransportSession import com.vitorpamplona.quartz.nip01Core.core.Event 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.LnZapRequestEvent import kotlinx.coroutines.CoroutineScope @@ -224,7 +227,7 @@ class NestViewModelTest { id: String, createdAt: Long, body: String, - ) = com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent( + ) = LiveActivitiesChatMessageEvent( id = id, pubKey = alice, createdAt = createdAt, @@ -233,15 +236,21 @@ class NestViewModelTest { 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 // chronological transcript on screen. - vm.onChatEvent(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 = "1".repeat(64), createdAt = 200L, body = "second"))) + vm.onChatEvent(note(chat(id = "2".repeat(64), createdAt = 100L, body = "first"))) val messages = vm.chat.value assertEquals(2, messages.size) - assertEquals("first", messages[0].content) - assertEquals("second", messages[1].content) + assertEquals("second", messages[0].event!!.content) + assertEquals("first", messages[1].event!!.content) } @Test @@ -250,7 +259,7 @@ class NestViewModelTest { val vm = newViewModel { FakeNestsListener() } val alice = "a".repeat(64) val msg = - com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent( + LiveActivitiesChatMessageEvent( id = "1".repeat(64), pubKey = alice, createdAt = 100L, @@ -259,10 +268,13 @@ class NestViewModelTest { sig = "0".repeat(128), ) + val note = Note(msg.id) + note.event = msg + // Same id re-emitted by the relay on reconnect must not // produce a duplicate row. - vm.onChatEvent(msg) - vm.onChatEvent(msg) + vm.onChatEvent(note) + vm.onChatEvent(note) assertEquals(1, vm.chat.value.size) }