fix: sign a q quote-tag when replying to a kind-9 group message

The NIP-29 group composer always built replies with ChatEvent.build and ignored
replyTo entirely, so a reply to a kind-9 message quoted its parent in the UI but
the signed event carried no NIP-18 `q` tag (and no `p` notify to the author) —
unlike the public-chat and live-activity paths, which use `.reply(...)`.

Route group replies through ChatEvent.reply when replyTo is set: it emits the
`q` quote tag, and we add a `p` tag to the parent's author.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
This commit is contained in:
Claude
2026-07-09 23:17:03 +00:00
parent 7c7608913d
commit d4e71881e8
@@ -77,6 +77,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag
import com.vitorpamplona.quartz.nip01Core.tags.references.references
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
@@ -550,17 +551,38 @@ open class ChannelNewMessageViewModel :
// NIP-29 group message: a kind-9 chat scoped to the group with an
// `h` tag. The event is published only to the group's host relay
// (channel.relays()), where relay29 authorizes and routes it.
ChatEvent.build(tagger.message) {
hTag(channel.groupId.id)
val replyingToEvent = replyTo.value?.toEventHint<ChatEvent>()
if (replyingToEvent != null) {
// A reply quotes its parent via a NIP-18 `q` tag (added by ChatEvent.reply)
// + a `p` notify to its author. The group scope (`h`) alone doesn't link a
// reply to what it answers — without the `q` tag the quote renders in the
// composer but is missing from the signed event.
ChatEvent.reply(tagger.message, replyingToEvent) {
hTag(channel.groupId.id)
pTag(replyingToEvent.toPTag())
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
emojis(emojis)
imetas(usedAttachments)
emojis(emojis)
imetas(usedAttachments)
}
} else {
ChatEvent.build(tagger.message) {
hTag(channel.groupId.id)
hashtags(findHashtags(tagger.message))
references(findURLs(tagger.message))
quotes(findNostrUris(tagger.message))
contentWarningReason?.let { contentWarning(it) }
localExpirationDate?.let { expiration(it) }
emojis(emojis)
imetas(usedAttachments)
}
}
}