fix(chats): stable per-chatroom LazyColumn key for messages list

The chatroom list keyed each row by `if (index == 0) index else item.idHex`.
Two problems:

1. Position 0 was hardcoded to key `0`, so when a new chatroom moved to
   the top, the existing composition slot was reused with state from the
   previous chatroom — observed as "the row updated and reordered but
   still shows the old result" right at the top.
2. For other positions, the key was the latest message's `idHex`. When a
   new message arrived in any chatroom the chatroom's representative
   Note got replaced (different idHex), so Compose threw away the row
   and rebuilt it from scratch — wasted work.

Fix: derive a stable key from chatroom identity instead of message id —
nostr group id for marmot rooms, channel id for public/ephemeral
channels, sorted user set for DMs. Falls back to `item.idHex` for
unrecognized event types (drafts etc.). Reorders now move the row;
new-message updates re-use the slot.
This commit is contained in:
Claude
2026-04-26 15:44:28 +00:00
parent 6aecfe016b
commit 06340dbdf9
@@ -25,14 +25,16 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
@@ -45,6 +47,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChatroomHeaderCompose
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
@Composable
fun ChatroomListFeedView(
@@ -103,14 +111,16 @@ private fun FeedLoaded(
) {
val items by loaded.feed.collectAsStateWithLifecycle()
val myPubKey = accountViewModel.userProfile().pubkeyHex
LazyColumn(
contentPadding = rememberFeedContentPadding(FeedPadding),
state = listState,
) {
itemsIndexed(
items(
items.list,
key = { index, item -> if (index == 0) index else item.idHex },
) { _, item ->
key = { item -> chatroomLazyKey(item, myPubKey) },
) { item ->
Row(Modifier.fillMaxWidth()) {
ChatroomHeaderCompose(
item,
@@ -125,3 +135,40 @@ private fun FeedLoaded(
}
}
}
// Stable per-chatroom key — derived from chatroom identity, not the latest
// message id, so reorders move the row instead of recreating it.
private fun chatroomLazyKey(
item: Note,
myPubKey: HexKey,
): String {
item.inGatherers
?.firstNotNullOfOrNull { it as? MarmotGroupChatroom }
?.let { return "marmot:${it.nostrGroupId}" }
return when (val event = item.event) {
is ChannelMessageEvent -> {
"ch:${event.channelId() ?: item.idHex}"
}
is ChannelMetadataEvent -> {
"ch:${event.channelId() ?: item.idHex}"
}
is ChannelCreateEvent -> {
"ch:${event.id}"
}
is EphemeralChatEvent -> {
"eph:${event.roomId()?.toKey() ?: item.idHex}"
}
is ChatroomKeyable -> {
"dm:${event.chatroomKey(myPubKey).users.sorted().joinToString(",")}"
}
else -> {
item.idHex
}
}
}