Page MenuHomePhorge

D11090.1768533187.diff
No OneTemporary

Size
17 KB
Referenced Files
None
Subscribers
None

D11090.1768533187.diff

diff --git a/lib/ops/keyserver-store-ops.js b/lib/ops/keyserver-store-ops.js
--- a/lib/ops/keyserver-store-ops.js
+++ b/lib/ops/keyserver-store-ops.js
@@ -130,7 +130,23 @@
},
};
+function getKeyserversToRemoveFromNotifsStore(
+ ops: $ReadOnlyArray<KeyserverStoreOperation>,
+): $ReadOnlyArray<string> {
+ const removeKeyserversOperations: Array<RemoveKeyserversOperation> = [];
+ for (const op of ops) {
+ if (op.type === 'remove_keyservers') {
+ removeKeyserversOperations.push(op);
+ }
+ }
+
+ return removeKeyserversOperations
+ .map(operation => operation.payload.ids)
+ .flat();
+}
+
export {
keyserverStoreOpsHandlers,
convertKeyserverInfoToClientDBKeyserverInfo,
+ getKeyserversToRemoveFromNotifsStore,
};
diff --git a/lib/selectors/thread-selectors.js b/lib/selectors/thread-selectors.js
--- a/lib/selectors/thread-selectors.js
+++ b/lib/selectors/thread-selectors.js
@@ -3,6 +3,7 @@
import _compact from 'lodash/fp/compact.js';
import _filter from 'lodash/fp/filter.js';
import _flow from 'lodash/fp/flow.js';
+import _groupBy from 'lodash/fp/groupBy.js';
import _map from 'lodash/fp/map.js';
import _mapValues from 'lodash/fp/mapValues.js';
import _orderBy from 'lodash/fp/orderBy.js';
@@ -18,6 +19,7 @@
} from './calendar-filter-selectors.js';
import { relativeMemberInfoSelectorForMembersOfThread } from './user-selectors.js';
import genesis from '../facts/genesis.js';
+import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js';
import {
getAvatarForThread,
getRandomDefaultEmojiAvatar,
@@ -288,6 +290,31 @@
).length,
);
+const extendedUnreadCount: (state: BaseAppState<>) => {
+ +[keyserverID: string]: number,
+} = createSelector(
+ (state: BaseAppState<>) => state.threadStore.threadInfos,
+ (threadInfos: RawThreadInfos): { +[keyserverID: string]: number } => {
+ const keyserverToThreads = _groupBy(threadInfo =>
+ extractKeyserverIDFromID(threadInfo.id),
+ )(
+ values(threadInfos).filter(threadInfo =>
+ threadInHomeChatList(threadInfo),
+ ),
+ );
+
+ const keyserverUnreadCountPairs = Object.entries(keyserverToThreads).map(
+ ([keyserverID, keyserverThreadInfos]) => [
+ keyserverID,
+ keyserverThreadInfos.filter(threadInfo => threadInfo.currentUser.unread)
+ .length,
+ ],
+ );
+
+ return Object.fromEntries(keyserverUnreadCountPairs);
+ },
+);
+
const unreadBackgroundCount: (state: BaseAppState<>) => number = createSelector(
(state: BaseAppState<>) => state.threadStore.threadInfos,
(threadInfos: RawThreadInfos): number =>
@@ -547,6 +574,7 @@
childThreadInfos,
containedThreadInfos,
unreadCount,
+ extendedUnreadCount,
unreadBackgroundCount,
unreadCountSelectorForCommunity,
otherUsersButNoOtherAdmins,
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
@@ -18,6 +18,7 @@
import app.comm.android.ExpoUtils;
import app.comm.android.MainActivity;
import app.comm.android.R;
+import app.comm.android.fbjni.CommMMKV;
import app.comm.android.fbjni.CommSecureStore;
import app.comm.android.fbjni.GlobalDBSingleton;
import app.comm.android.fbjni.MessageOperationsUtilities;
@@ -45,8 +46,11 @@
private static final String ENCRYPTION_FAILED_KEY = "encryptionFailed";
private static final String GROUP_NOTIF_IDS_KEY = "groupNotifIDs";
private static final String COLLAPSE_ID_KEY = "collapseKey";
+ private static final String KEYSERVER_ID_KEY = "keyserverID";
private static final String CHANNEL_ID = "default";
private static final long[] VIBRATION_SPEC = {500, 500};
+ private static final String MMKV_KEYSERVER_PREFIX = "KEYSERVER.";
+ private static final String MMKV_UNREAD_COUNT_SUFFIX = ".UNREAD_COUNT";
private Bitmap displayableNotificationLargeIcon;
private NotificationManager notificationManager;
private LocalBroadcastManager localBroadcastManager;
@@ -108,18 +112,10 @@
handleNotificationRescind(message);
}
- String badge = message.getData().get(BADGE_KEY);
- if (badge != null) {
- try {
- int badgeCount = Integer.parseInt(badge);
- if (badgeCount > 0) {
- ShortcutBadger.applyCount(this, badgeCount);
- } else {
- ShortcutBadger.removeCount(this);
- }
- } catch (NumberFormatException e) {
- Log.w("COMM", "Invalid badge count", e);
- }
+ try {
+ handleUnreadCountUpdate(message);
+ } catch (Exception e) {
+ Log.w("COMM", "Unread count update failure.", e);
}
String badgeOnly = message.getData().get(BADGE_ONLY_KEY);
@@ -214,6 +210,55 @@
}
}
+ private void handleUnreadCountUpdate(RemoteMessage message) {
+ String badge = message.getData().get(BADGE_KEY);
+ if (badge == null) {
+ return;
+ }
+
+ if (message.getData().get(KEYSERVER_ID_KEY) == null) {
+ throw new RuntimeException("Received badge update without keyserver ID.");
+ }
+ String senderKeyserverID = message.getData().get(KEYSERVER_ID_KEY);
+ String senderKeyserverUnreadCountKey =
+ 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;
+ }
+ CommMMKV.setInt(senderKeyserverUnreadCountKey, senderKeyserverUnreadCount);
+
+ int totalUnreadCount = senderKeyserverUnreadCount;
+ String[] allKeys = CommMMKV.getAllKeys();
+ for (String key : allKeys) {
+ if (key.equals(senderKeyserverUnreadCountKey)) {
+ continue;
+ }
+
+ if (!key.startsWith(MMKV_KEYSERVER_PREFIX) ||
+ !key.endsWith(MMKV_UNREAD_COUNT_SUFFIX)) {
+ continue;
+ }
+
+ Integer unreadCount = CommMMKV.getInt(key, -1);
+ if (unreadCount == null) {
+ continue;
+ }
+
+ totalUnreadCount += unreadCount;
+ }
+
+ if (totalUnreadCount > 0) {
+ ShortcutBadger.applyCount(this, totalUnreadCount);
+ } else {
+ ShortcutBadger.removeCount(this);
+ }
+ }
+
private void addToThreadGroupAndDisplay(
String notificationID,
NotificationCompat.Builder notificationBuilder,
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
@@ -1,4 +1,5 @@
#import "NotificationService.h"
+#import "CommMMKV.h"
#import "Logger.h"
#import "NotificationsCryptoModule.h"
#import "StaffUtils.h"
@@ -10,6 +11,7 @@
NSString *const encryptedPayloadKey = @"encryptedPayload";
NSString *const encryptionFailureKey = @"encryptionFailure";
NSString *const collapseIDKey = @"collapseID";
+NSString *const keyserverIDKey = @"keyserverID";
const std::string callingProcessName = "NSE";
// The context for this constant can be found here:
// https://linear.app/comm/issue/ENG-3074#comment-bd2f5e28
@@ -140,7 +142,30 @@
addObject:[NSString stringWithUTF8String:persistErrorMessage.c_str()]];
}
- // Step 3: (optional) rescind read notifications
+ // 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()) +
+ " during unread count calculation.";
+ }
+
+ if (unreadCountCalculationError.size()) {
+ [errorMessages
+ addObject:[NSString stringWithUTF8String:unreadCountCalculationError
+ .c_str()]];
+ }
+ }
+
+ // Step 4: (optional) rescind read notifications
// Message payload persistence is a higher priority task, so it has
// to happen prior to potential notification center clearing.
@@ -172,7 +197,7 @@
publicUserContent = [[UNNotificationContent alloc] init];
}
- // Step 4: (optional) execute notification coalescing
+ // Step 5: (optional) execute notification coalescing
if ([self isCollapsible:content.userInfo]) {
std::string coalescingErrorMessage;
try {
@@ -202,7 +227,7 @@
}
}
- // Step 5: (optional) create empty notification that
+ // Step 6: (optional) create empty notification that
// only provides badge count.
if ([self needsSilentBadgeUpdate:content.userInfo]) {
UNMutableNotificationContent *badgeOnlyContent =
@@ -211,7 +236,7 @@
publicUserContent = badgeOnlyContent;
}
- // Step 5: notify main app that there is data
+ // Step 7: notify main app that there is data
// to transfer to SQLite and redux.
[self sendNewMessageInfosNotification];
@@ -415,6 +440,49 @@
[payload[backgroundNotificationTypeKey] isEqualToString:@"CLEAR"];
}
+- (void)calculateTotalUnreadCountInPlace:
+ (UNMutableNotificationContent *)content {
+ if (!content.userInfo[keyserverIDKey]) {
+ throw std::runtime_error("Received badge update without keyserver ID.");
+ }
+ std::string senderKeyserverID =
+ std::string([content.userInfo[keyserverIDKey] UTF8String]);
+
+ static const std::string keyserverPrefix = "KEYSERVER.";
+ static const std::string unreadCountSuffix = ".UNREAD_COUNT";
+ std::string senderKeyserverUnreadCountKey =
+ keyserverPrefix + senderKeyserverID + unreadCountSuffix;
+
+ int senderKeyserverUnreadCount = [content.badge intValue];
+ comm::CommMMKV::setInt(
+ senderKeyserverUnreadCountKey, senderKeyserverUnreadCount);
+
+ int totalUnreadCount = senderKeyserverUnreadCount;
+ std::vector<std::string> allKeys = comm::CommMMKV::getAllKeys();
+ for (const auto &key : allKeys) {
+ if (key == senderKeyserverUnreadCountKey) {
+ continue;
+ }
+
+ if (key.size() < keyserverPrefix.size() + unreadCountSuffix.size() ||
+ key.compare(0, keyserverPrefix.size(), keyserverPrefix) ||
+ key.compare(
+ key.size() - unreadCountSuffix.size(),
+ unreadCountSuffix.size(),
+ unreadCountSuffix)) {
+ continue;
+ }
+
+ std::optional<int> unreadCount = comm::CommMMKV::getInt(key, -1);
+ if (!unreadCount.has_value()) {
+ continue;
+ }
+ totalUnreadCount += unreadCount.value();
+ }
+
+ content.badge = @(totalUnreadCount);
+}
+
- (BOOL)needsSilentBadgeUpdate:(NSDictionary *)payload {
// TODO: refactor this check by introducing
// badgeOnly property in iOS notification payload
diff --git a/native/push/push-handler.react.js b/native/push/push-handler.react.js
--- a/native/push/push-handler.react.js
+++ b/native/push/push-handler.react.js
@@ -1,7 +1,6 @@
// @flow
import * as Haptics from 'expo-haptics';
-import invariant from 'invariant';
import * as React from 'react';
import { LogBox, Platform } from 'react-native';
import { Notification as InAppNotification } from 'react-native-in-app-message';
@@ -17,20 +16,18 @@
} from 'lib/actions/device-actions.js';
import { saveMessagesActionType } from 'lib/actions/message-actions.js';
import {
- connectionSelector,
deviceTokensSelector,
updatesCurrentAsOfSelector,
} from 'lib/selectors/keyserver-selectors.js';
import {
threadInfoSelector,
- unreadCount,
+ extendedUnreadCount,
} from 'lib/selectors/thread-selectors.js';
import { isLoggedIn } from 'lib/selectors/user-selectors.js';
import { mergePrefixIntoBody } from 'lib/shared/notif-utils.js';
import type { RawMessageInfo } from 'lib/types/message-types.js';
import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import type { Dispatch } from 'lib/types/redux-types.js';
-import { type ConnectionInfo } from 'lib/types/socket-types.js';
import type { GlobalTheme } from 'lib/types/theme-types.js';
import {
convertNonPendingIDToNewSchema,
@@ -78,6 +75,7 @@
addLifecycleListener,
getCurrentLifecycleState,
} from '../lifecycle/lifecycle.js';
+import { commCoreModule } from '../native-modules.js';
import { replaceWithThreadActionType } from '../navigation/action-types.js';
import { activeMessageListSelector } from '../navigation/nav-selectors.js';
import { NavContext } from '../navigation/navigation-context.js';
@@ -100,7 +98,7 @@
// Navigation state
+activeThread: ?string,
// Redux state
- +unreadCount: number,
+ +unreadCount: { +[keyserverID: string]: number },
+deviceTokens: {
+[keyserverID: string]: ?string,
},
@@ -108,7 +106,6 @@
+[id: string]: ThreadInfo,
},
+notifPermissionAlertInfo: NotifPermissionAlertInfo,
- +connection: ConnectionInfo,
+updatesCurrentAsOf: number,
+activeTheme: ?GlobalTheme,
+loggedIn: boolean,
@@ -211,9 +208,7 @@
);
}
- if (this.props.connection.status === 'connected') {
- this.updateBadgeCount();
- }
+ void this.updateBadgeCount();
}
componentWillUnmount() {
@@ -267,14 +262,7 @@
if (this.props.activeThread !== prevProps.activeThread) {
this.clearNotifsOfThread();
}
-
- if (
- this.props.connection.status === 'connected' &&
- (prevProps.connection.status !== 'connected' ||
- this.props.unreadCount !== prevProps.unreadCount)
- ) {
- this.updateBadgeCount();
- }
+ void this.updateBadgeCount();
for (const threadID of this.openThreadOnceReceived) {
const threadInfo = this.props.threadInfos[threadID];
@@ -312,12 +300,41 @@
}
}
- updateBadgeCount() {
- const curUnreadCount = this.props.unreadCount;
+ async updateBadgeCount() {
+ const curUnreadCounts = this.props.unreadCount;
+
+ let totalUnreadCount = 0;
+ const notifStorageUnreadCounts: Array<{
+ +id: string,
+ unreadCount: number,
+ }> = [];
+
+ for (const keyserverID in curUnreadCounts) {
+ totalUnreadCount += curUnreadCounts[keyserverID];
+ notifStorageUnreadCounts.push({
+ id: keyserverID,
+ unreadCount: curUnreadCounts[keyserverID],
+ });
+ }
+
if (Platform.OS === 'ios') {
- CommIOSNotifications.setBadgesCount(curUnreadCount);
+ CommIOSNotifications.setBadgesCount(totalUnreadCount);
} else if (Platform.OS === 'android') {
- CommAndroidNotifications.setBadge(curUnreadCount);
+ CommAndroidNotifications.setBadge(totalUnreadCount);
+ }
+
+ try {
+ await commCoreModule.updateKeyserverDataInNotifStorage(
+ notifStorageUnreadCounts,
+ );
+ } catch (e) {
+ if (__DEV__) {
+ Alert.alert(
+ 'MMKV error',
+ 'Failed to update keyserver data in MMKV.' + e.message,
+ );
+ }
+ console.log(e);
}
}
@@ -699,16 +716,12 @@
React.memo<BaseProps>(function ConnectedPushHandler(props: BaseProps) {
const navContext = React.useContext(NavContext);
const activeThread = activeMessageListSelector(navContext);
- const boundUnreadCount = useSelector(unreadCount);
+ const boundUnreadCount = useSelector(extendedUnreadCount);
const deviceTokens = useSelector(deviceTokensSelector);
const threadInfos = useSelector(threadInfoSelector);
const notifPermissionAlertInfo = useSelector(
state => state.notifPermissionAlertInfo,
);
- const connection = useSelector(
- connectionSelector(authoritativeKeyserverID),
- );
- invariant(connection, 'keyserver missing from keyserverStore');
const updatesCurrentAsOf = useSelector(
updatesCurrentAsOfSelector(authoritativeKeyserverID),
);
@@ -728,7 +741,6 @@
deviceTokens={deviceTokens}
threadInfos={threadInfos}
notifPermissionAlertInfo={notifPermissionAlertInfo}
- connection={connection}
updatesCurrentAsOf={updatesCurrentAsOf}
activeTheme={activeTheme}
loggedIn={loggedIn}
diff --git a/native/redux/redux-utils.js b/native/redux/redux-utils.js
--- a/native/redux/redux-utils.js
+++ b/native/redux/redux-utils.js
@@ -2,7 +2,10 @@
import { useSelector as reactReduxUseSelector } from 'react-redux';
-import { keyserverStoreOpsHandlers } from 'lib/ops/keyserver-store-ops.js';
+import {
+ keyserverStoreOpsHandlers,
+ getKeyserversToRemoveFromNotifsStore,
+} from 'lib/ops/keyserver-store-ops.js';
import { messageStoreOpsHandlers } from 'lib/ops/message-store-ops.js';
import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.js';
import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js';
@@ -42,6 +45,8 @@
userStoreOpsHandlers.convertOpsToClientDBOps(userStoreOperations);
const convertedKeyserverStoreOperations =
keyserverStoreOpsHandlers.convertOpsToClientDBOps(keyserverStoreOperations);
+ const keyserversToRemoveFromNotifsStore =
+ getKeyserversToRemoveFromNotifsStore(keyserverStoreOperations);
try {
const promises = [];
@@ -83,6 +88,13 @@
),
);
}
+ if (keyserversToRemoveFromNotifsStore.length > 0) {
+ promises.push(
+ commCoreModule.removeKeyserverDataFromNotifStorage(
+ keyserversToRemoveFromNotifsStore,
+ ),
+ );
+ }
await Promise.all(promises);
} catch (e) {
if (isTaskCancelledError(e)) {

File Metadata

Mime Type
text/plain
Expires
Fri, Jan 16, 3:13 AM (16 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5941576
Default Alt Text
D11090.1768533187.diff (17 KB)

Event Timeline