refactor(notifications): centralize age + self gates, predicate observer
Audit of every notify() path found self-exclusion + 15-min age checks duplicated across nearly all of them, with two inconsistencies (DM kind 4 and zap kind 9735 were missing explicit self-checks). Reactions, zaps, and chess also re-ran isTaggedUser even though consumeFromCache already routes by the same `p` tag. Collapse the duplication into two layers: - Observer layer (NotificationDispatcher): 15-min rolling age cutoff is now enforced by a predicate on LocalCache.observeNewEvents, before any account routing happens. The Nostr Filter grammar's `since` is a fixed value from dispatcher start; the predicate re-evaluates TimeUtils. fifteenMinutesAgo() per event so the window rolls forward as wall-clock time advances. - Per-account layer (dispatchForAccount): right after the call/wake-up branch and the MainActivity.isResumed gate, drop events authored by the current account. Kept per-account (not observer-wide) because in a multi-account session account A's outgoing event legitimately becomes account B's incoming notification on the same device, so a device-wide author-exclusion set would swallow A→B. Mechanically, NewEventMatchingFilter now takes an optional predicate so observers can reject on fields the Filter grammar can't express (like a moving `createdAt` cutoff). LocalCache.observeNewEvents adds a matching overload that forwards it. With the gates centralized, every notify() method drops its own age and self-author checks (and for reaction/zap, the redundant isTaggedUser). notifyWelcome keeps its own guards because it's dispatched directly from processMarmotWelcomeFlow, bypassing the observer and dispatchForAccount. Calls and wake-ups also bypass the shared gates by their short-circuit return before the shared block. https://claude.ai/code/session_01GQDJxiHPogdzCNhUBN7Pjc
This commit is contained in:
+7
-1
@@ -31,9 +31,15 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
* accumulate a list — it simply calls [onNew] per matching event as it is
|
||||
* inserted into the cache. Useful for reactive event-triggered pipelines
|
||||
* (e.g. notifications) that need per-event delivery without list overhead.
|
||||
*
|
||||
* The optional [predicate] runs after the protocol [filter] matches; use it
|
||||
* for checks the Nostr [Filter] grammar can't express (e.g. rolling age
|
||||
* cutoffs, arbitrary tag shapes, derived content fields). The predicate has
|
||||
* to be fast — it runs on every new cache insertion.
|
||||
*/
|
||||
class NewEventMatchingFilter<T : Event>(
|
||||
private val filter: Filter,
|
||||
private val predicate: (Event) -> Boolean = { true },
|
||||
private val onNew: (T) -> Unit,
|
||||
) : Observable {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -41,7 +47,7 @@ class NewEventMatchingFilter<T : Event>(
|
||||
event: Event,
|
||||
note: Note,
|
||||
) {
|
||||
if (filter.match(event)) {
|
||||
if (filter.match(event) && predicate(event)) {
|
||||
onNew(event as T)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user