257 lines
10 KiB
TypeScript
257 lines
10 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {AppState, NativeModules, Platform} from 'react-native';
|
|
import DeviceInfo from 'react-native-device-info';
|
|
import {
|
|
Notification,
|
|
NotificationAction,
|
|
NotificationBackgroundFetchResult,
|
|
NotificationCategory,
|
|
NotificationCompletion,
|
|
Notifications,
|
|
NotificationTextInput,
|
|
Registered,
|
|
} from 'react-native-notifications';
|
|
|
|
import {dismissAllModals, popToRoot} from '@actions/navigation';
|
|
import {markChannelViewedAndRead, fetchPostActionWithRetry} from '@actions/views/channel';
|
|
import {getPosts} from '@actions/views/post';
|
|
import {loadFromPushNotification} from '@actions/views/root';
|
|
import {logout} from '@actions/views/user';
|
|
import {NavigationTypes, ViewTypes} from '@constants';
|
|
import {getLocalizedMessage} from '@i18n';
|
|
import {setDeviceToken} from '@mm-redux/actions/general';
|
|
import {General} from '@mm-redux/constants';
|
|
import EventEmitter from '@mm-redux/utils/event_emitter';
|
|
import {getCurrentLocale} from '@selectors/i18n';
|
|
import {getBadgeCount} from '@selectors/views';
|
|
import EphemeralStore from '@store/ephemeral_store';
|
|
import Store from '@store/store';
|
|
import {waitForHydration} from '@store/utils';
|
|
import {t} from '@utils/i18n';
|
|
|
|
import type {DispatchFunc} from '@mm-redux/types/actions';
|
|
|
|
const CATEGORY = 'CAN_REPLY';
|
|
const REPLY_ACTION = 'REPLY_ACTION';
|
|
const AndroidNotificationPreferences = Platform.OS === 'android' ? NativeModules.NotificationPreferences : null;
|
|
const NOTIFICATION_TYPE = {
|
|
CLEAR: 'clear',
|
|
MESSAGE: 'message',
|
|
SESSION: 'session',
|
|
};
|
|
|
|
interface NotificationWithChannel extends Notification {
|
|
identifier: string;
|
|
channel_id: string;
|
|
}
|
|
|
|
class PushNotifications {
|
|
configured = false;
|
|
|
|
constructor() {
|
|
Notifications.registerRemoteNotifications();
|
|
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
|
|
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
|
|
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
|
|
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
|
|
|
|
this.getInitialNotification();
|
|
}
|
|
|
|
cancelAllLocalNotifications() {
|
|
Notifications.cancelAllLocalNotifications();
|
|
}
|
|
|
|
clearNotifications = () => {
|
|
this.setApplicationIconBadgeNumber(0, true);
|
|
|
|
// TODO: Only cancel the local notifications that belong to this server
|
|
this.cancelAllLocalNotifications();
|
|
};
|
|
|
|
clearChannelNotifications = async (channelId: string) => {
|
|
if (Platform.OS === 'android') {
|
|
const notifications = await AndroidNotificationPreferences.getDeliveredNotifications();
|
|
const notificationForChannel = notifications.find((n: NotificationWithChannel) => n.channel_id === channelId);
|
|
if (notificationForChannel) {
|
|
AndroidNotificationPreferences.removeDeliveredNotifications(notificationForChannel.identifier, channelId);
|
|
}
|
|
} else {
|
|
const ids: string[] = [];
|
|
const notifications = await Notifications.ios.getDeliveredNotifications();
|
|
let badgeCount = notifications.length;
|
|
|
|
if (Store.redux) {
|
|
const totalMentions = getBadgeCount(Store.redux.getState());
|
|
if (totalMentions > -1) {
|
|
badgeCount = totalMentions;
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < notifications.length; i++) {
|
|
const notification = notifications[i] as NotificationWithChannel;
|
|
if (notification.channel_id === channelId) {
|
|
ids.push(notification.identifier);
|
|
}
|
|
}
|
|
|
|
if (ids.length) {
|
|
Notifications.ios.removeDeliveredNotifications(ids);
|
|
}
|
|
|
|
this.setApplicationIconBadgeNumber(badgeCount);
|
|
}
|
|
}
|
|
|
|
createReplyCategory = () => {
|
|
const {getState} = Store.redux!;
|
|
const state = getState();
|
|
const locale = getCurrentLocale(state);
|
|
|
|
const replyTitle = getLocalizedMessage(locale, t('mobile.push_notification_reply.title'));
|
|
const replyButton = getLocalizedMessage(locale, t('mobile.push_notification_reply.button'));
|
|
const replyPlaceholder = getLocalizedMessage(locale, t('mobile.push_notification_reply.placeholder'));
|
|
const replyTextInput: NotificationTextInput = {
|
|
buttonTitle: replyButton,
|
|
placeholder: replyPlaceholder,
|
|
};
|
|
const replyAction = new NotificationAction(REPLY_ACTION, 'background', replyTitle, true, replyTextInput);
|
|
return new NotificationCategory(CATEGORY, [replyAction]);
|
|
}
|
|
|
|
getInitialNotification = async () => {
|
|
const notification: NotificationWithData | undefined = await Notifications.getInitialNotification();
|
|
|
|
if (notification) {
|
|
EphemeralStore.setStartFromNotification(true);
|
|
notification.userInteraction = true;
|
|
|
|
// getInitialNotification may run before the store is set
|
|
// that is why we run on an interval until the store is available
|
|
// once we handle the notification the interval is cleared.
|
|
const interval = setInterval(() => {
|
|
if (Store.redux) {
|
|
clearInterval(interval);
|
|
this.handleNotification(notification);
|
|
}
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
handleNotification = (notification: NotificationWithData) => {
|
|
const {payload, foreground, userInteraction} = notification;
|
|
|
|
if (Store.redux && payload) {
|
|
const dispatch = Store.redux.dispatch as DispatchFunc;
|
|
|
|
waitForHydration(Store.redux, async () => {
|
|
switch (payload.type) {
|
|
case NOTIFICATION_TYPE.CLEAR:
|
|
dispatch(markChannelViewedAndRead(payload.channel_id, null, false));
|
|
break;
|
|
case NOTIFICATION_TYPE.MESSAGE:
|
|
// get the posts for the channel as soon as possible
|
|
dispatch(fetchPostActionWithRetry(getPosts(payload.channel_id)));
|
|
|
|
if (foreground) {
|
|
EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification);
|
|
} else if (userInteraction && !payload.userInfo?.local) {
|
|
dispatch(loadFromPushNotification(notification));
|
|
const componentId = EphemeralStore.getNavigationTopComponentId();
|
|
if (componentId) {
|
|
EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEBAR);
|
|
EventEmitter.emit(NavigationTypes.CLOSE_SETTINGS_SIDEBAR);
|
|
|
|
await dismissAllModals();
|
|
await popToRoot();
|
|
}
|
|
}
|
|
break;
|
|
case NOTIFICATION_TYPE.SESSION:
|
|
// eslint-disable-next-line no-console
|
|
console.log('Session expired notification');
|
|
dispatch(logout());
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
localNotification = (notification: Notification) => {
|
|
Notifications.postLocalNotification(notification);
|
|
}
|
|
|
|
onNotificationOpened = (notification: NotificationWithData, completion: () => void) => {
|
|
notification.userInteraction = true;
|
|
this.handleNotification(notification);
|
|
completion();
|
|
}
|
|
|
|
onNotificationReceivedBackground = (notification: NotificationWithData, completion: (response: NotificationBackgroundFetchResult) => void) => {
|
|
this.handleNotification(notification);
|
|
completion(NotificationBackgroundFetchResult.NO_DATA);
|
|
};
|
|
|
|
onNotificationReceivedForeground = (notification: NotificationWithData, completion: (response: NotificationCompletion) => void) => {
|
|
notification.foreground = AppState.currentState === 'active';
|
|
completion({alert: false, sound: true, badge: true});
|
|
this.handleNotification(notification);
|
|
};
|
|
|
|
onRemoteNotificationsRegistered = (event: Registered) => {
|
|
if (!this.configured) {
|
|
this.configured = true;
|
|
const {deviceToken} = event;
|
|
let prefix;
|
|
|
|
if (Platform.OS === 'ios') {
|
|
prefix = General.PUSH_NOTIFY_APPLE_REACT_NATIVE;
|
|
if (DeviceInfo.getBundleId().includes('rnbeta')) {
|
|
prefix = `${prefix}beta`;
|
|
}
|
|
} else {
|
|
prefix = General.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
|
|
}
|
|
|
|
EphemeralStore.deviceToken = `${prefix}:${deviceToken}`;
|
|
if (Store.redux) {
|
|
const dispatch = Store.redux.dispatch as DispatchFunc;
|
|
waitForHydration(Store.redux, () => {
|
|
this.requestNotificationReplyPermissions();
|
|
dispatch(setDeviceToken(EphemeralStore.deviceToken));
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
requestNotificationReplyPermissions = () => {
|
|
if (Platform.OS === 'ios') {
|
|
const replyCategory = this.createReplyCategory();
|
|
Notifications.setCategories([replyCategory]);
|
|
}
|
|
};
|
|
|
|
setApplicationIconBadgeNumber = async (value: number, force = false) => {
|
|
if (Platform.OS === 'ios') {
|
|
const count = value < 0 ? 0 : value;
|
|
const notifications = await Notifications.ios.getDeliveredNotifications();
|
|
if (count === 0 && (force || notifications.length === 0)) {
|
|
Notifications.ios.setBadgeCount(count);
|
|
}
|
|
}
|
|
};
|
|
|
|
scheduleNotification = (notification: Notification) => {
|
|
if (notification.fireDate) {
|
|
if (Platform.OS === 'ios') {
|
|
notification.fireDate = new Date(notification.fireDate).toISOString();
|
|
}
|
|
|
|
Notifications.postLocalNotification(notification);
|
|
}
|
|
};
|
|
}
|
|
|
|
export default new PushNotifications();
|