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 104eaf92f..7e0def40c 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java @@ -47,31 +47,46 @@ public class CustomPushNotification extends PushNotification { private static LinkedHashMap> channelIdToNotification = new LinkedHashMap>(); private static AppLifecycleFacade lifecycleFacade; private static Context context; + private static int badgeCount = 0; public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) { super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper); this.context = context; } - public static void clearNotification(int notificationId, String channelId) { + public static void clearNotification(Context mContext, int notificationId, String channelId) { if (notificationId != -1) { + Object objCount = channelIdToNotificationCount.get(channelId); + Integer count = -1; + + if (objCount != null) { + count = (Integer)objCount; + } + channelIdToNotificationCount.remove(channelId); channelIdToNotification.remove(channelId); - if (context != null) { - final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + if (mContext != null) { + final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(notificationId); + + if (count != -1) { + int total = CustomPushNotification.badgeCount - count; + int badgeCount = total < 0 ? 0 : total; + ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), badgeCount); + CustomPushNotification.badgeCount = badgeCount; + } } } } - public static void clearNotification(Context mContext, int notificationId, String channelId) { - if (notificationId != -1) { - channelIdToNotificationCount.remove(channelId); - channelIdToNotification.remove(channelId); - if (mContext != null) { - final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(notificationId); - } + public static void clearAllNotifications(Context mContext) { + channelIdToNotificationCount.clear(); + channelIdToNotification.clear(); + if (mContext != null) { + final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancelAll(); + ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), 0); } } @@ -119,7 +134,6 @@ public class CustomPushNotification extends PushNotification { channelIdToNotificationCount.remove(channelId); channelIdToNotification.remove(channelId); digestNotification(); - clearAllNotifications(); } @Override @@ -179,9 +193,11 @@ public class CustomPushNotification extends PushNotification { String largeIcon = bundle.getString("largeIcon"); Bundle b = bundle.getBundle("userInfo"); - if (b != null) { - notification.addExtras(b); + if (b == null) { + b = new Bundle(); } + b.putString("channel_id", channelId); + notification.addExtras(b); int smallIconResId; int largeIconResId; @@ -207,7 +223,8 @@ public class CustomPushNotification extends PushNotification { } if (numberString != null) { - ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString)); + CustomPushNotification.badgeCount = Integer.parseInt(numberString); + ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount); } int numMessages = getMessageCountInChannel(channelId); @@ -354,15 +371,11 @@ public class CustomPushNotification extends PushNotification { private void cancelNotification(Bundle data, int notificationId) { final String channelId = data.getString("channel_id"); + final String numberString = data.getString("badge"); - String numberString = data.getString("badge"); - if (numberString != null) { - ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString)); - } + CustomPushNotification.badgeCount = Integer.parseInt(numberString); + CustomPushNotification.clearNotification(mContext.getApplicationContext(), notificationId, channelId); - channelIdToNotificationCount.remove(channelId); - channelIdToNotification.remove(channelId); - final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(notificationId); + ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount); } } diff --git a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotificationDrawer.java b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotificationDrawer.java new file mode 100644 index 000000000..160a446be --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotificationDrawer.java @@ -0,0 +1,39 @@ +package com.mattermost.rnbeta; + +import android.content.Context; + +import com.wix.reactnativenotifications.core.AppLaunchHelper; +import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer; +import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer; +import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication; +import com.wix.reactnativenotifications.helpers.PushNotificationHelper; +import static com.wix.reactnativenotifications.Defs.LOGTAG; + +public class CustomPushNotificationDrawer extends PushNotificationsDrawer { + final protected Context mContext; + final protected AppLaunchHelper mAppLaunchHelper; + + protected CustomPushNotificationDrawer(Context context, AppLaunchHelper appLaunchHelper) { + super(context, appLaunchHelper); + mContext = context; + mAppLaunchHelper = appLaunchHelper; + } + + @Override + public void onAppInit() { + } + + @Override + public void onAppVisible() { + } + + @Override + public void onNotificationOpened() { + } + + @Override + public void onCancelAllLocalNotifications() { + CustomPushNotification.clearAllNotifications(mContext); + cancelAllScheduledNotifications(); + } +} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java index 2b32502dc..2a5d68d80 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -34,6 +34,8 @@ import com.reactnativenavigation.NavigationApplication; import com.wix.reactnativenotifications.RNNotificationsPackage; import com.wix.reactnativenotifications.core.notification.INotificationsApplication; import com.wix.reactnativenotifications.core.notification.IPushNotification; +import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer; +import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication; import com.wix.reactnativenotifications.core.AppLaunchHelper; import com.wix.reactnativenotifications.core.AppLifecycleFacade; import com.wix.reactnativenotifications.core.JsIOHelper; @@ -41,7 +43,7 @@ import com.wix.reactnativenotifications.core.JsIOHelper; import java.util.Arrays; import java.util.List; -public class MainApplication extends NavigationApplication implements INotificationsApplication { +public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication { public NotificationsLifecycleFacade notificationsLifecycleFacade; public Boolean sharedExtensionIsOpened = false; public Boolean replyFromPushNotification = false; @@ -117,4 +119,9 @@ public class MainApplication extends NavigationApplication implements INotificat new JsIOHelper() ); } + + @Override + public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) { + return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper); + } } 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 cf4a1010d..432a84137 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationPreferencesModule.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationPreferencesModule.java @@ -1,12 +1,15 @@ package com.mattermost.rnbeta; import android.app.Application; +import android.app.Notification; +import android.app.NotificationManager; import android.content.Context; import android.database.Cursor; import android.media.Ringtone; import android.media.RingtoneManager; import android.os.Bundle; import android.net.Uri; +import android.service.notification.StatusBarNotification; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.NativeModule; @@ -105,4 +108,29 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule { public void setShouldBlink(boolean blink) { mNotificationPreference.setShouldBlink(blink); } + + @ReactMethod + public void getDeliveredNotifications(final Promise promise) { + Context context = mApplication.getApplicationContext(); + final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + StatusBarNotification[] statusBarNotifications = notificationManager.getActiveNotifications(); + WritableArray result = Arguments.createArray(); + for (StatusBarNotification sbn:statusBarNotifications) { + WritableMap map = Arguments.createMap(); + Notification n = sbn.getNotification(); + Bundle bundle = n.extras; + int identifier = sbn.getId(); + String channelId = bundle.getString("channel_id"); + map.putInt("identifier", identifier); + map.putString("channel_id", channelId); + result.pushMap(map); + } + promise.resolve(result); + } + + @ReactMethod + public void removeDeliveredNotifications(int identifier, String channelId) { + Context context = mApplication.getApplicationContext(); + CustomPushNotification.clearNotification(context, identifier, channelId); + } } diff --git a/app/components/network_indicator/index.js b/app/components/network_indicator/index.js index 1a335d86e..f4c5dff71 100644 --- a/app/components/network_indicator/index.js +++ b/app/components/network_indicator/index.js @@ -6,6 +6,7 @@ import {connect} from 'react-redux'; import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates, logout} from 'mattermost-redux/actions/users'; import {init as initWebSocket, close as closeWebSocket} from 'mattermost-redux/actions/websocket'; +import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; import {connection} from 'app/actions/device'; import {getConnection, isLandscape} from 'app/selectors/device'; @@ -17,6 +18,7 @@ function mapStateToProps(state) { const websocketStatus = websocket.status; return { + currentChannelId: getCurrentChannelId(state), isLandscape: isLandscape(state), isOnline: getConnection(state), websocketErrorCount: websocket.error, diff --git a/app/components/network_indicator/network_indicator.js b/app/components/network_indicator/network_indicator.js index f178150cc..64ea491d8 100644 --- a/app/components/network_indicator/network_indicator.js +++ b/app/components/network_indicator/network_indicator.js @@ -19,6 +19,7 @@ import IonIcon from 'react-native-vector-icons/Ionicons'; import FormattedText from 'app/components/formatted_text'; import {DeviceTypes, ViewTypes} from 'app/constants'; import mattermostBucket from 'app/mattermost_bucket'; +import PushNotifications from 'app/push_notifications'; import networkConnectionListener, {checkConnection} from 'app/utils/network'; import {t} from 'app/utils/i18n'; import LocalConfig from 'assets/config'; @@ -48,6 +49,7 @@ export default class NetworkIndicator extends PureComponent { startPeriodicStatusUpdates: PropTypes.func.isRequired, stopPeriodicStatusUpdates: PropTypes.func.isRequired, }).isRequired, + currentChannelId: PropTypes.string, isLandscape: PropTypes.bool, isOnline: PropTypes.bool, websocketErrorCount: PropTypes.number, @@ -187,7 +189,14 @@ export default class NetworkIndicator extends PureComponent { }; handleAppStateChange = async (appState) => { - this.handleWebSocket(appState === 'active'); + const {currentChannelId} = this.props; + const active = appState === 'active'; + + this.handleWebSocket(active); + + if (active && currentChannelId) { + PushNotifications.clearChannelNotifications(currentChannelId); + } }; handleConnectionChange = (isConnected) => { @@ -209,7 +218,7 @@ export default class NetworkIndicator extends PureComponent { this.connect(); } }, CONNECTION_RETRY_TIMEOUT); - } + }; initializeWebSocket = async () => { const {formatMessage} = this.context.intl; diff --git a/app/push_notifications/push_notifications.android.js b/app/push_notifications/push_notifications.android.js index 9a93d2886..9e30212f1 100644 --- a/app/push_notifications/push_notifications.android.js +++ b/app/push_notifications/push_notifications.android.js @@ -1,12 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {AppRegistry, AppState} from 'react-native'; +import {AppRegistry, AppState, NativeModules} from 'react-native'; import {NotificationsAndroid, PendingNotifications} from 'react-native-notifications'; import Notification from 'react-native-notifications/notification.android'; import {emptyFunction} from 'app/utils/general'; +const {NotificationPreferences} = NativeModules; + class PushNotification { constructor() { this.onRegister = null; @@ -120,6 +122,14 @@ class PushNotification { resetNotification() { this.deviceNotification = null; } + + async clearChannelNotifications(channelId) { + const notifications = await NotificationPreferences.getDeliveredNotifications(); + const notificationForChannel = notifications.find((n) => n.channel_id === channelId); + if (notificationForChannel) { + NotificationPreferences.removeDeliveredNotifications(notificationForChannel.identifier, channelId); + } + } } export default new PushNotification(); diff --git a/app/push_notifications/push_notifications.ios.js b/app/push_notifications/push_notifications.ios.js index b1e5d2b4c..62aec806a 100644 --- a/app/push_notifications/push_notifications.ios.js +++ b/app/push_notifications/push_notifications.ios.js @@ -137,7 +137,8 @@ class PushNotification { } setApplicationIconBadgeNumber(number) { - NotificationsIOS.setBadgesCount(number); + const count = number < 0 ? 0 : number; + NotificationsIOS.setBadgesCount(count); } getNotification() { @@ -147,6 +148,23 @@ class PushNotification { resetNotification() { this.deviceNotification = null; } + + clearChannelNotifications(channelId) { + NotificationsIOS.getDeliveredNotifications((notifications) => { + const ids = []; + for (let i = 0; i < notifications.length; i++) { + const notification = notifications[i]; + + if (notification.userInfo.channel_id === channelId) { + ids.push(notification.identifier); + } + } + + if (ids.length) { + NotificationsIOS.removeDeliveredNotifications(ids); + } + }); + } } export default new PushNotification(); diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index 482a98fe4..6e2ec37e2 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -25,6 +25,7 @@ import StatusBar from 'app/components/status_bar'; import {DeviceTypes, ViewTypes} from 'app/constants'; import {preventDoubleTap} from 'app/utils/tap'; import PostTextbox from 'app/components/post_textbox'; +import PushNotifications from 'app/push_notifications'; import tracker from 'app/utils/time_tracker'; import LocalConfig from 'assets/config'; @@ -109,6 +110,11 @@ export default class Channel extends PureComponent { this.loadChannels(nextProps.currentTeamId); } + if (nextProps.currentChannelId !== this.props.currentChannelId && + nextProps.currentTeamId === this.props.currentTeamId) { + PushNotifications.clearChannelNotifications(nextProps.currentChannelId); + } + if (LocalConfig.EnableMobileClientUpgrade && !ClientUpgradeListener) { ClientUpgradeListener = require('app/components/client_upgrade_listener').default; } @@ -294,7 +300,6 @@ export default class Channel extends PureComponent { const loaderDimensions = this.channelLoaderDimensions(); - // console.warn('height', height, Date.now()) return ( @implementation AppDelegate +NSString* const NotificationClearAction = @"clear"; + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Clear keychain on first run in case of reinstallation @@ -69,9 +72,54 @@ [RNNotifications didFailToRegisterForRemoteNotificationsWithError:error]; } +-(void)cleanNotificationsFromChannel:(NSString *)channelId andUpdateBadge:(BOOL)updateBadge { + if ([UNUserNotificationCenter class]) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center getDeliveredNotificationsWithCompletionHandler:^(NSArray * _Nonnull notifications) { + NSMutableArray *notificationIds = [NSMutableArray new]; + + for (UNNotification *prevNotification in notifications) { + UNNotificationRequest *notificationRequest = [prevNotification request]; + UNNotificationContent *notificationContent = [notificationRequest content]; + NSString *identifier = [notificationRequest identifier]; + NSString* cId = [[notificationContent userInfo] objectForKey:@"channel_id"]; + + if ([cId isEqualToString: channelId]) { + [notificationIds addObject:identifier]; + } + } + + [center removeDeliveredNotificationsWithIdentifiers:notificationIds]; + NSInteger removed = (NSInteger)[notificationIds count] + 1; + if (removed > 0 && updateBadge) { + NSInteger badge = [UIApplication sharedApplication].applicationIconBadgeNumber; + NSInteger count = badge - removed; + if (count > 0) { + [[UIApplication sharedApplication] setApplicationIconBadgeNumber:count]; + } else { + [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; + } + } + }]; + } +} + // Required for the notification event. -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification { - [RNNotifications didReceiveRemoteNotification:notification]; +-(void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler { + UIApplicationState state = [UIApplication sharedApplication].applicationState; + NSString* action = [userInfo objectForKey:@"type"]; + NSString* channelId = [userInfo objectForKey:@"channel_id"]; + + if (action && [action isEqualToString: NotificationClearAction]) { + // If received a notification that a channel was read, remove all notifications from that channel (only with app in foreground/background) + [self cleanNotificationsFromChannel:channelId andUpdateBadge:NO]; + } else if (state == UIApplicationStateInactive) { + // When the notification is opened + [self cleanNotificationsFromChannel:channelId andUpdateBadge:YES]; + } + + [RNNotifications didReceiveRemoteNotification:userInfo]; + completionHandler(UIBackgroundFetchResultNoData); } // Required for the localNotification event. diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index bb964d4b1..6a696d450 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -117,6 +117,7 @@ UIBackgroundModes fetch + remote-notification UILaunchStoryboardName LaunchScreen