From 5425b09fef611bf7eda7157f0a32c2c9cff862a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 22:11:44 +0000 Subject: [PATCH] fix: don't treat a group reaction as the group's latest message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit filterRelevantRelayGroupMessages picked ANY group-scoped note (isGroupScoped() = carries the group's `h` tag), so a reaction (kind 7) to my message became the group's "last message" on the Messages tab — a wrong row that the chat renderer can't display. Whitelist actual chat content (kind 9 chat / 1068 poll / 11 thread / 1111 comment) and reject reactions, deletions, labels, etc. Apply the same guard to the feed()'s channel-notes scan as defense in depth. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj --- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 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 a945ac5ca6..08e632594d 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 @@ -33,12 +33,16 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable +import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip29RelayGroups.GroupId import com.vitorpamplona.quartz.nip29RelayGroups.groupId import com.vitorpamplona.quartz.nip29RelayGroups.isGroupScoped +import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent +import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent +import com.vitorpamplona.quartz.nipC7Chats.ChatEvent class ChatroomListKnownFeedFilter( val account: Account, @@ -103,10 +107,11 @@ class ChatroomListKnownFeedFilter( account.relayGroupList.liveRelayGroupList.value.mapNotNull { groupTag -> val relay = RelayUrlNormalizer.normalizeOrNull(groupTag.relayUrl) ?: return@mapNotNull null val channel = LocalCache.getOrCreateRelayGroupChannel(GroupId(groupTag.groupId, relay)) - // Newest loaded message, or a placeholder row so a just-joined group shows up on - // Messages before its first kind-9 arrives (mirrors the Marmot-group path above). + // Newest loaded chat message, or a placeholder row so a just-joined group shows + // up on Messages before its first kind-9 arrives (mirrors the Marmot-group path + // above). Content kinds only — never a reaction/deletion as the "last message". channel.notes - .filter { _, it -> account.isAcceptable(it) && it.event != null } + .filter { _, it -> account.isAcceptable(it) && it.event != null && it.isRelayGroupChatContent() } .sortedByDefaultFeedOrder() .firstOrNull() ?: channel.placeholderNote() @@ -284,6 +289,15 @@ class ChatroomListKnownFeedFilter( return newRelevantEphemeralChats } + // Only actual group content represents a room in the Messages list. A reaction (kind 7), + // deletion, label, etc. carries the group's `h` tag too, so isGroupScoped() alone would let a + // reaction to my message become the group's "last message" — a wrong, unrenderable row. + private fun Note.isRelayGroupChatContent(): Boolean = + when (event) { + is ChatEvent, is PollEvent, is ThreadEvent, is CommentEvent -> true + else -> false + } + /** Latest message per joined NIP-29 group (inline view mode), keyed by group id. */ private fun filterRelevantRelayGroupMessages( newItems: Set, @@ -295,7 +309,7 @@ class ChatroomListKnownFeedFilter( .mapTo(HashSet()) { it.groupId } val result = mutableMapOf() newItems.forEach { newNote -> - val gid = newNote.event?.takeIf { it.isGroupScoped() }?.groupId() + val gid = newNote.event?.takeIf { it.isGroupScoped() && newNote.isRelayGroupChatContent() }?.groupId() if (gid != null && gid in joinedGroupIds && account.isAcceptable(newNote)) { val lastNote = result[gid] if (lastNote == null || (newNote.createdAt() ?: 0L) > (lastNote.createdAt() ?: 0L)) {