Merge pull request #2737 from vitorpamplona/claude/fix-group-message-notifications-UNKZq
Add notifications for Marmot group messages (kind:445)
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.
|
||||
*
|
||||
* 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: ChatEvent,
|
||||
nostrGroupId: String,
|
||||
account: Account,
|
||||
) = withWakeLock {
|
||||
Log.d(TAG, "New Marmot Group Message to Notify")
|
||||
|
||||
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()
|
||||
// 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 =
|
||||
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,
|
||||
|
||||
+21
@@ -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
|
||||
@@ -222,4 +223,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: ChatEvent,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -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
|
||||
@@ -660,6 +661,21 @@ 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. 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user