From 306fbba3a74daeabc7cea82a58ab353361423c35 Mon Sep 17 00:00:00 2001 From: Anurag Shivarathri Date: Thu, 30 Jun 2022 21:26:14 +0530 Subject: [PATCH] [Gekidou MM-44135 MM-44134 MM-40088] Group & Clear channel and thread notifications when CRT is on (#6429) * Android * iOS changes * client changes * Misc * Update CustomPushNotification.java Co-authored-by: Mattermod --- .../helpers/CustomPushNotificationHelper.java | 16 +- .../rnbeta/CustomPushNotification.java | 162 +++++++++++------- .../rnbeta/NotificationDismissService.java | 5 +- .../rnbeta/NotificationPreferencesModule.java | 8 +- app/actions/local/channel.ts | 4 +- app/actions/remote/thread.ts | 9 +- app/init/push_notifications.ts | 26 ++- ios/Mattermost/AppDelegate.mm | 25 ++- types/global/push_notifications.d.ts | 1 + 9 files changed, 178 insertions(+), 78 deletions(-) diff --git a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java index ad8da1f4b..b01d7a69f 100644 --- a/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java +++ b/android/app/src/main/java/com/mattermost/helpers/CustomPushNotificationHelper.java @@ -101,6 +101,16 @@ public class CustomPushNotificationHelper { userInfoBundle.putString("channel_id", channelId); } + String postId = bundle.getString("post_id"); + if (postId != null) { + userInfoBundle.putString("post_id", postId); + } + + String rootId = bundle.getString("root_id"); + if (rootId != null) { + userInfoBundle.putString("root_id", rootId); + } + notification.addExtras(userInfoBundle); } @@ -145,12 +155,16 @@ public class CustomPushNotificationHelper { String channelId = bundle.getString("channel_id"); String postId = bundle.getString("post_id"); + String rootId = bundle.getString("root_id"); int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID; + Boolean is_crt_enabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true"); + String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId; + addNotificationExtras(notification, bundle); setNotificationIcons(context, notification, bundle); setNotificationMessagingStyle(context, notification, bundle); - setNotificationGroup(notification, channelId, createSummary); + setNotificationGroup(notification, groupId, createSummary); setNotificationBadgeType(notification); setNotificationChannel(notification, bundle); diff --git a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java index aa00cf6af..1a919ff10 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java @@ -16,11 +16,9 @@ import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.Objects; @@ -36,7 +34,6 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade; import com.wix.reactnativenotifications.core.JsIOHelper; import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME; -import org.json.JSONArray; import org.json.JSONObject; public class CustomPushNotification extends PushNotification { @@ -71,7 +68,7 @@ public class CustomPushNotification extends PushNotification { editor.apply(); } - Map> inputMap = new HashMap<>(); + Map> inputMap = new HashMap<>(); saveNotificationsMap(context, inputMap); } } catch (PackageManager.NameNotFoundException e) { @@ -79,55 +76,71 @@ public class CustomPushNotification extends PushNotification { } } - public static void cancelNotification(Context context, String channelId, Integer notificationId) { + public static void cancelNotification(Context context, String channelId, String rootId, Integer notificationId, Boolean isCRTEnabled) { if (!android.text.TextUtils.isEmpty(channelId)) { - Map> notificationsInChannel = loadNotificationsMap(context); - List notifications = notificationsInChannel.get(channelId); + final String notificationIdStr = notificationId.toString(); + final Boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId); + final String groupId = isThreadNotification ? rootId : channelId; + Map> notificationsInChannel = loadNotificationsMap(context); + Map notifications = notificationsInChannel.get(groupId); if (notifications == null) { return; } final NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.cancel(notificationId); - notifications.remove(notificationId); + notifications.remove(notificationIdStr); final StatusBarNotification[] statusNotifications = notificationManager.getActiveNotifications(); boolean hasMore = false; for (final StatusBarNotification status : statusNotifications) { - if (status.getNotification().extras.getString("channel_id").equals(channelId)) { - hasMore = true; + Bundle bundle = status.getNotification().extras; + if (isThreadNotification) { + hasMore = bundle.getString("root_id").equals(rootId); + } else { + hasMore = bundle.getString("channel_id").equals(channelId); + } + if (hasMore) { break; } } if (!hasMore) { - notificationsInChannel.remove(channelId); + notificationsInChannel.remove(groupId); + } else { + notificationsInChannel.put(groupId, notifications); } saveNotificationsMap(context, notificationsInChannel); } } - public static void clearChannelNotifications(Context context, String channelId) { + public static void clearChannelNotifications(Context context, String channelId, String rootId, Boolean isCRTEnabled) { if (!android.text.TextUtils.isEmpty(channelId)) { - Map> notificationsInChannel = loadNotificationsMap(context); - List notifications = notificationsInChannel.get(channelId); + final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + + // rootId is available only when CRT is enabled & clearing the thread + final boolean isClearThread = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId); + + Map> notificationsInChannel = loadNotificationsMap(context); + String groupId = isClearThread ? rootId : channelId; + Map notifications = notificationsInChannel.get(groupId); + if (notifications == null) { return; } - notificationsInChannel.remove(channelId); + notificationsInChannel.remove(groupId); saveNotificationsMap(context, notificationsInChannel); - for (final Integer notificationId : notifications) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - notificationManager.cancel(notificationId); - } + notifications.forEach( + (notificationIdStr, post) -> notificationManager.cancel(Integer.valueOf(notificationIdStr)) + ); } } public static void clearAllNotifications(Context context) { if (context != null) { - Map> notificationsInChannel = loadNotificationsMap(context); + Map> notificationsInChannel = loadNotificationsMap(context); notificationsInChannel.clear(); saveNotificationsMap(context, notificationsInChannel); final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); @@ -142,6 +155,8 @@ public class CustomPushNotification extends PushNotification { final String ackId = initialData.getString("ack_id"); final String postId = initialData.getString("post_id"); final String channelId = initialData.getString("channel_id"); + final String rootId = initialData.getString("root_id"); + final boolean isCRTEnabled = initialData.getString("is_crt_enabled") != null && initialData.getString("is_crt_enabled").equals("true"); final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true"); int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID; if (postId != null) { @@ -188,25 +203,40 @@ public class CustomPushNotification extends PushNotification { // Data will be fetch from within the JS context instead. dataHelper.fetchAndStoreDataForPushNotification(mNotificationProps.asBundle()); } + try { + JSONObject post = new JSONObject(); + if (!android.text.TextUtils.isEmpty(rootId)) { + post.put("root_id", rootId); + } + if (!android.text.TextUtils.isEmpty(postId)) { + post.put("post_id", postId); + } - Map> notificationsInChannel = loadNotificationsMap(mContext); - List list = notificationsInChannel.get(channelId); - if (list == null) { - list = Collections.synchronizedList(new ArrayList<>(0)); + final Boolean isThreadNotification = isCRTEnabled && post.has("root_id"); + final String groupId = isThreadNotification ? rootId : channelId; + + Map> notificationsInChannel = loadNotificationsMap(mContext); + Map notifications = notificationsInChannel.get(groupId); + if (notifications == null) { + notifications = Collections.synchronizedMap(new HashMap()); + } + + if (notifications.size() > 0) { + createSummary = false; + } + + notifications.put(String.valueOf(notificationId), post); + + if (createSummary) { + // Add the summary notification id as well + notifications.put(String.valueOf(notificationId + 1), new JSONObject()); + } + + notificationsInChannel.put(groupId, notifications); + saveNotificationsMap(mContext, notificationsInChannel); + } catch(Exception e) { + e.printStackTrace(); } - - list.add(0, notificationId); - if (list.size() > 1) { - createSummary = false; - } - - if (createSummary) { - // Add the summary notification id as well - list.add(0, notificationId + 1); - } - - notificationsInChannel.put(channelId, list); - saveNotificationsMap(mContext, notificationsInChannel); } } @@ -214,7 +244,7 @@ public class CustomPushNotification extends PushNotification { } break; case PUSH_TYPE_CLEAR: - clearChannelNotifications(mContext, channelId); + clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled); break; } @@ -229,21 +259,11 @@ public class CustomPushNotification extends PushNotification { Bundle data = mNotificationProps.asBundle(); final String channelId = data.getString("channel_id"); - final String postId = data.getString("post_id"); - Integer notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID; - - if (postId != null) { - notificationId = postId.hashCode(); - } + final String rootId = data.getString("root_id"); + final Boolean isCRTEnabled = data.getBoolean("is_crt_enabled"); if (channelId != null) { - Map> notificationsInChannel = loadNotificationsMap(mContext); - List notifications = notificationsInChannel.get(channelId); - if (notifications != null) { - notifications.remove(notificationId); - } - saveNotificationsMap(mContext, notificationsInChannel); - clearChannelNotifications(mContext, channelId); + clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled); } } @@ -293,7 +313,7 @@ public class CustomPushNotification extends PushNotification { return serverUrl; } - private static void saveNotificationsMap(Context context, Map> inputMap) { + private static void saveNotificationsMap(Context context, Map> inputMap) { SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE); if (pSharedPref != null) { JSONObject json = new JSONObject(inputMap); @@ -305,23 +325,41 @@ public class CustomPushNotification extends PushNotification { } } - private static Map> loadNotificationsMap(Context context) { - Map> outputMap = new HashMap<>(); + /** + * Map Structure + * + * { + * channel_id1 | thread_id1: { + * notification_id1: { + * post_id: 'p1', + * root_id: 'r1', + * } + * } + * } + * + */ + private static Map> loadNotificationsMap(Context context) { + Map> outputMap = new HashMap<>(); if (context != null) { SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE); try { if (pSharedPref != null) { String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_CHANNEL, (new JSONObject()).toString()); JSONObject json = new JSONObject(jsonString); - Iterator keysItr = json.keys(); - while (keysItr.hasNext()) { - String key = keysItr.next(); - JSONArray array = json.getJSONArray(key); - List values = new ArrayList<>(); - for (int i = 0; i < array.length(); ++i) { - values.add(array.getInt(i)); + + // Can be a channel_id or thread_id + Iterator groupIdsItr = json.keys(); + while (groupIdsItr.hasNext()) { + String groupId = groupIdsItr.next(); + JSONObject notificationsJSONObj = json.getJSONObject(groupId); + Map notifications = new HashMap<>(); + Iterator notificationIdKeys = notificationsJSONObj.keys(); + while(notificationIdKeys.hasNext()) { + String notificationId = notificationIdKeys.next(); + JSONObject post = notificationsJSONObj.getJSONObject(notificationId); + notifications.put(notificationId, post); } - outputMap.put(key, values); + outputMap.put(groupId, notifications); } } } catch (Exception e) { diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java index d17e58424..f770914c7 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java @@ -20,6 +20,9 @@ public class NotificationDismissService extends IntentService { final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent); final String channelId = bundle.getString("channel_id"); final String postId = bundle.getString("post_id"); + final String rootId = bundle.getString("root_id"); + final Boolean isCRTEnabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true"); + int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID; if (postId != null) { notificationId = postId.hashCode(); @@ -27,7 +30,7 @@ public class NotificationDismissService extends IntentService { notificationId = channelId.hashCode(); } - CustomPushNotification.cancelNotification(context, channelId, notificationId); + CustomPushNotification.cancelNotification(context, channelId, rootId, notificationId, isCRTEnabled); Log.i("ReactNative", "Dismiss notification"); } } diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationPreferencesModule.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationPreferencesModule.java index 8e5e5c365..59511d585 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationPreferencesModule.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationPreferencesModule.java @@ -51,6 +51,10 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule { WritableMap map = Arguments.createMap(); Notification n = sbn.getNotification(); Bundle bundle = n.extras; + String postId = bundle.getString("post_id"); + map.putString("post_id", postId); + String rootId = bundle.getString("root_id"); + map.putString("root_id", rootId); String channelId = bundle.getString("channel_id"); map.putString("channel_id", channelId); result.pushMap(map); @@ -59,8 +63,8 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule { } @ReactMethod - public void removeDeliveredNotifications(String channelId) { + public void removeDeliveredNotifications(String channelId, String rootId, Boolean isCRTEnabled) { Context context = mApplication.getApplicationContext(); - CustomPushNotification.clearChannelNotifications(context, channelId); + CustomPushNotification.clearChannelNotifications(context, channelId, rootId, isCRTEnabled); } } diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index 8ca979a01..4aff65d79 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -17,6 +17,7 @@ import { import {queryPreferencesByCategoryAndName} from '@queries/servers/preference'; import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId} from '@queries/servers/system'; import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team'; +import {getIsCRTEnabled} from '@queries/servers/thread'; import {getCurrentUser, queryUsersById} from '@queries/servers/user'; import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; @@ -175,7 +176,8 @@ export async function markChannelAsViewed(serverUrl: string, channelId: string, m.viewedAt = member.lastViewedAt; m.lastViewedAt = Date.now(); }); - PushNotifications.cancelChannelNotifications(channelId); + const isCRTEnabled = await getIsCRTEnabled(database); + PushNotifications.cancelChannelNotifications(channelId, undefined, isCRTEnabled); if (!prepareRecordsOnly) { await operator.batchRecords([member]); } diff --git a/app/actions/remote/thread.ts b/app/actions/remote/thread.ts index 26bf06ef2..15862278b 100644 --- a/app/actions/remote/thread.ts +++ b/app/actions/remote/thread.ts @@ -5,6 +5,7 @@ import {markTeamThreadsAsRead, processReceivedThreads, switchToThread, updateThr import {fetchPostThread} from '@actions/remote/post'; import {General} from '@constants'; import DatabaseManager from '@database/manager'; +import PushNotifications from '@init/push_notifications'; import NetworkManager from '@managers/network_manager'; import {getChannelById} from '@queries/servers/channel'; import {getPostById} from '@queries/servers/post'; @@ -156,7 +157,7 @@ export const updateTeamThreadsAsRead = async (serverUrl: string, teamId: string) } }; -export const markThreadAsRead = async (serverUrl: string, teamId: string, threadId: string) => { +export const markThreadAsRead = async (serverUrl: string, teamId: string | undefined, threadId: string) => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { @@ -187,6 +188,12 @@ export const markThreadAsRead = async (serverUrl: string, teamId: string, thread unread_mentions: 0, }); + const isCRTEnabled = await getIsCRTEnabled(database); + const post = await getPostById(database, threadId); + if (post) { + PushNotifications.cancelChannelNotifications(post.channelId, threadId, isCRTEnabled); + } + return {data}; } catch (error) { forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts index def9a826a..8c28f4744 100644 --- a/app/init/push_notifications.ts +++ b/app/init/push_notifications.ts @@ -17,6 +17,7 @@ import { import {storeDeviceToken} from '@actions/app/global'; import {markChannelAsViewed} from '@actions/local/channel'; import {backgroundNotification, openNotification} from '@actions/remote/notifications'; +import {markThreadAsRead} from '@actions/remote/thread'; import {Device, Events, Navigation, Screens} from '@constants'; import DatabaseManager from '@database/manager'; import {getTotalMentionsForServer} from '@database/subscription/unreads'; @@ -55,9 +56,9 @@ class PushNotifications { Notifications.cancelAllLocalNotifications(); }; - cancelChannelNotifications = async (channelId: string) => { + cancelChannelNotifications = async (channelId: string, rootId?: string, isCRTEnabled?: boolean) => { const notifications = await NativeNotifications.getDeliveredNotifications(); - this.cancelNotificationsForChannel(notifications, channelId); + this.cancelNotificationsForChannel(notifications, channelId, rootId, isCRTEnabled); }; cancelChannelsNotifications = async (channelIds: string[]) => { @@ -67,15 +68,25 @@ class PushNotifications { } }; - cancelNotificationsForChannel = (notifications: NotificationWithChannel[], channelId: string) => { + cancelNotificationsForChannel = (notifications: NotificationWithChannel[], channelId: string, rootId?: string, isCRTEnabled?: boolean) => { if (Platform.OS === 'android') { - NativeNotifications.removeDeliveredNotifications(channelId); + NativeNotifications.removeDeliveredNotifications(channelId, rootId, isCRTEnabled); } else { const ids: string[] = []; + const clearThreads = Boolean(rootId); for (const notification of notifications) { if (notification.channel_id === channelId) { - ids.push(notification.identifier); + let doesNotificationMatch = true; + if (clearThreads) { + doesNotificationMatch = notification.thread === rootId; + } else if (isCRTEnabled) { + // Do not match when CRT is enabled BUT post is not a root post + doesNotificationMatch = !notification.root_id; + } + if (doesNotificationMatch) { + ids.push(notification.identifier); + } } } @@ -128,9 +139,10 @@ class PushNotifications { if (database) { const isCRTEnabled = await getIsCRTEnabled(database); if (isCRTEnabled && payload.root_id) { - return; + markThreadAsRead(serverUrl, payload.team_id, payload.post_id); + } else { + markChannelAsViewed(serverUrl, payload.channel_id, false); } - markChannelAsViewed(serverUrl, payload.channel_id, false); } } }; diff --git a/ios/Mattermost/AppDelegate.mm b/ios/Mattermost/AppDelegate.mm index 379523022..5f860d1b4 100644 --- a/ios/Mattermost/AppDelegate.mm +++ b/ios/Mattermost/AppDelegate.mm @@ -133,6 +133,8 @@ MattermostBucket* bucket = nil; UIApplicationState state = [UIApplication sharedApplication].applicationState; NSString* action = [userInfo objectForKey:@"type"]; NSString* channelId = [userInfo objectForKey:@"channel_id"]; + NSString* rootId = [userInfo objectForKey:@"root_id"]; + BOOL isCRTEnabled = [userInfo objectForKey:@"is_crt_enabled"]; BOOL isClearAction = (action && [action isEqualToString: NOTIFICATION_CLEAR_ACTION]); BOOL isTestAction = (action && [action isEqualToString: NOTIFICATION_TEST_ACTION]); @@ -142,8 +144,11 @@ MattermostBucket* bucket = nil; } if (isClearAction) { + // When CRT is OFF: // If received a notification that a channel was read, remove all notifications from that channel (only with app in foreground/background) - [self cleanNotificationsFromChannel:channelId]; + // When CRT is ON: + // When rootId is nil, clear channel's root post notifications or else clear all thread notifications + [self cleanNotificationsFromChannel:channelId :rootId :isCRTEnabled]; [[GekidouWrapper default] postNotificationReceipt:userInfo]; } @@ -204,7 +209,7 @@ MattermostBucket* bucket = nil; return extraModules; } --(void)cleanNotificationsFromChannel:(NSString *)channelId { +-(void)cleanNotificationsFromChannel:(NSString *)channelId :(NSString *)rootId :(BOOL)isCRTEnabled { if ([UNUserNotificationCenter class]) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center getDeliveredNotificationsWithCompletionHandler:^(NSArray * _Nonnull notifications) { @@ -215,9 +220,23 @@ MattermostBucket* bucket = nil; UNNotificationContent *notificationContent = [notificationRequest content]; NSString *identifier = [notificationRequest identifier]; NSString* cId = [[notificationContent userInfo] objectForKey:@"channel_id"]; + NSString* pId = [[notificationContent userInfo] objectForKey:@"post_id"]; + NSString* rId = [[notificationContent userInfo] objectForKey:@"root_id"]; if ([cId isEqualToString: channelId]) { - [notificationIds addObject:identifier]; + BOOL doesNotificationMatch = true; + if (isCRTEnabled) { + // Check if it is a thread notification + if (rootId != nil) { + doesNotificationMatch = [pId isEqualToString: rootId] || [rId isEqualToString: rootId]; + } else { + // With CRT ON, remove notifications without rootId + doesNotificationMatch = rId == nil; + } + } + if (doesNotificationMatch) { + [notificationIds addObject:identifier]; + } } } diff --git a/types/global/push_notifications.d.ts b/types/global/push_notifications.d.ts index e8a49eff5..406cc99eb 100644 --- a/types/global/push_notifications.d.ts +++ b/types/global/push_notifications.d.ts @@ -49,4 +49,5 @@ interface NotificationWithData extends Notification { declare class NotificationWithChannel extends Notification { channel_id?: string; + root_id?: string; }