diff --git a/modules/BlueskyNSE/NotificationService.swift b/modules/BlueskyNSE/NotificationService.swift index 384180d8b0..e842ac3421 100644 --- a/modules/BlueskyNSE/NotificationService.swift +++ b/modules/BlueskyNSE/NotificationService.swift @@ -3,53 +3,113 @@ import UIKit let APP_GROUP = "group.app.bsky" +enum NotificationType: String { + case Like = "like" + case Repost = "repost" + case Follow = "follow" + case Reply = "reply" + case Quote = "quote" + case ChatMessage = "chat-message" + case MarkReadGeneric = "mark-read-generic" + case MarkReadMessages = "mark-read-messages" +} + +enum BadgeType: String { + case Generic = "badgeCountGeneric" + case Messages = "badgeCountMessages" +} + +enum BadgeOperation { + case Increment + case Decrement +} + class NotificationService: UNNotificationServiceExtension { var prefs = UserDefaults(suiteName: APP_GROUP) override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { guard let bestAttempt = createCopy(request.content), - let reason = request.content.userInfo["reason"] as? String + let reasonString = request.content.userInfo["reason"] as? String, + let reason = NotificationType(rawValue: reasonString) else { contentHandler(request.content) return } - - if reason == "chat-message" { + + switch reason { + case NotificationType.Like, NotificationType.Repost, NotificationType.Follow, NotificationType.Reply, NotificationType.Quote: + mutateWithBadge(bestAttempt, badgeType: BadgeType.Generic, operation: BadgeOperation.Increment) + + case NotificationType.ChatMessage: mutateWithChatMessage(bestAttempt) - } else { - mutateWithBadge(bestAttempt) + + case NotificationType.MarkReadGeneric: + mutateWithBadge(bestAttempt, badgeType: BadgeType.Generic, operation: BadgeOperation.Decrement) + + case NotificationType.MarkReadMessages: + mutateWithBadge(bestAttempt, badgeType: BadgeType.Messages, operation: BadgeOperation.Decrement) } - + contentHandler(bestAttempt) } - + override func serviceExtensionTimeWillExpire() { // If for some reason the alloted time expires, we don't actually want to display a notification } - + func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? { return content.mutableCopy() as? UNMutableNotificationContent } - - func mutateWithBadge(_ content: UNMutableNotificationContent) { - var count = prefs?.integer(forKey: "badgeCount") ?? 0 - count += 1 - - // Set the new badge number for the notification, then store that value for using later - content.badge = NSNumber(value: count) - prefs?.setValue(count, forKey: "badgeCount") + + func getDecrementedBadgeCount(current: Int, decrementBy by: Int) -> Int { + let new = current - by + if new < 0 { + return 0 + } + return new } - + + func mutateWithBadge(_ content: UNMutableNotificationContent, badgeType type: BadgeType, operation: BadgeOperation) { + var genericCount = prefs?.integer(forKey: BadgeType.Generic.rawValue) ?? 0 + var messagesCount = prefs?.integer(forKey: BadgeType.Messages.rawValue) ?? 0 + + if type == BadgeType.Generic { + if operation == BadgeOperation.Decrement { + if let decrementBy = content.userInfo["decrementBy"] as? Int { + genericCount = getDecrementedBadgeCount(current: genericCount, decrementBy: decrementBy) + } else { + genericCount = 0 + } + } else { + genericCount += 1 + } + prefs?.setValue(genericCount, forKey: BadgeType.Generic.rawValue) + } else if type == BadgeType.Messages { + if operation == BadgeOperation.Decrement { + if let decrementBy = content.userInfo["decrementBy"] as? Int { + messagesCount = getDecrementedBadgeCount(current: messagesCount, decrementBy: decrementBy) + } else { + messagesCount = getDecrementedBadgeCount(current: messagesCount, decrementBy: 1) + } + } else { + genericCount += 1 + } + prefs?.setValue(messagesCount, forKey: BadgeType.Generic.rawValue) + } + + content.badge = NSNumber(value: genericCount + messagesCount) + } + func mutateWithChatMessage(_ content: UNMutableNotificationContent) { if self.prefs?.bool(forKey: "playSoundChat") == true { mutateWithDmSound(content) } } - + func mutateWithDefaultSound(_ content: UNMutableNotificationContent) { content.sound = UNNotificationSound.default } - + func mutateWithDmSound(_ content: UNMutableNotificationContent) { content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff")) } diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt index 0a8737b88f..198187ae82 100644 --- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt @@ -3,6 +3,17 @@ package expo.modules.backgroundnotificationhandler import android.content.Context import com.google.firebase.messaging.RemoteMessage +enum class NotificationType(val type: String) { + Like("like"), + Repost("repost"), + Follow("follow"), + Reply("reply"), + Quote("quote"), + ChatMessage("chat-message"), + MarkReadGeneric("mark-read-generic"), + MarkReadMessages("mark-read-messages"), +} + class BackgroundNotificationHandler( private val context: Context, private val notifInterface: BackgroundNotificationHandlerInterface @@ -13,8 +24,12 @@ class BackgroundNotificationHandler( return } - if (remoteMessage.data["reason"] == "chat-message") { + val type = NotificationType.valueOf(remoteMessage.data["reason"] ?: return) + + if (type == NotificationType.ChatMessage) { mutateWithChatMessage(remoteMessage) + } else if (type == NotificationType.MarkReadGeneric || type == NotificationType.MarkReadMessages) { + return } notifInterface.showMessage(remoteMessage) diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt index c876f899ac..1c6cdee0d0 100644 --- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt @@ -67,7 +67,7 @@ class ExpoBackgroundNotificationHandlerModule : Module() { NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings) } - AsyncFunction("setBadgeCountAsync") { _: Int -> + AsyncFunction("setBadgeCountAsync") { _: String, _: Int -> // This does nothing on Android } } diff --git a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift index 5f8c7fc3bb..64f4ec6a4e 100644 --- a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift +++ b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift @@ -114,8 +114,22 @@ public class ExpoBackgroundNotificationHandlerModule: Module { } } - AsyncFunction("setBadgeCountAsync") { (count: Int) in - userDefaults?.setValue(count, forKey: "badgeCount") + AsyncFunction("setBadgeCountAsync") { (type: BadgeCountType, count: Int) in + userDefaults?.setValue(count, forKey: type.toKeyName()) + } + } +} + +enum BadgeCountType : String, Enumerable { + case generic + case messages + + func toKeyName() -> String { + switch self { + case .generic: + return "badgeCountGeneric" + case .messages: + return "badgeCountMessages" } } } diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts index b74148db48..29c6c9a176 100644 --- a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts +++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts @@ -31,7 +31,10 @@ export type ExpoBackgroundNotificationHandlerModule = { forKey: keyof BackgroundNotificationHandlerPreferences, value: string[], ) => Promise - setBadgeCountAsync: (count: number) => Promise + setBadgeCountAsync: ( + type: 'generic' | 'messages', + count: number, + ) => Promise } // TODO there are more preferences in the native code, however they have not been added here yet. diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts index 893548e183..7d2f51c856 100644 --- a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts +++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts @@ -24,5 +24,5 @@ export const BackgroundNotificationHandler = { removeFromStringArrayAsync: async (_: string, __: string) => {}, addManyToStringArrayAsync: async (_: string, __: string[]) => {}, removeManyFromStringArrayAsync: async (_: string, __: string[]) => {}, - setBadgeCountAsync: async (_: number) => {}, + setBadgeCountAsync: async (_: 'generic' | 'messages', __: number) => {}, } as ExpoBackgroundNotificationHandlerModule diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts index 347062bebe..c39318ca93 100644 --- a/src/lib/hooks/useNotificationHandler.ts +++ b/src/lib/hooks/useNotificationHandler.ts @@ -26,6 +26,8 @@ type NotificationReason = | 'reply' | 'quote' | 'chat-message' + | 'mark-read-generic' + | 'mark-read-messages' type NotificationPayload = | { @@ -194,6 +196,20 @@ export function useNotificationsHandler() { shouldPlaySound: false, shouldSetBadge: false, } + } else if ( + payload.reason === 'mark-read-generic' || + payload.reason === 'mark-read-messages' + ) { + logger.debug( + `Notifications: ${payload.reason}`, + {}, + logger.DebugContext.notifications, + ) + return { + shouldShowAlert: false, + shouldPlaySound: false, + shouldSetBadge: false, + } } // Any notification other than a chat message should invalidate the unread page