mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
Merge pull request #3307 from vitorpamplona/claude/custom-emojis-push-notifications-8n3h87
feat(notifications): render NIP-30 custom emoji reactions as avatar badge
This commit is contained in:
+19
-7
@@ -69,6 +69,7 @@ import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
@@ -766,14 +767,24 @@ class EventNotificationConsumer(
|
||||
val userPicture = author.profilePicture()
|
||||
|
||||
val reactionContent = event.content
|
||||
val reactionSymbol =
|
||||
when {
|
||||
reactionContent == ReactionEvent.LIKE || reactionContent.isBlank() -> "\uD83E\uDD19"
|
||||
reactionContent == ReactionEvent.DISLIKE -> "\uD83D\uDC4E"
|
||||
else -> reactionContent
|
||||
}
|
||||
|
||||
val title = "$reactionSymbol $user"
|
||||
// NIP-30 custom emoji: content is ":shortcode:" backed by an ["emoji", shortcode, url]
|
||||
// tag. The image can't be rendered as text, so it is surfaced as a badge on the author's
|
||||
// avatar (see sendReactionNotification) and the title keeps just the author's name.
|
||||
val customEmojiUrl = CustomEmoji.createEmojiMap(event.tags)[reactionContent]
|
||||
|
||||
val title =
|
||||
if (customEmojiUrl != null) {
|
||||
user
|
||||
} else {
|
||||
val reactionSymbol =
|
||||
when {
|
||||
reactionContent == ReactionEvent.LIKE || reactionContent.isBlank() -> "\uD83E\uDD19"
|
||||
reactionContent == ReactionEvent.DISLIKE -> "\uD83D\uDC4E"
|
||||
else -> reactionContent
|
||||
}
|
||||
"$reactionSymbol $user"
|
||||
}
|
||||
|
||||
val reactedContent =
|
||||
reactedNote
|
||||
@@ -813,6 +824,7 @@ class EventNotificationConsumer(
|
||||
userPicture,
|
||||
noteUri,
|
||||
applicationContext,
|
||||
emojiUrl = customEmojiUrl,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+41
-1
@@ -26,6 +26,9 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.service.notification.StatusBarNotification
|
||||
import androidx.core.app.NotificationCompat
|
||||
@@ -221,6 +224,7 @@ object NotificationUtils {
|
||||
pictureUrl: String?,
|
||||
uri: String,
|
||||
applicationContext: Context,
|
||||
emojiUrl: String? = null,
|
||||
) {
|
||||
getOrCreateReactionChannel(applicationContext)
|
||||
val channelId = stringRes(applicationContext, R.string.app_notification_reactions_channel_id)
|
||||
@@ -231,6 +235,7 @@ object NotificationUtils {
|
||||
messageTitle = messageTitle,
|
||||
time = time,
|
||||
pictureUrl = pictureUrl,
|
||||
badgeUrl = emojiUrl,
|
||||
uri = uri,
|
||||
channelId = channelId,
|
||||
notificationGroupKey = REACTION_GROUP_KEY,
|
||||
@@ -423,6 +428,36 @@ object NotificationUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the badge image (e.g. a NIP-30 custom-emoji reaction) onto the bottom-right corner
|
||||
* of the base avatar. Returns the base unchanged if the badge can't be loaded, or the badge
|
||||
* alone if there is no base avatar.
|
||||
*/
|
||||
private suspend fun overlayBadge(
|
||||
base: Bitmap?,
|
||||
badgeUrl: String,
|
||||
applicationContext: Context,
|
||||
): Bitmap? {
|
||||
val badge = loadBitmap(badgeUrl, applicationContext) ?: return base
|
||||
if (base == null) return badge
|
||||
|
||||
return withContext(Dispatchers.Default) {
|
||||
val result = base.copy(base.config ?: Bitmap.Config.ARGB_8888, true)
|
||||
val canvas = Canvas(result)
|
||||
val badgeSize = minOf(result.width, result.height) * 0.45f
|
||||
val dest =
|
||||
RectF(
|
||||
result.width - badgeSize,
|
||||
result.height - badgeSize,
|
||||
result.width.toFloat(),
|
||||
result.height.toFloat(),
|
||||
)
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { isFilterBitmap = true }
|
||||
canvas.drawBitmap(badge, null, dest, paint)
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun NotificationManager.sendDMNotificationStyled(
|
||||
id: String,
|
||||
messageBody: String,
|
||||
@@ -625,6 +660,7 @@ object NotificationUtils {
|
||||
summaryText: String,
|
||||
applicationContext: Context,
|
||||
inlineReply: InlineReplyTarget? = null,
|
||||
badgeUrl: String? = null,
|
||||
) {
|
||||
val notId = id.hashCode()
|
||||
|
||||
@@ -632,6 +668,10 @@ object NotificationUtils {
|
||||
|
||||
val bitmap = pictureUrl?.let { loadBitmap(it, applicationContext) }
|
||||
|
||||
// For custom-emoji (NIP-30) reactions, the emoji is an image URL that can't render
|
||||
// as text in the notification, so overlay it as a badge on the author's avatar.
|
||||
val largeIcon = badgeUrl?.let { overlayBadge(bitmap, it, applicationContext) } ?: bitmap
|
||||
|
||||
val contentIntent =
|
||||
Intent(applicationContext, MainActivity::class.java).apply { data = uri.toUri() }
|
||||
|
||||
@@ -662,7 +702,7 @@ object NotificationUtils {
|
||||
.setContentTitle(messageTitle)
|
||||
.setContentText(messageBody)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(messageBody))
|
||||
.setLargeIcon(bitmap)
|
||||
.setLargeIcon(largeIcon)
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.setPublicVersion(builderPublic.build())
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
|
||||
Reference in New Issue
Block a user