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.nip84Highlights.HighlightEvent
|
||||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent
|
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent
|
||||||
|
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
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(
|
suspend fun decryptZapContentAuthor(
|
||||||
event: LnZapRequestEvent,
|
event: LnZapRequestEvent,
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
|
|||||||
+21
@@ -51,6 +51,7 @@ import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
|
|||||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent
|
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent
|
||||||
|
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -222,4 +223,24 @@ class NotificationDispatcher(
|
|||||||
Log.e(TAG, "Failed to dispatch Welcome notification ${event.id}", e)
|
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.CallOfferEvent
|
||||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRejectEvent
|
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRejectEvent
|
||||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRenegotiateEvent
|
import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRenegotiateEvent
|
||||||
|
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
@@ -660,6 +661,21 @@ class GroupEventHandler(
|
|||||||
// re-persist would silently grow the on-disk log).
|
// re-persist would silently grow the on-disk log).
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
manager.persistDecryptedMessage(result.groupId, result.innerEventJson)
|
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