feat(notifications): mentions channel, per-thread grouping, inline reply
- Split kind-1 routing in EventNotificationConsumer: if the reply target is authored by the current account it still lands on the Replies channel; any other p-tagged kind-1 (plain mention, citation, reply to someone else in a thread we're in) lands on a new dedicated Mentions channel. Users can disable either independently from Android settings. - Group reply notifications by thread root. Compute the root via the NIP-10 marked/unmarked root markers for kind 1 and the NIP-22 root event/address tags for kind 1111, falling back to the direct parent. Use a per-thread group key + stable per-thread summary id so Android collapses "5 replies to your thread" into one entry instead of one big flat stack. - Add an inline Reply action on reply notifications (kind 1 + kind 1111) using RemoteInput, mirroring the DM flow. NotificationReplyReceiver now handles PUBLIC_REPLY_ACTION: resolves the target event from LocalCache and signs a NIP-10 reply (TextNoteEvent.build with replyingTo hint) or a NIP-22 comment (CommentEvent.replyBuilder), broadcast via account.signAndComputeBroadcast. The relay proxy is held open for the duration via the same runOnRelay helper the DM flow uses. https://claude.ai/code/session_01GQDJxiHPogdzCNhUBN7Pjc
This commit is contained in:
+67
-14
@@ -38,8 +38,10 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.call.notification.CallNotifier
|
import com.vitorpamplona.amethyst.service.call.notification.CallNotifier
|
||||||
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.InlineReplyTarget
|
||||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendChessNotification
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendChessNotification
|
||||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
|
||||||
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendMentionNotification
|
||||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendReactionNotification
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendReactionNotification
|
||||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendReplyNotification
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendReplyNotification
|
||||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
|
||||||
@@ -665,14 +667,19 @@ class EventNotificationConsumer(
|
|||||||
// don't notify for own notes
|
// don't notify for own notes
|
||||||
if (event.pubKey == account.signer.pubKey) return
|
if (event.pubKey == account.signer.pubKey) return
|
||||||
|
|
||||||
// must be a reply (NIP-10) — plain mentions are not handled on this channel.
|
val replyTargetId = event.replyingTo()
|
||||||
val replyTargetId = event.replyingTo() ?: return
|
|
||||||
|
|
||||||
// only notify when the reply targets a note authored by the current account.
|
if (replyTargetId != null) {
|
||||||
val repliedNote = LocalCache.getNoteIfExists(replyTargetId) ?: return
|
val repliedNote = LocalCache.getNoteIfExists(replyTargetId)
|
||||||
if (repliedNote.author?.pubkeyHex != account.signer.pubKey) return
|
if (repliedNote?.author?.pubkeyHex == account.signer.pubKey) {
|
||||||
|
val threadRoot = event.markedRoot()?.eventId ?: event.unmarkedRoot()?.eventId ?: replyTargetId
|
||||||
|
notifyReply(event, account, repliedNote.event?.content, threadRoot)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
notifyReply(event, account, repliedNote.event?.content)
|
// Not a reply to us but we're p-tagged — a mention or citation.
|
||||||
|
notifyMention(event, account)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun notify(
|
private suspend fun notify(
|
||||||
@@ -698,13 +705,20 @@ class EventNotificationConsumer(
|
|||||||
.replyingTo()
|
.replyingTo()
|
||||||
?.let { LocalCache.getNoteIfExists(it)?.event?.content }
|
?.let { LocalCache.getNoteIfExists(it)?.event?.content }
|
||||||
|
|
||||||
notifyReply(event, account, parentContent)
|
val threadRoot =
|
||||||
|
event.rootEventIds().firstOrNull()
|
||||||
|
?: event.rootAddressIds().firstOrNull()
|
||||||
|
?: event.replyingToAddressOrEvent()
|
||||||
|
?: event.id
|
||||||
|
|
||||||
|
notifyReply(event, account, parentContent, threadRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun notifyReply(
|
private suspend fun notifyReply(
|
||||||
event: Event,
|
event: Event,
|
||||||
account: Account,
|
account: Account,
|
||||||
parentContent: String?,
|
parentContent: String?,
|
||||||
|
threadRootId: String,
|
||||||
) {
|
) {
|
||||||
val replyNote = LocalCache.getNoteIfExists(event.id) ?: return
|
val replyNote = LocalCache.getNoteIfExists(event.id) ?: return
|
||||||
|
|
||||||
@@ -747,13 +761,52 @@ class EventNotificationConsumer(
|
|||||||
|
|
||||||
notificationManager()
|
notificationManager()
|
||||||
.sendReplyNotification(
|
.sendReplyNotification(
|
||||||
event.id,
|
id = event.id,
|
||||||
content,
|
messageBody = content,
|
||||||
title,
|
messageTitle = title,
|
||||||
event.createdAt,
|
time = event.createdAt,
|
||||||
userPicture,
|
pictureUrl = userPicture,
|
||||||
noteUri,
|
uri = noteUri,
|
||||||
applicationContext,
|
applicationContext = applicationContext,
|
||||||
|
threadRootId = threadRootId,
|
||||||
|
inlineReply = InlineReplyTarget(accountNpub = accountNpub, targetEventId = event.id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun notifyMention(
|
||||||
|
event: TextNoteEvent,
|
||||||
|
account: Account,
|
||||||
|
) {
|
||||||
|
val note = LocalCache.getNoteIfExists(event.id) ?: return
|
||||||
|
|
||||||
|
val author = LocalCache.getOrCreateUser(event.pubKey)
|
||||||
|
val user = author.toBestDisplayName()
|
||||||
|
val userPicture = author.profilePicture()
|
||||||
|
|
||||||
|
val title = stringRes(applicationContext, R.string.app_notification_mentions_channel_message, user)
|
||||||
|
|
||||||
|
val content =
|
||||||
|
event.content
|
||||||
|
.split("\n")
|
||||||
|
.firstOrNull { it.isNotBlank() }
|
||||||
|
?.take(280)
|
||||||
|
?: ""
|
||||||
|
|
||||||
|
val accountNpub =
|
||||||
|
account.signer.pubKey
|
||||||
|
.hexToByteArray()
|
||||||
|
.toNpub()
|
||||||
|
val noteUri = note.toNEvent() + ACCOUNT_QUERY_PARAM + accountNpub
|
||||||
|
|
||||||
|
notificationManager()
|
||||||
|
.sendMentionNotification(
|
||||||
|
id = event.id,
|
||||||
|
messageBody = content,
|
||||||
|
messageTitle = title,
|
||||||
|
time = event.createdAt,
|
||||||
|
pictureUrl = userPicture,
|
||||||
|
uri = noteUri,
|
||||||
|
applicationContext = applicationContext,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+72
-6
@@ -28,8 +28,12 @@ import androidx.core.app.RemoteInput
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.LocalPreferences
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||||
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -69,11 +73,39 @@ class NotificationReplyReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
if (members.isEmpty()) return
|
if (members.isEmpty()) return
|
||||||
|
|
||||||
|
runOnRelay(notificationManager, notificationId) {
|
||||||
|
sendReply(accountNpub, members, replyText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationUtils.PUBLIC_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 targetEventId = intent.getStringExtra(NotificationUtils.KEY_TARGET_EVENT_ID) ?: return
|
||||||
|
|
||||||
|
runOnRelay(notificationManager, notificationId) {
|
||||||
|
sendPublicReply(accountNpub, targetEventId, replyText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runOnRelay(
|
||||||
|
notificationManager: NotificationManager,
|
||||||
|
notificationId: Int,
|
||||||
|
block: suspend () -> Unit,
|
||||||
|
) {
|
||||||
val pendingResult = goAsync()
|
val pendingResult = goAsync()
|
||||||
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// activates the relay to send the message.
|
|
||||||
val collectionJob =
|
val collectionJob =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
Amethyst.instance.relayProxyClientConnector.relayServices
|
Amethyst.instance.relayProxyClientConnector.relayServices
|
||||||
@@ -81,22 +113,18 @@ class NotificationReplyReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sendReply(accountNpub, members, replyText)
|
block()
|
||||||
notificationManager.cancel(notificationId)
|
notificationManager.cancel(notificationId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
Log.e("NotificationReply") { "Failed to send reply: ${e.message}" }
|
Log.e("NotificationReply") { "Failed to send reply: ${e.message}" }
|
||||||
} finally {
|
} finally {
|
||||||
pendingResult.finish()
|
pendingResult.finish()
|
||||||
|
|
||||||
// closes the relay connection.
|
|
||||||
collectionJob.cancel()
|
collectionJob.cancel()
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendReply(
|
private suspend fun sendReply(
|
||||||
accountNpub: String,
|
accountNpub: String,
|
||||||
@@ -111,4 +139,42 @@ class NotificationReplyReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
account.sendNip17PrivateMessage(template)
|
account.sendNip17PrivateMessage(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun sendPublicReply(
|
||||||
|
accountNpub: String,
|
||||||
|
targetEventId: String,
|
||||||
|
replyText: String,
|
||||||
|
) {
|
||||||
|
val accountSettings = LocalPreferences.loadAccountConfigFromEncryptedStorage(accountNpub) ?: return
|
||||||
|
val account = Amethyst.instance.accountsCache.loadAccount(accountSettings)
|
||||||
|
|
||||||
|
val targetEvent = LocalCache.getNoteIfExists(targetEventId)?.event ?: return
|
||||||
|
|
||||||
|
val template =
|
||||||
|
when (targetEvent) {
|
||||||
|
is TextNoteEvent -> {
|
||||||
|
TextNoteEvent.build(
|
||||||
|
note = replyText,
|
||||||
|
replyingTo = EventHintBundle(targetEvent),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is CommentEvent -> {
|
||||||
|
CommentEvent.replyBuilder(
|
||||||
|
msg = replyText,
|
||||||
|
replyingTo = EventHintBundle(targetEvent),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// Non-threaded events (e.g. long-form articles) use NIP-22 comments.
|
||||||
|
CommentEvent.replyBuilder(
|
||||||
|
msg = replyText,
|
||||||
|
replyingTo = EventHintBundle(targetEvent),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
account.signAndComputeBroadcast(template)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-4
@@ -49,25 +49,39 @@ object NotificationUtils {
|
|||||||
private var reactionChannel: NotificationChannel? = null
|
private var reactionChannel: NotificationChannel? = null
|
||||||
private var chessChannel: NotificationChannel? = null
|
private var chessChannel: NotificationChannel? = null
|
||||||
private var replyChannel: NotificationChannel? = null
|
private var replyChannel: NotificationChannel? = null
|
||||||
|
private var mentionChannel: 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"
|
||||||
private const val REACTION_GROUP_KEY = "com.vitorpamplona.amethyst.REACTION_NOTIFICATION"
|
private const val REACTION_GROUP_KEY = "com.vitorpamplona.amethyst.REACTION_NOTIFICATION"
|
||||||
private const val CHESS_GROUP_KEY = "com.vitorpamplona.amethyst.CHESS_NOTIFICATION"
|
private const val CHESS_GROUP_KEY = "com.vitorpamplona.amethyst.CHESS_NOTIFICATION"
|
||||||
private const val REPLY_GROUP_KEY = "com.vitorpamplona.amethyst.REPLY_NOTIFICATION"
|
const val REPLY_GROUP_KEY_PREFIX = "com.vitorpamplona.amethyst.REPLY_NOTIFICATION"
|
||||||
|
private const val MENTION_GROUP_KEY = "com.vitorpamplona.amethyst.MENTION_NOTIFICATION"
|
||||||
|
|
||||||
const val REPLY_ACTION = "com.vitorpamplona.amethyst.REPLY_ACTION"
|
const val REPLY_ACTION = "com.vitorpamplona.amethyst.REPLY_ACTION"
|
||||||
|
const val PUBLIC_REPLY_ACTION = "com.vitorpamplona.amethyst.PUBLIC_REPLY_ACTION"
|
||||||
const val MARK_READ_ACTION = "com.vitorpamplona.amethyst.MARK_READ_ACTION"
|
const val MARK_READ_ACTION = "com.vitorpamplona.amethyst.MARK_READ_ACTION"
|
||||||
const val KEY_REPLY_TEXT = "key_reply_text"
|
const val KEY_REPLY_TEXT = "key_reply_text"
|
||||||
const val KEY_NOTIFICATION_ID = "key_notification_id"
|
const val KEY_NOTIFICATION_ID = "key_notification_id"
|
||||||
const val KEY_ACCOUNT_NPUB = "key_account_npub"
|
const val KEY_ACCOUNT_NPUB = "key_account_npub"
|
||||||
const val KEY_CHATROOM_MEMBERS = "key_chatroom_members"
|
const val KEY_CHATROOM_MEMBERS = "key_chatroom_members"
|
||||||
|
const val KEY_TARGET_EVENT_ID = "key_target_event_id"
|
||||||
|
|
||||||
private const val DM_SUMMARY_ID = 0x10000
|
private const val DM_SUMMARY_ID = 0x10000
|
||||||
private const val ZAP_SUMMARY_ID = 0x20000
|
private const val ZAP_SUMMARY_ID = 0x20000
|
||||||
private const val REACTION_SUMMARY_ID = 0x40000
|
private const val REACTION_SUMMARY_ID = 0x40000
|
||||||
private const val CHESS_SUMMARY_ID = 0x30000
|
private const val CHESS_SUMMARY_ID = 0x30000
|
||||||
private const val REPLY_SUMMARY_ID = 0x50000
|
private const val REPLY_SUMMARY_ID_BASE = 0x50000
|
||||||
|
private const val MENTION_SUMMARY_ID = 0x60000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives a stable summary notification id for a per-thread reply group.
|
||||||
|
* Uses the thread root id hash mixed with the base id so different threads
|
||||||
|
* don't collide with each other or with the other channel summaries.
|
||||||
|
*/
|
||||||
|
fun replySummaryIdFor(threadRootId: String): Int = REPLY_SUMMARY_ID_BASE xor threadRootId.hashCode()
|
||||||
|
|
||||||
|
fun replyGroupKeyFor(threadRootId: String): String = "$REPLY_GROUP_KEY_PREFIX:$threadRootId"
|
||||||
|
|
||||||
fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel {
|
fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel {
|
||||||
if (dmChannel != null) return dmChannel!!
|
if (dmChannel != null) return dmChannel!!
|
||||||
@@ -174,6 +188,27 @@ object NotificationUtils {
|
|||||||
return replyChannel!!
|
return replyChannel!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getOrCreateMentionChannel(applicationContext: Context): NotificationChannel {
|
||||||
|
if (mentionChannel != null) return mentionChannel!!
|
||||||
|
|
||||||
|
mentionChannel =
|
||||||
|
NotificationChannel(
|
||||||
|
stringRes(applicationContext, R.string.app_notification_mentions_channel_id),
|
||||||
|
stringRes(applicationContext, R.string.app_notification_mentions_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT,
|
||||||
|
).apply {
|
||||||
|
description =
|
||||||
|
stringRes(applicationContext, R.string.app_notification_mentions_channel_description)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationManager: NotificationManager =
|
||||||
|
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannel(mentionChannel!!)
|
||||||
|
|
||||||
|
return mentionChannel!!
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun NotificationManager.sendReactionNotification(
|
suspend fun NotificationManager.sendReactionNotification(
|
||||||
id: String,
|
id: String,
|
||||||
messageBody: String,
|
messageBody: String,
|
||||||
@@ -238,6 +273,8 @@ object NotificationUtils {
|
|||||||
pictureUrl: String?,
|
pictureUrl: String?,
|
||||||
uri: String,
|
uri: String,
|
||||||
applicationContext: Context,
|
applicationContext: Context,
|
||||||
|
threadRootId: String,
|
||||||
|
inlineReply: InlineReplyTarget? = null,
|
||||||
) {
|
) {
|
||||||
getOrCreateReplyChannel(applicationContext)
|
getOrCreateReplyChannel(applicationContext)
|
||||||
val channelId = stringRes(applicationContext, R.string.app_notification_replies_channel_id)
|
val channelId = stringRes(applicationContext, R.string.app_notification_replies_channel_id)
|
||||||
@@ -250,14 +287,54 @@ object NotificationUtils {
|
|||||||
pictureUrl = pictureUrl,
|
pictureUrl = pictureUrl,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
channelId = channelId,
|
channelId = channelId,
|
||||||
notificationGroupKey = REPLY_GROUP_KEY,
|
notificationGroupKey = replyGroupKeyFor(threadRootId),
|
||||||
category = NotificationCompat.CATEGORY_SOCIAL,
|
category = NotificationCompat.CATEGORY_SOCIAL,
|
||||||
summaryId = REPLY_SUMMARY_ID,
|
summaryId = replySummaryIdFor(threadRootId),
|
||||||
summaryText = stringRes(applicationContext, R.string.app_notification_replies_summary),
|
summaryText = stringRes(applicationContext, R.string.app_notification_replies_summary),
|
||||||
applicationContext = applicationContext,
|
applicationContext = applicationContext,
|
||||||
|
inlineReply = inlineReply,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun NotificationManager.sendMentionNotification(
|
||||||
|
id: String,
|
||||||
|
messageBody: String,
|
||||||
|
messageTitle: String,
|
||||||
|
time: Long,
|
||||||
|
pictureUrl: String?,
|
||||||
|
uri: String,
|
||||||
|
applicationContext: Context,
|
||||||
|
) {
|
||||||
|
getOrCreateMentionChannel(applicationContext)
|
||||||
|
val channelId = stringRes(applicationContext, R.string.app_notification_mentions_channel_id)
|
||||||
|
|
||||||
|
sendNotification(
|
||||||
|
id = id,
|
||||||
|
messageBody = messageBody,
|
||||||
|
messageTitle = messageTitle,
|
||||||
|
time = time,
|
||||||
|
pictureUrl = pictureUrl,
|
||||||
|
uri = uri,
|
||||||
|
channelId = channelId,
|
||||||
|
notificationGroupKey = MENTION_GROUP_KEY,
|
||||||
|
category = NotificationCompat.CATEGORY_SOCIAL,
|
||||||
|
summaryId = MENTION_SUMMARY_ID,
|
||||||
|
summaryText = stringRes(applicationContext, R.string.app_notification_mentions_summary),
|
||||||
|
applicationContext = applicationContext,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload for wiring a RemoteInput-powered inline reply action onto a public
|
||||||
|
* note notification. The receiver resolves the target event from LocalCache
|
||||||
|
* via [targetEventId] and signs the appropriate kind (1 for NIP-10, 1111
|
||||||
|
* for NIP-22) under the account identified by [accountNpub].
|
||||||
|
*/
|
||||||
|
data class InlineReplyTarget(
|
||||||
|
val accountNpub: String,
|
||||||
|
val targetEventId: String,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun NotificationManager.sendZapNotification(
|
suspend fun NotificationManager.sendZapNotification(
|
||||||
id: String,
|
id: String,
|
||||||
messageBody: String,
|
messageBody: String,
|
||||||
@@ -490,6 +567,7 @@ object NotificationUtils {
|
|||||||
summaryId: Int,
|
summaryId: Int,
|
||||||
summaryText: String,
|
summaryText: String,
|
||||||
applicationContext: Context,
|
applicationContext: Context,
|
||||||
|
inlineReply: InlineReplyTarget? = null,
|
||||||
) {
|
) {
|
||||||
val notId = id.hashCode()
|
val notId = id.hashCode()
|
||||||
|
|
||||||
@@ -536,6 +614,40 @@ object NotificationUtils {
|
|||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setWhen(time * 1000)
|
.setWhen(time * 1000)
|
||||||
|
|
||||||
|
if (inlineReply != 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 = PUBLIC_REPLY_ACTION
|
||||||
|
putExtra(KEY_NOTIFICATION_ID, notId)
|
||||||
|
putExtra(KEY_ACCOUNT_NPUB, inlineReply.accountNpub)
|
||||||
|
putExtra(KEY_TARGET_EVENT_ID, inlineReply.targetEventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
notify(notId, builder.build())
|
notify(notId, builder.build())
|
||||||
|
|
||||||
sendGroupSummary(channelId, notificationGroupKey, summaryId, summaryText, applicationContext)
|
sendGroupSummary(channelId, notificationGroupKey, summaryId, summaryText, applicationContext)
|
||||||
|
|||||||
@@ -882,6 +882,11 @@
|
|||||||
<string name="app_notification_replies_channel_message">%1$s replied</string>
|
<string name="app_notification_replies_channel_message">%1$s replied</string>
|
||||||
<string name="app_notification_replies_channel_message_for">on: %1$s</string>
|
<string name="app_notification_replies_channel_message_for">on: %1$s</string>
|
||||||
<string name="app_notification_replies_summary">New replies</string>
|
<string name="app_notification_replies_summary">New replies</string>
|
||||||
|
<string name="app_notification_mentions_channel_id" translatable="false">MentionsID</string>
|
||||||
|
<string name="app_notification_mentions_channel_name">Mentions</string>
|
||||||
|
<string name="app_notification_mentions_channel_description">Notifies you when somebody mentions or cites you in a post</string>
|
||||||
|
<string name="app_notification_mentions_channel_message">%1$s mentioned you</string>
|
||||||
|
<string name="app_notification_mentions_summary">New mentions</string>
|
||||||
|
|
||||||
<!-- Call notifications and UI -->
|
<!-- Call notifications and UI -->
|
||||||
<string name="app_notification_calls_channel_name">Incoming calls</string>
|
<string name="app_notification_calls_channel_name">Incoming calls</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user