fix(notifications): route Welcome events via direct invocation

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 <group>" notification.
This commit is contained in:
Claude
2026-04-23 22:14:03 +00:00
parent c84fc6fcee
commit dd30f03bba
3 changed files with 47 additions and 28 deletions
@@ -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"
@@ -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)
}
}
}
@@ -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 <group>" 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 -> {