From 478c4cc35641b116ab08e2162416f569ccc47e12 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 23 Mar 2026 16:32:56 +0100 Subject: [PATCH] feat(chess): notifications for challenge accepted and opponent moves Add Android push notifications for chess events: - LiveChessGameAcceptEvent (kind 30065): notifies when opponent accepts challenge - LiveChessMoveEvent (kind 30066): notifies when opponent makes a move - New Chess notification channel alongside existing DM and Zap channels - Chess kinds added to NotificationFeedFilter for in-app notification feed Co-Authored-By: Claude Opus 4.6 --- .../EventNotificationConsumer.kt | 71 +++++++++++++++++++ .../notifications/NotificationUtils.kt | 52 ++++++++++++++ .../dal/NotificationFeedFilter.kt | 4 ++ amethyst/src/main/res/values/strings.xml | 7 ++ 4 files changed, 134 insertions(+) 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 2ccd1a069..55f0849b9 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 @@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendChessNotification import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification import com.vitorpamplona.amethyst.ui.note.showAmount @@ -47,6 +48,8 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent +import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils import java.math.BigDecimal @@ -108,6 +111,8 @@ class EventNotificationConsumer( is LnZapEvent -> notify(innerEvent, account) is ChatMessageEvent -> notify(innerEvent, account) is ChatMessageEncryptedFileHeaderEvent -> notify(innerEvent, account) + is LiveChessGameAcceptEvent -> notify(innerEvent, account) + is LiveChessMoveEvent -> notify(innerEvent, account) } } } @@ -481,6 +486,72 @@ class EventNotificationConsumer( } } + private suspend fun notify( + event: LiveChessGameAcceptEvent, + account: Account, + ) { + Log.d(TAG, "New Chess Challenge Accept to Notify") + if ( + event.createdAt > TimeUtils.fifteenMinutesAgo() && + event.pubKey != account.signer.pubKey + ) { + val author = LocalCache.getOrCreateUser(event.pubKey) + val user = author.toBestDisplayName() + val userPicture = author.profilePicture() + val title = stringRes(applicationContext, R.string.app_notification_chess_channel_name) + val content = stringRes(applicationContext, R.string.app_notification_chess_challenge_accepted, user) + val noteUri = + "notifications$ACCOUNT_QUERY_PARAM" + + account.signer.pubKey + .hexToByteArray() + .toNpub() + + notificationManager() + .sendChessNotification( + event.id, + content, + title, + event.createdAt, + userPicture, + noteUri, + applicationContext, + ) + } + } + + private suspend fun notify( + event: LiveChessMoveEvent, + account: Account, + ) { + Log.d(TAG, "New Chess Move to Notify") + if ( + event.createdAt > TimeUtils.fifteenMinutesAgo() && + event.pubKey != account.signer.pubKey + ) { + val author = LocalCache.getOrCreateUser(event.pubKey) + val user = author.toBestDisplayName() + val userPicture = author.profilePicture() + val title = stringRes(applicationContext, R.string.app_notification_chess_channel_name) + val content = stringRes(applicationContext, R.string.app_notification_chess_your_turn, user) + val noteUri = + "notifications$ACCOUNT_QUERY_PARAM" + + account.signer.pubKey + .hexToByteArray() + .toNpub() + + notificationManager() + .sendChessNotification( + event.id, + content, + title, + event.createdAt, + userPicture, + noteUri, + applicationContext, + ) + } + } + fun notificationManager(): NotificationManager = ContextCompat.getSystemService(applicationContext, NotificationManager::class.java) as NotificationManager 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 9c16a2e64..a418f223e 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 @@ -45,8 +45,10 @@ import kotlinx.coroutines.withContext object NotificationUtils { private var dmChannel: NotificationChannel? = null private var zapChannel: NotificationChannel? = null + private var chessChannel: NotificationChannel? = null 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 CHESS_GROUP_KEY = "com.vitorpamplona.amethyst.CHESS_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" @@ -56,6 +58,7 @@ object NotificationUtils { private const val DM_SUMMARY_ID = 0x10000 private const val ZAP_SUMMARY_ID = 0x20000 + private const val CHESS_SUMMARY_ID = 0x30000 fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel { if (dmChannel != null) return dmChannel!! @@ -99,6 +102,55 @@ object NotificationUtils { return zapChannel!! } + fun getOrCreateChessChannel(applicationContext: Context): NotificationChannel { + if (chessChannel != null) return chessChannel!! + + chessChannel = + NotificationChannel( + stringRes(applicationContext, R.string.app_notification_chess_channel_id), + stringRes(applicationContext, R.string.app_notification_chess_channel_name), + NotificationManager.IMPORTANCE_DEFAULT, + ).apply { + description = + stringRes(applicationContext, R.string.app_notification_chess_channel_description) + } + + val notificationManager: NotificationManager = + applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + notificationManager.createNotificationChannel(chessChannel!!) + + return chessChannel!! + } + + suspend fun NotificationManager.sendChessNotification( + id: String, + messageBody: String, + messageTitle: String, + time: Long, + pictureUrl: String?, + uri: String, + applicationContext: Context, + ) { + getOrCreateChessChannel(applicationContext) + val channelId = stringRes(applicationContext, R.string.app_notification_chess_channel_id) + + sendNotification( + id = id, + messageBody = messageBody, + messageTitle = messageTitle, + time = time, + pictureUrl = pictureUrl, + uri = uri, + channelId = channelId, + notificationGroupKey = CHESS_GROUP_KEY, + category = NotificationCompat.CATEGORY_SOCIAL, + summaryId = CHESS_SUMMARY_ID, + summaryText = stringRes(applicationContext, R.string.app_notification_chess_summary), + applicationContext = applicationContext, + ) + } + suspend fun NotificationManager.sendZapNotification( id: String, messageBody: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index 2e2f89eba..f03728166 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -58,6 +58,8 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEven import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent +import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent +import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent @@ -83,6 +85,8 @@ class NotificationFeedFilter( CalendarRSVPEvent.KIND, ClassifiedsEvent.KIND, LiveActivitiesEvent.KIND, + LiveChessGameAcceptEvent.KIND, + LiveChessMoveEvent.KIND, LongTextNoteEvent.KIND, NipTextEvent.KIND, VideoVerticalEvent.KIND, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index bf05100bf..cc337d2e0 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -738,6 +738,13 @@ New messages New zaps + ChessID + Chess + Notifies you about chess game events + %1$s accepted your chess challenge + %1$s moved — your turn + Chess updates + Notify: Join Conversation