forked from Ivasoft/mattermost-mobile
300 lines
12 KiB
TypeScript
300 lines
12 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {AppState, DeviceEventEmitter, Platform} from 'react-native';
|
|
import {
|
|
Notification,
|
|
NotificationAction,
|
|
NotificationBackgroundFetchResult,
|
|
NotificationCategory,
|
|
NotificationCompletion,
|
|
Notifications,
|
|
NotificationTextInput,
|
|
Registered,
|
|
} from 'react-native-notifications';
|
|
import {requestNotifications} from 'react-native-permissions';
|
|
|
|
import {storeDeviceToken} from '@actions/app/global';
|
|
import {markChannelAsViewed} from '@actions/local/channel';
|
|
import {backgroundNotification, openNotification} from '@actions/remote/notifications';
|
|
import {markThreadAsRead} from '@actions/remote/thread';
|
|
import {Device, Events, Navigation, PushNotification, Screens} from '@constants';
|
|
import DatabaseManager from '@database/manager';
|
|
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
|
import NativeNotifications from '@notifications';
|
|
import {getServerDisplayName} from '@queries/app/servers';
|
|
import {getCurrentChannelId} from '@queries/servers/system';
|
|
import {getIsCRTEnabled, getThreadById} from '@queries/servers/thread';
|
|
import {dismissOverlay, showOverlay} from '@screens/navigation';
|
|
import EphemeralStore from '@store/ephemeral_store';
|
|
import NavigationStore from '@store/navigation_store';
|
|
import {isBetaApp} from '@utils/general';
|
|
import {isMainActivity, isTablet} from '@utils/helpers';
|
|
import {logInfo} from '@utils/log';
|
|
import {convertToNotificationData} from '@utils/notification';
|
|
|
|
class PushNotifications {
|
|
configured = false;
|
|
|
|
init(register: boolean) {
|
|
if (register) {
|
|
this.registerIfNeeded();
|
|
}
|
|
|
|
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
|
|
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
|
|
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
|
|
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
|
|
}
|
|
|
|
async registerIfNeeded() {
|
|
const isRegistered = await Notifications.isRegisteredForRemoteNotifications();
|
|
if (!isRegistered) {
|
|
await requestNotifications(['alert', 'sound', 'badge']);
|
|
Notifications.registerRemoteNotifications();
|
|
}
|
|
}
|
|
|
|
createReplyCategory = () => {
|
|
const replyTitle = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.title'));
|
|
const replyButton = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.button'));
|
|
const replyPlaceholder = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.placeholder'));
|
|
const replyTextInput: NotificationTextInput = {buttonTitle: replyButton, placeholder: replyPlaceholder};
|
|
const replyAction = new NotificationAction(PushNotification.REPLY_ACTION, 'background', replyTitle, true, replyTextInput);
|
|
return new NotificationCategory(PushNotification.CATEGORY, [replyAction]);
|
|
};
|
|
|
|
getServerUrlFromNotification = async (notification: NotificationWithData) => {
|
|
const {payload} = notification;
|
|
|
|
if (!payload?.channel_id && (!payload?.server_url || !payload.server_id)) {
|
|
return payload?.server_url;
|
|
}
|
|
|
|
let serverUrl = payload.server_url;
|
|
if (!serverUrl && payload.server_id) {
|
|
serverUrl = await DatabaseManager.getServerUrlFromIdentifier(payload.server_id);
|
|
}
|
|
|
|
return serverUrl;
|
|
};
|
|
|
|
handleClearNotification = async (notification: NotificationWithData) => {
|
|
const {payload} = notification;
|
|
const serverUrl = await this.getServerUrlFromNotification(notification);
|
|
|
|
if (serverUrl && payload?.channel_id) {
|
|
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
|
if (database) {
|
|
const isCRTEnabled = await getIsCRTEnabled(database);
|
|
if (isCRTEnabled && payload.root_id) {
|
|
const thread = await getThreadById(database, payload.root_id);
|
|
if (thread?.isFollowing) {
|
|
markThreadAsRead(serverUrl, payload.team_id, payload.post_id);
|
|
}
|
|
} else {
|
|
markChannelAsViewed(serverUrl, payload.channel_id);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
handleInAppNotification = async (serverUrl: string, notification: NotificationWithData) => {
|
|
const {payload} = notification;
|
|
|
|
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
|
if (database) {
|
|
const isTabletDevice = await isTablet();
|
|
const displayName = await getServerDisplayName(serverUrl);
|
|
const channelId = await getCurrentChannelId(database);
|
|
const isCRTEnabled = await getIsCRTEnabled(database);
|
|
let serverName;
|
|
if (Object.keys(DatabaseManager.serverDatabases).length > 1) {
|
|
serverName = displayName;
|
|
}
|
|
|
|
const isThreadNotification = Boolean(payload?.root_id);
|
|
|
|
const isSameChannelNotification = payload?.channel_id === channelId;
|
|
const isSameThreadNotification = isThreadNotification && payload?.root_id === EphemeralStore.getCurrentThreadId();
|
|
|
|
let isInChannelScreen = NavigationStore.getVisibleScreen() === Screens.CHANNEL;
|
|
if (isTabletDevice) {
|
|
isInChannelScreen = NavigationStore.getVisibleTab() === Screens.HOME;
|
|
}
|
|
const isInThreadScreen = NavigationStore.getVisibleScreen() === Screens.THREAD;
|
|
|
|
// Conditions:
|
|
// 1. If not in channel screen or thread screen, show the notification
|
|
const condition1 = !isInChannelScreen && !isInThreadScreen;
|
|
|
|
// 2. If is in channel screen,
|
|
// - Show notification of other channels
|
|
// or
|
|
// - Show notification if CRT is enabled and it's a thread notification (doesn't matter if it's the same channel)
|
|
const condition2 = isInChannelScreen && (!isSameChannelNotification || (isCRTEnabled && isThreadNotification));
|
|
|
|
// 3. If is in thread screen,
|
|
// - Show the notification if it doesn't belong to the thread
|
|
const condition3 = isInThreadScreen && !isSameThreadNotification;
|
|
|
|
if (condition1 || condition2 || condition3) {
|
|
DeviceEventEmitter.emit(Navigation.NAVIGATION_SHOW_OVERLAY);
|
|
|
|
const screen = Screens.IN_APP_NOTIFICATION;
|
|
const passProps = {
|
|
notification,
|
|
serverName,
|
|
serverUrl,
|
|
};
|
|
|
|
// Dismiss the screen if it's already visible or else it blocks the navigation
|
|
await dismissOverlay(screen);
|
|
|
|
showOverlay(screen, passProps);
|
|
}
|
|
}
|
|
};
|
|
|
|
handleMessageNotification = async (notification: NotificationWithData) => {
|
|
const {payload, foreground, userInteraction} = notification;
|
|
const serverUrl = await this.getServerUrlFromNotification(notification);
|
|
if (serverUrl) {
|
|
if (foreground) {
|
|
// Move this to a local action
|
|
this.handleInAppNotification(serverUrl, notification);
|
|
} else if (userInteraction && !payload?.userInfo?.local) {
|
|
// Handle notification tapped
|
|
openNotification(serverUrl, notification);
|
|
} else {
|
|
backgroundNotification(serverUrl, notification);
|
|
}
|
|
}
|
|
};
|
|
|
|
handleSessionNotification = async (notification: NotificationWithData) => {
|
|
logInfo('Session expired notification');
|
|
|
|
const serverUrl = await this.getServerUrlFromNotification(notification);
|
|
|
|
if (serverUrl) {
|
|
if (notification.userInteraction) {
|
|
DeviceEventEmitter.emit(Events.SESSION_EXPIRED, serverUrl);
|
|
} else {
|
|
DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl});
|
|
}
|
|
}
|
|
};
|
|
|
|
processNotification = async (notification: NotificationWithData) => {
|
|
const {payload} = notification;
|
|
|
|
if (payload) {
|
|
switch (payload.type) {
|
|
case PushNotification.NOTIFICATION_TYPE.CLEAR:
|
|
this.handleClearNotification(notification);
|
|
break;
|
|
case PushNotification.NOTIFICATION_TYPE.MESSAGE:
|
|
this.handleMessageNotification(notification);
|
|
break;
|
|
case PushNotification.NOTIFICATION_TYPE.SESSION:
|
|
this.handleSessionNotification(notification);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
localNotification = (notification: Notification) => {
|
|
Notifications.postLocalNotification(notification);
|
|
};
|
|
|
|
// This triggers when a notification is tapped and the app was in the background (iOS)
|
|
onNotificationOpened = (incoming: Notification, completion: () => void) => {
|
|
const notification = convertToNotificationData(incoming, false);
|
|
notification.userInteraction = true;
|
|
|
|
this.processNotification(notification);
|
|
completion();
|
|
};
|
|
|
|
// This triggers when the app was in the background (iOS)
|
|
onNotificationReceivedBackground = async (incoming: Notification, completion: (response: NotificationBackgroundFetchResult) => void) => {
|
|
const notification = convertToNotificationData(incoming, false);
|
|
this.processNotification(notification);
|
|
|
|
completion(NotificationBackgroundFetchResult.NEW_DATA);
|
|
};
|
|
|
|
// This triggers when the app was in the foreground (Android and iOS)
|
|
// Also triggers when the app was in the background (Android)
|
|
onNotificationReceivedForeground = (incoming: Notification, completion: (response: NotificationCompletion) => void) => {
|
|
const notification = convertToNotificationData(incoming, false);
|
|
if (AppState.currentState !== 'inactive') {
|
|
notification.foreground = AppState.currentState === 'active' && isMainActivity();
|
|
|
|
this.processNotification(notification);
|
|
}
|
|
completion({alert: false, sound: true, badge: true});
|
|
};
|
|
|
|
onRemoteNotificationsRegistered = async (event: Registered) => {
|
|
if (!this.configured) {
|
|
this.configured = true;
|
|
const {deviceToken} = event;
|
|
let prefix;
|
|
|
|
if (Platform.OS === 'ios') {
|
|
prefix = Device.PUSH_NOTIFY_APPLE_REACT_NATIVE;
|
|
if (isBetaApp) {
|
|
prefix = `${prefix}beta`;
|
|
}
|
|
} else {
|
|
prefix = Device.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
|
|
}
|
|
|
|
storeDeviceToken(`${prefix}-v2:${deviceToken}`);
|
|
|
|
// Store the device token in the default database
|
|
this.requestNotificationReplyPermissions();
|
|
}
|
|
return null;
|
|
};
|
|
|
|
removeChannelNotifications = async (serverUrl: string, channelId: string) => {
|
|
NativeNotifications.removeChannelNotifications(serverUrl, channelId);
|
|
};
|
|
|
|
removeServerNotifications = (serverUrl: string) => {
|
|
NativeNotifications.removeServerNotifications(serverUrl);
|
|
};
|
|
|
|
removeThreadNotifications = async (serverUrl: string, threadId: string) => {
|
|
NativeNotifications.removeThreadNotifications(serverUrl, threadId);
|
|
};
|
|
|
|
requestNotificationReplyPermissions = () => {
|
|
if (Platform.OS === 'ios') {
|
|
const replyCategory = this.createReplyCategory();
|
|
Notifications.setCategories([replyCategory]);
|
|
}
|
|
};
|
|
|
|
scheduleNotification = (notification: Notification) => {
|
|
if (notification.fireDate) {
|
|
if (Platform.OS === 'ios') {
|
|
notification.fireDate = new Date(notification.fireDate).toISOString();
|
|
}
|
|
|
|
return Notifications.postLocalNotification(notification);
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
cancelScheduleNotification = (notificationId: number) => {
|
|
Notifications.cancelLocalNotification(notificationId);
|
|
};
|
|
}
|
|
|
|
export default new PushNotifications();
|