mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
fix(concord): build thread replies as kind-1111 NIP-22 comments, not kind-9 quotes
Concord replies were kind-9 chat messages carrying a `q` tag. In the Concord model (matching Soapbox Armada) a `q` on a kind-9 is an inline *quote*, which clients deliberately keep OUT of threads — a real thread reply is a kind-1111 NIP-22 comment. So our replies rendered (and were sent to Armada) as inline quotes, never grouping into a message's thread. ChannelChat.reply now builds a CommentEvent via CommentEvent.replyBuilder: the uppercase K/E/P tags pin the immutable thread root and the lowercase k/e/p tags point at the immediate parent (root inherited when the parent is itself a comment, so the root is stable at any depth), plus the same channel/epoch binding every Chat Plane rumor carries. This is byte-compatible with Armada's buildV2CommentTags, so replies thread correctly in both directions. The read path already accepts these (they carry the binding, and consumeConcordRumor handles CommentEvent), so incoming Armada thread replies now land bound to their channel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
This commit is contained in:
+2
-2
@@ -135,7 +135,7 @@ object ConcordActions {
|
||||
return ConcordStreamEnvelope.wrap(rumor, channel, authorSigner, encrypted = true)
|
||||
}
|
||||
|
||||
/** Builds an encrypted-seal reply wrap (kind 9 quoting [parent]) on the [channel] plane. */
|
||||
/** Builds an encrypted-seal thread-reply wrap (kind-1111 NIP-22 comment on [parent]) on the [channel] plane. */
|
||||
suspend fun buildChannelReply(
|
||||
authorSigner: NostrSigner,
|
||||
channel: GroupKey,
|
||||
@@ -145,7 +145,7 @@ object ConcordActions {
|
||||
text: String,
|
||||
createdAt: Long,
|
||||
): Event {
|
||||
val rumor = ChannelChat.reply(authorSigner.pubKey, channelId, epoch, text, parent.id, parent.pubKey, createdAt)
|
||||
val rumor = ChannelChat.reply(authorSigner.pubKey, channelId, epoch, text, parent, createdAt)
|
||||
return ConcordStreamEnvelope.wrap(rumor, channel, authorSigner, encrypted = true)
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.commons.model.concord
|
||||
import com.vitorpamplona.amethyst.commons.actions.ConcordActions
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityFactory
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEntry
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.ChannelChat
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
@@ -80,12 +81,16 @@ class ConcordCommunitySessionTest {
|
||||
assertEquals("🤙", reaction.content)
|
||||
assertEquals(message.id, reaction.tags.first { it[0] == "e" }[1])
|
||||
|
||||
// A reply decrypts as a kind-9 quoting the parent via a `q` tag.
|
||||
// A reply decrypts as a kind-1111 NIP-22 thread comment: uppercase `E` at the thread
|
||||
// root and lowercase `e` at the immediate parent (both the message here), still bound
|
||||
// to the channel so it groups into the message's thread — the shape Armada threads.
|
||||
val replyWrap = ConcordActions.buildChannelReply(owner, general, community.generalChannelIdHex, community.rootEpoch, message, "gm back", 4L)
|
||||
assertTrue(session.ingest(replyWrap))
|
||||
val reply = captured.map { it.third }.first { it.content == "gm back" }
|
||||
assertEquals(9, reply.kind)
|
||||
assertEquals(message.id, reply.tags.first { it[0] == "q" }[1])
|
||||
assertEquals(1111, reply.kind)
|
||||
assertEquals(message.id, reply.tags.first { it[0] == "E" }[1])
|
||||
assertEquals(message.id, reply.tags.first { it[0] == "e" }[1])
|
||||
assertTrue(ChannelChat.isBoundTo(reply, community.generalChannelIdHex, community.rootEpoch))
|
||||
|
||||
// A stray wrap from a different community is ignored.
|
||||
val outsider = ConcordCommunityFactory.create(owner, "Other", createdAt = 1L, relays = listOf("wss://r.example"))
|
||||
|
||||
+20
-12
@@ -24,6 +24,8 @@ import com.vitorpamplona.quartz.concord.cord03Channels.tags.ChannelTag
|
||||
import com.vitorpamplona.quartz.concord.cord03Channels.tags.EpochTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
@@ -63,26 +65,32 @@ object ChannelChat {
|
||||
)
|
||||
|
||||
/**
|
||||
* Builds an unsigned kind-9 reply rumor bound to [channelId]/[epoch], quoting
|
||||
* [parentId] (a `q` tag, NIP-C7 style) and crediting its author with a `p` tag.
|
||||
* Reuses [message], so it is a normal channel message that also threads.
|
||||
* Builds an unsigned kind-1111 **thread reply** ([CommentEvent], NIP-22) to
|
||||
* [parent], bound to [channelId]/[epoch].
|
||||
*
|
||||
* A thread reply is a NIP-22 comment — NOT a kind-9 message with a `q` tag
|
||||
* (which NIP-C7 reserves for *inline quotes* that clients deliberately keep out
|
||||
* of threads). [CommentEvent.replyBuilder] emits the uppercase `K`/`E`/`P`
|
||||
* pointers at the immutable thread root and the lowercase `k`/`e`/`p` pointers
|
||||
* at the immediate [parent] (inheriting the root when [parent] is itself a
|
||||
* comment, so the root is stable at any depth). We add the same
|
||||
* `["channel", …]` + `["epoch", …]` binding every Chat Plane rumor carries, so
|
||||
* the reply is verifiable against the plane it arrives on. This is exactly the
|
||||
* shape Soapbox Armada builds and groups into a message's thread.
|
||||
*/
|
||||
fun reply(
|
||||
authorPubKey: HexKey,
|
||||
channelId: HexKey,
|
||||
epoch: Long,
|
||||
text: String,
|
||||
parentId: HexKey,
|
||||
parentAuthor: HexKey,
|
||||
parent: Event,
|
||||
createdAt: Long,
|
||||
): Event =
|
||||
message(
|
||||
authorPubKey = authorPubKey,
|
||||
channelId = channelId,
|
||||
epoch = epoch,
|
||||
text = text,
|
||||
createdAt = createdAt,
|
||||
extraTags = arrayOf(arrayOf("q", parentId), arrayOf("p", parentAuthor)),
|
||||
RumorAssembler.assembleRumor(
|
||||
authorPubKey,
|
||||
CommentEvent.replyBuilder(text, EventHintBundle(parent), createdAt) {
|
||||
channelBinding(channelId, epoch)
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user