fix: don't render Marmot reactions/deletions as chat bubbles
WhiteNoise threads its kind:445 payloads across three inner kinds: kind:9 for chat, kind:7 for emoji reactions, kind:5 for unreacts. The Amethyst ingest pipeline was routing every inner event into the group chatroom feed, so a kind:7 reaction rendered as a chat bubble whose only content was the emoji, quoting the liked message via the target `e` tag. That looked identical to a threaded reply. The actual kind:9 reply from `wn messages send --reply-to` never arrived, which made the two look swapped in the UI. Three changes to untangle this: - MarmotGroupList.addMessage/restoreMessage now skip inner events with kind:5 and kind:7 before they enter `MarmotGroupChatroom.messages`. The reaction is still consumed by LocalCache so it attaches to the target note's reaction row, and the deletion still revokes that reaction — they just don't appear as standalone bubbles. - LocalCache.computeReplyTo learned to derive thread parents for `ChatEvent` (kind:9) from plain NIP-10 `e` tags in addition to the existing NIP-18 `q` tag path. WhiteNoise emits `e`-tagged replies; without this the reply bubble had no quote context in the feed. - tools/marmot-interop/marmot-interop.sh: `wn messages send` exposes its `reply_to` field through clap v4, which renames snake_case to kebab-case by default. The script was passing `--reply_to`, which clap rejected; the `|| true` + redirected stderr hid the error and no reply was ever published. Use `--reply-to`. https://claude.ai/code/session_01K3g1uWLhByoEdBS77zdF32
This commit is contained in:
+25
@@ -47,6 +47,7 @@ class MarmotGroupList(
|
||||
nostrGroupId: HexKey,
|
||||
msg: Note,
|
||||
) {
|
||||
if (!isDisplayableFeedMessage(msg)) return
|
||||
val chatroom = getOrCreateGroup(nostrGroupId)
|
||||
val isSelfAuthored = msg.author?.pubkeyHex == ownerPubKey
|
||||
// Use the quiet path for our own messages so the relay round-trip
|
||||
@@ -74,6 +75,7 @@ class MarmotGroupList(
|
||||
nostrGroupId: HexKey,
|
||||
msg: Note,
|
||||
) {
|
||||
if (!isDisplayableFeedMessage(msg)) return
|
||||
val chatroom = getOrCreateGroup(nostrGroupId)
|
||||
if (chatroom.restoreMessageSync(msg)) {
|
||||
noteToGroupIndex.getOrCreate(msg.idHex) { nostrGroupId }
|
||||
@@ -135,4 +137,27 @@ class MarmotGroupList(
|
||||
rooms.forEach { key, _ -> result.add(key) }
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this inner event should appear as its own bubble in the group
|
||||
* chat feed. Side-channel kinds (reactions, deletions) must still be
|
||||
* consumed into LocalCache — they drive the reaction row on the target
|
||||
* note and, for kind:5, revoke a prior reaction — but they must NOT show
|
||||
* up as standalone messages.
|
||||
*
|
||||
* Needed because WhiteNoise emits plain kind:7 reactions (emoji content +
|
||||
* `e` tag) and kind:5 unreacts inside kind:445, and the Marmot pipeline
|
||||
* blindly routed every inner event into the chatroom. The reaction then
|
||||
* rendered as a chat bubble containing just the emoji, with a quoted
|
||||
* citation of the target message — which reads exactly like a reply.
|
||||
*/
|
||||
private fun isDisplayableFeedMessage(msg: Note): Boolean {
|
||||
val kind = msg.event?.kind ?: return true
|
||||
return kind != MARMOT_INNER_KIND_REACTION && kind != MARMOT_INNER_KIND_DELETION
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MARMOT_INNER_KIND_DELETION = 5
|
||||
private const val MARMOT_INNER_KIND_REACTION = 7
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user