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 <noreply@anthropic.com>
This commit is contained in:
davotoula
2026-03-23 16:32:56 +01:00
parent 292c6de08a
commit 478c4cc356
4 changed files with 134 additions and 0 deletions
@@ -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
@@ -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,
@@ -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,
+7
View File
@@ -738,6 +738,13 @@
<string name="app_notification_dms_summary">New messages</string>
<string name="app_notification_zaps_summary">New zaps</string>
<string name="app_notification_chess_channel_id" translatable="false">ChessID</string>
<string name="app_notification_chess_channel_name">Chess</string>
<string name="app_notification_chess_channel_description">Notifies you about chess game events</string>
<string name="app_notification_chess_challenge_accepted">%1$s accepted your chess challenge</string>
<string name="app_notification_chess_your_turn">%1$s moved — your turn</string>
<string name="app_notification_chess_summary">Chess updates</string>
<string name="reply_notify">Notify: </string>
<string name="channel_list_join_conversation">Join Conversation</string>