Automated cherry pick of #3001 (#3030)

* Include in-app notifications in iOS badge number

* Make foreground notifications key a const
This commit is contained in:
Mattermost Build
2019-07-24 23:05:25 +02:00
committed by Miguel Alatzar
parent 1f8e853e41
commit ce325a4ab1
5 changed files with 202 additions and 25 deletions

View File

@@ -2,12 +2,14 @@
// 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();
@@ -51,6 +53,8 @@ class PushNotification {
if (this.onNotification) {
this.onNotification(this.deviceNotification);
}
this.trackForegroundNotification(data.channel_id);
};
handleReply = (action, completed) => {
@@ -130,6 +134,10 @@ class PushNotification {
message: notification.getMessage(),
};
this.handleNotification(info, true, false);
NotificationsIOS.getBadgesCount((count) => {
this.setApplicationIconBadgeNumber(count + 1);
});
};
onNotificationOpened = (notification) => {
@@ -160,10 +168,22 @@ class PushNotification {
}
clearChannelNotifications(channelId) {
NotificationsIOS.getDeliveredNotifications((notifications) => {
const ids = [];
let badgeCount = notifications.length;
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;
const ids = [];
for (let i = 0; i < notifications.length; i++) {
const notification = notifications[i];
@@ -180,6 +200,16 @@ class PushNotification {
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));
}
}
export default new PushNotification();

View File

@@ -0,0 +1,136 @@
// 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';
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));
}),
NotificationAction: jest.fn(),
NotificationCategory: jest.fn(),
};
});
describe('PushNotification', () => {
const channel1ID = 'channel-1-id';
const channel2ID = 'channel-2-id';
const notification = {
getData: jest.fn(),
getMessage: jest.fn(),
};
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 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 () => {
const deliveredNotifications = [
// Three channel1 delivered notifications
{
identifier: 'channel1-1',
userInfo: {channel_id: channel1ID},
},
{
identifier: 'channel1-2',
userInfo: {channel_id: channel1ID},
},
{
identifier: 'channel1-3',
userInfo: {channel_id: channel1ID},
},
// Two channel2 delivered notifications
{
identifier: 'channel2-1',
userInfo: {channel_id: channel2ID},
},
{
identifier: 'channel2-2',
userInfo: {channel_id: channel2ID},
},
];
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);
NotificationsIOS.setBadgesCount(notificationCount);
NotificationsIOS.getBadgesCount((count) => expect(count).toBe(notificationCount));
// Clear channel1 notifications
const setApplicationIconBadgeNumber = jest.spyOn(PushNotification, 'setApplicationIconBadgeNumber');
await PushNotification.clearChannelNotifications(channel1ID);
NotificationsIOS.getDeliveredNotifications(async (deliveredNotifs) => {
expect(deliveredNotifs.length).toBe(2);
const channel1DeliveredNotifications = deliveredNotifs.filter((n) => n.userInfo.channel_id === channel1ID);
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.length).toBe(0);
expect(channel2ForegroundNotifications.length).toBe(1);
const badgeNumber = channel2DeliveredNotifications.lenth + channel2ForegroundNotifications.length;
expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(badgeNumber);
});
});
});

50
package-lock.json generated
View File

@@ -7674,8 +7674,7 @@
"ansi-regex": {
"version": "2.1.1",
"resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"optional": true
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"aproba": {
"version": "1.2.0",
@@ -7696,8 +7695,7 @@
"balanced-match": {
"version": "1.0.0",
"resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"optional": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"brace-expansion": {
"version": "1.1.11",
@@ -7717,20 +7715,17 @@
"code-point-at": {
"version": "1.1.0",
"resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"optional": true
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"concat-map": {
"version": "0.0.1",
"resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"optional": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"console-control-strings": {
"version": "1.1.0",
"resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"core-util-is": {
"version": "1.0.2",
@@ -7847,8 +7842,7 @@
"inherits": {
"version": "2.0.3",
"resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"optional": true
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
"version": "1.3.5",
@@ -7881,8 +7875,7 @@
"minimist": {
"version": "0.0.8",
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"optional": true
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"minipass": {
"version": "2.3.5",
@@ -7986,8 +7979,7 @@
"number-is-nan": {
"version": "1.0.1",
"resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"optional": true
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"object-assign": {
"version": "4.1.1",
@@ -8084,8 +8076,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": false,
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"optional": true
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
@@ -8183,14 +8174,12 @@
"wrappy": {
"version": "1.0.2",
"resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"optional": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"yallist": {
"version": "3.0.3",
"resolved": false,
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"optional": true
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
}
}
},
@@ -13854,6 +13843,23 @@
}
}
},
"mock-async-storage": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/mock-async-storage/-/mock-async-storage-2.2.0.tgz",
"integrity": "sha512-HpMe9KNKxlyiRyOXFBrEOSQ1Idcoz1k5W5J+6C8zYzjasSxFJlrA5wVVjPR4U6zJJtrbuky1c0s1o3whzgghSQ==",
"dev": true,
"requires": {
"deepmerge": "^3.3.0"
},
"dependencies": {
"deepmerge": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz",
"integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==",
"dev": true
}
}
},
"moment": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",

View File

@@ -98,6 +98,7 @@
"jsdom-global": "3.0.2",
"metro-react-native-babel-preset": "0.54.0",
"mmjstool": "github:mattermost/mattermost-utilities",
"mock-async-storage": "2.2.0",
"nyc": "14.1.1",
"react-dom": "16.8.6",
"react-test-renderer": "16.8.6",

View File

@@ -1,10 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import MockAsyncStorage from 'mock-async-storage';
import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
const mockImpl = new MockAsyncStorage();
jest.mock('@react-native-community/async-storage', () => mockImpl);
/* eslint-disable no-console */
jest.mock('NativeModules', () => {