mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
fix(notifications): popup for Marmot group messages (kind:445)
Welcomes (kind:444) already had a direct-dispatch path because they have no `p` tag and the cache-observer route can't match them to an account. Group messages (kind:445) have the same problem — recipients are routed by the `h` tag carrying the nostr_group_id — but were silently missed, so the user only saw a popup when first added to a group, never for any chat that followed. Mirror the notifyWelcome path: after MarmotInboundProcessor decrypts and verifies an ApplicationMessage and we've persisted the inner event for the first time, fire notifyGroupMessage on NotificationDispatcher. The notifier filters to ChatEvent (kind:9) so reactions, deletions and control messages stay silent (matching how NIP-17 only notifies on kind:14), applies the same 15-min freshness and self-author gates as the other DM paths, and uses the marmot:<groupHex>?account=<npub> deep-link scheme so taps land in the right chatroom. Only fires on first-time decryption (isNew) so a relay re-broadcast or on-disk persist replay can't double-notify.
This commit is contained in:
+62
@@ -83,6 +83,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.TimeoutCancellationException
|
||||
@@ -495,6 +496,67 @@ class EventNotificationConsumer(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Marmot kind:445 group messages have no `p` tag (recipients are routed
|
||||
* by the `h` tag carrying the nostr_group_id), so the cache-observer path
|
||||
* in [NotificationDispatcher] can't match them to an account. They're
|
||||
* dispatched here directly from [com.vitorpamplona.amethyst.ui.screen.loggedIn.GroupEventHandler]
|
||||
* once [com.vitorpamplona.quartz.marmot.MarmotInboundProcessor] has
|
||||
* 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.
|
||||
*/
|
||||
suspend fun notifyGroupMessage(
|
||||
innerEvent: Event,
|
||||
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
|
||||
|
||||
// old event being re-broadcast
|
||||
if (innerEvent.createdAt < TimeUtils.fifteenMinutesAgo()) return@withWakeLock
|
||||
// a message we ourselves sent
|
||||
if (innerEvent.pubKey == account.signer.pubKey) return@withWakeLock
|
||||
|
||||
val chatroom = account.marmotGroupList.getOrCreateGroup(nostrGroupId)
|
||||
val groupName = chatroom.displayName.value?.takeIf { it.isNotBlank() } ?: "Private group"
|
||||
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.
|
||||
val body = innerEvent.content.takeIf { it.isNotBlank() } ?: "New message"
|
||||
|
||||
val accountNpub =
|
||||
account.signer.pubKey
|
||||
.hexToByteArray()
|
||||
.toNpub()
|
||||
// marmot:<groupHex>?account=<npub> — same scheme as notifyWelcome,
|
||||
// taps deep-link straight to the group's chatroom.
|
||||
val noteUri = "marmot:$nostrGroupId$ACCOUNT_QUERY_PARAM$accountNpub"
|
||||
|
||||
notificationManager()
|
||||
.sendDMNotification(
|
||||
id = innerEvent.id,
|
||||
messageBody = "$senderName: $body",
|
||||
senderName = groupName,
|
||||
time = innerEvent.createdAt,
|
||||
pictureUrl = senderPicture,
|
||||
uri = noteUri,
|
||||
applicationContext = applicationContext,
|
||||
accountNpub = accountNpub,
|
||||
accountPictureUrl = account.userProfile().profilePicture(),
|
||||
chatroomMembers = null,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun decryptZapContentAuthor(
|
||||
event: LnZapRequestEvent,
|
||||
signer: NostrSigner,
|
||||
|
||||
+20
@@ -222,4 +222,24 @@ class NotificationDispatcher(
|
||||
Log.e(TAG, "Failed to dispatch Welcome notification ${event.id}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct-invocation entry point for Marmot kind:445 group messages.
|
||||
* Bypasses the cache-observer path because GroupEvents are routed by
|
||||
* the `h` tag (nostr_group_id), not by `p` tag. Called from
|
||||
* [com.vitorpamplona.amethyst.ui.screen.loggedIn.GroupEventHandler]
|
||||
* once the MLS-decrypted inner event has been parsed and indexed.
|
||||
*/
|
||||
suspend fun notifyGroupMessage(
|
||||
innerEvent: Event,
|
||||
nostrGroupId: String,
|
||||
account: Account,
|
||||
) {
|
||||
try {
|
||||
consumer.notifyGroupMessage(innerEvent, nostrGroupId, account)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e(TAG, "Failed to dispatch Group Message notification ${innerEvent.id}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -660,6 +660,18 @@ class GroupEventHandler(
|
||||
// re-persist would silently grow the on-disk log).
|
||||
if (isNew) {
|
||||
manager.persistDecryptedMessage(result.groupId, result.innerEventJson)
|
||||
|
||||
// GroupEvents have no `p` tag, so the cache-observer
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user