fix(buzz): update Messages-list preview + unread dot for Buzz stream channels

A Buzz stream-channel chat message is a kind-40002 StreamMessageV2Event, not a
NIP-C7 kind-9 ChatEvent. It is `h`-scoped and attaches to the same
RelayGroupChannel as a kind-9, but `isGroupChatContent()` only recognized
ChatEvent/PollEvent/ThreadEvent/CommentEvent, so the Messages-list "newest
message" logic (initial scan `newestChatNote` + the live additive
`filterRelevantRelayGroupMessages`) and the unread-dot check all skipped it.
Result: a Buzz channel's row never reflected its real chat and never updated
live as new messages arrived.

Include StreamMessageV2Event in `isGroupChatContent()` (the Buzz dialect of
NIP-29), fixing the Messages preview and unread dot in one place. Adds a
regression test asserting kind-40002 counts as group chat content while a
group-scoped reaction does not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dtx8Shek4sSAXmHjnNMJF
This commit is contained in:
Claude
2026-07-25 00:38:34 +00:00
parent 9ca79ec2c6
commit affc547b0e
2 changed files with 27 additions and 2 deletions
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.quartz.nip29RelayGroups
import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
@@ -69,6 +70,9 @@ fun Event.isGroupScoped(): Boolean = groupId() != null
* to my group message is group-scoped too, so without this distinction it would be
* picked as the group's "last message" and render as a bogus, unopenable row on the
* Messages tab. Content kinds: 9 ([ChatEvent]), 1068 ([PollEvent]), 11
* ([ThreadEvent]), 1111 ([CommentEvent]).
* ([ThreadEvent]), 1111 ([CommentEvent]), and 40002 ([StreamMessageV2Event]) — the
* Buzz dialect's stream-channel chat message, which is `h`-scoped and attaches to the
* same channel as a kind-9, so it must count as a room's newest message too (otherwise
* a Buzz channel's Messages-list preview and unread dot never reflect its real chat).
*/
fun Event.isGroupChatContent(): Boolean = this is ChatEvent || this is PollEvent || this is ThreadEvent || this is CommentEvent
fun Event.isGroupChatContent(): Boolean = this is ChatEvent || this is PollEvent || this is ThreadEvent || this is CommentEvent || this is StreamMessageV2Event
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.quartz.nip29RelayGroups
import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
@@ -253,6 +254,26 @@ class Nip29ArmadaInteropTest {
assertEquals(gid, react.groupId())
}
@Test
fun buzzStreamMessageCountsAsGroupChatContent() {
// A kind-9 chat message is group content.
val kind9 = parse(ChatEvent.KIND, arrayOf(arrayOf("h", gid)), content = "gm", pubKey = alice)
assertTrue(kind9.isGroupChatContent())
// A Buzz stream-channel chat message (kind 40002) is `h`-scoped and attaches to the same
// RelayGroupChannel as a kind-9, so it must count as a room's newest message too — otherwise
// a Buzz channel's Messages-list preview and unread dot never reflect its real chat.
val buzz = parse(StreamMessageV2Event.KIND, arrayOf(arrayOf("h", gid)), content = "hi from buzz", pubKey = alice)
assertTrue(buzz is StreamMessageV2Event)
assertEquals(gid, buzz.groupId())
assertTrue(buzz.isGroupChatContent())
// A reaction is group-scoped but NOT content — it must never become a room's last message.
val react = parse(ReactionEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("e", "ee".repeat(32))), content = "🔥", pubKey = alice)
assertTrue(react.isGroupScoped())
assertFalse(react.isGroupChatContent())
}
@Test
fun ungroupedEventHasNoGroupId() {
val e = parse(ChatEvent.KIND, arrayOf(arrayOf("t", "hi")), content = "hi", pubKey = alice)