fix(notifications): resolve addressable events to their replaceable note

LocalCache.getNoteIfExists(event.id) returns the id-keyed version note,
which has its replyTo moved to the replaceable note during insertion
(consumeBaseReplaceable). The id-keyed lookup therefore returns an empty
replyTo for AddressableEvent kinds — LongTextNoteEvent, WikiNoteEvent,
LiveChess*, VideoHorizontal/Vertical — and NotificationFeedFilter's
replyTo-based check in tagsAnEventByUser silently fails for replies into
long-form articles or wiki notes.

Switch to LocalCache.getNoteIfExists(event), which dispatches on
AddressableEvent and returns the address-keyed note with proper replyTo.
Applied in three places: the dispatcher predicate, consumeFromCache's
per-event match, and dispatchForAccount's muted-thread check (the last
one only handles non-addressable kinds today but is updated for
consistency).
This commit is contained in:
Claude
2026-05-19 18:32:49 +00:00
parent 36e285f684
commit c75c2013fb
2 changed files with 14 additions and 4 deletions
@@ -159,12 +159,14 @@ class EventNotificationConsumer(
// the same way).
//
// One LocalCache lookup per event regardless of account count —
// the note is the same for every saved account.
// the note is the same for every saved account. Use the Event
// overload so AddressableEvent kinds resolve to their replaceable
// note (id-keyed version has empty replyTo after insertion).
val matchingNote: Note? =
if (event is WakeUpEvent) {
null
} else {
LocalCache.getNoteIfExists(event.id) ?: return@withWakeLock
LocalCache.getNoteIfExists(event) ?: return@withWakeLock
}
LocalPreferences.allSavedAccounts().forEach { savedAccount ->
@@ -230,7 +232,7 @@ class EventNotificationConsumer(
// RepostEvent / GenericRepostEvent aren't routed below — push doesn't
// notify on reposts at all today.)
if (event is ReactionEvent || event is LnZapEvent) {
val target = LocalCache.getNoteIfExists(event.id)?.replyTo?.lastOrNull()
val target = LocalCache.getNoteIfExists(event)?.replyTo?.lastOrNull()
if (target != null && account.isThreadMuted(account.resolveThreadRoot(target))) return
}
@@ -193,7 +193,15 @@ class NotificationDispatcher(
if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return@predicate false
if (event is WakeUpEvent) return@predicate true
val note = LocalCache.getNoteIfExists(event.id) ?: return@predicate false
// getNoteIfExists(event) — not (event.id) — so
// AddressableEvent kinds (LongTextNote, WikiNote,
// LiveChess*, VideoHorizontal/Vertical) resolve to
// their address-keyed replaceable note. The id-keyed
// version note has its replyTo moved away during
// insertion (LocalCache.consumeBaseReplaceable), so
// tagsAnEventByUser's replyTo check would otherwise
// always miss for replies into addressable posts.
val note = LocalCache.getNoteIfExists(event) ?: return@predicate false
pubkeys.any { pubkey ->
event.isTaggedUser(pubkey) &&
NotificationFeedFilter.tagsAnEventByUser(note, pubkey)