2022-02-17 04:55:32 +01:00
|
|
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
|
2022-04-06 07:43:26 +02:00
|
|
|
import Foundation
|
2022-04-21 08:42:35 +02:00
|
|
|
import GRDB
|
2022-02-17 04:55:32 +01:00
|
|
|
import UserNotifications
|
2022-04-06 07:43:26 +02:00
|
|
|
import SignalUtilitiesKit
|
|
|
|
import SessionMessagingKit
|
2022-02-17 04:55:32 +01:00
|
|
|
|
|
|
|
public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
2022-04-19 08:36:40 +02:00
|
|
|
private var notifications: [String: UNNotificationRequest] = [:]
|
2022-05-08 14:01:39 +02:00
|
|
|
|
2022-08-19 08:58:47 +02:00
|
|
|
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) {
|
2022-06-21 09:43:27 +02:00
|
|
|
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
|
2022-02-25 04:58:20 +01:00
|
|
|
|
2022-07-08 09:53:48 +02:00
|
|
|
// Ensure we should be showing a notification for the thread
|
|
|
|
guard thread.shouldShowNotification(db, for: interaction, isMessageRequest: isMessageRequest) else {
|
2022-02-17 04:55:32 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-07-08 09:53:48 +02:00
|
|
|
let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
|
2022-08-19 08:58:47 +02:00
|
|
|
let groupName: String = SessionThread.displayName(
|
|
|
|
threadId: thread.id,
|
|
|
|
variant: thread.variant,
|
|
|
|
closedGroupName: (try? thread.closedGroup.fetchOne(db))?.name,
|
|
|
|
openGroupName: (try? thread.openGroup.fetchOne(db))?.name
|
|
|
|
)
|
2022-06-08 06:29:51 +02:00
|
|
|
var notificationTitle: String = senderName
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2023-02-20 02:56:48 +01:00
|
|
|
if thread.variant == .legacyGroup || thread.variant == .group || thread.variant == .community {
|
2022-06-16 05:14:56 +02:00
|
|
|
if thread.onlyNotifyForMentions && !interaction.hasMention {
|
2022-02-17 04:55:32 +01:00
|
|
|
// Ignore PNs if the group is set to only notify for mentions
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-19 08:58:47 +02:00
|
|
|
notificationTitle = String(
|
|
|
|
format: NotificationStrings.incomingGroupMessageTitleFormat,
|
|
|
|
senderName,
|
|
|
|
groupName
|
|
|
|
)
|
2022-02-17 04:55:32 +01:00
|
|
|
}
|
|
|
|
|
2022-06-08 06:29:51 +02:00
|
|
|
let snippet: String = (interaction.previewText(db)
|
2022-04-21 08:42:35 +02:00
|
|
|
.filterForDisplay?
|
2022-06-08 06:29:51 +02:00
|
|
|
.replacingMentions(for: thread.id))
|
|
|
|
.defaulting(to: "APN_Message".localized())
|
2022-04-21 08:42:35 +02:00
|
|
|
|
|
|
|
var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ]
|
|
|
|
userInfo[NotificationServiceExtension.threadIdKey] = thread.id
|
2022-02-17 04:55:32 +01:00
|
|
|
|
|
|
|
let notificationContent = UNMutableNotificationContent()
|
|
|
|
notificationContent.userInfo = userInfo
|
2022-04-22 10:47:11 +02:00
|
|
|
notificationContent.sound = thread.notificationSound
|
|
|
|
.defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound)
|
2022-04-21 08:42:35 +02:00
|
|
|
.notificationSound(isQuiet: false)
|
2022-02-17 04:55:32 +01:00
|
|
|
|
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-05-10 09:42:15 +02:00
|
|
|
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
2022-08-24 09:33:10 +02:00
|
|
|
.defaulting(to: .defaultPreviewType)
|
2022-05-10 09:42:15 +02:00
|
|
|
|
|
|
|
switch previewType {
|
|
|
|
case .nameAndPreview:
|
|
|
|
notificationContent.title = notificationTitle
|
|
|
|
notificationContent.body = snippet
|
|
|
|
|
|
|
|
case .nameNoPreview:
|
|
|
|
notificationContent.title = notificationTitle
|
|
|
|
notificationContent.body = NotificationStrings.incomingMessageBody
|
|
|
|
|
|
|
|
case .noNameNoPreview:
|
|
|
|
notificationContent.title = "Session"
|
|
|
|
notificationContent.body = NotificationStrings.incomingMessageBody
|
2022-02-17 04:55:32 +01:00
|
|
|
}
|
|
|
|
|
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-04-21 08:42:35 +02:00
|
|
|
if isMessageRequest {
|
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()
|
|
|
|
}
|
|
|
|
|
2022-08-19 08:58:47 +02:00
|
|
|
// Add request (try to group notifications for interactions from open groups)
|
|
|
|
let identifier: String = interaction.notificationIdentifier(
|
2023-02-20 02:56:48 +01:00
|
|
|
shouldGroupMessagesForThread: (thread.variant == .community)
|
2022-08-19 08:58:47 +02:00
|
|
|
)
|
2022-06-08 06:29:51 +02:00
|
|
|
var trigger: UNNotificationTrigger?
|
|
|
|
|
2023-02-20 02:56:48 +01:00
|
|
|
if thread.variant == .community {
|
2022-08-19 08:58:47 +02:00
|
|
|
trigger = UNTimeIntervalNotificationTrigger(
|
|
|
|
timeInterval: Notifications.delayForGroupedNotifications,
|
|
|
|
repeats: false
|
|
|
|
)
|
2022-06-08 06:29:51 +02:00
|
|
|
|
2022-08-19 08:58:47 +02:00
|
|
|
let numberExistingNotifications: Int? = notifications[identifier]?
|
2022-06-08 06:29:51 +02:00
|
|
|
.content
|
|
|
|
.userInfo[NotificationServiceExtension.threadNotificationCounter]
|
2022-08-19 08:58:47 +02:00
|
|
|
.asType(Int.self)
|
|
|
|
var numberOfNotifications: Int = (numberExistingNotifications ?? 1)
|
2022-06-08 06:29:51 +02:00
|
|
|
|
2022-08-19 08:58:47 +02:00
|
|
|
if numberExistingNotifications != nil {
|
2022-06-08 06:29:51 +02:00
|
|
|
numberOfNotifications += 1 // Add one for the current notification
|
2022-08-19 08:58:47 +02:00
|
|
|
|
|
|
|
notificationContent.title = (previewType == .noNameNoPreview ?
|
|
|
|
notificationContent.title :
|
|
|
|
groupName
|
|
|
|
)
|
2022-06-08 06:29:51 +02:00
|
|
|
notificationContent.body = String(
|
|
|
|
format: NotificationStrings.incomingCollapsedMessagesBody,
|
|
|
|
"\(numberOfNotifications)"
|
|
|
|
)
|
2022-04-19 08:36:40 +02:00
|
|
|
}
|
2022-06-08 06:29:51 +02:00
|
|
|
|
2022-04-19 08:36:40 +02:00
|
|
|
notificationContent.userInfo[NotificationServiceExtension.threadNotificationCounter] = numberOfNotifications
|
|
|
|
}
|
2022-06-08 06:29:51 +02:00
|
|
|
|
2022-08-22 03:30:06 +02:00
|
|
|
addNotifcationRequest(
|
2022-08-19 08:58:47 +02:00
|
|
|
identifier: identifier,
|
2022-08-22 03:30:06 +02:00
|
|
|
notificationContent: notificationContent,
|
2022-08-19 08:58:47 +02:00
|
|
|
trigger: trigger
|
|
|
|
)
|
2022-04-07 07:10:38 +02:00
|
|
|
}
|
|
|
|
|
2022-06-08 06:29:51 +02:00
|
|
|
public func notifyUser(_ db: Database, forIncomingCall interaction: Interaction, in thread: SessionThread) {
|
|
|
|
// No call notifications for muted or group threads
|
|
|
|
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
|
2023-01-27 04:51:04 +01:00
|
|
|
guard
|
2023-02-20 02:56:48 +01:00
|
|
|
thread.variant != .legacyGroup &&
|
|
|
|
thread.variant != .group &&
|
|
|
|
thread.variant != .community
|
2023-01-27 04:51:04 +01:00
|
|
|
else { return }
|
2022-06-08 06:29:51 +02:00
|
|
|
guard
|
2022-06-09 10:37:44 +02:00
|
|
|
interaction.variant == .infoCall,
|
2022-06-08 06:29:51 +02:00
|
|
|
let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8),
|
|
|
|
let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode(
|
|
|
|
CallMessage.MessageInfo.self,
|
|
|
|
from: infoMessageData
|
|
|
|
)
|
|
|
|
else { return }
|
|
|
|
|
|
|
|
// Only notify missed calls
|
|
|
|
guard messageInfo.state == .missed || messageInfo.state == .permissionDenied else { return }
|
2022-04-07 07:10:38 +02:00
|
|
|
|
2022-06-08 06:29:51 +02:00
|
|
|
var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ]
|
|
|
|
userInfo[NotificationServiceExtension.threadIdKey] = thread.id
|
2022-04-07 07:10:38 +02:00
|
|
|
|
|
|
|
let notificationContent = UNMutableNotificationContent()
|
|
|
|
notificationContent.userInfo = userInfo
|
2022-06-08 06:29:51 +02:00
|
|
|
notificationContent.sound = thread.notificationSound
|
|
|
|
.defaulting(
|
|
|
|
to: db[.defaultNotificationSound]
|
|
|
|
.defaulting(to: Preferences.Sound.defaultNotificationSound)
|
|
|
|
)
|
|
|
|
.notificationSound(isQuiet: false)
|
2022-04-07 07:10:38 +02:00
|
|
|
|
|
|
|
// Badge Number
|
|
|
|
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
|
|
|
|
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
|
|
|
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
|
|
|
|
2022-09-05 07:44:42 +02:00
|
|
|
notificationContent.title = "Session"
|
2022-04-07 07:10:38 +02:00
|
|
|
notificationContent.body = ""
|
2022-06-08 06:29:51 +02:00
|
|
|
|
2022-09-05 07:44:42 +02:00
|
|
|
let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
|
|
|
|
|
2022-06-08 06:29:51 +02:00
|
|
|
if messageInfo.state == .permissionDenied {
|
|
|
|
notificationContent.body = String(
|
|
|
|
format: "modal_call_missed_tips_explanation".localized(),
|
2022-09-05 07:44:42 +02:00
|
|
|
senderName
|
2022-06-08 06:29:51 +02:00
|
|
|
)
|
2022-04-07 07:10:38 +02:00
|
|
|
}
|
|
|
|
|
2022-08-22 03:30:06 +02:00
|
|
|
addNotifcationRequest(
|
|
|
|
identifier: UUID().uuidString,
|
|
|
|
notificationContent: notificationContent,
|
|
|
|
trigger: nil
|
|
|
|
)
|
2022-06-14 09:10:03 +02:00
|
|
|
}
|
|
|
|
|
2022-07-25 07:39:56 +02:00
|
|
|
public func notifyUser(_ db: Database, forReaction reaction: Reaction, in thread: SessionThread) {
|
|
|
|
let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true)
|
2022-04-21 08:42:35 +02:00
|
|
|
|
2022-07-25 07:39:56 +02:00
|
|
|
// No reaction notifications for muted, group threads or message requests
|
|
|
|
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
|
2023-01-27 04:51:04 +01:00
|
|
|
guard
|
2023-02-20 02:56:48 +01:00
|
|
|
thread.variant != .legacyGroup &&
|
|
|
|
thread.variant != .group &&
|
|
|
|
thread.variant != .community
|
2023-01-27 04:51:04 +01:00
|
|
|
else { return }
|
2022-07-25 07:39:56 +02:00
|
|
|
guard !isMessageRequest else { return }
|
2022-06-17 06:26:23 +02:00
|
|
|
|
2022-07-25 07:39:56 +02:00
|
|
|
let senderName: String = Profile.displayName(db, id: reaction.authorId, threadVariant: thread.variant)
|
2022-06-17 06:26:23 +02:00
|
|
|
let notificationTitle = "Session"
|
2022-07-25 07:39:56 +02:00
|
|
|
var notificationBody = String(format: "EMOJI_REACTS_NOTIFICATION".localized(), senderName, reaction.emoji)
|
|
|
|
|
|
|
|
// Title & body
|
|
|
|
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
|
|
|
.defaulting(to: .nameAndPreview)
|
|
|
|
|
|
|
|
switch previewType {
|
|
|
|
case .nameAndPreview: break
|
2022-06-17 06:26:23 +02:00
|
|
|
default: notificationBody = NotificationStrings.incomingMessageBody
|
2022-03-21 03:55:51 +01:00
|
|
|
}
|
2022-06-17 06:26:23 +02:00
|
|
|
|
2022-07-25 07:39:56 +02:00
|
|
|
var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ]
|
|
|
|
userInfo[NotificationServiceExtension.threadIdKey] = thread.id
|
2022-06-17 06:26:23 +02:00
|
|
|
|
|
|
|
let notificationContent = UNMutableNotificationContent()
|
|
|
|
notificationContent.userInfo = userInfo
|
2022-07-25 07:39:56 +02:00
|
|
|
notificationContent.sound = thread.notificationSound
|
|
|
|
.defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound)
|
|
|
|
.notificationSound(isQuiet: false)
|
2022-06-17 06:26:23 +02:00
|
|
|
notificationContent.title = notificationTitle
|
|
|
|
notificationContent.body = notificationBody
|
2022-06-14 09:10:03 +02:00
|
|
|
|
2022-06-17 06:26:23 +02:00
|
|
|
addNotifcationRequest(identifier: UUID().uuidString, notificationContent: notificationContent, trigger: nil)
|
2022-02-17 04:55:32 +01:00
|
|
|
}
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
public func cancelNotifications(identifiers: [String]) {
|
2022-02-17 04:55:32 +01:00
|
|
|
let notificationCenter = UNUserNotificationCenter.current()
|
2022-04-21 08:42:35 +02:00
|
|
|
notificationCenter.removePendingNotificationRequests(withIdentifiers: identifiers)
|
|
|
|
notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers)
|
2022-02-17 04:55:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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?) {
|
2022-07-25 07:39:56 +02:00
|
|
|
let request = UNNotificationRequest(identifier: identifier, content: notificationContent, trigger: trigger)
|
|
|
|
|
2022-06-14 09:10:03 +02:00
|
|
|
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 {
|
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
func replacingMentions(for threadID: String) -> String {
|
2022-02-17 04:55:32 +01:00
|
|
|
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
|
2022-04-06 07:43:26 +02:00
|
|
|
|
2022-04-21 08:42:35 +02:00
|
|
|
if let displayName: String = Profile.displayNameNoFallback(id: publicKey) {
|
2022-02-17 04:55:32 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|