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 5860829872..1a0fed0dc5 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 @@ -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, ) } 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 27b56d05bd..600939abe7 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 @@ -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)