diff --git a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java --- a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java +++ b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java @@ -68,6 +68,8 @@ private static final String MMKV_KEY_SEPARATOR = "."; private static final String MMKV_KEYSERVER_PREFIX = "KEYSERVER"; private static final String MMKV_UNREAD_COUNT_SUFFIX = "UNREAD_COUNT"; + private static final String MMKV_UNREAD_THICK_THREADS = + "NOTIFS.UNREAD_THICK_THREADS"; private Bitmap displayableNotificationLargeIcon; private NotificationManager notificationManager; private LocalBroadcastManager localBroadcastManager; @@ -244,13 +246,22 @@ for (StatusBarNotification notification : notificationManager.getActiveNotifications()) { String tag = notification.getTag(); + Bundle data = notification.getNotification().extras; + + boolean thinThreadRescind = + tag != null && rescindID != null && tag.equals(rescindID); + + boolean thickThreadRescind = tag != null && data != null && + threadID.equals(data.getString("threadID")); + boolean isGroupMember = threadID.equals(notification.getNotification().getGroup()); + boolean isGroupSummary = (notification.getNotification().flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY; - if (tag != null && tag.equals(rescindID)) { + if (thinThreadRescind || thickThreadRescind) { notificationManager.cancel(notification.getTag(), notification.getId()); } else if ( isGroupMember && isGroupSummary && StaffUtils.isStaffRelease()) { @@ -271,30 +282,38 @@ } private void handleUnreadCountUpdate(RemoteMessage message) { - if (message.getData().get(KEYSERVER_ID_KEY) == null) { - return; - } + if (message.getData().get(KEYSERVER_ID_KEY) != null && + message.getData().get(BADGE_KEY) != null) { + String badge = message.getData().get(BADGE_KEY); + String senderKeyserverID = message.getData().get(KEYSERVER_ID_KEY); + String senderKeyserverUnreadCountKey = String.join( + MMKV_KEY_SEPARATOR, + MMKV_KEYSERVER_PREFIX, + senderKeyserverID, + MMKV_UNREAD_COUNT_SUFFIX); - String badge = message.getData().get(BADGE_KEY); - if (badge == null) { - return; + int senderKeyserverUnreadCount; + try { + senderKeyserverUnreadCount = Integer.parseInt(badge); + } catch (NumberFormatException e) { + Log.w("COMM", "Invalid badge count", e); + return; + } + CommMMKV.setInt( + senderKeyserverUnreadCountKey, senderKeyserverUnreadCount); } - String senderKeyserverID = message.getData().get(KEYSERVER_ID_KEY); - String senderKeyserverUnreadCountKey = String.join( - MMKV_KEY_SEPARATOR, - MMKV_KEYSERVER_PREFIX, - senderKeyserverID, - MMKV_UNREAD_COUNT_SUFFIX); - - int senderKeyserverUnreadCount; - try { - senderKeyserverUnreadCount = Integer.parseInt(badge); - } catch (NumberFormatException e) { - Log.w("COMM", "Invalid badge count", e); - return; + if (message.getData().get(SENDER_DEVICE_ID_KEY) != null && + message.getData().get(THREAD_ID_KEY) != null && + message.getData().get(RESCIND_KEY) != null) { + CommMMKV.removeElementFromStringSet( + MMKV_UNREAD_THICK_THREADS, message.getData().get(THREAD_ID_KEY)); + } else if ( + message.getData().get(SENDER_DEVICE_ID_KEY) != null && + message.getData().get(THREAD_ID_KEY) != null) { + CommMMKV.addElementToStringSet( + MMKV_UNREAD_THICK_THREADS, message.getData().get(THREAD_ID_KEY)); } - CommMMKV.setInt(senderKeyserverUnreadCountKey, senderKeyserverUnreadCount); int totalUnreadCount = 0; String[] allKeys = CommMMKV.getAllKeys(); @@ -313,6 +332,8 @@ totalUnreadCount += unreadCount; } + totalUnreadCount += CommMMKV.getStringSetSize(MMKV_UNREAD_THICK_THREADS); + if (totalUnreadCount > 0) { ShortcutBadger.applyCount(this, totalUnreadCount); } else { 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 @@ -12,6 +12,7 @@ NSString *const backgroundNotificationTypeKey = @"backgroundNotifType"; NSString *const messageInfosKey = @"messageInfos"; +NSString *const threadIDKey = @"threadID"; NSString *const encryptedPayloadKey = @"encryptedPayload"; NSString *const encryptionFailedKey = @"encryptionFailed"; NSString *const collapseIDKey = @"collapseID"; @@ -28,6 +29,7 @@ const std::string mmkvKeySeparator = "."; const std::string mmkvKeyserverPrefix = "KEYSERVER"; const std::string mmkvUnreadCountSuffix = "UNREAD_COUNT"; +const std::string unreadThickThreads = "NOTIFS.UNREAD_THICK_THREADS"; // The context for this constant can be found here: // https://linear.app/comm/issue/ENG-3074#comment-bd2f5e28 @@ -177,27 +179,25 @@ } // Step 3: Cumulative unread count calculation - if (content.badge) { - std::string unreadCountCalculationError; - try { - @try { - [self calculateTotalUnreadCountInPlace:content]; - } @catch (NSException *e) { - unreadCountCalculationError = - "Obj-C exception: " + std::string([e.name UTF8String]) + - " during unread count calculation."; - } - } catch (const std::exception &e) { - unreadCountCalculationError = "C++ exception: " + std::string(e.what()) + + std::string unreadCountCalculationError; + try { + @try { + [self calculateTotalUnreadCountInPlace:content]; + } @catch (NSException *e) { + unreadCountCalculationError = + "Obj-C exception: " + std::string([e.name UTF8String]) + " during unread count calculation."; } + } catch (const std::exception &e) { + unreadCountCalculationError = "C++ exception: " + std::string(e.what()) + + " during unread count calculation."; + } - if (unreadCountCalculationError.size() && - comm::StaffUtils::isStaffRelease()) { - [errorMessages - addObject:[NSString stringWithUTF8String:unreadCountCalculationError - .c_str()]]; - } + if (unreadCountCalculationError.size() && + comm::StaffUtils::isStaffRelease()) { + [errorMessages + addObject:[NSString stringWithUTF8String:unreadCountCalculationError + .c_str()]]; } // Step 4: (optional) rescind read notifications @@ -208,11 +208,21 @@ std::string rescindErrorMessage; try { @try { - [self removeNotificationsWithCondition:^BOOL( - UNNotification *_Nonnull notif) { - return [content.userInfo[@"notificationId"] - isEqualToString:notif.request.content.userInfo[@"id"]]; - }]; + if (content.userInfo[@"notificationId"]) { + // thin thread rescind + [self removeNotificationsWithCondition:^BOOL( + UNNotification *_Nonnull notif) { + return [content.userInfo[@"notificationId"] + isEqualToString:notif.request.content.userInfo[@"id"]]; + }]; + } else if (content.userInfo[threadIDKey]) { + // thick thread rescind + [self removeNotificationsWithCondition:^BOOL( + UNNotification *_Nonnull notif) { + return [content.userInfo[threadIDKey] + isEqualToString:notif.request.content.userInfo[threadIDKey]]; + }]; + } } @catch (NSException *e) { rescindErrorMessage = "Obj-C exception: " + std::string([e.name UTF8String]) + @@ -505,20 +515,32 @@ - (void)calculateTotalUnreadCountInPlace: (UNMutableNotificationContent *)content { - if (!content.userInfo[keyserverIDKey]) { - return; - } - std::string senderKeyserverID = - std::string([content.userInfo[keyserverIDKey] UTF8String]); - std::string senderKeyserverUnreadCountKey = joinStrings( - mmkvKeySeparator, - {mmkvKeyserverPrefix, senderKeyserverID, mmkvUnreadCountSuffix}); + if (content.userInfo[keyserverIDKey] && content.badge) { + std::string senderKeyserverID = + std::string([content.userInfo[keyserverIDKey] UTF8String]); + std::string senderKeyserverUnreadCountKey = joinStrings( + mmkvKeySeparator, + {mmkvKeyserverPrefix, senderKeyserverID, mmkvUnreadCountSuffix}); + + int senderKeyserverUnreadCount = [content.badge intValue]; + comm::CommMMKV::setInt( + senderKeyserverUnreadCountKey, senderKeyserverUnreadCount); + } - int senderKeyserverUnreadCount = [content.badge intValue]; - comm::CommMMKV::setInt( - senderKeyserverUnreadCountKey, senderKeyserverUnreadCount); + if (content.userInfo[senderDeviceIDKey] && content.userInfo[threadIDKey] && + [self isRescind:content.userInfo]) { + comm::CommMMKV::removeElementFromStringSet( + unreadThickThreads, + std::string([content.userInfo[threadIDKey] UTF8String])); + } else if ( + content.userInfo[senderDeviceIDKey] && content.userInfo[threadIDKey]) { + comm::CommMMKV::addElementToStringSet( + unreadThickThreads, + std::string([content.userInfo[threadIDKey] UTF8String])); + } + // calculate unread counts from keyservers int totalUnreadCount = 0; std::vector allKeys = comm::CommMMKV::getAllKeys(); for (const auto &key : allKeys) { @@ -539,6 +561,9 @@ totalUnreadCount += unreadCount.value(); } + // calculate unread counts from thick threads + totalUnreadCount += comm::CommMMKV::getStringSetSize(unreadThickThreads); + content.badge = @(totalUnreadCount); } @@ -658,10 +683,10 @@ mutableUserInfo[needsSilentBadgeUpdateKey] = @(YES); } - NSString *threadID = decryptedPayload[@"threadID"]; + NSString *threadID = decryptedPayload[threadIDKey]; if (threadID) { content.threadIdentifier = threadID; - mutableUserInfo[@"threadID"] = threadID; + mutableUserInfo[threadIDKey] = threadID; if (mutableAps) { mutableAps[@"thread-id"] = threadID; } @@ -678,7 +703,7 @@ // The rest have been already decrypted and handled. static NSArray *handledKeys = - @[ @"merged", @"badge", @"threadID" ]; + @[ @"merged", @"badge", threadIDKey ]; for (NSString *payloadKey in decryptedPayload) { if ([handledKeys containsObject:payloadKey]) {