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:
parent
b4d7a23e54
commit
1eb046a7fd
12 changed files with 215 additions and 34 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@
|
|||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
|
|
|
|||
Loading…
Reference in a new issue