my fork of the bluesky client

Improvements to NSE (#4992)

authored by hailey.at and committed by

GitHub 53b095ad e93cbbd5

+61 -12
+61 -12
modules/BlueskyNSE/NotificationService.swift
··· 2 2 import UIKit 3 3 4 4 let APP_GROUP = "group.app.bsky" 5 + typealias ContentHandler = (UNNotificationContent) -> Void 6 + 7 + // This extension allows us to do some processing of the received notification 8 + // data before displaying the notification to the user. In our use case, there 9 + // are a few particular things that we want to do: 10 + // 11 + // - Determine whether we should play a sound for the notification 12 + // - Download and display any images for the notification 13 + // - Update the badge count accordingly 14 + // 15 + // The extension may or may not create a new process to handle a notification. 16 + // It is also possible that multiple notifications will be processed by the 17 + // same instance of `NotificationService`, though these will happen in 18 + // parallel. 19 + // 20 + // Because multiple instances of `NotificationService` may exist, we should 21 + // be careful in accessing preferences that will be mutated _by the 22 + // extension itself_. For example, we should not worry about `playChatSound` 23 + // changing, since we never mutate that value within the extension itself. 24 + // However, since we mutate `badgeCount` frequently, we should ensure that 25 + // these updates always run sync with each other and that the have access 26 + // to the most recent values. 5 27 6 28 class NotificationService: UNNotificationServiceExtension { 7 - var prefs = UserDefaults(suiteName: APP_GROUP) 29 + private var contentHandler: ContentHandler? 30 + private var bestAttempt: UNMutableNotificationContent? 8 31 9 32 override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { 10 - guard let bestAttempt = createCopy(request.content), 33 + self.contentHandler = contentHandler 34 + 35 + guard let bestAttempt = NSEUtil.createCopy(request.content), 11 36 let reason = request.content.userInfo["reason"] as? String 12 37 else { 13 38 contentHandler(request.content) 14 39 return 15 40 } 16 41 42 + self.bestAttempt = bestAttempt 17 43 if reason == "chat-message" { 18 44 mutateWithChatMessage(bestAttempt) 19 45 } else { 20 46 mutateWithBadge(bestAttempt) 21 47 } 22 48 49 + // Any image downloading (or other network tasks) should be handled at the end 50 + // of this block. Otherwise, if there is a timeout and serviceExtensionTimeWillExpire 51 + // gets called, we might not have all the needed mutations completed in time. 52 + 23 53 contentHandler(bestAttempt) 24 54 } 25 55 26 56 override func serviceExtensionTimeWillExpire() { 27 - // If for some reason the alloted time expires, we don't actually want to display a notification 57 + guard let contentHandler = self.contentHandler, 58 + let bestAttempt = self.bestAttempt else { 59 + return 60 + } 61 + contentHandler(bestAttempt) 28 62 } 29 63 30 - func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? { 31 - return content.mutableCopy() as? UNMutableNotificationContent 32 - } 64 + // MARK: Mutations 33 65 34 66 func mutateWithBadge(_ content: UNMutableNotificationContent) { 35 - var count = prefs?.integer(forKey: "badgeCount") ?? 0 36 - count += 1 67 + NSEUtil.shared.prefsQueue.sync { 68 + var count = NSEUtil.shared.prefs?.integer(forKey: "badgeCount") ?? 0 69 + count += 1 37 70 38 - // Set the new badge number for the notification, then store that value for using later 39 - content.badge = NSNumber(value: count) 40 - prefs?.setValue(count, forKey: "badgeCount") 71 + // Set the new badge number for the notification, then store that value for using later 72 + content.badge = NSNumber(value: count) 73 + NSEUtil.shared.prefs?.setValue(count, forKey: "badgeCount") 74 + } 41 75 } 42 76 43 77 func mutateWithChatMessage(_ content: UNMutableNotificationContent) { 44 - if self.prefs?.bool(forKey: "playSoundChat") == true { 78 + if NSEUtil.shared.prefs?.bool(forKey: "playSoundChat") == true { 45 79 mutateWithDmSound(content) 46 80 } 47 81 } ··· 54 88 content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff")) 55 89 } 56 90 } 91 + 92 + // NSEUtil's purpose is to create a shared instance of `UserDefaults` across 93 + // `NotificationService` instances. It also includes a queue so that we can process 94 + // updates to `UserDefaults` in parallel. 95 + 96 + private class NSEUtil { 97 + static let shared = NSEUtil() 98 + 99 + var prefs = UserDefaults(suiteName: APP_GROUP) 100 + var prefsQueue = DispatchQueue(label: "NSEPrefsQueue") 101 + 102 + static func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? { 103 + return content.mutableCopy() as? UNMutableNotificationContent 104 + } 105 + }