diff --git a/Session/Meta/AppDelegate.m b/Session/Meta/AppDelegate.m index 1da921767..9a0ccf17a 100644 --- a/Session/Meta/AppDelegate.m +++ b/Session/Meta/AppDelegate.m @@ -742,7 +742,6 @@ static NSTimeInterval launchStartedAt; - (LKAppMode)getCurrentAppMode { LKAppMode appMode = [self getAppModeOrSystemDefault]; - UIWindow *window = UIApplication.sharedApplication.keyWindow; return appMode; } diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 1a85d446c..15cf77447 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -589,7 +589,8 @@ "call_timeout" = "Unanswered Call"; "voice_call" = "Voice Call"; "video_call" = "Video Call"; -"APN_Message" = "You've got a new message"; +"APN_Message" = "You've got a new message."; +"APN_Collapsed_Messages" = "You've got %@ new messages."; "system_mode_theme" = "System"; "dark_mode_theme" = "Dark"; "light_mode_theme" = "Light"; diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 4b3b9f04b..fc80e8a71 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -37,6 +37,7 @@ struct AppNotificationUserInfoKey { static let threadId = "Signal.AppNotificationsUserInfoKey.threadId" static let callBackNumber = "Signal.AppNotificationsUserInfoKey.callBackNumber" static let localCallId = "Signal.AppNotificationsUserInfoKey.localCallId" + static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter" } extension AppNotificationCategory { @@ -80,9 +81,9 @@ extension AppNotificationAction { } } -// Delay notification of incoming messages when it's likely to be read by a linked device to -// avoid notifying a user on their phone while a conversation is actively happening on desktop. -let kNotificationDelayForRemoteRead: TimeInterval = 5 +// Delay notification of incoming messages when it's a background polling to +// avoid too many notifications fired at the same time +let kNotificationDelayForBackgroumdPoll: TimeInterval = 5 let kAudioNotificationsThrottleCount = 2 let kAudioNotificationsThrottleInterval: TimeInterval = 5 @@ -201,6 +202,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { owsFailDebug("unexpected thread: \(thread)") return } + default: + notificationTitle = "Session" } var notificationBody: String? @@ -209,6 +212,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { notificationBody = NotificationStrings.incomingMessageBody case .namePreview: notificationBody = messageText + default: + notificationBody = NotificationStrings.incomingMessageBody } guard let threadId = thread.uniqueId else { @@ -220,7 +225,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { // Don't reply from lockscreen if anyone in this conversation is // "no longer verified". - var category = AppNotificationCategory.incomingMessage + let category = AppNotificationCategory.incomingMessage let userInfo = [ AppNotificationUserInfoKey.threadId: threadId diff --git a/Session/Notifications/UserNotificationsAdaptee.swift b/Session/Notifications/UserNotificationsAdaptee.swift index 13c648fe6..e5997f9a9 100644 --- a/Session/Notifications/UserNotificationsAdaptee.swift +++ b/Session/Notifications/UserNotificationsAdaptee.swift @@ -96,17 +96,18 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { let content = UNMutableNotificationContent() content.categoryIdentifier = category.identifier content.userInfo = userInfo + let isReplacingNotification = replacingIdentifier != nil + var isBackgroudPoll = false + if let threadIdentifier = userInfo[AppNotificationUserInfoKey.threadId] as? String { + content.threadIdentifier = threadIdentifier + isBackgroudPoll = replacingIdentifier == threadIdentifier + } let isAppActive = UIApplication.shared.applicationState == .active if let sound = sound, sound != OWSSound.none { content.sound = sound.notificationSound(isQuiet: isAppActive) } - - var notificationIdentifier: String = UUID().uuidString - if let replacingIdentifier = replacingIdentifier { - notificationIdentifier = replacingIdentifier - Logger.debug("replacing notification with identifier: \(notificationIdentifier)") - cancelNotification(identifier: notificationIdentifier) - } + + let notificationIdentifier = isReplacingNotification ? replacingIdentifier! : UUID().uuidString if shouldPresentNotification(category: category, userInfo: userInfo) { if let displayableTitle = title?.filterForDisplay { @@ -119,10 +120,26 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { // Play sound and vibrate, but without a `body` no banner will show. Logger.debug("supressing notification body") } + + let trigger: UNNotificationTrigger? + if isBackgroudPoll { + trigger = UNTimeIntervalNotificationTrigger(timeInterval: kNotificationDelayForBackgroumdPoll, repeats: false) + let numberOfNotifications: Int + if let lastRequest = notifications[notificationIdentifier], let counter = lastRequest.content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] as? Int { + numberOfNotifications = counter + 1 + content.body = String(format: NotificationStrings.incomingCollapsedMessagesBody, "\(numberOfNotifications)") + } else { + numberOfNotifications = 1 + } + content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] = numberOfNotifications + } else { + trigger = nil + } - let request = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: nil) + let request = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger) Logger.debug("presenting notification with identifier: \(notificationIdentifier)") + if isReplacingNotification { cancelNotification(identifier: notificationIdentifier) } notificationCenter.add(request) notifications[notificationIdentifier] = request } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index da45e46ac..8796742bc 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -381,7 +381,9 @@ extension MessageReceiver { // Notify the user if needed guard (isMainAppAndActive || isBackgroundPoll), let tsIncomingMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) as? TSIncomingMessage, let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return tsMessageID } - tsIncomingMessage.setNotificationIdentifier(UUID().uuidString, transaction: transaction) + // Use the same identifier for notifications when in backgroud polling to prevent spam + let notificationIdentifier = isBackgroundPoll ? thread.uniqueId : UUID().uuidString + tsIncomingMessage.setNotificationIdentifier(notificationIdentifier, transaction: transaction) DispatchQueue.main.async { Storage.read { transaction in SSKEnvironment.shared.notificationsManager!.notifyUser(for: tsIncomingMessage, in: thread, transaction: transaction) diff --git a/SignalUtilitiesKit/Utilities/CommonStrings.swift b/SignalUtilitiesKit/Utilities/CommonStrings.swift index a5e4649d3..2d7199593 100644 --- a/SignalUtilitiesKit/Utilities/CommonStrings.swift +++ b/SignalUtilitiesKit/Utilities/CommonStrings.swift @@ -50,6 +50,9 @@ public class NotificationStrings: NSObject { @objc static public let incomingMessageBody = NSLocalizedString("APN_Message", comment: "notification body") + + @objc + static public let incomingCollapsedMessagesBody = NSLocalizedString("APN_Collapsed_Messages", comment: "collapsed notification body for background polling") @objc static public let incomingGroupMessageTitleFormat = NSLocalizedString("NEW_GROUP_MESSAGE_NOTIFICATION_TITLE", comment: "notification title. Embeds {{author name}} and {{group name}}")