diff --git a/modules/BlueskyNSE/NotificationService.swift b/modules/BlueskyNSE/NotificationService.swift index 481402890f..0f9c0191b6 100644 --- a/modules/BlueskyNSE/NotificationService.swift +++ b/modules/BlueskyNSE/NotificationService.swift @@ -4,6 +4,30 @@ import UIKit let APP_GROUP = "group.app.bsky" typealias ContentHandler = (UNNotificationContent) -> Void +enum NotificationType: String, CaseIterable { + case like + case repost + case follow + case reply + case quote + case chatMessage = "chat-message" + case markReadGeneric = "mark-read-generic" + case markReadMessages = "mark-read-messages" + case starterPackJoined = "starterpack-joined" +} + +enum BadgeType: String, CaseIterable { + case generic + case messages +} + +enum BadgeOperation { + case increment + case decrement +} + +let INCREMENTED_FOR_KEY = "incremented-for-convos" + // This extension allows us to do some processing of the received notification // data before displaying the notification to the user. In our use case, there // are a few particular things that we want to do: @@ -30,27 +54,32 @@ class NotificationService: UNNotificationServiceExtension { private var bestAttempt: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { - self.contentHandler = contentHandler guard let bestAttempt = NSEUtil.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 } - + + self.contentHandler = contentHandler self.bestAttempt = bestAttempt - if reason == "chat-message" { - mutateWithChatMessage(bestAttempt) - } else { - mutateWithBadge(bestAttempt) + + NSEUtil.shared.prefsQueue.sync { + switch reason { + case .like, .repost, .follow, .reply, .quote, .starterPackJoined: + NSEUtil.mutateWithBadge(bestAttempt, badgeType: .generic, operation: .increment) + case .chatMessage: + NSEUtil.mutateWithChatMessage(bestAttempt) + NSEUtil.mutateWithBadge(bestAttempt, badgeType: .messages, operation: .increment) + case .markReadGeneric: + NSEUtil.mutateWithBadge(bestAttempt, badgeType: .generic, operation: .decrement) + case .markReadMessages: + NSEUtil.mutateWithBadge(bestAttempt, badgeType: .messages, operation: .decrement) + } + contentHandler(bestAttempt) } - - // Any image downloading (or other network tasks) should be handled at the end - // of this block. Otherwise, if there is a timeout and serviceExtensionTimeWillExpire - // gets called, we might not have all the needed mutations completed in time. - - contentHandler(bestAttempt) } override func serviceExtensionTimeWillExpire() { @@ -60,38 +89,14 @@ class NotificationService: UNNotificationServiceExtension { } contentHandler(bestAttempt) } - - // MARK: Mutations - - func mutateWithBadge(_ content: UNMutableNotificationContent) { - NSEUtil.shared.prefsQueue.sync { - var count = NSEUtil.shared.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) - NSEUtil.shared.prefs?.setValue(count, forKey: "badgeCount") - } - } - - func mutateWithChatMessage(_ content: UNMutableNotificationContent) { - if NSEUtil.shared.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")) - } } // NSEUtil's purpose is to create a shared instance of `UserDefaults` across // `NotificationService` instances. It also includes a queue so that we can process // updates to `UserDefaults` in parallel. +// +// Any time that you increment or decrement counts for notifications, you should use +// the prefsQueue so that things remain in sync. private class NSEUtil { static let shared = NSEUtil() @@ -99,7 +104,91 @@ private class NSEUtil { var prefs = UserDefaults(suiteName: APP_GROUP) var prefsQueue = DispatchQueue(label: "NSEPrefsQueue") + // MARK: - Utils + static func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? { return content.mutableCopy() as? UNMutableNotificationContent } + + static func getDecrementedBadgeCount(current: Int, decrementBy by: Int) -> Int { + let new = current - by + if new < 0 { + return 0 + } + return new + } + + // MARK: - Mutations + + static func mutateWithBadge(_ content: UNMutableNotificationContent, + badgeType type: BadgeType, + operation: BadgeOperation) { + var genericCount = Self.shared.prefs?.integer(forKey: BadgeType.generic.rawValue) ?? 0 + var messagesCount = Self.shared.prefs?.integer(forKey: BadgeType.messages.rawValue) ?? 0 + + if type == .generic { + if operation == .decrement { + genericCount = 0 + } else { + genericCount += 1 + } + Self.shared.prefs?.setValue(genericCount, forKey: BadgeType.generic.rawValue) + // TEMPORARY - since we have not implemented message count clearing on the server, we'll clear + // those here as well. + Self.shared.prefs?.setValue(messagesCount, forKey: BadgeType.messages.rawValue) + } else if type == .messages { + // Not yet implemented, but here's the logic + if operation == .decrement, + Self.shouldDecrementForConvo(content) { + messagesCount = Self.getDecrementedBadgeCount(current: messagesCount, decrementBy: 1) + } else if operation == .increment, + shouldIncrementForConvo(content) { + messagesCount += 1 + } + } + } + + static func mutateWithChatMessage(_ content: UNMutableNotificationContent) { + if Self.shared.prefs?.bool(forKey: "playSoundChat") == true { + Self.mutateWithDmSound(content) + } + } + + static func mutateWithDefaultSound(_ content: UNMutableNotificationContent) { + content.sound = UNNotificationSound.default + } + + static func mutateWithDmSound(_ content: UNMutableNotificationContent) { + content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff")) + } + + static func shouldIncrementForConvo(_ content: UNMutableNotificationContent) -> Bool { + guard let convoId = content.userInfo["convoId"] as? String, + var dict = Self.shared.prefs?.dictionary(forKey: INCREMENTED_FOR_KEY) as? [String: Bool] else { + return false + } + + if dict["convoId"] == true { + return false + } + + dict[convoId] = true + Self.shared.prefs?.set(dict, forKey: INCREMENTED_FOR_KEY) + return true + } + + static func shouldDecrementForConvo(_ content: UNMutableNotificationContent) -> Bool { + guard let convoId = content.userInfo["convoId"] as? String, + var dict = Self.shared.prefs?.dictionary(forKey: INCREMENTED_FOR_KEY) as? [String: Bool] else { + return false + } + + if dict["convoId"] != true { + return false + } + + dict.removeValue(forKey: convoId) + Self.shared.prefs?.set(dict, forKey: INCREMENTED_FOR_KEY) + return true + } }