fix(chats): match public-channel rows by channel id when merging updates

ChatroomListKnownFeedFilter.updateListWith only recognized
ChannelMessageEvent when checking the old list for an existing row to
replace. When a public channel's row was first populated from a
ChannelCreateEvent or ChannelMetadataEvent (no message had arrived
yet), the next incoming ChannelMessageEvent failed to match, so the
filter appended a second note for the same channel. Both rows then
produced the same PublicChannelLazyKey, crashing the LazyColumn with
"Key was already used".

Resolve the channel id from any IsInPublicChatChannel event (covers
ChannelMessageEvent and ChannelMetadataEvent) and fall back to the
event id for ChannelCreateEvent, mirroring how the lazy key is built
in ChatroomListFeedView.
This commit is contained in:
Claude
2026-05-16 12:09:31 +00:00
parent 8c73cc1d3b
commit 13c3ddd446
@@ -30,6 +30,8 @@ import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
class ChatroomListKnownFeedFilter(
@@ -111,7 +113,7 @@ class ChatroomListKnownFeedFilter(
newRelevantPublicMessages.forEach { newNotePair ->
var hasUpdated = false
oldList.forEach { oldNote ->
val channelId = (oldNote.event as? ChannelMessageEvent)?.channelId()
val channelId = publicChannelIdOf(oldNote)
if (newNotePair.key == channelId) {
hasUpdated = true
if ((newNotePair.value.createdAt() ?: 0L) > (oldNote.createdAt() ?: 0L)) {
@@ -260,4 +262,16 @@ class ChatroomListKnownFeedFilter(
}
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
// The chat list keys public-channel rows by channel id, so the existing row
// for a channel may be backed by a ChannelCreateEvent or ChannelMetadataEvent
// (when no message has arrived yet) — not only a ChannelMessageEvent. Matching
// only ChannelMessageEvent here would leave the old row in place and append the
// new one, producing a duplicate LazyColumn key.
private fun publicChannelIdOf(note: Note): String? =
when (val event = note.event) {
is IsInPublicChatChannel -> event.channelId()
is ChannelCreateEvent -> event.id
else -> null
}
}