diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml
index 7339ba1bb..4aae58459 100644
--- a/amethyst/src/main/AndroidManifest.xml
+++ b/amethyst/src/main/AndroidManifest.xml
@@ -240,6 +240,11 @@
+
+
+
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 c3160d01e..2ccd1a069 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
@@ -183,7 +183,7 @@ class EventNotificationConsumer(
}
}
- private fun notify(
+ private suspend fun notify(
event: ChatMessageEncryptedFileHeaderEvent,
account: Account,
) {
@@ -210,13 +210,13 @@ class EventNotificationConsumer(
val content = chatNote.event?.content ?: ""
val user = chatNote.author?.toBestDisplayName() ?: ""
val userPicture = chatNote.author?.profilePicture()
- val noteUri =
- chatNote.toNEvent() + ACCOUNT_QUERY_PARAM +
- account.signer.pubKey
- .hexToByteArray()
- .toNpub()
+ val accountNpub =
+ account.signer.pubKey
+ .hexToByteArray()
+ .toNpub()
+ val chatroomMembers = chatRoom.users.joinToString(",")
+ val noteUri = chatNote.toNEvent() + ACCOUNT_QUERY_PARAM + accountNpub
- // TODO: Show Image on notification
notificationManager()
.sendDMNotification(
event.id,
@@ -226,12 +226,15 @@ class EventNotificationConsumer(
userPicture,
noteUri,
applicationContext,
+ accountNpub = accountNpub,
+ accountPictureUrl = account.userProfile().profilePicture(),
+ chatroomMembers = chatroomMembers,
)
}
}
}
- private fun notify(
+ private suspend fun notify(
event: ChatMessageEvent,
account: Account,
) {
@@ -255,20 +258,25 @@ class EventNotificationConsumer(
val content = chatNote.event?.content ?: ""
val user = chatNote.author?.toBestDisplayName() ?: ""
val userPicture = chatNote.author?.profilePicture()
- val noteUri =
- chatNote.toNEvent() + ACCOUNT_QUERY_PARAM +
- account.signer.pubKey
- .hexToByteArray()
- .toNpub()
+ val accountNpub =
+ account.signer.pubKey
+ .hexToByteArray()
+ .toNpub()
+ val chatroomMembers = chatRoom.users.joinToString(",")
+ val noteUri = chatNote.toNEvent() + ACCOUNT_QUERY_PARAM + accountNpub
+
notificationManager()
.sendDMNotification(
- event.id,
- content,
- user,
- event.createdAt,
- userPicture,
- noteUri,
- applicationContext,
+ id = event.id,
+ messageBody = content,
+ senderName = user,
+ time = event.createdAt,
+ pictureUrl = userPicture,
+ uri = noteUri,
+ applicationContext = applicationContext,
+ accountNpub = accountNpub,
+ accountPictureUrl = account.userProfile().profilePicture(),
+ chatroomMembers = chatroomMembers,
)
}
}
@@ -297,13 +305,25 @@ class EventNotificationConsumer(
decryptContent(note, account.signer)?.let { content ->
val user = note.author?.toBestDisplayName() ?: ""
val userPicture = note.author?.profilePicture()
- val noteUri =
- note.toNEvent() + ACCOUNT_QUERY_PARAM +
- account.signer.pubKey
- .hexToByteArray()
- .toNpub()
+ val accountNpub =
+ account.signer.pubKey
+ .hexToByteArray()
+ .toNpub()
+ val noteUri = note.toNEvent() + ACCOUNT_QUERY_PARAM + accountNpub
+
notificationManager()
- .sendDMNotification(event.id, content, user, event.createdAt, userPicture, noteUri, applicationContext)
+ .sendDMNotification(
+ id = event.id,
+ messageBody = content,
+ senderName = user,
+ time = event.createdAt,
+ pictureUrl = userPicture,
+ uri = noteUri,
+ applicationContext = applicationContext,
+ accountNpub = accountNpub,
+ accountPictureUrl = account.userProfile().profilePicture(),
+ chatroomMembers = null,
+ )
}
}
}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt
new file mode 100644
index 000000000..f5009118e
--- /dev/null
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.amethyst.service.notifications
+
+import android.app.NotificationManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.RemoteInput
+import androidx.core.content.ContextCompat
+import com.vitorpamplona.amethyst.Amethyst
+import com.vitorpamplona.amethyst.LocalPreferences
+import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
+import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
+import com.vitorpamplona.quartz.utils.Log
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlin.coroutines.cancellation.CancellationException
+
+class NotificationReplyReceiver : BroadcastReceiver() {
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
+ override fun onReceive(
+ context: Context,
+ intent: Intent,
+ ) {
+ val notificationId = intent.getIntExtra(NotificationUtils.KEY_NOTIFICATION_ID, 0)
+ val notificationManager =
+ ContextCompat.getSystemService(context, NotificationManager::class.java)
+ as NotificationManager
+
+ when (intent.action) {
+ NotificationUtils.MARK_READ_ACTION -> {
+ notificationManager.cancel(notificationId)
+ }
+
+ NotificationUtils.REPLY_ACTION -> {
+ val replyText =
+ RemoteInput
+ .getResultsFromIntent(intent)
+ ?.getCharSequence(NotificationUtils.KEY_REPLY_TEXT)
+ ?.toString()
+
+ if (replyText.isNullOrBlank()) return
+
+ val accountNpub = intent.getStringExtra(NotificationUtils.KEY_ACCOUNT_NPUB) ?: return
+ val chatroomMembersStr = intent.getStringExtra(NotificationUtils.KEY_CHATROOM_MEMBERS) ?: return
+ val members = chatroomMembersStr.split(",").filter { it.isNotBlank() }
+
+ if (members.isEmpty()) return
+
+ val pendingResult = goAsync()
+
+ scope.launch {
+ // activates the relay to send the message.
+ val collectionJob =
+ scope.launch {
+ Amethyst.instance.relayProxyClientConnector.relayServices
+ .collect()
+ }
+
+ try {
+ sendReply(accountNpub, members, replyText)
+ notificationManager.cancel(notificationId)
+ } catch (e: Exception) {
+ if (e is CancellationException) throw e
+ Log.e("NotificationReply", "Failed to send reply: ${e.message}")
+ } finally {
+ pendingResult.finish()
+
+ // closes the relay connection.
+ collectionJob.cancel()
+ }
+ }
+ }
+ }
+ }
+
+ private suspend fun sendReply(
+ accountNpub: String,
+ chatroomMembers: List,
+ replyText: String,
+ ) {
+ val accountSettings = LocalPreferences.loadAccountConfigFromEncryptedStorage(accountNpub) ?: return
+ val account = Amethyst.instance.accountsCache.loadAccount(accountSettings)
+
+ val recipients = chatroomMembers.map { PTag(it) }
+ val template = ChatMessageEvent.build(msg = replyText, to = recipients)
+
+ account.sendNip17PrivateMessage(template)
+ }
+}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt
index 2472c097d..9c16a2e64 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt
@@ -25,23 +25,37 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
+import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.service.notification.StatusBarNotification
import androidx.core.app.NotificationCompat
+import androidx.core.app.Person
+import androidx.core.app.RemoteInput
+import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import coil3.ImageLoader
import coil3.asDrawable
-import coil3.executeBlocking
import coil3.request.ImageRequest
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.MainActivity
import com.vitorpamplona.amethyst.ui.stringRes
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
object NotificationUtils {
private var dmChannel: NotificationChannel? = null
private var zapChannel: NotificationChannel? = null
private const val DM_GROUP_KEY = "com.vitorpamplona.amethyst.DM_NOTIFICATION"
private const val ZAP_GROUP_KEY = "com.vitorpamplona.amethyst.ZAP_NOTIFICATION"
+ const val REPLY_ACTION = "com.vitorpamplona.amethyst.REPLY_ACTION"
+ const val MARK_READ_ACTION = "com.vitorpamplona.amethyst.MARK_READ_ACTION"
+ const val KEY_REPLY_TEXT = "key_reply_text"
+ const val KEY_NOTIFICATION_ID = "key_notification_id"
+ const val KEY_ACCOUNT_NPUB = "key_account_npub"
+ const val KEY_CHATROOM_MEMBERS = "key_chatroom_members"
+
+ private const val DM_SUMMARY_ID = 0x10000
+ private const val ZAP_SUMMARY_ID = 0x20000
fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel {
if (dmChannel != null) return dmChannel!!
@@ -50,13 +64,12 @@ object NotificationUtils {
NotificationChannel(
stringRes(applicationContext, R.string.app_notification_dms_channel_id),
stringRes(applicationContext, R.string.app_notification_dms_channel_name),
- NotificationManager.IMPORTANCE_DEFAULT,
+ NotificationManager.IMPORTANCE_HIGH,
).apply {
description =
stringRes(applicationContext, R.string.app_notification_dms_channel_description)
}
- // Register the channel with the system
val notificationManager: NotificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -78,7 +91,6 @@ object NotificationUtils {
stringRes(applicationContext, R.string.app_notification_zaps_channel_description)
}
- // Register the channel with the system
val notificationManager: NotificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -87,7 +99,7 @@ object NotificationUtils {
return zapChannel!!
}
- fun NotificationManager.sendZapNotification(
+ suspend fun NotificationManager.sendZapNotification(
id: String,
messageBody: String,
messageTitle: String,
@@ -96,109 +108,109 @@ object NotificationUtils {
uri: String,
applicationContext: Context,
) {
- val zapChannel = getOrCreateZapChannel(applicationContext)
+ getOrCreateZapChannel(applicationContext)
val channelId = stringRes(applicationContext, R.string.app_notification_zaps_channel_id)
sendNotification(
- id,
- messageBody,
- messageTitle,
- time,
- pictureUrl,
- uri,
- channelId,
- ZAP_GROUP_KEY,
- applicationContext,
+ id = id,
+ messageBody = messageBody,
+ messageTitle = messageTitle,
+ time = time,
+ pictureUrl = pictureUrl,
+ uri = uri,
+ channelId = channelId,
+ notificationGroupKey = ZAP_GROUP_KEY,
+ category = NotificationCompat.CATEGORY_SOCIAL,
+ summaryId = ZAP_SUMMARY_ID,
+ summaryText = stringRes(applicationContext, R.string.app_notification_zaps_summary),
+ applicationContext = applicationContext,
)
}
- fun NotificationManager.sendDMNotification(
+ suspend fun NotificationManager.sendDMNotification(
id: String,
messageBody: String,
- messageTitle: String,
+ senderName: String,
time: Long,
pictureUrl: String?,
uri: String,
applicationContext: Context,
+ accountNpub: String? = null,
+ accountPictureUrl: String? = null,
+ chatroomMembers: String? = null,
) {
- val dmChannel = getOrCreateDMChannel(applicationContext)
+ getOrCreateDMChannel(applicationContext)
val channelId = stringRes(applicationContext, R.string.app_notification_dms_channel_id)
- sendNotification(
- id,
- messageBody,
- messageTitle,
- time,
- pictureUrl,
- uri,
- channelId,
- DM_GROUP_KEY,
- applicationContext,
+ sendDMNotificationStyled(
+ id = id,
+ messageBody = messageBody,
+ senderName = senderName,
+ time = time,
+ pictureUrl = pictureUrl,
+ uri = uri,
+ channelId = channelId,
+ applicationContext = applicationContext,
+ accountNpub = accountNpub,
+ accountPictureUrl = accountPictureUrl,
+ chatroomMembers = chatroomMembers,
)
}
- fun NotificationManager.sendNotification(
+ private suspend fun loadBitmap(
+ pictureUrl: String,
+ applicationContext: Context,
+ ): Bitmap? =
+ withContext(Dispatchers.IO) {
+ try {
+ val request = ImageRequest.Builder(applicationContext).data(pictureUrl).build()
+ val imageLoader = ImageLoader(applicationContext)
+ val result = imageLoader.execute(request)
+ (result.image?.asDrawable(applicationContext.resources) as? BitmapDrawable)?.bitmap
+ } catch (_: Exception) {
+ null
+ }
+ }
+
+ private suspend fun NotificationManager.sendDMNotificationStyled(
id: String,
messageBody: String,
- messageTitle: String,
+ senderName: String,
time: Long,
pictureUrl: String?,
uri: String,
channelId: String,
- notificationGroupKey: String,
- applicationContext: Context,
- ) {
- if (pictureUrl != null) {
- val request = ImageRequest.Builder(applicationContext).data(pictureUrl).build()
-
- val imageLoader = ImageLoader(applicationContext)
- val imageResult = imageLoader.executeBlocking(request)
- sendNotificationInner(
- id = id,
- messageBody = messageBody,
- messageTitle = messageTitle,
- time = time,
- picture = imageResult.image?.asDrawable(applicationContext.resources) as? BitmapDrawable,
- uri = uri,
- channelId,
- notificationGroupKey,
- applicationContext = applicationContext,
- )
- } else {
- sendNotificationInner(
- id = id,
- messageBody = messageBody,
- messageTitle = messageTitle,
- time = time,
- picture = null,
- uri = uri,
- channelId,
- notificationGroupKey,
- applicationContext = applicationContext,
- )
- }
- }
-
- private fun NotificationManager.sendNotificationInner(
- id: String,
- messageBody: String,
- messageTitle: String,
- time: Long,
- picture: BitmapDrawable?,
- uri: String,
- channelId: String,
- notificationGroupKey: String,
applicationContext: Context,
+ accountNpub: String?,
+ accountPictureUrl: String?,
+ chatroomMembers: String?,
) {
val notId = id.hashCode()
- // dont notify twice
- val notifications: Array = getActiveNotifications()
- for (notification in notifications) {
- if (notification.id == notId) {
- return
- }
- }
+ if (isDuplicate(notId)) return
+
+ val bitmap = pictureUrl?.let { loadBitmap(it, applicationContext) }
+ val accountBitmap = accountPictureUrl?.let { loadBitmap(it, applicationContext) }
+
+ val senderIcon = bitmap?.let { IconCompat.createWithBitmap(it) }
+ val accountIcon = accountBitmap?.let { IconCompat.createWithBitmap(it) }
+
+ val sender =
+ Person
+ .Builder()
+ .setName(senderName)
+ .apply { senderIcon?.let { setIcon(it) } }
+ .build()
+
+ val messagingStyle =
+ NotificationCompat
+ .MessagingStyle(
+ Person
+ .Builder()
+ .setName("Me")
+ .setIcon(accountIcon)
+ .build(),
+ ).addMessage(messageBody, time * 1000, sender)
val contentIntent =
Intent(applicationContext, MainActivity::class.java).apply { data = uri.toUri() }
@@ -208,41 +220,198 @@ object NotificationUtils {
applicationContext,
notId,
contentIntent,
- PendingIntent.FLAG_MUTABLE,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
- // Build the notification
val builderPublic =
NotificationCompat
- .Builder(
- applicationContext,
- channelId,
- ).setSmallIcon(R.drawable.amethyst)
- .setContentTitle(messageTitle)
+ .Builder(applicationContext, channelId)
+ .setSmallIcon(R.drawable.amethyst)
+ .setContentTitle(senderName)
.setContentText(stringRes(applicationContext, R.string.app_notification_private_message))
- .setLargeIcon(picture?.bitmap)
+ .setLargeIcon(bitmap)
.setContentIntent(contentPendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setWhen(time * 1000)
- // Build the notification
val builder =
NotificationCompat
- .Builder(
- applicationContext,
- channelId,
- ).setSmallIcon(R.drawable.amethyst)
- .setContentTitle(messageTitle)
- .setContentText(messageBody)
- .setLargeIcon(picture?.bitmap)
+ .Builder(applicationContext, channelId)
+ .setSmallIcon(R.drawable.amethyst)
+ .setLargeIcon(bitmap)
+ .setStyle(messagingStyle)
.setContentIntent(contentPendingIntent)
.setPublicVersion(builderPublic.build())
.setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+ .setGroup(DM_GROUP_KEY)
+ .setAutoCancel(true)
+ .setWhen(time * 1000)
+
+ // Direct Reply action
+ if (accountNpub != null && chatroomMembers != null) {
+ val remoteInput =
+ RemoteInput
+ .Builder(KEY_REPLY_TEXT)
+ .setLabel(stringRes(applicationContext, R.string.app_notification_reply_label))
+ .build()
+
+ val replyIntent =
+ Intent(applicationContext, NotificationReplyReceiver::class.java).apply {
+ action = REPLY_ACTION
+ putExtra(KEY_NOTIFICATION_ID, notId)
+ putExtra(KEY_ACCOUNT_NPUB, accountNpub)
+ putExtra(KEY_CHATROOM_MEMBERS, chatroomMembers)
+ }
+
+ val replyPendingIntent =
+ PendingIntent.getBroadcast(
+ applicationContext,
+ notId,
+ replyIntent,
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+ )
+
+ val replyAction =
+ NotificationCompat.Action
+ .Builder(R.drawable.amethyst, stringRes(applicationContext, R.string.app_notification_reply_label), replyPendingIntent)
+ .addRemoteInput(remoteInput)
+ .setAllowGeneratedReplies(true)
+ .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
+ .build()
+
+ builder.addAction(replyAction)
+ }
+
+ // Mark as Read action
+ val markReadIntent =
+ Intent(applicationContext, NotificationReplyReceiver::class.java).apply {
+ action = MARK_READ_ACTION
+ putExtra(KEY_NOTIFICATION_ID, notId)
+ }
+
+ val markReadPendingIntent =
+ PendingIntent.getBroadcast(
+ applicationContext,
+ notId + 1,
+ markReadIntent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+ )
+
+ val markReadAction =
+ NotificationCompat.Action
+ .Builder(R.drawable.amethyst, stringRes(applicationContext, R.string.app_notification_mark_read_label), markReadPendingIntent)
+ .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
+ .build()
+
+ builder.addAction(markReadAction)
+
+ notify(notId, builder.build())
+
+ // Group summary notification
+ sendGroupSummary(channelId, DM_GROUP_KEY, DM_SUMMARY_ID, stringRes(applicationContext, R.string.app_notification_dms_summary), applicationContext)
+ }
+
+ private suspend fun NotificationManager.sendNotification(
+ id: String,
+ messageBody: String,
+ messageTitle: String,
+ time: Long,
+ pictureUrl: String?,
+ uri: String,
+ channelId: String,
+ notificationGroupKey: String,
+ category: String,
+ summaryId: Int,
+ summaryText: String,
+ applicationContext: Context,
+ ) {
+ val notId = id.hashCode()
+
+ if (isDuplicate(notId)) return
+
+ val bitmap = pictureUrl?.let { loadBitmap(it, applicationContext) }
+
+ val contentIntent =
+ Intent(applicationContext, MainActivity::class.java).apply { data = uri.toUri() }
+
+ val contentPendingIntent =
+ PendingIntent.getActivity(
+ applicationContext,
+ notId,
+ contentIntent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+ )
+
+ val builderPublic =
+ NotificationCompat
+ .Builder(applicationContext, channelId)
+ .setSmallIcon(R.drawable.amethyst)
+ .setContentTitle(messageTitle)
+ .setContentText(stringRes(applicationContext, R.string.app_notification_private_message))
+ .setLargeIcon(bitmap)
+ .setContentIntent(contentPendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setAutoCancel(true)
+ .setWhen(time * 1000)
+
+ val builder =
+ NotificationCompat
+ .Builder(applicationContext, channelId)
+ .setSmallIcon(R.drawable.amethyst)
+ .setContentTitle(messageTitle)
+ .setContentText(messageBody)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(messageBody))
+ .setLargeIcon(bitmap)
+ .setContentIntent(contentPendingIntent)
+ .setPublicVersion(builderPublic.build())
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(category)
+ .setGroup(notificationGroupKey)
.setAutoCancel(true)
.setWhen(time * 1000)
notify(notId, builder.build())
+
+ sendGroupSummary(channelId, notificationGroupKey, summaryId, summaryText, applicationContext)
+ }
+
+ private fun NotificationManager.sendGroupSummary(
+ channelId: String,
+ groupKey: String,
+ summaryId: Int,
+ summaryText: String,
+ applicationContext: Context,
+ ) {
+ val activeCount = activeNotifications.count { it.notification.group == groupKey && it.id != summaryId }
+
+ if (activeCount < 2) return
+
+ val summaryBuilder =
+ NotificationCompat
+ .Builder(applicationContext, channelId)
+ .setSmallIcon(R.drawable.amethyst)
+ .setGroup(groupKey)
+ .setGroupSummary(true)
+ .setAutoCancel(true)
+ .setStyle(
+ NotificationCompat
+ .InboxStyle()
+ .setSummaryText(summaryText),
+ )
+
+ notify(summaryId, summaryBuilder.build())
+ }
+
+ private fun NotificationManager.isDuplicate(notId: Int): Boolean {
+ val notifications: Array = activeNotifications
+ for (notification in notifications) {
+ if (notification.id == notId) {
+ return true
+ }
+ }
+ return false
}
/** Cancels all notifications. */
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt
index ec65f5c14..ec2ad0893 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt
@@ -428,7 +428,9 @@ private fun NavigateIfIntentRequested(
actionableNextPage?.let { nextRoute ->
val npub = runCatching { URI(intentNextPage.removePrefix("nostr:")).findParameterValue("account") }.getOrNull()
if (npub != null && accountSessionManager.currentAccountNPub() != npub) {
- accountSessionManager.checkAndSwitchUserSync(npub, nextRoute)
+ accountSessionManager.checkAndSwitchUserSync(npub) { account ->
+ uriToRoute(intentNextPage, account)
+ }
} else {
val currentRoute = getRouteWithArguments(nextRoute::class, nav.controller)
if (!isSameRoute(currentRoute, nextRoute)) {
@@ -484,7 +486,9 @@ private fun NavigateIfIntentRequested(
scope.launch {
val npub = runCatching { URI(uri.removePrefix("nostr:")).findParameterValue("account") }.getOrNull()
if (npub != null && accountSessionManager.currentAccountNPub() != npub) {
- accountSessionManager.checkAndSwitchUserSync(npub, newPage)
+ accountSessionManager.checkAndSwitchUserSync(npub) { newAccount ->
+ uriToRoute(uri, newAccount)
+ }
} else {
val currentRoute = getRouteWithArguments(newPage::class, nav.controller)
if (!isSameRoute(currentRoute, newPage)) {
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt
index 3549f612c..9e9fcf4b3 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt
@@ -112,11 +112,11 @@ class AccountSessionManager(
}
}
- private suspend fun loginWithDefaultAccount(route: Route? = null) {
+ private suspend fun loginWithDefaultAccount(routeBuilder: ((account: Account) -> Route?)? = null) {
val accountSettings = localPreferences.loadAccountConfigFromEncryptedStorage()
if (accountSettings != null) {
- startUI(accountSettings, route)
+ startUI(accountSettings, routeBuilder)
} else {
requestLoginUI()
}
@@ -184,11 +184,11 @@ class AccountSessionManager(
fun startUI(
accountSettings: AccountSettings,
- route: Route? = null,
+ routeBuilder: ((account: Account) -> Route?)? = null,
) {
val account = accountsCache.loadAccount(accountSettings)
_accountContent.update {
- AccountState.LoggedIn(account, route)
+ AccountState.LoggedIn(account, routeBuilder?.invoke(account))
}
}
@@ -280,7 +280,7 @@ class AccountSessionManager(
localPreferences.setDefaultAccount(accountSettings)
- startUI(accountSettings, route = Route.ImportFollowsSelectUser)
+ startUI(accountSettings, routeBuilder = { Route.ImportFollowsSelectUser })
scope.launch(Dispatchers.IO) {
delay(2000) // waits for the new user to connect to the new relays.
@@ -329,12 +329,12 @@ class AccountSessionManager(
suspend fun checkAndSwitchUserSync(
npub: String,
- route: Route,
+ routeBuilder: ((account: Account) -> Route?)? = null,
): Boolean {
if (npub != localPreferences.currentAccount()) {
val account = localPreferences.allSavedAccounts().firstOrNull { it.npub == npub }
if (account != null) {
- switchUserSync(account, route)
+ switchUserSync(account, routeBuilder)
return true
}
}
@@ -343,10 +343,10 @@ class AccountSessionManager(
private suspend fun switchUserSync(
accountInfo: AccountInfo,
- route: Route? = null,
+ routeBuilder: ((account: Account) -> Route?)? = null,
) {
localPreferences.switchToAccount(accountInfo)
- loginWithDefaultAccount(route)
+ loginWithDefaultAccount(routeBuilder)
}
fun currentAccountNPub() =
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index d0f119791..a93534129 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -732,6 +732,11 @@
From %1$s
for %1$s
+ Reply
+ Mark Read
+ New messages
+ New zaps
+
Notify:
Join Conversation