Android/iOS leave notifications until channel is read (#2321)

* iOS clear notifications from a specific channel when clear push notification received

* Android leave notifications until channel is read

* use redux-connect currentChannelId in network indicator
This commit is contained in:
Elias Nahum 2018-11-19 15:20:44 -03:00 committed by Harrison Healey
parent b4d7a23e54
commit 1eb046a7fd
12 changed files with 215 additions and 34 deletions

View file

@ -47,31 +47,46 @@ public class CustomPushNotification extends PushNotification {
private static LinkedHashMap<String,List<Bundle>> channelIdToNotification = new LinkedHashMap<String,List<Bundle>>();
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);
}
}

View file

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

View file

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

View file

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

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -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 (
<MainSidebar
ref={this.channelSidebarRef}

View file

@ -60,11 +60,12 @@ class ChannelDrawerButton extends PureComponent {
componentDidMount() {
EventEmitter.on('drawer_opacity', this.setOpacity);
PushNotifications.setApplicationIconBadgeNumber(this.props.mentionCount);
}
componentWillReceiveProps(nextProps) {
PushNotifications.setApplicationIconBadgeNumber(nextProps.mentionCount);
componentDidUpdate(prevProps) {
if (prevProps.mentionCount !== this.props.mentionCount) {
PushNotifications.setApplicationIconBadgeNumber(this.props.mentionCount);
}
}
componentWillUnmount() {

View file

@ -20,9 +20,12 @@
#import "RCCManager.h"
#import "RNNotifications.h"
#import "SessionManager.h"
#import <UserNotifications/UserNotifications.h>
@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<UNNotification *> * _Nonnull notifications) {
NSMutableArray<NSString *> *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.

View file

@ -117,6 +117,7 @@
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>