Merge pull request #1884 from vitorpamplona/claude/improve-android-push-notifications-9DzMr
Add direct reply and mark read actions to DM notifications
This commit is contained in:
@@ -240,6 +240,11 @@
|
|||||||
<action android:name="com.shared.NOSTR" />
|
<action android:name="com.shared.NOSTR" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".service.notifications.NotificationReplyReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+37
-17
@@ -183,7 +183,7 @@ class EventNotificationConsumer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notify(
|
private suspend fun notify(
|
||||||
event: ChatMessageEncryptedFileHeaderEvent,
|
event: ChatMessageEncryptedFileHeaderEvent,
|
||||||
account: Account,
|
account: Account,
|
||||||
) {
|
) {
|
||||||
@@ -210,13 +210,13 @@ class EventNotificationConsumer(
|
|||||||
val content = chatNote.event?.content ?: ""
|
val content = chatNote.event?.content ?: ""
|
||||||
val user = chatNote.author?.toBestDisplayName() ?: ""
|
val user = chatNote.author?.toBestDisplayName() ?: ""
|
||||||
val userPicture = chatNote.author?.profilePicture()
|
val userPicture = chatNote.author?.profilePicture()
|
||||||
val noteUri =
|
val accountNpub =
|
||||||
chatNote.toNEvent() + ACCOUNT_QUERY_PARAM +
|
|
||||||
account.signer.pubKey
|
account.signer.pubKey
|
||||||
.hexToByteArray()
|
.hexToByteArray()
|
||||||
.toNpub()
|
.toNpub()
|
||||||
|
val chatroomMembers = chatRoom.users.joinToString(",")
|
||||||
|
val noteUri = chatNote.toNEvent() + ACCOUNT_QUERY_PARAM + accountNpub
|
||||||
|
|
||||||
// TODO: Show Image on notification
|
|
||||||
notificationManager()
|
notificationManager()
|
||||||
.sendDMNotification(
|
.sendDMNotification(
|
||||||
event.id,
|
event.id,
|
||||||
@@ -226,12 +226,15 @@ class EventNotificationConsumer(
|
|||||||
userPicture,
|
userPicture,
|
||||||
noteUri,
|
noteUri,
|
||||||
applicationContext,
|
applicationContext,
|
||||||
|
accountNpub = accountNpub,
|
||||||
|
accountPictureUrl = account.userProfile().profilePicture(),
|
||||||
|
chatroomMembers = chatroomMembers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notify(
|
private suspend fun notify(
|
||||||
event: ChatMessageEvent,
|
event: ChatMessageEvent,
|
||||||
account: Account,
|
account: Account,
|
||||||
) {
|
) {
|
||||||
@@ -255,20 +258,25 @@ class EventNotificationConsumer(
|
|||||||
val content = chatNote.event?.content ?: ""
|
val content = chatNote.event?.content ?: ""
|
||||||
val user = chatNote.author?.toBestDisplayName() ?: ""
|
val user = chatNote.author?.toBestDisplayName() ?: ""
|
||||||
val userPicture = chatNote.author?.profilePicture()
|
val userPicture = chatNote.author?.profilePicture()
|
||||||
val noteUri =
|
val accountNpub =
|
||||||
chatNote.toNEvent() + ACCOUNT_QUERY_PARAM +
|
|
||||||
account.signer.pubKey
|
account.signer.pubKey
|
||||||
.hexToByteArray()
|
.hexToByteArray()
|
||||||
.toNpub()
|
.toNpub()
|
||||||
|
val chatroomMembers = chatRoom.users.joinToString(",")
|
||||||
|
val noteUri = chatNote.toNEvent() + ACCOUNT_QUERY_PARAM + accountNpub
|
||||||
|
|
||||||
notificationManager()
|
notificationManager()
|
||||||
.sendDMNotification(
|
.sendDMNotification(
|
||||||
event.id,
|
id = event.id,
|
||||||
content,
|
messageBody = content,
|
||||||
user,
|
senderName = user,
|
||||||
event.createdAt,
|
time = event.createdAt,
|
||||||
userPicture,
|
pictureUrl = userPicture,
|
||||||
noteUri,
|
uri = noteUri,
|
||||||
applicationContext,
|
applicationContext = applicationContext,
|
||||||
|
accountNpub = accountNpub,
|
||||||
|
accountPictureUrl = account.userProfile().profilePicture(),
|
||||||
|
chatroomMembers = chatroomMembers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,13 +305,25 @@ class EventNotificationConsumer(
|
|||||||
decryptContent(note, account.signer)?.let { content ->
|
decryptContent(note, account.signer)?.let { content ->
|
||||||
val user = note.author?.toBestDisplayName() ?: ""
|
val user = note.author?.toBestDisplayName() ?: ""
|
||||||
val userPicture = note.author?.profilePicture()
|
val userPicture = note.author?.profilePicture()
|
||||||
val noteUri =
|
val accountNpub =
|
||||||
note.toNEvent() + ACCOUNT_QUERY_PARAM +
|
|
||||||
account.signer.pubKey
|
account.signer.pubKey
|
||||||
.hexToByteArray()
|
.hexToByteArray()
|
||||||
.toNpub()
|
.toNpub()
|
||||||
|
val noteUri = note.toNEvent() + ACCOUNT_QUERY_PARAM + accountNpub
|
||||||
|
|
||||||
notificationManager()
|
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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+113
@@ -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<String>,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+265
-96
@@ -25,23 +25,37 @@ import android.app.NotificationManager
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.service.notification.StatusBarNotification
|
import android.service.notification.StatusBarNotification
|
||||||
import androidx.core.app.NotificationCompat
|
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 androidx.core.net.toUri
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.asDrawable
|
import coil3.asDrawable
|
||||||
import coil3.executeBlocking
|
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
object NotificationUtils {
|
object NotificationUtils {
|
||||||
private var dmChannel: NotificationChannel? = null
|
private var dmChannel: NotificationChannel? = null
|
||||||
private var zapChannel: NotificationChannel? = null
|
private var zapChannel: NotificationChannel? = null
|
||||||
private const val DM_GROUP_KEY = "com.vitorpamplona.amethyst.DM_NOTIFICATION"
|
private const val DM_GROUP_KEY = "com.vitorpamplona.amethyst.DM_NOTIFICATION"
|
||||||
private const val ZAP_GROUP_KEY = "com.vitorpamplona.amethyst.ZAP_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 {
|
fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel {
|
||||||
if (dmChannel != null) return dmChannel!!
|
if (dmChannel != null) return dmChannel!!
|
||||||
@@ -50,13 +64,12 @@ object NotificationUtils {
|
|||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
stringRes(applicationContext, R.string.app_notification_dms_channel_id),
|
stringRes(applicationContext, R.string.app_notification_dms_channel_id),
|
||||||
stringRes(applicationContext, R.string.app_notification_dms_channel_name),
|
stringRes(applicationContext, R.string.app_notification_dms_channel_name),
|
||||||
NotificationManager.IMPORTANCE_DEFAULT,
|
NotificationManager.IMPORTANCE_HIGH,
|
||||||
).apply {
|
).apply {
|
||||||
description =
|
description =
|
||||||
stringRes(applicationContext, R.string.app_notification_dms_channel_description)
|
stringRes(applicationContext, R.string.app_notification_dms_channel_description)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the channel with the system
|
|
||||||
val notificationManager: NotificationManager =
|
val notificationManager: NotificationManager =
|
||||||
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
@@ -78,7 +91,6 @@ object NotificationUtils {
|
|||||||
stringRes(applicationContext, R.string.app_notification_zaps_channel_description)
|
stringRes(applicationContext, R.string.app_notification_zaps_channel_description)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the channel with the system
|
|
||||||
val notificationManager: NotificationManager =
|
val notificationManager: NotificationManager =
|
||||||
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
@@ -87,7 +99,7 @@ object NotificationUtils {
|
|||||||
return zapChannel!!
|
return zapChannel!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NotificationManager.sendZapNotification(
|
suspend fun NotificationManager.sendZapNotification(
|
||||||
id: String,
|
id: String,
|
||||||
messageBody: String,
|
messageBody: String,
|
||||||
messageTitle: String,
|
messageTitle: String,
|
||||||
@@ -96,109 +108,109 @@ object NotificationUtils {
|
|||||||
uri: String,
|
uri: String,
|
||||||
applicationContext: Context,
|
applicationContext: Context,
|
||||||
) {
|
) {
|
||||||
val zapChannel = getOrCreateZapChannel(applicationContext)
|
getOrCreateZapChannel(applicationContext)
|
||||||
val channelId = stringRes(applicationContext, R.string.app_notification_zaps_channel_id)
|
val channelId = stringRes(applicationContext, R.string.app_notification_zaps_channel_id)
|
||||||
|
|
||||||
sendNotification(
|
sendNotification(
|
||||||
id,
|
id = id,
|
||||||
messageBody,
|
messageBody = messageBody,
|
||||||
messageTitle,
|
messageTitle = messageTitle,
|
||||||
time,
|
time = time,
|
||||||
pictureUrl,
|
pictureUrl = pictureUrl,
|
||||||
uri,
|
uri = uri,
|
||||||
channelId,
|
channelId = channelId,
|
||||||
ZAP_GROUP_KEY,
|
notificationGroupKey = ZAP_GROUP_KEY,
|
||||||
applicationContext,
|
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,
|
id: String,
|
||||||
messageBody: String,
|
messageBody: String,
|
||||||
messageTitle: String,
|
senderName: String,
|
||||||
time: Long,
|
time: Long,
|
||||||
pictureUrl: String?,
|
pictureUrl: String?,
|
||||||
uri: String,
|
uri: String,
|
||||||
applicationContext: Context,
|
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)
|
val channelId = stringRes(applicationContext, R.string.app_notification_dms_channel_id)
|
||||||
|
|
||||||
sendNotification(
|
sendDMNotificationStyled(
|
||||||
id,
|
id = id,
|
||||||
messageBody,
|
messageBody = messageBody,
|
||||||
messageTitle,
|
senderName = senderName,
|
||||||
time,
|
time = time,
|
||||||
pictureUrl,
|
pictureUrl = pictureUrl,
|
||||||
uri,
|
uri = uri,
|
||||||
channelId,
|
channelId = channelId,
|
||||||
DM_GROUP_KEY,
|
applicationContext = 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,
|
id: String,
|
||||||
messageBody: String,
|
messageBody: String,
|
||||||
messageTitle: String,
|
senderName: String,
|
||||||
time: Long,
|
time: Long,
|
||||||
pictureUrl: String?,
|
pictureUrl: String?,
|
||||||
uri: String,
|
uri: String,
|
||||||
channelId: 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,
|
applicationContext: Context,
|
||||||
|
accountNpub: String?,
|
||||||
|
accountPictureUrl: String?,
|
||||||
|
chatroomMembers: String?,
|
||||||
) {
|
) {
|
||||||
val notId = id.hashCode()
|
val notId = id.hashCode()
|
||||||
|
|
||||||
// dont notify twice
|
if (isDuplicate(notId)) return
|
||||||
val notifications: Array<StatusBarNotification> = getActiveNotifications()
|
|
||||||
for (notification in notifications) {
|
val bitmap = pictureUrl?.let { loadBitmap(it, applicationContext) }
|
||||||
if (notification.id == notId) {
|
val accountBitmap = accountPictureUrl?.let { loadBitmap(it, applicationContext) }
|
||||||
return
|
|
||||||
}
|
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 =
|
val contentIntent =
|
||||||
Intent(applicationContext, MainActivity::class.java).apply { data = uri.toUri() }
|
Intent(applicationContext, MainActivity::class.java).apply { data = uri.toUri() }
|
||||||
@@ -208,41 +220,198 @@ object NotificationUtils {
|
|||||||
applicationContext,
|
applicationContext,
|
||||||
notId,
|
notId,
|
||||||
contentIntent,
|
contentIntent,
|
||||||
PendingIntent.FLAG_MUTABLE,
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Build the notification
|
|
||||||
val builderPublic =
|
val builderPublic =
|
||||||
NotificationCompat
|
NotificationCompat
|
||||||
.Builder(
|
.Builder(applicationContext, channelId)
|
||||||
applicationContext,
|
.setSmallIcon(R.drawable.amethyst)
|
||||||
channelId,
|
.setContentTitle(senderName)
|
||||||
).setSmallIcon(R.drawable.amethyst)
|
|
||||||
.setContentTitle(messageTitle)
|
|
||||||
.setContentText(stringRes(applicationContext, R.string.app_notification_private_message))
|
.setContentText(stringRes(applicationContext, R.string.app_notification_private_message))
|
||||||
.setLargeIcon(picture?.bitmap)
|
.setLargeIcon(bitmap)
|
||||||
.setContentIntent(contentPendingIntent)
|
.setContentIntent(contentPendingIntent)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setWhen(time * 1000)
|
.setWhen(time * 1000)
|
||||||
|
|
||||||
// Build the notification
|
|
||||||
val builder =
|
val builder =
|
||||||
NotificationCompat
|
NotificationCompat
|
||||||
.Builder(
|
.Builder(applicationContext, channelId)
|
||||||
applicationContext,
|
.setSmallIcon(R.drawable.amethyst)
|
||||||
channelId,
|
.setLargeIcon(bitmap)
|
||||||
).setSmallIcon(R.drawable.amethyst)
|
.setStyle(messagingStyle)
|
||||||
.setContentTitle(messageTitle)
|
|
||||||
.setContentText(messageBody)
|
|
||||||
.setLargeIcon(picture?.bitmap)
|
|
||||||
.setContentIntent(contentPendingIntent)
|
.setContentIntent(contentPendingIntent)
|
||||||
.setPublicVersion(builderPublic.build())
|
.setPublicVersion(builderPublic.build())
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.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)
|
.setAutoCancel(true)
|
||||||
.setWhen(time * 1000)
|
.setWhen(time * 1000)
|
||||||
|
|
||||||
notify(notId, builder.build())
|
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<StatusBarNotification> = activeNotifications
|
||||||
|
for (notification in notifications) {
|
||||||
|
if (notification.id == notId) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cancels all notifications. */
|
/** Cancels all notifications. */
|
||||||
|
|||||||
@@ -428,7 +428,9 @@ private fun NavigateIfIntentRequested(
|
|||||||
actionableNextPage?.let { nextRoute ->
|
actionableNextPage?.let { nextRoute ->
|
||||||
val npub = runCatching { URI(intentNextPage.removePrefix("nostr:")).findParameterValue("account") }.getOrNull()
|
val npub = runCatching { URI(intentNextPage.removePrefix("nostr:")).findParameterValue("account") }.getOrNull()
|
||||||
if (npub != null && accountSessionManager.currentAccountNPub() != npub) {
|
if (npub != null && accountSessionManager.currentAccountNPub() != npub) {
|
||||||
accountSessionManager.checkAndSwitchUserSync(npub, nextRoute)
|
accountSessionManager.checkAndSwitchUserSync(npub) { account ->
|
||||||
|
uriToRoute(intentNextPage, account)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val currentRoute = getRouteWithArguments(nextRoute::class, nav.controller)
|
val currentRoute = getRouteWithArguments(nextRoute::class, nav.controller)
|
||||||
if (!isSameRoute(currentRoute, nextRoute)) {
|
if (!isSameRoute(currentRoute, nextRoute)) {
|
||||||
@@ -484,7 +486,9 @@ private fun NavigateIfIntentRequested(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
val npub = runCatching { URI(uri.removePrefix("nostr:")).findParameterValue("account") }.getOrNull()
|
val npub = runCatching { URI(uri.removePrefix("nostr:")).findParameterValue("account") }.getOrNull()
|
||||||
if (npub != null && accountSessionManager.currentAccountNPub() != npub) {
|
if (npub != null && accountSessionManager.currentAccountNPub() != npub) {
|
||||||
accountSessionManager.checkAndSwitchUserSync(npub, newPage)
|
accountSessionManager.checkAndSwitchUserSync(npub) { newAccount ->
|
||||||
|
uriToRoute(uri, newAccount)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val currentRoute = getRouteWithArguments(newPage::class, nav.controller)
|
val currentRoute = getRouteWithArguments(newPage::class, nav.controller)
|
||||||
if (!isSameRoute(currentRoute, newPage)) {
|
if (!isSameRoute(currentRoute, newPage)) {
|
||||||
|
|||||||
+9
-9
@@ -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()
|
val accountSettings = localPreferences.loadAccountConfigFromEncryptedStorage()
|
||||||
|
|
||||||
if (accountSettings != null) {
|
if (accountSettings != null) {
|
||||||
startUI(accountSettings, route)
|
startUI(accountSettings, routeBuilder)
|
||||||
} else {
|
} else {
|
||||||
requestLoginUI()
|
requestLoginUI()
|
||||||
}
|
}
|
||||||
@@ -184,11 +184,11 @@ class AccountSessionManager(
|
|||||||
|
|
||||||
fun startUI(
|
fun startUI(
|
||||||
accountSettings: AccountSettings,
|
accountSettings: AccountSettings,
|
||||||
route: Route? = null,
|
routeBuilder: ((account: Account) -> Route?)? = null,
|
||||||
) {
|
) {
|
||||||
val account = accountsCache.loadAccount(accountSettings)
|
val account = accountsCache.loadAccount(accountSettings)
|
||||||
_accountContent.update {
|
_accountContent.update {
|
||||||
AccountState.LoggedIn(account, route)
|
AccountState.LoggedIn(account, routeBuilder?.invoke(account))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ class AccountSessionManager(
|
|||||||
|
|
||||||
localPreferences.setDefaultAccount(accountSettings)
|
localPreferences.setDefaultAccount(accountSettings)
|
||||||
|
|
||||||
startUI(accountSettings, route = Route.ImportFollowsSelectUser)
|
startUI(accountSettings, routeBuilder = { Route.ImportFollowsSelectUser })
|
||||||
|
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
delay(2000) // waits for the new user to connect to the new relays.
|
delay(2000) // waits for the new user to connect to the new relays.
|
||||||
@@ -329,12 +329,12 @@ class AccountSessionManager(
|
|||||||
|
|
||||||
suspend fun checkAndSwitchUserSync(
|
suspend fun checkAndSwitchUserSync(
|
||||||
npub: String,
|
npub: String,
|
||||||
route: Route,
|
routeBuilder: ((account: Account) -> Route?)? = null,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (npub != localPreferences.currentAccount()) {
|
if (npub != localPreferences.currentAccount()) {
|
||||||
val account = localPreferences.allSavedAccounts().firstOrNull { it.npub == npub }
|
val account = localPreferences.allSavedAccounts().firstOrNull { it.npub == npub }
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
switchUserSync(account, route)
|
switchUserSync(account, routeBuilder)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,10 +343,10 @@ class AccountSessionManager(
|
|||||||
|
|
||||||
private suspend fun switchUserSync(
|
private suspend fun switchUserSync(
|
||||||
accountInfo: AccountInfo,
|
accountInfo: AccountInfo,
|
||||||
route: Route? = null,
|
routeBuilder: ((account: Account) -> Route?)? = null,
|
||||||
) {
|
) {
|
||||||
localPreferences.switchToAccount(accountInfo)
|
localPreferences.switchToAccount(accountInfo)
|
||||||
loginWithDefaultAccount(route)
|
loginWithDefaultAccount(routeBuilder)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun currentAccountNPub() =
|
fun currentAccountNPub() =
|
||||||
|
|||||||
@@ -732,6 +732,11 @@
|
|||||||
<string name="app_notification_zaps_channel_message_from">From %1$s</string>
|
<string name="app_notification_zaps_channel_message_from">From %1$s</string>
|
||||||
<string name="app_notification_zaps_channel_message_for">for %1$s</string>
|
<string name="app_notification_zaps_channel_message_for">for %1$s</string>
|
||||||
|
|
||||||
|
<string name="app_notification_reply_label">Reply</string>
|
||||||
|
<string name="app_notification_mark_read_label">Mark Read</string>
|
||||||
|
<string name="app_notification_dms_summary">New messages</string>
|
||||||
|
<string name="app_notification_zaps_summary">New zaps</string>
|
||||||
|
|
||||||
<string name="reply_notify">Notify: </string>
|
<string name="reply_notify">Notify: </string>
|
||||||
|
|
||||||
<string name="channel_list_join_conversation">Join Conversation</string>
|
<string name="channel_list_join_conversation">Join Conversation</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user