fix(concord): notify on reactions to my Concord messages

NotificationFeedFilter derived `isConcord` from the note's own gatherers
(is it in a joined ConcordChannel), but LocalCache.consumeConcordRumor only
attaches kind-9 messages and kind-1111 replies to the channel — never a kind-7
reaction. So a reaction's `isConcord` was always false, it didn't bypass the
follow filter, and since a fellow member usually isn't a follow it was dropped
in Curated/Selected mode.

Recognize a reaction/repost as Concord through its TARGET instead: if
`replyTo.lastOrNull()` is a message in a community I've joined, it bypasses the
follow filter exactly like a reply. Relevance is still the existing p-tag gate,
so only reactions that actually tag me notify (a well-formed NIP-25 kind-7
p-tags the reacted author, which is what our own ChannelChat.reaction writes).
The "Messages in notifications" toggle now gates only Concord messages, not
reactions — a like isn't a message, so it follows the same rule as any other
reaction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-07-14 21:58:48 -04:00
co-authored by Claude Opus 4.8
parent 08c1d1d539
commit 5b54eb88a5
@@ -449,14 +449,30 @@ class NotificationFeedFilter(
// gatherer reference from every account/community that ever touched them, so require the
// community to be one THIS account has currently joined (mirrors the Marmot check above) —
// otherwise a note from a prior account or a left community would leak onto Notifications.
val isConcord =
it.inGatherers?.any { g ->
fun Note?.inJoinedConcordCommunity() =
this?.inGatherers?.any { g ->
g is ConcordChannel && account.concordSessions.sessionFor(g.channelId.communityId) != null
} == true
// Concord is a messaging feature, so honor the same "Messages in notifications" toggle that
// silences DMs and Marmot groups above.
if (isConcord && !showMessages) return false
val isConcordMessage = it.inJoinedConcordCommunity()
// A like/repost is NOT itself attached to the channel gatherer — only chat messages/replies are
// (see LocalCache.consumeConcordRumor) — so `inGatherers` never flags it as Concord, and its
// author (a fellow member) usually isn't a follow, so it falls through the follow filter and is
// dropped. Recognize it through its TARGET: a reaction/repost pointing at a message in a
// community I've joined is a Concord reaction, and bypasses the follow filter like a reply does.
// Relevance (does it target ME) is still enforced below by the p-tag gate — a well-formed kind-7
// p-tags the reacted author (NIP-25), which is exactly what our own ChannelChat.reaction writes.
val isConcordReaction =
(noteEvent is ReactionEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent) &&
it.replyTo?.lastOrNull().inJoinedConcordCommunity()
val isConcord = isConcordMessage || isConcordReaction
// Concord CHAT (a message/reply) honors the "Messages in notifications" toggle that silences DMs
// and Marmot groups above. A reaction isn't a message — regular reactions ignore that toggle, so
// Concord reactions do too (only isConcordMessage is gated).
if (isConcordMessage && !showMessages) return false
// Global keeps every event that p-tags the user; Selected (and the
// follow/list modes) also applies the per-kind relevance heuristics.