my fork of the bluesky client
1import UserNotifications
2import UIKit
3
4let APP_GROUP = "group.app.bsky"
5typealias 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.
27
28class NotificationService: UNNotificationServiceExtension {
29 private var contentHandler: ContentHandler?
30 private var bestAttempt: UNMutableNotificationContent?
31
32 override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
33 self.contentHandler = contentHandler
34
35 guard let bestAttempt = NSEUtil.createCopy(request.content),
36 let reason = request.content.userInfo["reason"] as? String
37 else {
38 contentHandler(request.content)
39 return
40 }
41
42 self.bestAttempt = bestAttempt
43 if reason == "chat-message" {
44 mutateWithChatMessage(bestAttempt)
45 } else {
46 mutateWithBadge(bestAttempt)
47 }
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
53 contentHandler(bestAttempt)
54 }
55
56 override func serviceExtensionTimeWillExpire() {
57 guard let contentHandler = self.contentHandler,
58 let bestAttempt = self.bestAttempt else {
59 return
60 }
61 contentHandler(bestAttempt)
62 }
63
64 // MARK: Mutations
65
66 func mutateWithBadge(_ content: UNMutableNotificationContent) {
67 NSEUtil.shared.prefsQueue.sync {
68 var count = NSEUtil.shared.prefs?.integer(forKey: "badgeCount") ?? 0
69 count += 1
70
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 }
75 }
76
77 func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
78 if NSEUtil.shared.prefs?.bool(forKey: "playSoundChat") == true {
79 mutateWithDmSound(content)
80 }
81 }
82
83 func mutateWithDefaultSound(_ content: UNMutableNotificationContent) {
84 content.sound = UNNotificationSound.default
85 }
86
87 func mutateWithDmSound(_ content: UNMutableNotificationContent) {
88 content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
89 }
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
96private 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}