From 30d8d1bb6f8800e4536bca3fa3c788926d664a9f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 15:57:36 +0000 Subject: [PATCH] refactor(notifications): type notifyGroupMessage as ChatEvent The previous signature took Event and runtime-checked kind == 9 inside the notifier. That left the contract implicit: any caller could pass a reaction or control message and the call would silently no-op. Type the parameter as ChatEvent so the kind:9 restriction is structural, and move the narrowing (`is ChatEvent`) to the GroupEventHandler call site where the inner event is parsed. Reactions, deletions, and other inner kinds now can't reach the notifier in the first place. Drops the runtime kind check and the now-stale comment about reactions. --- .../notifications/EventNotificationConsumer.kt | 14 +++++++------- .../notifications/NotificationDispatcher.kt | 3 ++- .../loggedIn/DecryptAndIndexProcessor.kt | 18 +++++++++++------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 4b74716121..2d78756a15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -505,18 +505,17 @@ class EventNotificationConsumer( * decrypted the outer ChaCha20-Poly1305 layer and verified the inner * MLS-signed payload. * - * Only kind:9 chat messages produce a notification — reactions, control - * messages, and deletions stay silent, mirroring how NIP-17 (kind:14) - * is the only DM kind we notify. + * Typed to [ChatEvent] so the caller has to narrow first — reactions, + * control messages, and deletions stay silent at the type level, + * mirroring how NIP-17 (kind:14) is the only DM kind we notify. */ suspend fun notifyGroupMessage( - innerEvent: Event, + innerEvent: ChatEvent, nostrGroupId: String, account: Account, ) = withWakeLock { Log.d(TAG, "New Marmot Group Message to Notify") - if (innerEvent.kind != ChatEvent.KIND) return@withWakeLock if (!notificationManager().areNotificationsEnabled()) return@withWakeLock if (MainActivity.isResumed) return@withWakeLock @@ -530,8 +529,9 @@ class EventNotificationConsumer( val sender = LocalCache.getOrCreateUser(innerEvent.pubKey) val senderName = sender.toBestDisplayName() val senderPicture = sender.profilePicture() - // Show the message body when present; reactions/empty payloads fall - // back to a generic prompt so the popup is still actionable. + // Defensive fallback for the rare empty-content ChatEvent so the + // popup is still actionable. Non-chat inner kinds were filtered + // out at the call site by the ChatEvent type narrowing. val body = innerEvent.content.takeIf { it.isNotBlank() } ?: "New message" val accountNpub = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt index 02edd09763..df17bfe015 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent +import com.vitorpamplona.quartz.nipC7Chats.ChatEvent import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CancellationException @@ -231,7 +232,7 @@ class NotificationDispatcher( * once the MLS-decrypted inner event has been parsed and indexed. */ suspend fun notifyGroupMessage( - innerEvent: Event, + innerEvent: ChatEvent, nostrGroupId: String, account: Account, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 91afd97845..b9538f08c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallIceCandidateEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRejectEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRenegotiateEvent +import com.vitorpamplona.quartz.nipC7Chats.ChatEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException import kotlinx.coroutines.sync.Mutex @@ -665,13 +666,16 @@ class GroupEventHandler( // notification path can't route them. Fire the popup // directly here — only on first-time decryption, so // a relay re-broadcast or persist-replay doesn't - // double-notify. The notifier itself filters by - // inner kind (chat only) and freshness. - Amethyst.instance.notificationDispatcher.notifyGroupMessage( - innerEvent, - result.groupId, - account, - ) + // double-notify. Restrict to ChatEvent (kind:9) so + // reactions, deletions, and control messages stay + // silent. + if (innerEvent is ChatEvent) { + Amethyst.instance.notificationDispatcher.notifyGroupMessage( + innerEvent, + result.groupId, + account, + ) + } } }