2022-02-17 04:55:32 +01:00
|
|
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
|
|
|
|
import SignalUtilitiesKit
|
|
|
|
import UserNotifications
|
|
|
|
|
|
|
|
public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|
|
|
|
2022-04-19 08:36:40 +02:00
|
|
|
private var notifications: [String: UNNotificationRequest] = [:]
|
|
|
|
|
2022-02-17 04:55:32 +01:00
|
|
|
public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
|
2022-02-25 04:58:20 +01:00
|
|
|
guard !thread.isMuted else { return }
|
|
|
|
guard let threadID = thread.uniqueId else { return }
|
|
|
|
|
|
|
|
// If the thread is a message request and the user hasn't hidden message requests then we need
|
|
|
|
// to check if this is the only message request thread (group threads can't be message requests
|
|
|
|
// so just ignore those and if the user has hidden message requests then we want to show the
|
|
|
|
// notification regardless of how many message requests there are)
|
2022-03-21 03:55:51 +01:00
|
|
|
if !thread.isGroupThread() && thread.isMessageRequest(using: transaction) && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
2022-02-25 04:58:20 +01:00
|
|
|
let threads = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
|
|
|
let numMessageRequests = threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
|
|
|
|
|
|
|
// Allow this to show a notification if there are no message requests (ie. this is the first one)
|
2022-02-25 05:18:27 +01:00
|
|
|
guard numMessageRequests == 0 else { return }
|
2022-02-25 04:58:20 +01:00
|
|
|
}
|
2022-03-21 03:55:51 +01:00
|
|
|
else if thread.isMessageRequest(using: transaction) && CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
2022-02-25 04:58:20 +01:00
|
|
|
// If there are other interactions on this thread already then don't show the notification
|
2022-03-10 06:50:48 +01:00
|
|
|
if thread.numberOfInteractions(with: transaction) > 1 { return }
|
2022-02-25 04:58:20 +01:00
|
|
|
|
|
|
|
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = false
|
2022-02-17 04:55:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
let senderPublicKey = incomingMessage.authorId
|
2022-03-15 06:06:53 +01:00
|
|
|
let userPublicKey = GeneralUtilities.getUserPublicKey()
|
2022-02-17 04:55:32 +01:00
|
|
|
guard senderPublicKey != userPublicKey else {
|
|
|
|
// Ignore PNs for messages sent by the current user
|
|
|
|
// after handling the message. Otherwise the closed
|
|
|
|
// group self-send messages won't show.
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-19 08:36:40 +02:00
|
|
|
let identifier = incomingMessage.notificationIdentifier ?? UUID().uuidString
|
|
|
|
let isBackgroudPoll = identifier == threadID
|
|
|
|
|
2022-02-17 04:55:32 +01:00
|
|
|
let context = Contact.context(for: thread)
|
2022-03-21 03:55:51 +01:00
|
|
|
let senderName = Storage.shared.getContact(with: senderPublicKey, using: transaction)?.displayName(for: context) ?? senderPublicKey
|
2022-02-17 04:55:32 +01:00
|
|
|
|
|
|
|
var notificationTitle = senderName
|
|
|
|
if let group = thread as? TSGroupThread {
|
|
|
|
if group.isOnlyNotifyingForMentions && !incomingMessage.isUserMentioned {
|
|
|
|
// Ignore PNs if the group is set to only notify for mentions
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var groupName = thread.name(with: transaction)
|
|
|
|
if groupName.count < 1 {
|
|
|
|
groupName = MessageStrings.newGroupDefaultTitle
|
|
|
|
}
|
2022-04-19 08:36:40 +02:00
|
|
|
notificationTitle = isBackgroudPoll ? groupName : String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, groupName)
|
2022-02-17 04:55:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
let snippet = incomingMessage.previewText(with: transaction).filterForDisplay?.replacingMentions(for: threadID, using: transaction)
|
|
|
|
?? "APN_Message".localized()
|
|
|
|
|
|
|
|
var userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ]
|
|
|
|
userInfo[NotificationServiceExtension.threadIdKey] = threadID
|
|
|
|
|
|
|
|
let notificationContent = UNMutableNotificationContent()
|
|
|
|
notificationContent.userInfo = userInfo
|
|
|
|
notificationContent.sound = OWSSounds.notificationSound(for: thread).notificationSound(isQuiet: false)
|
|
|
|
|
2022-02-25 04:58:20 +01:00
|
|
|
// Badge Number
|
|
|
|
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
|
|
|
|
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
|
|
|
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
|
|
|
|
|
|
|
// Title & body
|
2022-02-17 04:55:32 +01:00
|
|
|
let notificationsPreference = Environment.shared.preferences!.notificationPreviewType()
|
|
|
|
switch notificationsPreference {
|
|
|
|
case .namePreview:
|
|
|
|
notificationContent.title = notificationTitle
|
|
|
|
notificationContent.body = snippet
|
|
|
|
case .nameNoPreview:
|
|
|
|
notificationContent.title = notificationTitle
|
|
|
|
notificationContent.body = NotificationStrings.incomingMessageBody
|
|
|
|
case .noNameNoPreview:
|
|
|
|
notificationContent.title = "Session"
|
|
|
|
notificationContent.body = NotificationStrings.incomingMessageBody
|
|
|
|
default: break
|
|
|
|
}
|
|
|
|
|
2022-02-25 04:58:20 +01:00
|
|
|
// If it's a message request then overwrite the body to be something generic (only show a notification
|
|
|
|
// when receiving a new message request if there aren't any others or the user had hidden them)
|
2022-03-21 03:55:51 +01:00
|
|
|
if thread.isMessageRequest(using: transaction) {
|
2022-03-10 00:42:08 +01:00
|
|
|
notificationContent.title = "Session"
|
2022-02-25 04:58:20 +01:00
|
|
|
notificationContent.body = "MESSAGE_REQUESTS_NOTIFICATION".localized()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add request
|
2022-04-19 08:36:40 +02:00
|
|
|
let trigger: UNNotificationTrigger?
|
|
|
|
if isBackgroudPoll {
|
|
|
|
trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
|
|
|
|
let numberOfNotifications: Int
|
|
|
|
if let lastRequest = notifications[identifier], let counter = lastRequest.content.userInfo[NotificationServiceExtension.threadNotificationCounter] as? Int {
|
|
|
|
numberOfNotifications = counter + 1
|
|
|
|
notificationContent.body = String(format: NotificationStrings.incomingCollapsedMessagesBody, "\(numberOfNotifications)")
|
|
|
|
} else {
|
|
|
|
numberOfNotifications = 1
|
|
|
|
}
|
|
|
|
notificationContent.userInfo[NotificationServiceExtension.threadNotificationCounter] = numberOfNotifications
|
|
|
|
} else {
|
|
|
|
trigger = nil
|
|
|
|
}
|
2022-06-14 09:10:03 +02:00
|
|
|
|
|
|
|
addNotifcationRequest(identifier: identifier, notificationContent: notificationContent, trigger: trigger)
|
2022-04-07 07:10:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public func notifyUser(forIncomingCall callInfoMessage: TSInfoMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
|
|
|
|
guard !thread.isMuted else { return }
|
|
|
|
guard !thread.isGroupThread() else { return } // Calls shouldn't happen in groups
|
|
|
|
guard let threadID = thread.uniqueId else { return }
|
|
|
|
guard [ .missed, .permissionDenied ].contains(callInfoMessage.callState) else { return } // Only notify missed call
|
|
|
|
|
|
|
|
var userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ]
|
|
|
|
userInfo[NotificationServiceExtension.threadIdKey] = threadID
|
|
|
|
|
|
|
|
let notificationContent = UNMutableNotificationContent()
|
|
|
|
notificationContent.userInfo = userInfo
|
|
|
|
notificationContent.sound = OWSSounds.notificationSound(for: thread).notificationSound(isQuiet: false)
|
|
|
|
|
|
|
|
// Badge Number
|
|
|
|
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
|
|
|
|
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
|
|
|
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
|
|
|
|
|
|
|
notificationContent.title = callInfoMessage.previewText(with: transaction)
|
|
|
|
notificationContent.body = ""
|
|
|
|
if callInfoMessage.callState == .permissionDenied {
|
|
|
|
notificationContent.body = String(format: "modal_call_missed_tips_explanation".localized(), thread.name(with: transaction))
|
|
|
|
}
|
|
|
|
|
2022-06-14 09:10:03 +02:00
|
|
|
addNotifcationRequest(identifier: UUID().uuidString, notificationContent: notificationContent, trigger: nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
public func notifyUser(forReaction reactMessage: ReactMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
|
|
|
|
|
2022-02-17 04:55:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public func cancelNotification(_ identifier: String) {
|
|
|
|
let notificationCenter = UNUserNotificationCenter.current()
|
|
|
|
notificationCenter.removePendingNotificationRequests(withIdentifiers: [ identifier ])
|
|
|
|
notificationCenter.removeDeliveredNotifications(withIdentifiers: [ identifier ])
|
|
|
|
}
|
|
|
|
|
|
|
|
public func clearAllNotifications() {
|
|
|
|
let notificationCenter = UNUserNotificationCenter.current()
|
|
|
|
notificationCenter.removeAllPendingNotificationRequests()
|
|
|
|
notificationCenter.removeAllDeliveredNotifications()
|
|
|
|
}
|
2022-06-14 09:10:03 +02:00
|
|
|
|
|
|
|
private func addNotifcationRequest(identifier: String, notificationContent: UNNotificationContent, trigger: UNNotificationTrigger?) {
|
|
|
|
let request = UNNotificationRequest(identifier: identifier, content: notificationContent, trigger: nil)
|
|
|
|
SNLog("Add remote notification request: \(notificationContent.body)")
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
|
|
UNUserNotificationCenter.current().add(request) { error in
|
|
|
|
if let error = error {
|
|
|
|
SNLog("Failed to add notification request due to error:\(error)")
|
|
|
|
}
|
|
|
|
semaphore.signal()
|
|
|
|
}
|
|
|
|
semaphore.wait()
|
|
|
|
SNLog("Finish adding remote notification request")
|
|
|
|
}
|
2022-02-17 04:55:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private extension String {
|
|
|
|
|
|
|
|
func replacingMentions(for threadID: String, using transaction: YapDatabaseReadTransaction) -> String {
|
|
|
|
var result = self
|
|
|
|
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]{66}", options: [])
|
|
|
|
var mentions: [(range: NSRange, publicKey: String)] = []
|
|
|
|
var m0 = regex.firstMatch(in: result, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: result.utf16.count))
|
|
|
|
while let m1 = m0 {
|
|
|
|
let publicKey = String((result as NSString).substring(with: m1.range).dropFirst()) // Drop the @
|
|
|
|
var matchEnd = m1.range.location + m1.range.length
|
|
|
|
let displayName = Storage.shared.getContact(with: publicKey, using: transaction)?.displayName(for: .regular)
|
|
|
|
if let displayName = displayName {
|
|
|
|
result = (result as NSString).replacingCharacters(in: m1.range, with: "@\(displayName)")
|
|
|
|
mentions.append((range: NSRange(location: m1.range.location, length: displayName.utf16.count + 1), publicKey: publicKey)) // + 1 to include the @
|
|
|
|
matchEnd = m1.range.location + displayName.utf16.count
|
|
|
|
}
|
|
|
|
m0 = regex.firstMatch(in: result, options: .withoutAnchoringBounds, range: NSRange(location: matchEnd, length: result.utf16.count - matchEnd))
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|