[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 <mattermod@users.noreply.github.com>
This commit is contained in:
Anurag Shivarathri 2022-06-30 21:26:14 +05:30 committed by GitHub
parent f1665fa0ee
commit 306fbba3a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 178 additions and 78 deletions

View file

@ -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);

View file

@ -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<String, List<Integer>> inputMap = new HashMap<>();
Map<String, Map<String, JSONObject>> 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<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
List<Integer> 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<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
Map<String, JSONObject> 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<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
List<Integer> 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<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
String groupId = isClearThread ? rootId : channelId;
Map<String, JSONObject> 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<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
Map<String, Map<String, JSONObject>> 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<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
List<Integer> 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<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(mContext);
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
if (notifications == null) {
notifications = Collections.synchronizedMap(new HashMap<String, JSONObject>());
}
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<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
List<Integer> 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<String, List<Integer>> inputMap) {
private static void saveNotificationsMap(Context context, Map<String, Map<String, JSONObject>> 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<String, List<Integer>> loadNotificationsMap(Context context) {
Map<String, List<Integer>> outputMap = new HashMap<>();
/**
* Map Structure
*
* {
* channel_id1 | thread_id1: {
* notification_id1: {
* post_id: 'p1',
* root_id: 'r1',
* }
* }
* }
*
*/
private static Map<String, Map<String, JSONObject>> loadNotificationsMap(Context context) {
Map<String, Map<String, JSONObject>> 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<String> keysItr = json.keys();
while (keysItr.hasNext()) {
String key = keysItr.next();
JSONArray array = json.getJSONArray(key);
List<Integer> 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<String> groupIdsItr = json.keys();
while (groupIdsItr.hasNext()) {
String groupId = groupIdsItr.next();
JSONObject notificationsJSONObj = json.getJSONObject(groupId);
Map<String, JSONObject> notifications = new HashMap<>();
Iterator<String> 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) {

View file

@ -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");
}
}

View file

@ -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);
}
}

View file

@ -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]);
}

View file

@ -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);

View file

@ -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);
}
}
};

View file

@ -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<UNNotification *> * _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];
}
}
}

View file

@ -49,4 +49,5 @@ interface NotificationWithData extends Notification {
declare class NotificationWithChannel extends Notification {
channel_id?: string;
root_id?: string;
}