From dd30f03bba519cbf7a7046a207a6539378f54fa2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 22:14:03 +0000 Subject: [PATCH] fix(notifications): route Welcome events via direct invocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WelcomeEvent (kind 444) has no `p` tag, so the cache-observer path's tag-based account matching in consumeFromCache silently dropped them. Route Welcomes instead via a dedicated notifyWelcome entry point invoked from processMarmotWelcomeFlow — the one place we reliably know which account the invite was for (the one whose signer just unsealed it and joined the MLS group). Drop the dead processWelcome workaround from the old notify(WelcomeEvent) body; with the cache-first architecture, MLS processing always completes before the notification fires. - NotificationDispatcher: remove WelcomeEvent from the observer filter, expose a public `notifyWelcome(event, account)` entry. - EventNotificationConsumer: rename notify(WelcomeEvent) to public notifyWelcome, add notification-enabled + foreground-suppression checks, drop the manager.processWelcome fallback. - DecryptAndIndexProcessor: on WelcomeResult.Joined, invoke Amethyst.instance.notificationDispatcher.notifyWelcome to fire the "You've been added to " notification. --- .../EventNotificationConsumer.kt | 44 +++++++------------ .../notifications/NotificationDispatcher.kt | 25 ++++++++++- .../loggedIn/DecryptAndIndexProcessor.kt | 6 +++ 3 files changed, 47 insertions(+), 28 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 3903dccef..e40adf983 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 @@ -49,7 +49,6 @@ import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent -import com.vitorpamplona.quartz.marmot.WelcomeResult import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray @@ -179,7 +178,8 @@ class EventNotificationConsumer( is ReactionEvent -> notify(event, account) is LiveChessGameAcceptEvent -> notifyChessEvent(event, account, R.string.app_notification_chess_challenge_accepted) is LiveChessMoveEvent -> notifyChessEvent(event, account, R.string.app_notification_chess_your_turn) - is WelcomeEvent -> notify(event, account) + // WelcomeEvent is dispatched directly from processMarmotWelcomeFlow + // (no `p` tag, so tag-based matching doesn't work). } } @@ -371,38 +371,28 @@ class EventNotificationConsumer( } } - private suspend fun notify( + /** + * Welcomes have no `p` tag, so [consumeFromCache]'s tag-based account match + * can't route them. They are instead dispatched here directly by + * [com.vitorpamplona.amethyst.ui.screen.loggedIn.processMarmotWelcomeFlow] + * after [MarmotManager.processWelcome] joins the group — which is also the + * only place we reliably know which account the invite was for. + */ + suspend fun notifyWelcome( event: WelcomeEvent, account: Account, - ) { + ) = withWakeLock { Log.d(TAG, "New Marmot Welcome to Notify") + if (!notificationManager().areNotificationsEnabled()) return@withWakeLock + if (MainActivity.isResumed) return@withWakeLock + // old event being re-broadcast - if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return + if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return@withWakeLock // a welcome we ourselves emitted - if (event.pubKey == account.signer.pubKey) return + if (event.pubKey == account.signer.pubKey) return@withWakeLock - val nostrGroupId = event.nostrGroupId() ?: return - val manager = account.marmotManager ?: return - - // Best-effort: process the welcome here so the chatroom is hydrated - // before composing the notification body. The push-notification - // background path does NOT go through Account.eventProcessor, so - // without this the invitee would only join the group later, when - // they next open the app and the relay subscription redelivers. - if (!manager.isMember(nostrGroupId)) { - try { - val result = manager.processWelcome(event, nostrGroupId) - if (result is WelcomeResult.Joined) { - val chatroom = account.marmotGroupList.getOrCreateGroup(result.nostrGroupId) - manager.syncMetadataTo(result.nostrGroupId, chatroom) - account.marmotGroupList.notifyGroupChanged(result.nostrGroupId) - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.w(TAG) { "Failed to process Marmot Welcome from notification path: ${e.message}" } - } - } + val nostrGroupId = event.nostrGroupId() ?: return@withWakeLock val chatroom = account.marmotGroupList.getOrCreateGroup(nostrGroupId) val groupName = chatroom.displayName.value?.takeIf { it.isNotBlank() } ?: "a private group" 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 0788e20e7..f4ff5f82d 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 @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.notifications import android.content.Context +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent @@ -63,6 +64,11 @@ class NotificationDispatcher( // listed here — by the time we care, Account.newNotesPreProcessor has // already unwrapped them and inserted the inner payload into LocalCache, // which fires the observer a second time on the inner event. + // + // WelcomeEvent (kind:444) is also excluded: it has no `p` tag, so + // consumeFromCache can't route it. It's delivered directly via + // [notifyWelcome] from processMarmotWelcomeFlow, which does know the + // recipient account. private val NOTIFICATION_KINDS = listOf( // Direct-arrival @@ -75,7 +81,6 @@ class NotificationDispatcher( // Unwrapped from GiftWrap → Seal ChatMessageEvent.KIND, ChatMessageEncryptedFileHeaderEvent.KIND, - WelcomeEvent.KIND, // Unwrapped from EphemeralGiftWrap CallOfferEvent.KIND, ) @@ -106,4 +111,22 @@ class NotificationDispatcher( job?.cancel() job = null } + + /** + * Direct-invocation entry point for [WelcomeEvent]. Bypasses the + * cache-observer path because Welcomes have no `p` tag for account + * routing. Called from processMarmotWelcomeFlow once MLS group join + * succeeds — at which point we know which account the invite was for. + */ + suspend fun notifyWelcome( + event: WelcomeEvent, + account: Account, + ) { + try { + consumer.notifyWelcome(event, account) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e(TAG, "Failed to dispatch Welcome notification ${event.id}", e) + } + } } 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 943e2d2d7..6cbd30046 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 @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.call.CallManager import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.model.Account @@ -397,6 +398,11 @@ private suspend fun processMarmotWelcomeFlow( if (result.needsKeyPackageRotation) { account.publishMarmotKeyPackages() } + + // Fire the "You've been added to " notification. Welcomes + // have no `p` tag, so the cache-observer path can't route them; + // this is the single point where we know the recipient account. + Amethyst.instance.notificationDispatcher.notifyWelcome(innerEvent, account) } is WelcomeResult.AlreadyJoined -> {