Merge remote-tracking branch 'origin/main' into claude/fix-marmot-polling-R83Bs

This commit is contained in:
Claude
2026-04-22 23:41:40 +00:00
15 changed files with 407 additions and 39 deletions
@@ -69,6 +69,21 @@ class MarmotGroupChatroom(
*/
var ownerSentMessage: Boolean = false
/**
* Classifies this group for the Known/New Requests split.
*
* Rules:
* - If the local user has already participated ([ownerSentMessage]), Known.
* - If the group has no known members and no messages yet, New Requests
* (we don't know who invited us, so it's untrusted by default).
* - Otherwise, Known iff at least one admin is in the follow set.
*/
fun isKnown(followingKeySet: Set<HexKey>): Boolean {
if (ownerSentMessage) return true
if (memberCount.value == 0 && messages.isEmpty()) return false
return adminPubkeys.value.any { it in followingKeySet }
}
// Synthetic note used by list views to represent the group when no
// messages have been received yet. Lazily created and kept stable so
// equality-based feed diffing treats it as the same row across refreshes.
@@ -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
}
}