diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm --- a/native/ios/NotificationService/NotificationService.mm +++ b/native/ios/NotificationService/NotificationService.mm @@ -9,10 +9,15 @@ NSString *const messageInfosKey = @"messageInfos"; NSString *const encryptedPayloadKey = @"encryptedPayload"; NSString *const encryptionFailureKey = @"encryptionFailure"; +NSString *const collapseIDKey = @"collapseID"; const std::string callingProcessName = "NSE"; // The context for this constant can be found here: // https://linear.app/comm/issue/ENG-3074#comment-bd2f5e28 int64_t const notificationRemovalDelay = (int64_t)(0.1 * NSEC_PER_SEC); +// Apple gives us about 30 seconds to process single notification, +// se we let any semaphore wait for at most 20 seconds +int64_t const semaphoreAwaitTimeLimit = (int64_t)(20 * NSEC_PER_SEC); + CFStringRef newMessageInfosDarwinNotification = CFSTR("app.comm.darwin_new_message_infos"); @@ -130,9 +135,11 @@ std::string rescindErrorMessage; try { @try { - [self - removeNotificationWithIdentifier:content - .userInfo[@"notificationId"]]; + [self removeNotificationsWithCondition:^BOOL( + UNNotification *_Nonnull notif) { + return [content.userInfo[@"notificationId"] + isEqualToString:notif.request.content.userInfo[@"id"]]; + }]; } @catch (NSException *e) { rescindErrorMessage = "Obj-C exception: " + std::string([e.name UTF8String]) + @@ -152,6 +159,38 @@ publicUserContent = [[UNNotificationContent alloc] init]; } + // Step 4: (optional) execute notification coalescing + if ([self isCollapsible:content.userInfo]) { + std::string coalescingErrorMessage; + try { + @try { + [self displayLocalNotificationFromContent:content + forCollapseKey:content + .userInfo[collapseIDKey]]; + } @catch (NSException *e) { + coalescingErrorMessage = + "Obj-C exception: " + std::string([e.name UTF8String]) + + " during notification coalescing."; + } + } catch (const std::exception &e) { + coalescingErrorMessage = "C++ exception: " + std::string(e.what()) + + " during notification coalescing."; + } + + if (coalescingErrorMessage.size()) { + [errorMessages + addObject:[NSString + stringWithUTF8String:coalescingErrorMessage.c_str()]]; + // Even if we fail to execute coalescing then public users + // should still see the original message. + publicUserContent = content; + } else { + publicUserContent = [[UNNotificationContent alloc] init]; + } + } + + // Step 5: (optional) create empty notification that + // only provides badge count. if ([self isBadgeOnly:content.userInfo]) { UNMutableNotificationContent *badgeOnlyContent = [[UNMutableNotificationContent alloc] init]; @@ -222,6 +261,23 @@ continue; } + if ([self isCollapsible:content.userInfo]) { + // If we get to this place it means we were unable to + // execute notification coalescing with local notification + // mechanism in time given to NSE to process notification. + if (!comm::StaffUtils::isStaffRelease()) { + handler(content); + continue; + } + + NSString *errorMessage = + @"NSE: Exceeded time limit to collapse a notitication."; + UNNotificationContent *errorContent = + [self buildContentForError:errorMessage]; + handler(errorContent); + continue; + } + if ([self shouldBeDecrypted:content.userInfo] && !content.userInfo[@"succesfullyDecrypted"]) { // If we get to this place it means we were unable to @@ -254,7 +310,8 @@ } } -- (void)removeNotificationWithIdentifier:(NSString *)identifier { +- (void)removeNotificationsWithCondition: + (BOOL (^)(UNNotification *_Nonnull))condition { dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); void (^delayedSemaphorePostCallback)() = ^() { @@ -268,18 +325,62 @@ [UNUserNotificationCenter.currentNotificationCenter getDeliveredNotificationsWithCompletionHandler:^( NSArray *_Nonnull notifications) { + NSMutableArray *notificationsToRemove = + [[NSMutableArray alloc] init]; for (UNNotification *notif in notifications) { - if ([identifier isEqual:notif.request.content.userInfo[@"id"]]) { - [UNUserNotificationCenter.currentNotificationCenter - removeDeliveredNotificationsWithIdentifiers:@[ - notif.request.identifier - ]]; + if (condition(notif)) { + [notificationsToRemove addObject:notif.request.identifier]; } } + [UNUserNotificationCenter.currentNotificationCenter + removeDeliveredNotificationsWithIdentifiers:notificationsToRemove]; delayedSemaphorePostCallback(); }]; - dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait( + semaphore, dispatch_time(DISPATCH_TIME_NOW, semaphoreAwaitTimeLimit)); +} + +- (void)displayLocalNotificationFromContent:(UNNotificationContent *)content + forCollapseKey:(NSString *)collapseKey { + UNMutableNotificationContent *localNotifContent = + [[UNMutableNotificationContent alloc] init]; + + localNotifContent.title = content.title; + localNotifContent.body = content.body; + localNotifContent.badge = content.badge; + localNotifContent.userInfo = content.userInfo; + + UNNotificationRequest *localNotifRequest = + [UNNotificationRequest requestWithIdentifier:collapseKey + content:localNotifContent + trigger:nil]; + + // We must wait until local notif display completion + // handler returns. Context: + // https://developer.apple.com/forums/thread/108340?answerId=331640022#331640022 + + dispatch_semaphore_t localNotifDisplaySemaphore = + dispatch_semaphore_create(0); + + __block NSError *localNotifDisplayError = nil; + [UNUserNotificationCenter.currentNotificationCenter + addNotificationRequest:localNotifRequest + withCompletionHandler:^(NSError *_Nullable error) { + if (error) { + localNotifDisplayError = error; + } + dispatch_semaphore_signal(localNotifDisplaySemaphore); + }]; + + dispatch_semaphore_wait( + localNotifDisplaySemaphore, + dispatch_time(DISPATCH_TIME_NOW, semaphoreAwaitTimeLimit)); + + if (localNotifDisplayError) { + throw std::runtime_error( + std::string([localNotifDisplayError.localizedDescription UTF8String])); + } } - (void)persistMessagePayload:(NSDictionary *)payload { @@ -326,6 +427,10 @@ return !payload[@"threadID"]; } +- (BOOL)isCollapsible:(NSDictionary *)payload { + return payload[collapseIDKey]; +} + - (UNNotificationContent *)getBadgeOnlyContentFor: (UNNotificationContent *)content { UNMutableNotificationContent *badgeOnlyContent =