forked from Ivasoft/mattermost-mobile
Gekidou session expired notification (#6639)
* Fix crash on Android when session expired notification is presented * react-native-notification patch for schedule fix on android and open local notification on iOS * Fix android scheduled session notification crash * patch react-native-navigation to support blur/focus AppState on Android * remove schedule session expired notification from login entry point * schedule session expired notification actions * add session manager * Handle open session expired notification
This commit is contained in:
@@ -46,6 +46,9 @@ public class CustomPushNotificationHelper {
|
||||
public static final int MESSAGE_NOTIFICATION_ID = 435345;
|
||||
public static final String NOTIFICATION_ID = "notificationId";
|
||||
public static final String NOTIFICATION = "notification";
|
||||
public static final String PUSH_TYPE_MESSAGE = "message";
|
||||
public static final String PUSH_TYPE_CLEAR = "clear";
|
||||
public static final String PUSH_TYPE_SESSION = "session";
|
||||
|
||||
private static NotificationChannel mHighImportanceChannel;
|
||||
private static NotificationChannel mMinImportanceChannel;
|
||||
@@ -54,17 +57,11 @@ public class CustomPushNotificationHelper {
|
||||
String message = bundle.getString("message", bundle.getString("body"));
|
||||
String senderId = bundle.getString("sender_id");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
String type = bundle.getString("type");
|
||||
if (senderId == null) {
|
||||
senderId = "sender_id";
|
||||
}
|
||||
Bundle userInfoBundle = bundle.getBundle("userInfo");
|
||||
String senderName = getSenderName(bundle);
|
||||
if (userInfoBundle != null) {
|
||||
boolean localPushNotificationTest = userInfoBundle.getBoolean("test");
|
||||
if (localPushNotificationTest) {
|
||||
senderName = "Test";
|
||||
}
|
||||
}
|
||||
|
||||
if (conversationTitle == null || !android.text.TextUtils.isEmpty(senderName.trim())) {
|
||||
message = removeSenderNameFromMessage(message, senderName);
|
||||
@@ -75,7 +72,7 @@ public class CustomPushNotificationHelper {
|
||||
.setKey(senderId)
|
||||
.setName(senderName);
|
||||
|
||||
if (serverUrl != null) {
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, senderId))));
|
||||
} catch (IOException e) {
|
||||
@@ -246,6 +243,10 @@ public class CustomPushNotificationHelper {
|
||||
title = bundle.getString("sender_name");
|
||||
}
|
||||
|
||||
if (android.text.TextUtils.isEmpty(title)) {
|
||||
title = bundle.getString("title", "");
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
@@ -259,14 +260,15 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
private static NotificationCompat.MessagingStyle getMessagingStyle(Context context, Bundle bundle) {
|
||||
NotificationCompat.MessagingStyle messagingStyle;
|
||||
String senderId = "me";
|
||||
final String senderId = "me";
|
||||
final String serverUrl = bundle.getString("server_url");
|
||||
final String type = bundle.getString("type");
|
||||
|
||||
Person.Builder sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName("Me");
|
||||
|
||||
if (serverUrl != null) {
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, "me"))));
|
||||
} catch (IOException e) {
|
||||
@@ -362,19 +364,6 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
|
||||
NotificationChannel notificationChannel = mHighImportanceChannel;
|
||||
|
||||
boolean testNotification = false;
|
||||
boolean localNotification = false;
|
||||
Bundle userInfoBundle = bundle.getBundle("userInfo");
|
||||
if (userInfoBundle != null) {
|
||||
testNotification = userInfoBundle.getBoolean("test");
|
||||
localNotification = userInfoBundle.getBoolean("local");
|
||||
}
|
||||
|
||||
if (testNotification || localNotification) {
|
||||
notificationChannel = mMinImportanceChannel;
|
||||
}
|
||||
|
||||
notification.setChannelId(notificationChannel.getId());
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,6 @@ import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
private static final String PUSH_TYPE_MESSAGE = "message";
|
||||
private static final String PUSH_TYPE_CLEAR = "clear";
|
||||
private static final String PUSH_TYPE_SESSION = "session";
|
||||
private final PushNotificationDataHelper dataHelper;
|
||||
|
||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
@@ -81,11 +78,11 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case PUSH_TYPE_MESSAGE:
|
||||
case PUSH_TYPE_SESSION:
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
|
||||
if (!mAppLifecycleFacade.isAppVisible()) {
|
||||
boolean createSummary = type.equals(PUSH_TYPE_MESSAGE);
|
||||
if (type.equals(PUSH_TYPE_MESSAGE)) {
|
||||
boolean createSummary = type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE);
|
||||
if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) {
|
||||
if (channelId != null) {
|
||||
Bundle notificationBundle = mNotificationProps.asBundle();
|
||||
if (serverUrl != null && !isReactInit) {
|
||||
@@ -107,7 +104,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
buildNotification(notificationId, createSummary);
|
||||
}
|
||||
break;
|
||||
case PUSH_TYPE_CLEAR:
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_CLEAR:
|
||||
NotificationHelper.clearChannelOrThreadNotifications(mContext, mNotificationProps.asBundle());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,12 @@ public class MainActivity extends NavigationActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
getReactGateway().onWindowFocusChanged(hasFocus);
|
||||
}
|
||||
|
||||
/*
|
||||
https://mattermost.atlassian.net/browse/MM-10601
|
||||
Required by react-native-hw-keyboard-event
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {getSessions} from '@actions/remote/session';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logWarning} from '@utils/log';
|
||||
import {scheduleExpiredNotification} from '@utils/notification';
|
||||
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
|
||||
@@ -50,27 +47,6 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
|
||||
return {error: clData.error};
|
||||
}
|
||||
|
||||
// schedule local push notification if needed
|
||||
if (clData.config) {
|
||||
if (clData.config.ExtendSessionLengthWithActivity !== 'true') {
|
||||
const timeOut = setTimeout(async () => {
|
||||
clearTimeout(timeOut);
|
||||
let sessions: Session[]|undefined;
|
||||
|
||||
try {
|
||||
sessions = await getSessions(serverUrl, 'me');
|
||||
} catch (e) {
|
||||
logWarning('Failed to get user sessions', e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessions && Array.isArray(sessions)) {
|
||||
scheduleExpiredNotification(sessions, clData.config?.SiteName || serverUrl, user.locale);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const entryData = await entry(serverUrl, '', '');
|
||||
|
||||
if ('error' in entryData) {
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
import {DeviceEventEmitter, Platform} from 'react-native';
|
||||
|
||||
import {Database, Events} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {getCurrentUserId, getCommonSystemValues} from '@queries/servers/system';
|
||||
import {queryServerName} from '@queries/app/servers';
|
||||
import {getCurrentUserId, getCommonSystemValues, getExpiredSession} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {logWarning} from '@utils/log';
|
||||
import {logWarning, logError} from '@utils/log';
|
||||
import {scheduleExpiredNotification} from '@utils/notification';
|
||||
import {getCSRFFromCookie} from '@utils/security';
|
||||
import {getDeviceTimezone, isTimezoneEnabled} from '@utils/timezone';
|
||||
|
||||
@@ -91,7 +96,7 @@ export const forceLogoutIfNecessary = async (serverUrl: string, err: ClientError
|
||||
return {error: null};
|
||||
};
|
||||
|
||||
export const getSessions = async (serverUrl: string, currentUserId: string) => {
|
||||
export const fetchSessions = async (serverUrl: string, currentUserId: string) => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
@@ -102,6 +107,7 @@ export const getSessions = async (serverUrl: string, currentUserId: string) => {
|
||||
try {
|
||||
return await client.getSessions(currentUserId);
|
||||
} catch (e) {
|
||||
logError('fetchSessions', e);
|
||||
await forceLogoutIfNecessary(serverUrl, e as ClientError);
|
||||
}
|
||||
|
||||
@@ -166,7 +172,7 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
|
||||
}
|
||||
};
|
||||
|
||||
export const logout = async (serverUrl: string, skipServerLogout = false, removeServer = false) => {
|
||||
export const logout = async (serverUrl: string, skipServerLogout = false, removeServer = false, skipEvents = false) => {
|
||||
if (!skipServerLogout) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
@@ -177,7 +183,65 @@ export const logout = async (serverUrl: string, skipServerLogout = false, remove
|
||||
}
|
||||
}
|
||||
|
||||
DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl, removeServer});
|
||||
if (!skipEvents) {
|
||||
DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl, removeServer});
|
||||
}
|
||||
};
|
||||
|
||||
export const cancelSessionNotification = async (serverUrl: string) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const expiredSession = await getExpiredSession(database);
|
||||
const rechable = (await NetInfo.fetch()).isInternetReachable;
|
||||
|
||||
if (expiredSession?.notificationId && rechable) {
|
||||
PushNotifications.cancelScheduleNotification(parseInt(expiredSession.notificationId, 10));
|
||||
operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
|
||||
value: '',
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logError('cancelSessionNotification', e);
|
||||
}
|
||||
};
|
||||
|
||||
export const scheduleSessionNotification = async (serverUrl: string) => {
|
||||
try {
|
||||
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const sessions = await fetchSessions(serverUrl, 'me');
|
||||
const user = await getCurrentUser(database);
|
||||
const serverName = await queryServerName(appDatabase, serverUrl);
|
||||
|
||||
await cancelSessionNotification(serverUrl);
|
||||
|
||||
if (sessions) {
|
||||
const session = await findSession(serverUrl, sessions);
|
||||
|
||||
if (session) {
|
||||
const sessionId = session.id;
|
||||
const notificationId = scheduleExpiredNotification(serverUrl, session, serverName, user?.locale);
|
||||
operator.handleSystem({
|
||||
systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
|
||||
value: {
|
||||
id: sessionId,
|
||||
notificationId,
|
||||
expiresAt: session.expires_at,
|
||||
},
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logError('scheduleExpiredNotification', e);
|
||||
await forceLogoutIfNecessary(serverUrl, e as ClientError);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendPasswordResetEmail = async (serverUrl: string, email: string) => {
|
||||
@@ -251,3 +315,47 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
||||
return {error: error as ClientError, failed: false, time: 0};
|
||||
}
|
||||
};
|
||||
|
||||
async function findSession(serverUrl: string, sessions: Session[]) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
|
||||
const expiredSession = await getExpiredSession(database);
|
||||
const deviceToken = await getDeviceToken(appDatabase);
|
||||
|
||||
// First try and find the session by the given identifier hyqddef7jjdktqiyy36gxa8sqy
|
||||
let session = sessions.find((s) => s.id === expiredSession?.id);
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
|
||||
// Next try and find the session by deviceId
|
||||
if (deviceToken) {
|
||||
session = sessions.find((s) => s.device_id === deviceToken);
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
// Next try and find the session by the CSRF token
|
||||
const csrfToken = await getCSRFFromCookie(serverUrl);
|
||||
if (csrfToken) {
|
||||
session = sessions.find((s) => s.props?.csrf === csrfToken);
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
// Next try and find the session based on the OS
|
||||
// if multiple sessions exists with the same os type this can be inaccurate
|
||||
session = sessions.find((s) => s.props?.os.toLowerCase() === Platform.OS);
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
} catch (e) {
|
||||
logError('findSession', e);
|
||||
}
|
||||
|
||||
// At this point we did not find the session
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ export const SYSTEM_IDENTIFIERS = {
|
||||
RECENT_CUSTOM_STATUS: 'recentCustomStatus',
|
||||
RECENT_MENTIONS: 'recentMentions',
|
||||
RECENT_REACTIONS: 'recentReactions',
|
||||
SESSION_EXPIRATION: 'sessionExpiration',
|
||||
TEAM_HISTORY: 'teamHistory',
|
||||
WEBSOCKET: 'WebSocket',
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ export default keyMirror({
|
||||
PAUSE_KEYBOARD_TRACKING_VIEW: null,
|
||||
SERVER_LOGOUT: null,
|
||||
SERVER_VERSION_CHANGED: null,
|
||||
SESSION_EXPIRED: null,
|
||||
TAB_BAR_VISIBLE: null,
|
||||
TEAM_LOAD_ERROR: null,
|
||||
TEAM_SWITCH: null,
|
||||
|
||||
@@ -27,6 +27,7 @@ import Post from './post';
|
||||
import PostDraft from './post_draft';
|
||||
import Preferences from './preferences';
|
||||
import Profile from './profile';
|
||||
import PushNotification from './push_notification';
|
||||
import PushProxy from './push_proxy';
|
||||
import Screens from './screens';
|
||||
import ServerErrors from './server_errors';
|
||||
@@ -63,6 +64,7 @@ export {
|
||||
PostDraft,
|
||||
Preferences,
|
||||
Profile,
|
||||
PushNotification,
|
||||
PushProxy,
|
||||
Screens,
|
||||
ServerErrors,
|
||||
|
||||
18
app/constants/push_notification.ts
Normal file
18
app/constants/push_notification.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const CATEGORY = 'CAN_REPLY';
|
||||
|
||||
export const REPLY_ACTION = 'REPLY_ACTION';
|
||||
|
||||
export const NOTIFICATION_TYPE = {
|
||||
CLEAR: 'clear',
|
||||
MESSAGE: 'message',
|
||||
SESSION: 'session',
|
||||
};
|
||||
|
||||
export default {
|
||||
CATEGORY,
|
||||
NOTIFICATION_TYPE,
|
||||
REPLY_ACTION,
|
||||
};
|
||||
@@ -2,11 +2,11 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Emm from '@mattermost/react-native-emm';
|
||||
import {Alert, Linking, Platform} from 'react-native';
|
||||
import {Alert, DeviceEventEmitter, Linking, Platform} from 'react-native';
|
||||
import {Notifications} from 'react-native-notifications';
|
||||
|
||||
import {appEntry, pushNotificationEntry, upgradeEntry} from '@actions/remote/entry';
|
||||
import {Screens, DeepLink, Launch} from '@constants';
|
||||
import {Screens, DeepLink, Events, Launch, PushNotification} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getActiveServerUrl, getServerCredentials, removeServerCredentials} from '@init/credentials';
|
||||
import {getThemeForCurrentTeam} from '@queries/servers/preference';
|
||||
@@ -20,6 +20,8 @@ import {parseDeepLink} from '@utils/url';
|
||||
|
||||
import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkWithData, LaunchProps} from '@typings/launch';
|
||||
|
||||
const initialNotificationTypes = [PushNotification.NOTIFICATION_TYPE.MESSAGE, PushNotification.NOTIFICATION_TYPE.SESSION];
|
||||
|
||||
export const initialLaunch = async () => {
|
||||
const deepLinkUrl = await Linking.getInitialURL();
|
||||
if (deepLinkUrl) {
|
||||
@@ -29,7 +31,7 @@ export const initialLaunch = async () => {
|
||||
|
||||
const notification = await Notifications.getInitialNotification();
|
||||
let tapped = Platform.select({android: true, ios: false})!;
|
||||
if (Platform.OS === 'ios') {
|
||||
if (Platform.OS === 'ios' && notification) {
|
||||
// when a notification is received on iOS, getInitialNotification, will return the notification
|
||||
// as the app will initialized cause we are using background fetch,
|
||||
// that does not necessarily mean that the app was opened cause of the notification was tapped.
|
||||
@@ -38,8 +40,8 @@ export const initialLaunch = async () => {
|
||||
const delivered = await Notifications.ios.getDeliveredNotifications();
|
||||
tapped = delivered.find((d) => (d as unknown as NotificationData).ack_id === notification?.payload.ack_id) == null;
|
||||
}
|
||||
if (notification?.payload?.type === 'message' && tapped) {
|
||||
launchAppFromNotification(convertToNotificationData(notification));
|
||||
if (initialNotificationTypes.includes(notification?.payload?.type) && tapped) {
|
||||
launchAppFromNotification(convertToNotificationData(notification!));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,6 +69,12 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => {
|
||||
break;
|
||||
case Launch.Notification: {
|
||||
serverUrl = props.serverUrl;
|
||||
const extra = props.extra as NotificationWithData;
|
||||
const sessionExpiredNotification = Boolean(props.serverUrl && extra.payload?.type === PushNotification.NOTIFICATION_TYPE.SESSION);
|
||||
if (sessionExpiredNotification) {
|
||||
DeviceEventEmitter.emit(Events.SESSION_EXPIRED, serverUrl);
|
||||
return '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -110,17 +118,15 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => {
|
||||
},
|
||||
}],
|
||||
);
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
launchToHome({...props, launchType, serverUrl});
|
||||
|
||||
return;
|
||||
return launchToHome({...props, launchType, serverUrl});
|
||||
}
|
||||
}
|
||||
|
||||
launchToServer(props, resetNavigation);
|
||||
return launchToServer(props, resetNavigation);
|
||||
};
|
||||
|
||||
const launchToHome = async (props: LaunchProps) => {
|
||||
@@ -156,25 +162,26 @@ const launchToHome = async (props: LaunchProps) => {
|
||||
|
||||
if (nTeams) {
|
||||
logInfo('Launch app in Home screen');
|
||||
resetToHome(props);
|
||||
} else {
|
||||
logInfo('Launch app in Select Teams screen');
|
||||
resetToTeams();
|
||||
return resetToHome(props);
|
||||
}
|
||||
|
||||
logInfo('Launch app in Select Teams screen');
|
||||
return resetToTeams();
|
||||
};
|
||||
|
||||
const launchToServer = (props: LaunchProps, resetNavigation: Boolean) => {
|
||||
if (resetNavigation) {
|
||||
resetToSelectServer(props);
|
||||
return;
|
||||
return resetToSelectServer(props);
|
||||
}
|
||||
|
||||
// This is being called for Deeplinks, but needs to be revisited when
|
||||
// the implementation of deep links is complete
|
||||
const title = '';
|
||||
goToScreen(Screens.SERVER, title, {...props});
|
||||
return goToScreen(Screens.SERVER, title, {...props});
|
||||
};
|
||||
|
||||
export const relaunchApp = (props: LaunchProps, resetNavigation = false) => {
|
||||
launchApp(props, resetNavigation);
|
||||
return launchApp(props, resetNavigation);
|
||||
};
|
||||
|
||||
export const getLaunchPropsFromDeepLink = (deepLinkUrl: string): LaunchProps => {
|
||||
|
||||
@@ -18,7 +18,7 @@ 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, Screens} from '@constants';
|
||||
import {Device, Events, Navigation, PushNotification, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
||||
import NativeNotifications from '@notifications';
|
||||
@@ -32,14 +32,6 @@ import {isTablet} from '@utils/helpers';
|
||||
import {logInfo} from '@utils/log';
|
||||
import {convertToNotificationData} from '@utils/notification';
|
||||
|
||||
const CATEGORY = 'CAN_REPLY';
|
||||
const REPLY_ACTION = 'REPLY_ACTION';
|
||||
const NOTIFICATION_TYPE = {
|
||||
CLEAR: 'clear',
|
||||
MESSAGE: 'message',
|
||||
SESSION: 'session',
|
||||
};
|
||||
|
||||
class PushNotifications {
|
||||
configured = false;
|
||||
|
||||
@@ -56,15 +48,15 @@ class PushNotifications {
|
||||
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(REPLY_ACTION, 'background', replyTitle, true, replyTextInput);
|
||||
return new NotificationCategory(CATEGORY, [replyAction]);
|
||||
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 undefined;
|
||||
return payload?.server_url;
|
||||
}
|
||||
|
||||
let serverUrl = payload.server_url;
|
||||
@@ -156,7 +148,11 @@ class PushNotifications {
|
||||
const serverUrl = await this.getServerUrlFromNotification(notification);
|
||||
|
||||
if (serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl});
|
||||
if (notification.userInteraction) {
|
||||
DeviceEventEmitter.emit(Events.SESSION_EXPIRED, serverUrl);
|
||||
} else {
|
||||
DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,13 +161,13 @@ class PushNotifications {
|
||||
|
||||
if (payload) {
|
||||
switch (payload.type) {
|
||||
case NOTIFICATION_TYPE.CLEAR:
|
||||
case PushNotification.NOTIFICATION_TYPE.CLEAR:
|
||||
this.handleClearNotification(notification);
|
||||
break;
|
||||
case NOTIFICATION_TYPE.MESSAGE:
|
||||
case PushNotification.NOTIFICATION_TYPE.MESSAGE:
|
||||
this.handleMessageNotification(notification);
|
||||
break;
|
||||
case NOTIFICATION_TYPE.SESSION:
|
||||
case PushNotification.NOTIFICATION_TYPE.SESSION:
|
||||
this.handleSessionNotification(notification);
|
||||
break;
|
||||
}
|
||||
@@ -259,8 +255,14 @@ class PushNotifications {
|
||||
notification.fireDate = new Date(notification.fireDate).toISOString();
|
||||
}
|
||||
|
||||
Notifications.postLocalNotification(notification);
|
||||
return Notifications.postLocalNotification(notification);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
cancelScheduleNotification = (notificationId: number) => {
|
||||
Notifications.cancelLocalNotification(notificationId);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import CookieManager, {Cookie} from '@react-native-cookies/cookies';
|
||||
import {Alert, DeviceEventEmitter, Linking, Platform} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import {Alert, DeviceEventEmitter, Linking} from 'react-native';
|
||||
import semver from 'semver';
|
||||
|
||||
import LocalConfig from '@assets/config.json';
|
||||
import {Events, Sso, Launch} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {DEFAULT_LOCALE, getTranslations, resetMomentLocale, t} from '@i18n';
|
||||
import {getServerCredentials, removeServerCredentials} from '@init/credentials';
|
||||
import {Events, Sso} from '@constants';
|
||||
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import {getLaunchPropsFromDeepLink, relaunchApp} from '@init/launch';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import * as analytics from '@managers/analytics';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {deleteFileCache, deleteFileCacheByDir} from '@utils/file';
|
||||
|
||||
import type {jsAndNativeErrorHandler} from '@typings/global/error_handling';
|
||||
import type {LaunchType} from '@typings/launch';
|
||||
|
||||
type LinkingCallbackArg = {url: string};
|
||||
|
||||
type LogoutCallbackArg = {
|
||||
serverUrl: string;
|
||||
removeServer: boolean;
|
||||
}
|
||||
|
||||
class GlobalEventHandler {
|
||||
JavascriptAndNativeErrorHandler: jsAndNativeErrorHandler | undefined;
|
||||
|
||||
constructor() {
|
||||
DeviceEventEmitter.addListener(Events.SERVER_LOGOUT, this.onLogout);
|
||||
DeviceEventEmitter.addListener(Events.SERVER_VERSION_CHANGED, this.onServerVersionChanged);
|
||||
DeviceEventEmitter.addListener(Events.CONFIG_CHANGED, this.onServerConfigChanged);
|
||||
|
||||
@@ -68,75 +52,6 @@ class GlobalEventHandler {
|
||||
}
|
||||
};
|
||||
|
||||
clearCookies = async (serverUrl: string, webKit: boolean) => {
|
||||
try {
|
||||
const cookies = await CookieManager.get(serverUrl, webKit);
|
||||
const values = Object.values(cookies);
|
||||
values.forEach((cookie: Cookie) => {
|
||||
CookieManager.clearByName(serverUrl, cookie.name, webKit);
|
||||
});
|
||||
} catch (error) {
|
||||
// Nothing to clear
|
||||
}
|
||||
};
|
||||
|
||||
clearCookiesForServer = async (serverUrl: string) => {
|
||||
this.clearCookies(serverUrl, false);
|
||||
if (Platform.OS === 'ios') {
|
||||
// Also delete any cookies that were set by react-native-webview
|
||||
this.clearCookies(serverUrl, true);
|
||||
} else if (Platform.OS === 'android') {
|
||||
CookieManager.flush();
|
||||
}
|
||||
};
|
||||
|
||||
onLogout = async ({serverUrl, removeServer}: LogoutCallbackArg) => {
|
||||
await removeServerCredentials(serverUrl);
|
||||
PushNotifications.removeServerNotifications(serverUrl);
|
||||
|
||||
NetworkManager.invalidateClient(serverUrl);
|
||||
WebsocketManager.invalidateClient(serverUrl);
|
||||
|
||||
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const activeServerDisplayName = await DatabaseManager.getActiveServerDisplayName();
|
||||
if (removeServer) {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
} else {
|
||||
await DatabaseManager.deleteServerDatabase(serverUrl);
|
||||
}
|
||||
|
||||
const analyticsClient = analytics.get(serverUrl);
|
||||
if (analyticsClient) {
|
||||
analyticsClient.reset();
|
||||
analytics.invalidate(serverUrl);
|
||||
}
|
||||
|
||||
this.resetLocale();
|
||||
this.clearCookiesForServer(serverUrl);
|
||||
FastImage.clearDiskCache();
|
||||
deleteFileCache(serverUrl);
|
||||
deleteFileCacheByDir('mmPasteInput');
|
||||
deleteFileCacheByDir('thumbnails');
|
||||
if (Platform.OS === 'android') {
|
||||
deleteFileCacheByDir('image_cache');
|
||||
}
|
||||
|
||||
if (activeServerUrl === serverUrl) {
|
||||
let displayName = '';
|
||||
let launchType: LaunchType = Launch.AddServer;
|
||||
if (!Object.keys(DatabaseManager.serverDatabases).length) {
|
||||
EphemeralStore.theme = undefined;
|
||||
launchType = Launch.Normal;
|
||||
|
||||
if (activeServerDisplayName) {
|
||||
displayName = activeServerDisplayName;
|
||||
}
|
||||
}
|
||||
|
||||
relaunchApp({launchType, serverUrl, displayName}, true);
|
||||
}
|
||||
};
|
||||
|
||||
onServerConfigChanged = ({serverUrl, config}: {serverUrl: string; config: ClientConfig}) => {
|
||||
this.configureAnalytics(serverUrl, config);
|
||||
};
|
||||
@@ -162,21 +77,11 @@ class GlobalEventHandler {
|
||||
}
|
||||
};
|
||||
|
||||
resetLocale = async () => {
|
||||
if (Object.keys(DatabaseManager.serverDatabases).length) {
|
||||
const serverDatabase = await DatabaseManager.getActiveServerDatabase();
|
||||
const user = await getCurrentUser(serverDatabase!);
|
||||
resetMomentLocale(user?.locale);
|
||||
} else {
|
||||
resetMomentLocale();
|
||||
}
|
||||
};
|
||||
|
||||
serverUpgradeNeeded = async (serverUrl: string) => {
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
|
||||
if (credentials) {
|
||||
this.onLogout({serverUrl, removeServer: false});
|
||||
DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl, removeServer: false});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
203
app/managers/session_manager.ts
Normal file
203
app/managers/session_manager.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import CookieManager, {Cookie} from '@react-native-cookies/cookies';
|
||||
import {AppState, AppStateStatus, DeviceEventEmitter, Platform} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import {cancelSessionNotification, logout, scheduleSessionNotification} from '@actions/remote/session';
|
||||
import {resetMomentLocale} from '@app/i18n';
|
||||
import {deleteFileCache, deleteFileCacheByDir} from '@app/utils/file';
|
||||
import {Events, Launch} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getAllServerCredentials, removeServerCredentials} from '@init/credentials';
|
||||
import {relaunchApp} from '@init/launch';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import * as analytics from '@managers/analytics';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {queryServerName} from '@queries/app/servers';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {getThemeFromState} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {addNewServer} from '@utils/server';
|
||||
|
||||
import type {LaunchType} from '@typings/launch';
|
||||
|
||||
type LogoutCallbackArg = {
|
||||
serverUrl: string;
|
||||
removeServer: boolean;
|
||||
}
|
||||
|
||||
class SessionManager {
|
||||
private previousAppState: AppStateStatus;
|
||||
private scheduling = false;
|
||||
private terminatingSessionUrl: undefined|string;
|
||||
|
||||
constructor() {
|
||||
if (Platform.OS === 'android') {
|
||||
AppState.addEventListener('blur', () => {
|
||||
this.onAppStateChange('inactive');
|
||||
});
|
||||
AppState.addEventListener('focus', () => {
|
||||
this.onAppStateChange('active');
|
||||
});
|
||||
} else {
|
||||
AppState.addEventListener('change', this.onAppStateChange);
|
||||
}
|
||||
|
||||
DeviceEventEmitter.addListener(Events.SERVER_LOGOUT, this.onLogout);
|
||||
DeviceEventEmitter.addListener(Events.SESSION_EXPIRED, this.onSessionExpired);
|
||||
|
||||
this.previousAppState = AppState.currentState;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.cancelAll();
|
||||
}
|
||||
|
||||
private cancelAll = async () => {
|
||||
const serverCredentials = await getAllServerCredentials();
|
||||
for (const {serverUrl} of serverCredentials) {
|
||||
cancelSessionNotification(serverUrl);
|
||||
}
|
||||
};
|
||||
|
||||
private clearCookies = async (serverUrl: string, webKit: boolean) => {
|
||||
try {
|
||||
const cookies = await CookieManager.get(serverUrl, webKit);
|
||||
const values = Object.values(cookies);
|
||||
values.forEach((cookie: Cookie) => {
|
||||
CookieManager.clearByName(serverUrl, cookie.name, webKit);
|
||||
});
|
||||
} catch (error) {
|
||||
// Nothing to clear
|
||||
}
|
||||
};
|
||||
|
||||
private clearCookiesForServer = async (serverUrl: string) => {
|
||||
this.clearCookies(serverUrl, false);
|
||||
if (Platform.OS === 'ios') {
|
||||
// Also delete any cookies that were set by react-native-webview
|
||||
this.clearCookies(serverUrl, true);
|
||||
} else if (Platform.OS === 'android') {
|
||||
CookieManager.flush();
|
||||
}
|
||||
};
|
||||
|
||||
private scheduleAll = async () => {
|
||||
if (!this.scheduling) {
|
||||
this.scheduling = true;
|
||||
const serverCredentials = await getAllServerCredentials();
|
||||
const promises: Array<Promise<void>> = [];
|
||||
for (const {serverUrl} of serverCredentials) {
|
||||
promises.push(scheduleSessionNotification(serverUrl));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
this.scheduling = false;
|
||||
}
|
||||
};
|
||||
|
||||
private resetLocale = async () => {
|
||||
if (Object.keys(DatabaseManager.serverDatabases).length) {
|
||||
const serverDatabase = await DatabaseManager.getActiveServerDatabase();
|
||||
const user = await getCurrentUser(serverDatabase!);
|
||||
resetMomentLocale(user?.locale);
|
||||
} else {
|
||||
resetMomentLocale();
|
||||
}
|
||||
};
|
||||
|
||||
private terminateSession = async (serverUrl: string, removeServer: boolean) => {
|
||||
cancelSessionNotification(serverUrl);
|
||||
await removeServerCredentials(serverUrl);
|
||||
PushNotifications.removeServerNotifications(serverUrl);
|
||||
|
||||
NetworkManager.invalidateClient(serverUrl);
|
||||
WebsocketManager.invalidateClient(serverUrl);
|
||||
|
||||
if (removeServer) {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
} else {
|
||||
await DatabaseManager.deleteServerDatabase(serverUrl);
|
||||
}
|
||||
|
||||
const analyticsClient = analytics.get(serverUrl);
|
||||
if (analyticsClient) {
|
||||
analyticsClient.reset();
|
||||
analytics.invalidate(serverUrl);
|
||||
}
|
||||
|
||||
this.resetLocale();
|
||||
this.clearCookiesForServer(serverUrl);
|
||||
FastImage.clearDiskCache();
|
||||
deleteFileCache(serverUrl);
|
||||
deleteFileCacheByDir('mmPasteInput');
|
||||
deleteFileCacheByDir('thumbnails');
|
||||
if (Platform.OS === 'android') {
|
||||
deleteFileCacheByDir('image_cache');
|
||||
}
|
||||
};
|
||||
|
||||
private onAppStateChange = async (appState: AppStateStatus) => {
|
||||
if (appState === this.previousAppState) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.previousAppState = appState;
|
||||
switch (appState) {
|
||||
case 'active':
|
||||
setTimeout(this.cancelAll, 750);
|
||||
break;
|
||||
case 'inactive':
|
||||
this.scheduleAll();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private onLogout = async ({serverUrl, removeServer}: LogoutCallbackArg) => {
|
||||
if (this.terminatingSessionUrl === serverUrl) {
|
||||
return;
|
||||
}
|
||||
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const activeServerDisplayName = await DatabaseManager.getActiveServerDisplayName();
|
||||
|
||||
await this.terminateSession(serverUrl, removeServer);
|
||||
|
||||
if (activeServerUrl === serverUrl) {
|
||||
let displayName = '';
|
||||
let launchType: LaunchType = Launch.AddServer;
|
||||
if (!Object.keys(DatabaseManager.serverDatabases).length) {
|
||||
EphemeralStore.theme = undefined;
|
||||
launchType = Launch.Normal;
|
||||
|
||||
if (activeServerDisplayName) {
|
||||
displayName = activeServerDisplayName;
|
||||
}
|
||||
}
|
||||
|
||||
relaunchApp({launchType, serverUrl, displayName}, true);
|
||||
}
|
||||
};
|
||||
|
||||
private onSessionExpired = async (serverUrl: string) => {
|
||||
this.terminatingSessionUrl = serverUrl;
|
||||
await logout(serverUrl, false, false, true);
|
||||
await this.terminateSession(serverUrl, false);
|
||||
|
||||
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
const serverDisplayName = appDatabase ? await queryServerName(appDatabase, serverUrl) : undefined;
|
||||
|
||||
await relaunchApp({launchType: Launch.Normal, serverUrl, displayName: serverDisplayName}, true);
|
||||
if (activeServerUrl) {
|
||||
addNewServer(getThemeFromState(), serverUrl, serverDisplayName);
|
||||
} else {
|
||||
EphemeralStore.theme = undefined;
|
||||
}
|
||||
this.terminatingSessionUrl = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export default new SessionManager();
|
||||
@@ -422,3 +422,12 @@ export const observeAllowedThemesKeys = (database: Database) => {
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const getExpiredSession = async (database: Database) => {
|
||||
try {
|
||||
const session = await database.get<SystemModel>(SYSTEM).find(SYSTEM_IDENTIFIERS.SESSION_EXPIRATION);
|
||||
return (session?.value || {}) as SessionExpiration;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,6 +54,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
},
|
||||
totalHeight: {height: '100%'},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -112,6 +113,7 @@ const AccountScreen = ({currentUser, enableCustomUserStatuses, customStatusExpir
|
||||
<ScrollView
|
||||
alwaysBounceVertical={false}
|
||||
style={tabletSidebarStyle}
|
||||
contentContainerStyle={styles.totalHeight}
|
||||
>
|
||||
<AccountUserInfo
|
||||
user={currentUser}
|
||||
|
||||
@@ -158,7 +158,7 @@ Appearance.addChangeListener(() => {
|
||||
}
|
||||
});
|
||||
|
||||
function getThemeFromState(): Theme {
|
||||
export function getThemeFromState(): Theme {
|
||||
if (EphemeralStore.theme) {
|
||||
return EphemeralStore.theme;
|
||||
}
|
||||
@@ -193,7 +193,7 @@ export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}
|
||||
dismissModal({componentId: Screens.SSO});
|
||||
dismissModal({componentId: Screens.BOTTOM_SHEET});
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, false);
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
|
||||
NavigationStore.clearNavigationComponents();
|
||||
@@ -228,7 +228,7 @@ export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}
|
||||
}],
|
||||
};
|
||||
|
||||
Navigation.setRoot({
|
||||
return Navigation.setRoot({
|
||||
root: {stack},
|
||||
});
|
||||
}
|
||||
@@ -272,7 +272,7 @@ export function resetToSelectServer(passProps: LaunchProps) {
|
||||
},
|
||||
}];
|
||||
|
||||
Navigation.setRoot({
|
||||
return Navigation.setRoot({
|
||||
root: {
|
||||
stack: {
|
||||
children,
|
||||
@@ -288,7 +288,7 @@ export function resetToTeams() {
|
||||
|
||||
NavigationStore.clearNavigationComponents();
|
||||
|
||||
Navigation.setRoot({
|
||||
return Navigation.setRoot({
|
||||
root: {
|
||||
stack: {
|
||||
children: [{
|
||||
@@ -324,7 +324,7 @@ export function resetToTeams() {
|
||||
|
||||
export function goToScreen(name: string, title: string, passProps = {}, options = {}) {
|
||||
if (!isScreenRegistered(name)) {
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
|
||||
const theme = getThemeFromState();
|
||||
@@ -361,7 +361,7 @@ export function goToScreen(name: string, title: string, passProps = {}, options
|
||||
},
|
||||
};
|
||||
|
||||
Navigation.push(componentId, {
|
||||
return Navigation.push(componentId, {
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {Events} from '@constants';
|
||||
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import {popToRoot} from '@screens/navigation';
|
||||
import {sortByNewest} from '@utils/general';
|
||||
|
||||
export const convertToNotificationData = (notification: Notification, tapped = true): NotificationWithData => {
|
||||
if (!notification.payload) {
|
||||
@@ -25,7 +24,7 @@ export const convertToNotificationData = (notification: Notification, tapped = t
|
||||
channel_name: payload.channel_name,
|
||||
identifier: payload.identifier || notification.identifier,
|
||||
from_webhook: payload.from_webhook,
|
||||
message: ((payload.type === 'message') ? payload.message || notification.body : undefined),
|
||||
message: ((payload.type === 'message') ? payload.message || notification.body : payload.body),
|
||||
override_icon_url: payload.override_icon_url,
|
||||
override_username: payload.override_username,
|
||||
post_id: payload.post_id,
|
||||
@@ -75,26 +74,28 @@ export const emitNotificationError = (type: 'Team' | 'Channel') => {
|
||||
}, 500);
|
||||
};
|
||||
|
||||
export const scheduleExpiredNotification = async (sessions: Session[], siteName: string, locale = DEFAULT_LOCALE) => {
|
||||
const session = sessions.sort(sortByNewest)[0];
|
||||
export const scheduleExpiredNotification = (serverUrl: string, session: Session, serverName: string, locale = DEFAULT_LOCALE) => {
|
||||
const expiresAt = session?.expires_at || 0;
|
||||
const expiresInDays = Math.ceil(Math.abs(moment.duration(moment().diff(moment(expiresAt))).asDays()));
|
||||
const intl = createIntl({locale, messages: getTranslations(locale)});
|
||||
const body = intl.formatMessage({
|
||||
id: 'mobile.session_expired',
|
||||
defaultMessage: 'Session Expired: Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.',
|
||||
}, {siteName, daysCount: expiresInDays});
|
||||
defaultMessage: 'Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.',
|
||||
}, {siteName: serverName, daysCount: expiresInDays});
|
||||
const title = intl.formatMessage({id: 'mobile.session_expired.title', defaultMessage: 'Session Expired'});
|
||||
|
||||
if (expiresAt && body) {
|
||||
//@ts-expect-error: Does not need to set all Notification properties
|
||||
PushNotifications.scheduleNotification({
|
||||
if (expiresAt) {
|
||||
return PushNotifications.scheduleNotification({
|
||||
fireDate: expiresAt,
|
||||
body,
|
||||
payload: {
|
||||
userInfo: {
|
||||
local: true,
|
||||
},
|
||||
},
|
||||
title,
|
||||
|
||||
// @ts-expect-error need to be included in the notification payload
|
||||
ack_id: serverUrl,
|
||||
server_url: serverUrl,
|
||||
type: 'session',
|
||||
});
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
@@ -576,7 +576,8 @@
|
||||
"mobile.server_url.deeplink.emm.denied": "This app is controlled by an EMM and the DeepLink server url does not match the EMM allowed server",
|
||||
"mobile.server_url.empty": "Please enter a valid server URL",
|
||||
"mobile.server_url.invalid_format": "URL must start with http:// or https://",
|
||||
"mobile.session_expired": "Session Expired: Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.",
|
||||
"mobile.session_expired": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.",
|
||||
"mobile.session_expired.title": "Session Expired",
|
||||
"mobile.set_status.away": "Away",
|
||||
"mobile.set_status.dnd": "Do Not Disturb",
|
||||
"mobile.set_status.offline": "Offline",
|
||||
|
||||
2
index.ts
2
index.ts
@@ -15,6 +15,7 @@ import ManagedApp from './app/init/managed_app';
|
||||
import PushNotifications from './app/init/push_notifications';
|
||||
import GlobalEventHandler from './app/managers/global_event_handler';
|
||||
import NetworkManager from './app/managers/network_manager';
|
||||
import SessionManager from './app/managers/session_manager';
|
||||
import WebsocketManager from './app/managers/websocket_manager';
|
||||
import {registerScreens} from './app/screens';
|
||||
import NavigationStore from './app/store/navigation_store';
|
||||
@@ -75,6 +76,7 @@ Navigation.events().registerAppLaunchedListener(async () => {
|
||||
await NetworkManager.init(serverCredentials);
|
||||
await WebsocketManager.init(serverCredentials);
|
||||
PushNotifications.init();
|
||||
SessionManager.init();
|
||||
}
|
||||
|
||||
initialLaunch();
|
||||
|
||||
@@ -84,6 +84,23 @@ index a34598c..b035a76 100644
|
||||
if (navigationActivity != null) {
|
||||
navigationActivity.onCatalystInstanceDestroy();
|
||||
}
|
||||
diff --git a/node_modules/react-native-navigation/lib/android/app/src/reactNative68/java/com/reactnativenavigation/react/ReactGateway.java b/node_modules/react-native-navigation/lib/android/app/src/reactNative68/java/com/reactnativenavigation/react/ReactGateway.java
|
||||
index 035ec31..38109c1 100644
|
||||
--- a/node_modules/react-native-navigation/lib/android/app/src/reactNative68/java/com/reactnativenavigation/react/ReactGateway.java
|
||||
+++ b/node_modules/react-native-navigation/lib/android/app/src/reactNative68/java/com/reactnativenavigation/react/ReactGateway.java
|
||||
@@ -48,6 +48,12 @@ public class ReactGateway {
|
||||
}
|
||||
}
|
||||
|
||||
+ public void onWindowFocusChanged(boolean hasFocus) {
|
||||
+ if (host.hasInstance()) {
|
||||
+ host.getReactInstanceManager().onWindowFocusChange(hasFocus);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
public void onActivityPaused(NavigationActivity activity) {
|
||||
initializer.onActivityPaused(activity);
|
||||
jsDevReloadHandler.onActivityPaused(activity);
|
||||
diff --git a/node_modules/react-native-navigation/lib/ios/RNNOverlayWindow.m b/node_modules/react-native-navigation/lib/ios/RNNOverlayWindow.m
|
||||
index 934e7e7..19169a3 100644
|
||||
--- a/node_modules/react-native-navigation/lib/ios/RNNOverlayWindow.m
|
||||
|
||||
@@ -376,7 +376,7 @@ index 0000000..58ff887
|
||||
+ }
|
||||
+}
|
||||
diff --git a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java
|
||||
index a14089f..a339934 100644
|
||||
index a14089f..5238c75 100644
|
||||
--- a/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java
|
||||
+++ b/node_modules/react-native-notifications/lib/android/app/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java
|
||||
@@ -2,9 +2,15 @@ package com.wix.reactnativenotifications.core.notificationdrawer;
|
||||
@@ -395,10 +395,26 @@ index a14089f..a339934 100644
|
||||
|
||||
public class PushNotificationsDrawer implements IPushNotificationsDrawer {
|
||||
|
||||
@@ -62,4 +68,31 @@ public class PushNotificationsDrawer implements IPushNotificationsDrawer {
|
||||
@@ -49,17 +55,50 @@ public class PushNotificationsDrawer implements IPushNotificationsDrawer {
|
||||
public void onNotificationClearRequest(int id) {
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(id);
|
||||
+ cancelScheduledNotification(String.valueOf(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationClearRequest(String tag, int id) {
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(tag, id);
|
||||
+ cancelScheduledNotification(String.valueOf(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAllNotificationsClearRequest() {
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancelAll();
|
||||
}
|
||||
+ cancelAllScheduledNotifications();
|
||||
+ }
|
||||
+
|
||||
+ protected void cancelAllScheduledNotifications() {
|
||||
+ Log.i(LOGTAG, "Cancelling all scheduled notifications");
|
||||
@@ -413,6 +429,9 @@ index a14089f..a339934 100644
|
||||
+ Log.i(LOGTAG, "Cancelling scheduled notification: " + notificationId);
|
||||
+
|
||||
+ ScheduleNotificationHelper helper = ScheduleNotificationHelper.getInstance(mContext);
|
||||
+ if (!helper.getPreferencesKeys().contains(notificationId)) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ // Remove it from the alarm manger schedule
|
||||
+ Bundle bundle = new Bundle();
|
||||
@@ -425,7 +444,7 @@ index a14089f..a339934 100644
|
||||
+ // Remove it from the notification center
|
||||
+ final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
+ notificationManager.cancel(Integer.parseInt(notificationId));
|
||||
+ }
|
||||
}
|
||||
}
|
||||
diff --git a/node_modules/react-native-notifications/lib/dist/DTO/Notification.d.ts b/node_modules/react-native-notifications/lib/dist/DTO/Notification.d.ts
|
||||
index 7b2b3b1..3a2f872 100644
|
||||
@@ -462,3 +481,46 @@ index afd5c73..6036dda 100644
|
||||
}];
|
||||
}
|
||||
|
||||
diff --git a/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.m b/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.m
|
||||
index 5c8dd0b..1c7e575 100644
|
||||
--- a/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.m
|
||||
+++ b/node_modules/react-native-notifications/lib/ios/RNNotificationEventHandler.m
|
||||
@@ -6,11 +6,13 @@
|
||||
|
||||
@implementation RNNotificationEventHandler {
|
||||
RNNotificationsStore* _store;
|
||||
+ NSDate* wakeTime;
|
||||
}
|
||||
|
||||
- (instancetype)initWithStore:(RNNotificationsStore *)store {
|
||||
self = [super init];
|
||||
_store = store;
|
||||
+ wakeTime = [[NSDate alloc] init];
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -31,6 +33,15 @@
|
||||
- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(void))completionHandler {
|
||||
[_store setActionCompletionHandler:completionHandler withCompletionKey:response.notification.request.identifier];
|
||||
[RNEventEmitter sendEvent:RNNotificationOpened body:[RNNotificationParser parseNotificationResponse:response]];
|
||||
+ dispatch_async(dispatch_get_main_queue(), ^{
|
||||
+ NSDate* now = [[NSDate alloc] init];
|
||||
+ double interval = [now timeIntervalSinceDate:self->wakeTime];
|
||||
+ BOOL background = [[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground;
|
||||
+ NSDictionary* userInfo = response.notification.request.content.userInfo;
|
||||
+ if (interval < 1.0 && !background) {
|
||||
+ [self->_store setInitialNotification:userInfo];
|
||||
+ }
|
||||
+ });
|
||||
}
|
||||
|
||||
- (void)didReceiveBackgroundNotification:(NSDictionary *)userInfo withCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
|
||||
@@ -38,7 +49,7 @@
|
||||
NSString *uuid = [[NSUUID UUID] UUIDString];
|
||||
__block BOOL completionHandlerCalled = NO;
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
- [_store setBackgroundActionCompletionHandler:^(UIBackgroundFetchResult result) {
|
||||
+ [self->_store setBackgroundActionCompletionHandler:^(UIBackgroundFetchResult result) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
completionHandler(result);
|
||||
});
|
||||
|
||||
4
types/api/session.d.ts
vendored
4
types/api/session.d.ts
vendored
@@ -7,6 +7,10 @@ interface Session {
|
||||
device_id?: string;
|
||||
expires_at: number;
|
||||
user_id: string;
|
||||
props?: {
|
||||
os: string;
|
||||
csrf: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginActionResponse {
|
||||
|
||||
6
types/database/raw_values.d.ts
vendored
6
types/database/raw_values.d.ts
vendored
@@ -52,6 +52,12 @@ type ReactionsPerPost = {
|
||||
reactions: Reaction[];
|
||||
}
|
||||
|
||||
type SessionExpiration = {
|
||||
id: string;
|
||||
notificationId: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
type IdValue = {
|
||||
id: string;
|
||||
value: unknown;
|
||||
|
||||
Reference in New Issue
Block a user