diff --git a/Signal/src/environment/NotificationsManager.m b/Signal/src/environment/NotificationsManager.m index 1a2d8261c..ca16589a7 100644 --- a/Signal/src/environment/NotificationsManager.m +++ b/Signal/src/environment/NotificationsManager.m @@ -136,7 +136,7 @@ } notification.alertBody = alertBodyString; - [[PushManager sharedManager] presentNotification:notification]; + [[PushManager sharedManager] presentNotification:notification checkForCancel:NO]; } else { if ([Environment.preferences soundInForeground]) { AudioServicesPlayAlertSound(_newMessageSound); @@ -193,7 +193,7 @@ break; } - [[PushManager sharedManager] presentNotification:notification]; + [[PushManager sharedManager] presentNotification:notification checkForCancel:YES]; } else { if ([Environment.preferences soundInForeground]) { AudioServicesPlayAlertSound(_newMessageSound); diff --git a/Signal/src/network/PushManager.h b/Signal/src/network/PushManager.h index 97039713a..3482fc1b8 100644 --- a/Signal/src/network/PushManager.h +++ b/Signal/src/network/PushManager.h @@ -66,7 +66,10 @@ typedef void (^pushTokensSuccessBlock)(NSString *pushToken, NSString *voipToken) - (TOCFuture *)registerPushKitNotificationFuture; - (BOOL)supportsVOIPPush; -- (void)presentNotification:(UILocalNotification *)notification; +// If checkForCancel is set, the notification will be delayed for +// a moment. If a relevant cancel notification is received in that window, +// the notification will not be displayed. +- (void)presentNotification:(UILocalNotification *)notification checkForCancel:(BOOL)checkForCancel; - (void)cancelNotificationsWithThreadId:(NSString *)threadId; #pragma mark Push Notifications Delegate Methods diff --git a/Signal/src/network/PushManager.m b/Signal/src/network/PushManager.m index 420fab28e..a78391c75 100644 --- a/Signal/src/network/PushManager.m +++ b/Signal/src/network/PushManager.m @@ -23,6 +23,7 @@ @property TOCFutureSource *registerWithServerFutureSource; @property UIAlertView *missingPermissionsAlertView; @property (nonatomic, retain) NSMutableArray *currentNotifications; +@property (nonatomic, retain) NSMutableArray *pendingNotifications; @property (nonatomic) UIBackgroundTaskIdentifier callBackgroundTask; @property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic, readonly) OWSMessageFetcherJob *messageFetcherJob; @@ -75,7 +76,8 @@ cancelButtonTitle:NSLocalizedString(@"OK", @"") otherButtonTitles:nil, nil]; _callBackgroundTask = UIBackgroundTaskInvalid; - _currentNotifications = [NSMutableArray array]; + _currentNotifications = [NSMutableArray new]; + _pendingNotifications = [NSMutableArray new]; OWSSingletonAssert(); @@ -163,7 +165,7 @@ failedSendNotif.alertBody = [NSString stringWithFormat:NSLocalizedString(@"NOTIFICATION_SEND_FAILED", nil), [thread name]]; failedSendNotif.userInfo = @{ Signal_Thread_UserInfo_Key : thread.uniqueId }; - [self presentNotification:failedSendNotif]; + [self presentNotification:failedSendNotif checkForCancel:NO]; completionHandler(); }]; } @@ -429,19 +431,61 @@ NSString *const PushManagerUserInfoKeysCallBackSignalRecipientId = @"PushManager return NO; } -- (void)presentNotification:(UILocalNotification *)notification { - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - [self.currentNotifications addObject:notification]; +- (void)presentNotification:(UILocalNotification *)notification checkForCancel:(BOOL)checkForCancel +{ + OWSAssert([NSThread isMainThread]); + + NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key]; + if (checkForCancel && threadId != nil) { + [_pendingNotifications addObject:notification]; + + // The longer we wait, the more obsolete notifications we can suppress - + // but the more lag we introduce to notification delivery. + const CGFloat kDelaySeconds = 0.3f; + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * kDelaySeconds)), dispatch_get_main_queue(), ^{ + if (![_pendingNotifications containsObject:notification]) { + DDLogVerbose(@"%@ notification was cancelled before it was presented: %@, %@", + self.tag, + notification.alertBody, + threadId); + } else { + [_pendingNotifications removeObject:notification]; + [[UIApplication sharedApplication] scheduleLocalNotification:notification]; + [self.currentNotifications addObject:notification]; + } + }); + } else { + [[UIApplication sharedApplication] scheduleLocalNotification:notification]; + [self.currentNotifications addObject:notification]; + } } - (void)cancelNotificationsWithThreadId:(NSString *)threadId { + OWSAssert([NSThread isMainThread]); + + // Cull matching pending notifications. NSMutableArray *toDelete = [NSMutableArray array]; - [self.currentNotifications enumerateObjectsUsingBlock:^(UILocalNotification *notif, NSUInteger idx, BOOL *stop) { - if ([notif.userInfo[Signal_Thread_UserInfo_Key] isEqualToString:threadId]) { - [[UIApplication sharedApplication] cancelLocalNotification:notif]; - [toDelete addObject:notif]; - } + [self.pendingNotifications enumerateObjectsUsingBlock:^( + UILocalNotification *notification, NSUInteger idx, BOOL *stop) { + NSString *notificationThreadId = notification.userInfo[Signal_Thread_UserInfo_Key]; + OWSAssert(notificationThreadId != nil); + if ([notificationThreadId isEqualToString:threadId]) { + DDLogError(@"%@ cancelling delayed notification: %@, %@", self.tag, notification.alertBody, threadId); + [toDelete addObject:notification]; + } }]; + [self.pendingNotifications removeObjectsInArray:toDelete]; + + // Cull matching active notifications. + [toDelete removeAllObjects]; + [self.currentNotifications + enumerateObjectsUsingBlock:^(UILocalNotification *notification, NSUInteger idx, BOOL *stop) { + if ([notification.userInfo[Signal_Thread_UserInfo_Key] isEqualToString:threadId]) { + [[UIApplication sharedApplication] cancelLocalNotification:notification]; + [toDelete addObject:notification]; + } + }]; [self.currentNotifications removeObjectsInArray:toDelete]; }