NotificationManager.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import Foundation
  2. import UserNotifications
  3. import DcCore
  4. import UIKit
  5. public class NotificationManager {
  6. var incomingMsgObserver: NSObjectProtocol?
  7. var msgsNoticedObserver: NSObjectProtocol?
  8. private var dcContext: DcContext
  9. init(dcContext: DcContext) {
  10. self.dcContext = dcContext
  11. initIncomingMsgsObserver()
  12. initMsgsNoticedObserver()
  13. }
  14. public static func updateApplicationIconBadge(dcContext: DcContext, reset: Bool) {
  15. var unreadMessages = 0
  16. if !reset {
  17. unreadMessages = dcContext.getFreshMessages().count
  18. }
  19. DispatchQueue.main.async {
  20. UIApplication.shared.applicationIconBadgeNumber = unreadMessages
  21. }
  22. }
  23. public static func removeAllNotifications() {
  24. let nc = UNUserNotificationCenter.current()
  25. nc.removeAllDeliveredNotifications()
  26. nc.removeAllPendingNotificationRequests()
  27. }
  28. public static func removeNotificationsForChat(dcContext: DcContext, chatId: Int) {
  29. DispatchQueue.global(qos: .background).async {
  30. NotificationManager.removePendingNotificationsFor(dcContext: dcContext, chatId: chatId)
  31. NotificationManager.removeDeliveredNotificationsFor(dcContext: dcContext, chatId: chatId)
  32. NotificationManager.updateApplicationIconBadge(dcContext: dcContext, reset: false)
  33. }
  34. }
  35. private func initIncomingMsgsObserver() {
  36. incomingMsgObserver = NotificationCenter.default.addObserver(
  37. forName: dcNotificationIncoming,
  38. object: nil, queue: OperationQueue.main
  39. ) { notification in
  40. // make sure to balance each call to `beginBackgroundTask` with `endBackgroundTask`
  41. let backgroundTask = UIApplication.shared.beginBackgroundTask {
  42. // we cannot easily stop the task,
  43. // however, this handler should not be called as adding the notification should not take 30 seconds.
  44. logger.info("notification background task will end soon")
  45. }
  46. DispatchQueue.global(qos: .background).async { [weak self] in
  47. guard let self = self else { return }
  48. if let ui = notification.userInfo,
  49. let chatId = ui["chat_id"] as? Int,
  50. let messageId = ui["message_id"] as? Int,
  51. !UserDefaults.standard.bool(forKey: "notifications_disabled") {
  52. NotificationManager.updateApplicationIconBadge(dcContext: self.dcContext, reset: false)
  53. let chat = self.dcContext.getChat(chatId: chatId)
  54. if !chat.isMuted {
  55. let msg = self.dcContext.getMessage(id: messageId)
  56. let fromContact = self.dcContext.getContact(id: msg.fromContactId)
  57. let accountEmail = self.dcContext.getContact(id: Int(DC_CONTACT_ID_SELF)).email
  58. let content = UNMutableNotificationContent()
  59. content.title = chat.isGroup ? chat.name : msg.getSenderName(fromContact)
  60. content.body = msg.summary(chars: 80) ?? ""
  61. content.subtitle = chat.isGroup ? msg.getSenderName(fromContact) : ""
  62. content.userInfo = ui
  63. content.sound = .default
  64. if msg.type == DC_MSG_IMAGE || msg.type == DC_MSG_GIF,
  65. let url = msg.fileURL {
  66. do {
  67. // make a copy of the file first since UNNotificationAttachment will move attached files into the attachment data store
  68. // so that they can be accessed by all of the appropriate processes
  69. let tempUrl = url.deletingLastPathComponent()
  70. .appendingPathComponent("notification_tmp")
  71. .appendingPathExtension(url.pathExtension)
  72. try FileManager.default.copyItem(at: url, to: tempUrl)
  73. if let attachment = try? UNNotificationAttachment(identifier: Constants.notificationIdentifier, url: tempUrl, options: nil) {
  74. content.attachments = [attachment]
  75. }
  76. } catch let error {
  77. logger.error("Failed to copy file \(url) for notification preview generation: \(error)")
  78. }
  79. }
  80. let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
  81. if #available(iOS 12.0, *) {
  82. content.threadIdentifier = "\(accountEmail)\(chatId)"
  83. }
  84. let request = UNNotificationRequest(identifier: "\(Constants.notificationIdentifier).\(accountEmail).\(chatId).\(msg.messageId)",
  85. content: content,
  86. trigger: trigger)
  87. UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
  88. logger.info("notifications: added \(content.title) \(content.body) \(content.userInfo)")
  89. }
  90. }
  91. // this line should always be reached
  92. // and balances the call to `beginBackgroundTask` above.
  93. UIApplication.shared.endBackgroundTask(backgroundTask)
  94. }
  95. }
  96. }
  97. private func initMsgsNoticedObserver() {
  98. msgsNoticedObserver = NotificationCenter.default.addObserver(
  99. forName: dcMsgsNoticed,
  100. object: nil, queue: OperationQueue.main
  101. ) { [weak self] notification in
  102. guard let self = self else { return }
  103. if !UserDefaults.standard.bool(forKey: "notifications_disabled"),
  104. let ui = notification.userInfo,
  105. let chatId = ui["chat_id"] as? Int {
  106. NotificationManager.removeNotificationsForChat(dcContext: self.dcContext, chatId: chatId)
  107. }
  108. }
  109. }
  110. private static func removeDeliveredNotificationsFor(dcContext: DcContext, chatId: Int) {
  111. var identifiers = [String]()
  112. let nc = UNUserNotificationCenter.current()
  113. nc.getDeliveredNotifications { notifications in
  114. let accountEmail = dcContext.getContact(id: Int(DC_CONTACT_ID_SELF)).email
  115. for notification in notifications {
  116. if !notification.request.identifier.containsExact(subSequence: "\(Constants.notificationIdentifier).\(accountEmail).\(chatId)").isEmpty {
  117. identifiers.append(notification.request.identifier)
  118. }
  119. }
  120. nc.removeDeliveredNotifications(withIdentifiers: identifiers)
  121. }
  122. }
  123. private static func removePendingNotificationsFor(dcContext: DcContext, chatId: Int) {
  124. var identifiers = [String]()
  125. let nc = UNUserNotificationCenter.current()
  126. nc.getPendingNotificationRequests { notificationRequests in
  127. let accountEmail = dcContext.getContact(id: Int(DC_CONTACT_ID_SELF)).email
  128. for request in notificationRequests {
  129. if !request.identifier.containsExact(subSequence: "\(Constants.notificationIdentifier).\(accountEmail).\(chatId)").isEmpty {
  130. identifiers.append(request.identifier)
  131. }
  132. }
  133. nc.removePendingNotificationRequests(withIdentifiers: identifiers)
  134. }
  135. }
  136. deinit {
  137. NotificationCenter.default.removeObserver(self)
  138. }
  139. }