From 13c3ddd4469c54dba4e06b5b72dfe2bb969909b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 12:09:31 +0000 Subject: [PATCH 1/3] 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. --- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index 20699882cb..fff66acd53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -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): List = 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 + } } From f965d3b3312967d3636e598e221e22cc46724708 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 12:26:52 +0000 Subject: [PATCH 2/3] Revert "fix(chats): match public-channel rows by channel id when merging updates" This reverts commit 13c3ddd4469c54dba4e06b5b72dfe2bb969909b5. --- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index fff66acd53..20699882cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -30,8 +30,6 @@ 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( @@ -113,7 +111,7 @@ class ChatroomListKnownFeedFilter( newRelevantPublicMessages.forEach { newNotePair -> var hasUpdated = false oldList.forEach { oldNote -> - val channelId = publicChannelIdOf(oldNote) + val channelId = (oldNote.event as? ChannelMessageEvent)?.channelId() if (newNotePair.key == channelId) { hasUpdated = true if ((newNotePair.value.createdAt() ?: 0L) > (oldNote.createdAt() ?: 0L)) { @@ -262,16 +260,4 @@ class ChatroomListKnownFeedFilter( } override fun sort(items: Set): List = 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 - } } From 1c4ddfb2ce57f25646ac480ab4b95698baf32205 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 12:28:30 +0000 Subject: [PATCH 3/3] fix(chats): dedupe public channels in known list by channel id ChatroomListKnownFeedFilter.feed() iterated account.publicChatList.flow, a Set. ChannelTag uses identity equality (it's not a data class) and the set is built from raw tag parsing, so a channel list that names the same channel id twice (e.g., the same channel appearing in the public section and the encrypted private section, or repeated with different relay hints) produces multiple ChannelTag entries for the same channel. Each one resolved to the same newest Note via getOrCreatePublicChatChannel(it.eventId), so the feed contained the same Note twice and the chat list LazyColumn crashed with "Key PublicChannelLazyKey(channelId=...) was already used". Use the sibling flowSet (Set, already deduped by event id), which is the same source filterRelevantPublicMessages reads from. --- .../loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index 20699882cb..cba0f06476 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -55,10 +55,10 @@ class ChatroomListKnownFeedFilter( val publicChannels = account - .publicChatList.flow.value - .mapNotNull { it -> + .publicChatList.flowSet.value + .mapNotNull { channelId -> LocalCache - .getOrCreatePublicChatChannel(it.eventId) + .getOrCreatePublicChatChannel(channelId) .notes .filter { _, it -> account.isAcceptable(it) && it.event != null } .sortedWith(DefaultFeedOrder)