diff --git a/app/push_notifications/push_notifications.android.js b/app/push_notifications/push_notifications.android.js
index 9b15f6116..97ac14965 100644
--- a/app/push_notifications/push_notifications.android.js
+++ b/app/push_notifications/push_notifications.android.js
@@ -95,7 +95,8 @@ class PushNotification {
}
setApplicationIconBadgeNumber(number) {
- NotificationsAndroid.setBadgesCount(number);
+ const count = number < 0 ? 0 : number;
+ NotificationsAndroid.setBadgesCount(count);
}
getNotification() {
@@ -114,14 +115,9 @@ class PushNotification {
}
}
- clearForegroundNotifications = () => {
- // TODO: Implement as part of https://mattermost.atlassian.net/browse/MM-17110
- };
-
clearNotifications = () => {
this.setApplicationIconBadgeNumber(0);
this.cancelAllLocalNotifications(); // TODO: Only cancel the local notifications that belong to this server
- this.clearForegroundNotifications(); // TODO: Only clear the foreground notifications that belong to this server
}
}
diff --git a/app/push_notifications/push_notifications.ios.js b/app/push_notifications/push_notifications.ios.js
index 2130d8582..dae936ecf 100644
--- a/app/push_notifications/push_notifications.ios.js
+++ b/app/push_notifications/push_notifications.ios.js
@@ -2,14 +2,12 @@
// See LICENSE.txt for license information.
import {AppState} from 'react-native';
-import AsyncStorage from '@react-native-community/async-storage';
import NotificationsIOS, {NotificationAction, NotificationCategory} from 'react-native-notifications';
import ephemeralStore from 'app/store/ephemeral_store';
const CATEGORY = 'CAN_REPLY';
const REPLY_ACTION = 'REPLY_ACTION';
-export const FOREGROUND_NOTIFICATIONS_KEY = '@FOREGROUND_NOTIFICATIONS';
let replyCategory;
const replies = new Set();
@@ -53,10 +51,6 @@ class PushNotification {
if (this.onNotification) {
this.onNotification(this.deviceNotification);
}
-
- if (foreground) {
- this.trackForegroundNotification(data.channel_id);
- }
};
handleReply = (action, completed) => {
@@ -139,10 +133,6 @@ class PushNotification {
message: notification.getMessage(),
};
this.handleNotification(info, true, false);
-
- NotificationsIOS.getBadgesCount((count) => {
- this.setApplicationIconBadgeNumber(count + 1);
- });
};
onNotificationOpened = (notification) => {
@@ -173,21 +163,7 @@ class PushNotification {
}
clearChannelNotifications(channelId) {
- NotificationsIOS.getDeliveredNotifications(async (notifications) => {
- let foregroundNotifications;
- try {
- const value = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
- foregroundNotifications = JSON.parse(value) || {};
- } catch (e) {
- foregroundNotifications = {};
- }
-
- Reflect.deleteProperty(foregroundNotifications, channelId);
- AsyncStorage.setItem(FOREGROUND_NOTIFICATIONS_KEY, JSON.stringify(foregroundNotifications));
-
- const foregroundCount = Object.values(foregroundNotifications).reduce((a, b) => a + b, 0);
- let badgeCount = notifications.length + foregroundCount;
-
+ NotificationsIOS.getDeliveredNotifications((notifications) => {
const ids = [];
for (let i = 0; i < notifications.length; i++) {
const notification = notifications[i];
@@ -198,32 +174,14 @@ class PushNotification {
}
if (ids.length) {
- badgeCount -= ids.length;
NotificationsIOS.removeDeliveredNotifications(ids);
}
-
- this.setApplicationIconBadgeNumber(badgeCount);
});
}
- trackForegroundNotification = async (channelId) => {
- const value = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
- const foregroundNotifications = value ? JSON.parse(value) : {};
- if (!foregroundNotifications.hasOwnProperty(channelId)) {
- foregroundNotifications[channelId] = 0;
- }
- foregroundNotifications[channelId] += 1;
- await AsyncStorage.setItem(FOREGROUND_NOTIFICATIONS_KEY, JSON.stringify(foregroundNotifications));
- }
-
- clearForegroundNotifications = () => {
- AsyncStorage.removeItem(FOREGROUND_NOTIFICATIONS_KEY);
- };
-
clearNotifications = () => {
this.setApplicationIconBadgeNumber(0);
this.cancelAllLocalNotifications(); // TODO: Only cancel the local notifications that belong to this server
- this.clearForegroundNotifications(); // TODO: Only clear the foreground notifications that belong to this server
}
}
diff --git a/app/push_notifications/push_notifications.ios.test.js b/app/push_notifications/push_notifications.ios.test.js
index 0dda9a7fa..51d249cb1 100644
--- a/app/push_notifications/push_notifications.ios.test.js
+++ b/app/push_notifications/push_notifications.ios.test.js
@@ -1,9 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import AsyncStorage from '@react-native-community/async-storage';
import NotificationsIOS from 'react-native-notifications';
-import PushNotification, {FOREGROUND_NOTIFICATIONS_KEY} from './push_notifications.ios';
+import PushNotification from './push_notifications.ios';
jest.mock('react-native-notifications', () => {
let badgesCount = 0;
@@ -33,58 +32,8 @@ jest.mock('react-native-notifications', () => {
describe('PushNotification', () => {
const channel1ID = 'channel-1-id';
const channel2ID = 'channel-2-id';
- const notification = {
- getData: jest.fn(),
- getMessage: jest.fn(),
- };
- afterEach(() => AsyncStorage.clear());
-
- it('should track foreground notifications for channel', async () => {
- let item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
- expect(item).toBe(null);
-
- await PushNotification.trackForegroundNotification(channel1ID);
- item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
- expect(item).not.toBe(null);
- let foregroundNotifications = JSON.parse(item);
- expect(foregroundNotifications[channel1ID]).toBe(1);
- expect(foregroundNotifications[channel2ID]).toBe(undefined);
-
- await PushNotification.trackForegroundNotification(channel1ID);
- item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
- expect(item).not.toBe(null);
- foregroundNotifications = JSON.parse(item);
- expect(foregroundNotifications[channel1ID]).toBe(2);
- expect(foregroundNotifications[channel2ID]).toBe(undefined);
- });
-
- it('should NOT track foreground notifications for channel when opened', async () => {
- let item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
- expect(item).toBe(null);
-
- PushNotification.trackForegroundNotification = jest.fn();
- PushNotification.onNotificationOpened(notification);
- expect(PushNotification.trackForegroundNotification).not.toBeCalled();
- item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
- expect(item).toBe(null);
- });
-
- it('should increment badge number when foreground notification is received', () => {
- const setApplicationIconBadgeNumber = jest.spyOn(PushNotification, 'setApplicationIconBadgeNumber');
-
- NotificationsIOS.getBadgesCount((count) => expect(count).toBe(0));
-
- PushNotification.onNotificationReceivedForeground(notification);
- expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(1);
- NotificationsIOS.getBadgesCount((count) => expect(count).toBe(1));
-
- PushNotification.onNotificationReceivedForeground(notification);
- expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(2);
- NotificationsIOS.getBadgesCount((count) => expect(count).toBe(2));
- });
-
- it('should clear channel notifications and set correct badge number', async () => {
+ it('should clear channel notifications', async () => {
const deliveredNotifications = [
// Three channel1 delivered notifications
@@ -113,20 +62,13 @@ describe('PushNotification', () => {
];
NotificationsIOS.setDeliveredNotifications(deliveredNotifications);
- const foregroundNotifications = {
- [channel1ID]: 1,
- [channel2ID]: 1,
- };
- await AsyncStorage.setItem(FOREGROUND_NOTIFICATIONS_KEY, JSON.stringify(foregroundNotifications));
-
- const notificationCount = deliveredNotifications.length + Object.values(foregroundNotifications).reduce((a, b) => a + b);
- expect(notificationCount).toBe(7);
+ const notificationCount = deliveredNotifications.length;
+ expect(notificationCount).toBe(5);
NotificationsIOS.setBadgesCount(notificationCount);
NotificationsIOS.getBadgesCount((count) => expect(count).toBe(notificationCount));
// Clear channel1 notifications
- const setApplicationIconBadgeNumber = jest.spyOn(PushNotification, 'setApplicationIconBadgeNumber');
await PushNotification.clearChannelNotifications(channel1ID);
await NotificationsIOS.getDeliveredNotifications(async (deliveredNotifs) => {
@@ -135,30 +77,17 @@ describe('PushNotification', () => {
const channel2DeliveredNotifications = deliveredNotifs.filter((n) => n.userInfo.channel_id === channel2ID);
expect(channel1DeliveredNotifications.length).toBe(0);
expect(channel2DeliveredNotifications.length).toBe(2);
-
- const item = await AsyncStorage.getItem(FOREGROUND_NOTIFICATIONS_KEY);
- const foregroundNotifs = JSON.parse(item);
-
- const channel1ForegroundNotifications = foregroundNotifs[channel1ID];
- const channel2ForegroundNotifications = foregroundNotifs[channel2ID];
- expect(channel1ForegroundNotifications).toBe(undefined);
- expect(channel2ForegroundNotifications).toBe(1);
-
- const badgeNumber = channel2DeliveredNotifications.length + channel2ForegroundNotifications;
- expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(badgeNumber);
});
});
it('should clear all notifications', () => {
const setApplicationIconBadgeNumber = jest.spyOn(PushNotification, 'setApplicationIconBadgeNumber');
const cancelAllLocalNotifications = jest.spyOn(PushNotification, 'cancelAllLocalNotifications');
- const clearForegroundNotifications = jest.spyOn(PushNotification, 'clearForegroundNotifications');
PushNotification.clearNotifications();
expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(0);
expect(NotificationsIOS.setBadgesCount).toHaveBeenCalledWith(0);
expect(cancelAllLocalNotifications).toHaveBeenCalled();
expect(NotificationsIOS.cancelAllLocalNotifications).toHaveBeenCalled();
- expect(clearForegroundNotifications).toHaveBeenCalled();
});
});
diff --git a/app/screens/channel/channel_nav_bar/channel_drawer_button/__snapshots__/channel_drawer_button.test.js.snap b/app/screens/channel/channel_nav_bar/channel_drawer_button/__snapshots__/channel_drawer_button.test.js.snap
new file mode 100644
index 000000000..df71b3cb8
--- /dev/null
+++ b/app/screens/channel/channel_nav_bar/channel_drawer_button/__snapshots__/channel_drawer_button.test.js.snap
@@ -0,0 +1,102 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ChannelDrawerButton should match, full snapshot 1`] = `
+
+
+
+
+
+
+
+`;
+
+exports[`ChannelDrawerButton should match, full snapshot 2`] = `
+
+
+
+
+
+
+
+
+`;
diff --git a/app/screens/channel/channel_nav_bar/channel_drawer_button.js b/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.js
similarity index 69%
rename from app/screens/channel/channel_nav_bar/channel_drawer_button.js
rename to app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.js
index 4111d6977..9de6e7955 100644
--- a/app/screens/channel/channel_nav_bar/channel_drawer_button.js
+++ b/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.js
@@ -3,7 +3,6 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
-import {connect} from 'react-redux';
import {
TouchableOpacity,
View,
@@ -12,34 +11,44 @@ import {
import Icon from 'react-native-vector-icons/Ionicons';
import Badge from 'app/components/badge';
-import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+import PushNotifications from 'app/push_notifications';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import telemetry from 'app/telemetry';
-import {getUnreadsInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
-import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
-
-class ChannelDrawerButton extends PureComponent {
+export default class ChannelDrawerButton extends PureComponent {
static propTypes = {
- currentTeamId: PropTypes.string.isRequired,
openDrawer: PropTypes.func.isRequired,
- messageCount: PropTypes.number,
- mentionCount: PropTypes.number,
- myTeamMembers: PropTypes.object,
+ badgeCount: PropTypes.number,
theme: PropTypes.object,
visible: PropTypes.bool,
};
static defaultProps = {
+ badgeCount: 0,
currentChannel: {},
theme: {},
- messageCount: 0,
- mentionCount: 0,
visible: true,
};
+ componentDidMount() {
+ if (this.props.badgeCount > 0) {
+ // Only set the icon badge number if once the component mounts we have at least one mention
+ // reason is to prevent the notification in the notification center to get cleared
+ // while the app is retrieving unread mentions from the server
+ PushNotifications.setApplicationIconBadgeNumber(this.props.badgeCount);
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ // Once the component updates we know for sure if there are or not mentions when it mounted
+ // a) the app had mentions
+ if (prevProps.badgeCount !== this.props.badgeCount) {
+ PushNotifications.setApplicationIconBadgeNumber(this.props.badgeCount);
+ }
+ }
+
handlePress = preventDoubleTap(() => {
telemetry.start(['channel:open_drawer']);
this.props.openDrawer();
@@ -47,32 +56,13 @@ class ChannelDrawerButton extends PureComponent {
render() {
const {
- currentTeamId,
- mentionCount,
- messageCount,
- myTeamMembers,
+ badgeCount,
theme,
visible,
} = this.props;
const style = getStyleFromTheme(theme);
- let mentions = mentionCount;
- let messages = messageCount;
-
- const members = Object.values(myTeamMembers).filter((m) => m.team_id !== currentTeamId);
- members.forEach((m) => {
- mentions += (m.mention_count || 0);
- messages += (m.msg_count || 0);
- });
-
- let badgeCount = 0;
- if (mentions) {
- badgeCount = mentions;
- } else if (messages) {
- badgeCount = -1;
- }
-
let badge;
if (badgeCount && visible) {
badge = (
@@ -156,14 +146,3 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
},
};
});
-
-function mapStateToProps(state) {
- return {
- currentTeamId: getCurrentTeamId(state),
- myTeamMembers: getTeamMemberships(state),
- theme: getTheme(state),
- ...getUnreadsInCurrentTeam(state),
- };
-}
-
-export default connect(mapStateToProps)(ChannelDrawerButton);
diff --git a/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.test.js b/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.test.js
new file mode 100644
index 000000000..cd9eef22a
--- /dev/null
+++ b/app/screens/channel/channel_nav_bar/channel_drawer_button/channel_drawer_button.test.js
@@ -0,0 +1,129 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {shallow} from 'enzyme';
+import NotificationsIOS from 'react-native-notifications';
+
+import Preferences from 'mattermost-redux/constants/preferences';
+
+import Badge from 'app/components/badge';
+import PushNotification from 'app/push_notifications/push_notifications.ios';
+
+import ChannelDrawerButton from './channel_drawer_button';
+
+jest.mock('react-native-notifications', () => {
+ let badgesCount = 0;
+ let deliveredNotifications = {};
+
+ return {
+ getBadgesCount: jest.fn((callback) => callback(badgesCount)),
+ setBadgesCount: jest.fn((count) => {
+ badgesCount = count;
+ }),
+ addEventListener: jest.fn(),
+ setDeliveredNotifications: jest.fn((notifications) => {
+ deliveredNotifications = notifications;
+ }),
+ getDeliveredNotifications: jest.fn(async (callback) => {
+ await callback(deliveredNotifications);
+ }),
+ removeDeliveredNotifications: jest.fn((ids) => {
+ deliveredNotifications = deliveredNotifications.filter((n) => !ids.includes(n.identifier));
+ }),
+ cancelAllLocalNotifications: jest.fn(),
+ NotificationAction: jest.fn(),
+ NotificationCategory: jest.fn(),
+ };
+});
+
+describe('ChannelDrawerButton', () => {
+ const baseProps = {
+ openDrawer: jest.fn(),
+ badgeCount: 0,
+ theme: Preferences.THEMES.default,
+ visible: false,
+ };
+
+ afterEach(() => NotificationsIOS.setBadgesCount(0));
+
+ test('should match, full snapshot', () => {
+ const wrapper = shallow(
+
+ );
+
+ // no badge to show
+ expect(wrapper.getElement()).toMatchSnapshot();
+ expect(wrapper.find(Badge).length).toEqual(0);
+
+ // badge should render
+ wrapper.setProps({badgeCount: 1, visible: true});
+ expect(wrapper.getElement()).toMatchSnapshot();
+ expect(wrapper.find(Badge).length).toEqual(1);
+ });
+
+ test('should not set app icon badge on mount', () => {
+ const setApplicationIconBadgeNumber = jest.spyOn(PushNotification, 'setApplicationIconBadgeNumber');
+ const props = {
+ ...baseProps,
+ badgeCount: 0,
+ };
+
+ shallow(
+
+ );
+ expect(setApplicationIconBadgeNumber).not.toBeCalled();
+ NotificationsIOS.getBadgesCount((count) => expect(count).toBe(0));
+ });
+
+ test('should set app icon badge on mount', () => {
+ const setApplicationIconBadgeNumber = jest.spyOn(PushNotification, 'setApplicationIconBadgeNumber');
+ const props = {
+ ...baseProps,
+ badgeCount: 1,
+ };
+
+ shallow(
+
+ );
+ expect(setApplicationIconBadgeNumber).toHaveBeenCalledTimes(1);
+ NotificationsIOS.getBadgesCount((count) => expect(count).toBe(1));
+ });
+
+ test('should set app icon badge update', () => {
+ const setApplicationIconBadgeNumber = jest.spyOn(PushNotification, 'setApplicationIconBadgeNumber');
+ const props = {
+ ...baseProps,
+ badgeCount: 0,
+ };
+
+ const wrapper = shallow(
+
+ );
+ NotificationsIOS.getBadgesCount((count) => expect(count).toBe(0));
+
+ wrapper.setProps({badgeCount: 2});
+ expect(setApplicationIconBadgeNumber).toHaveBeenCalledTimes(1);
+ expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(2);
+ NotificationsIOS.getBadgesCount((count) => expect(count).toBe(2));
+ });
+
+ test('should set remove icon badge on update', () => {
+ const setApplicationIconBadgeNumber = jest.spyOn(PushNotification, 'setApplicationIconBadgeNumber');
+ const props = {
+ ...baseProps,
+ badgeCount: 0,
+ };
+
+ const wrapper = shallow(
+
+ );
+ wrapper.setProps({badgeCount: 2});
+ expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(2);
+ NotificationsIOS.getBadgesCount((count) => expect(count).toBe(2));
+
+ wrapper.setProps({badgeCount: -1});
+ expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(-1);
+ NotificationsIOS.getBadgesCount((count) => expect(count).toBe(0));
+ });
+});
diff --git a/app/screens/channel/channel_nav_bar/channel_drawer_button/index.js b/app/screens/channel/channel_nav_bar/channel_drawer_button/index.js
new file mode 100644
index 000000000..535664510
--- /dev/null
+++ b/app/screens/channel/channel_nav_bar/channel_drawer_button/index.js
@@ -0,0 +1,48 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+import {createSelector} from 'reselect';
+
+import {getUnreadsInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
+import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+
+import ChannelDrawerButton from './channel_drawer_button';
+
+const getBadgeCount = createSelector(
+ getCurrentTeamId,
+ getTeamMemberships,
+ (state, mentionCount) => mentionCount,
+ (state, _, messageCount) => messageCount,
+ (currentTeamId, myTeamMembers, mentionCount, messageCount) => {
+ let mentions = mentionCount;
+ let messages = messageCount;
+
+ const members = Object.values(myTeamMembers).filter((m) => m.team_id !== currentTeamId);
+ members.forEach((m) => {
+ mentions += (m.mention_count || 0);
+ messages += (m.msg_count || 0);
+ });
+
+ let badgeCount = 0;
+ if (mentions) {
+ badgeCount = mentions;
+ } else if (messages) {
+ badgeCount = -1;
+ }
+
+ return badgeCount;
+ }
+);
+
+function mapStateToProps(state) {
+ const {mentionCount, messageCount} = getUnreadsInCurrentTeam(state);
+
+ return {
+ badgeCount: getBadgeCount(state, mentionCount, messageCount),
+ theme: getTheme(state),
+ };
+}
+
+export default connect(mapStateToProps)(ChannelDrawerButton);