diff --git a/src/CONST.js b/src/CONST.js index b5e3d30516bd..2e61e3f1c876 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -652,6 +652,7 @@ const CONST = { }, }, TIMING: { + CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', SEARCH_RENDER: 'search_render', HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', REPORT_INITIAL_RENDER: 'report_initial_render', @@ -724,9 +725,6 @@ const CONST = { MAX_RETRY_WAIT_TIME_MS: 10 * 1000, PROCESS_REQUEST_DELAY_MS: 1000, MAX_PENDING_TIME_MS: 10 * 1000, - COMMAND: { - RECONNECT_APP: 'ReconnectApp', - }, }, DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index e76517e2059f..5a8185a03038 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -22,9 +22,6 @@ Onyx.connect({ // We use the AbortController API to terminate pending request in `cancelPendingRequests` let cancellationController = new AbortController(); -// To terminate pending ReconnectApp requests https://github.com/Expensify/App/issues/15627 -let reconnectAppCancellationController = new AbortController(); - /** * Send an HTTP request, and attempt to resolve the json response. * If there is a network error, we'll set the application offline. @@ -33,18 +30,12 @@ let reconnectAppCancellationController = new AbortController(); * @param {String} [method] * @param {Object} [body] * @param {Boolean} [canCancel] - * @param {String} [command] * @returns {Promise} */ -function processHTTPRequest(url, method = 'get', body = null, canCancel = true, command = '') { - let signal; - if (canCancel) { - signal = command === CONST.NETWORK.COMMAND.RECONNECT_APP ? reconnectAppCancellationController.signal : cancellationController.signal; - } - +function processHTTPRequest(url, method = 'get', body = null, canCancel = true) { return fetch(url, { // We hook requests to the same Controller signal, so we can cancel them all at once - signal, + signal: canCancel ? cancellationController.signal : undefined, method, body, }) @@ -136,12 +127,7 @@ function xhr(command, data, type = CONST.NETWORK.METHOD.POST, shouldUseSecure = }); const url = ApiUtils.getCommandURL({shouldUseSecure, command}); - return processHTTPRequest(url, type, formData, data.canCancel, command); -} - -function cancelPendingReconnectAppRequest() { - reconnectAppCancellationController.abort(); - reconnectAppCancellationController = new AbortController(); + return processHTTPRequest(url, type, formData, data.canCancel); } function cancelPendingRequests() { @@ -150,11 +136,9 @@ function cancelPendingRequests() { // We create a new instance because once `abort()` is called any future requests using the same controller would // automatically get rejected: https://dom.spec.whatwg.org/#abortcontroller-api-integration cancellationController = new AbortController(); - cancelPendingReconnectAppRequest(); } export default { xhr, cancelPendingRequests, - cancelPendingReconnectAppRequest, }; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index e943d529d86b..c2fe7d3475e0 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -32,6 +32,7 @@ import CentralPaneNavigator from './Navigators/CentralPaneNavigator'; import NAVIGATORS from '../../../NAVIGATORS'; import FullScreenNavigator from './Navigators/FullScreenNavigator'; import styles from '../../../styles/styles'; +import * as SessionUtils from '../../SessionUtils'; let currentUserEmail; Onyx.connect({ @@ -119,7 +120,14 @@ class AuthScreens extends React.Component { User.subscribeToUserEvents(); }); - App.openApp(); + // If we are on this screen then we are "logged in", but the user might not have "just logged in". They could be reopening the app + // or returning from background. If so, we'll assume they have some app data already and we can call reconnectApp() instead of openApp(). + if (SessionUtils.didUserLogInDuringSession()) { + App.openApp(); + } else { + App.reconnectApp(); + } + App.setUpPoliciesAndNavigate(this.props.session); if (this.props.lastOpenedPublicRoomID) { diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index bead882e2e15..86096b300c12 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -11,6 +11,19 @@ import Log from './Log'; import * as CurrencyUtils from './CurrencyUtils'; import isReportMessageAttachment from './isReportMessageAttachment'; +const allReports = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + callback: (report, key) => { + if (!key || !report) { + return; + } + + const reportID = CollectionUtils.extractCollectionItemID(key); + allReports[reportID] = report; + }, +}); + const allReportActions = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, @@ -440,6 +453,45 @@ function getReportAction(reportID, reportActionID) { return lodashGet(allReportActions, [reportID, reportActionID], {}); } +/** + * @returns {string} + */ +function getMostRecentReportActionLastModified() { + // Start with the oldest date possible + let mostRecentReportActionLastModified = new Date(0).toISOString(); + + // Flatten all the actions + // Loop over them all to find the one that is the most recent + const flatReportActions = _.flatten(_.map(allReportActions, (actions) => _.values(actions))); + _.each(flatReportActions, (action) => { + // Pending actions should not be counted here as a user could create a comment or some other action while offline and the server might know about + // messages they have not seen yet. + if (!_.isEmpty(action.pendingAction)) { + return; + } + + const lastModified = action.lastModified || action.created; + if (lastModified < mostRecentReportActionLastModified) { + return; + } + + mostRecentReportActionLastModified = lastModified; + }); + + // We might not have actions so we also look at the report objects to see if any have a lastVisibleActionLastModified that is more recent. We don't need to get + // any reports that have been updated before either a recently updated report or reportAction as we should be up to date on these + _.each(allReports, (report) => { + const reportLastVisibleActionLastModified = report.lastVisibleActionLastModified || report.lastVisibleActionCreated; + if (!reportLastVisibleActionLastModified || reportLastVisibleActionLastModified < mostRecentReportActionLastModified) { + return; + } + + mostRecentReportActionLastModified = reportLastVisibleActionLastModified; + }); + + return mostRecentReportActionLastModified; +} + /** * @param {*} chatReportID * @param {*} iouReportID @@ -496,6 +548,7 @@ export { isMoneyRequestAction, hasCommentThread, getLinkedTransactionID, + getMostRecentReportActionLastModified, getReportPreviewAction, isCreatedTaskReportAction, getParentReportAction, diff --git a/src/libs/SessionUtils.js b/src/libs/SessionUtils.js index 875b540e5599..7b1fc9f42d25 100644 --- a/src/libs/SessionUtils.js +++ b/src/libs/SessionUtils.js @@ -1,4 +1,7 @@ +import Onyx from 'react-native-onyx'; +import _ from 'underscore'; import lodashGet from 'lodash/get'; +import ONYXKEYS from '../ONYXKEYS'; /** * Determine if the transitioning user is logging in as a new user. @@ -28,7 +31,34 @@ function isLoggingInAsNewUser(transitionURL, sessionEmail) { return linkedEmail !== sessionEmail; } -export { - // eslint-disable-next-line import/prefer-default-export - isLoggingInAsNewUser, -}; +let loggedInDuringSession; + +// To tell if the user logged in during this session we will check the value of session.authToken once when the app's JS inits. When the user logs out +// we can reset this flag so that it can be updated again. +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (session) => { + if (!_.isUndefined(loggedInDuringSession)) { + return; + } + + if (session && session.authToken) { + loggedInDuringSession = false; + } else { + loggedInDuringSession = true; + } + }, +}); + +function resetDidUserLogInDuringSession() { + loggedInDuringSession = undefined; +} + +/** + * @returns {boolean} + */ +function didUserLogInDuringSession() { + return Boolean(loggedInDuringSession); +} + +export {isLoggingInAsNewUser, didUserLogInDuringSession, resetDidUserLogInDuringSession}; diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index d4012829b90c..8fd0ed6ce1ff 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -15,6 +15,8 @@ import ROUTES from '../../ROUTES'; import * as SessionUtils from '../SessionUtils'; import getCurrentUrl from '../Navigation/currentUrl'; import * as Session from './Session'; +import * as ReportActionsUtils from '../ReportActionsUtils'; +import Timing from './Timing'; let currentUserAccountID; Onyx.connect({ @@ -31,13 +33,6 @@ Onyx.connect({ initWithStoredValues: false, }); -let allPolicies = []; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY, - waitForCollectionCallback: true, - callback: (policies) => (allPolicies = policies), -}); - let preferredLocale; Onyx.connect({ key: ONYXKEYS.NVP_PREFERRED_LOCALE, @@ -125,42 +120,52 @@ AppState.addEventListener('change', (nextAppState) => { /** * Fetches data needed for app initialization + * @param {boolean} [isReconnecting] */ -function openApp() { +function openApp(isReconnecting = false) { isReadyToOpenApp.then(() => { - // We need a fresh connection/callback here to make sure that the list of policyIDs that is sent to OpenApp is the most updated list from Onyx const connectionID = Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, waitForCollectionCallback: true, callback: (policies) => { + // When the app reconnects we do a fast "sync" of the LHN and only return chats that have new messages. We achieve this by sending the most recent reportActionID. + // we have locally. And then only update the user about chats with messages that have occurred after that reportActionID. + // + // - Look through the local report actions and reports to find the most recently modified report action or report. + // - We send this to the server so that it can compute which new chats the user needs to see and return only those as an optimization. + const params = {policyIDList: getNonOptimisticPolicyIDs(policies)}; + if (isReconnecting) { + Timing.start(CONST.TIMING.CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION); + params.mostRecentReportActionLastModified = ReportActionsUtils.getMostRecentReportActionLastModified(); + Timing.end(CONST.TIMING.CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION, '', 500); + } Onyx.disconnect(connectionID); - API.read( - 'OpenApp', - {policyIDList: getNonOptimisticPolicyIDs(policies)}, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: true, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: false, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: false, - }, - ], - }, - ); + + // eslint-disable-next-line rulesdir/no-multiple-api-calls + const apiMethod = isReconnecting ? API.write : API.read; + apiMethod(isReconnecting ? 'ReconnectApp' : 'OpenApp', params, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + value: true, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + value: false, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + value: false, + }, + ], + }); }, }); }); @@ -170,33 +175,7 @@ function openApp() { * Refreshes data when the app reconnects */ function reconnectApp() { - API.write( - CONST.NETWORK.COMMAND.RECONNECT_APP, - {policyIDList: getNonOptimisticPolicyIDs(allPolicies)}, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: true, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: false, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: false, - }, - ], - }, - ); + openApp(true); } /** diff --git a/src/libs/actions/PersistedRequests.js b/src/libs/actions/PersistedRequests.js index e3aafd18c35d..d893ee255287 100644 --- a/src/libs/actions/PersistedRequests.js +++ b/src/libs/actions/PersistedRequests.js @@ -1,8 +1,6 @@ import Onyx from 'react-native-onyx'; import _ from 'underscore'; -import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; -import HttpUtils from '../HttpUtils'; let persistedRequests = []; @@ -19,12 +17,7 @@ function clear() { * @param {Array} requestsToPersist */ function save(requestsToPersist) { - HttpUtils.cancelPendingReconnectAppRequest(); - persistedRequests = _.chain(persistedRequests) - .concat(requestsToPersist) - .partition((request) => request.command !== CONST.NETWORK.COMMAND.RECONNECT_APP) - .flatten() - .value(); + persistedRequests = persistedRequests.concat(requestsToPersist); Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests); } diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index a500635222d6..a010621c4eea 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -10,6 +10,7 @@ import navigationRef from '../Navigation/navigationRef'; import SCREENS from '../../SCREENS'; import Navigation from '../Navigation/Navigation'; import * as ErrorUtils from '../ErrorUtils'; +import * as SessionUtils from '../SessionUtils'; let currentIsOffline; let currentShouldForceOffline; @@ -87,6 +88,7 @@ function redirectToSignIn(errorMessage) { NetworkConnection.clearReconnectionCallbacks(); clearStorageAndRedirect(errorMessage); resetHomeRouteParams(); + SessionUtils.resetDidUserLogInDuringSession(); } export default redirectToSignIn; diff --git a/src/libs/actions/Timing.js b/src/libs/actions/Timing.js index baaf666948ff..2be2cdc6fa63 100644 --- a/src/libs/actions/Timing.js +++ b/src/libs/actions/Timing.js @@ -2,6 +2,7 @@ import getPlatform from '../getPlatform'; import * as Environment from '../Environment/Environment'; import Firebase from '../Firebase'; import * as API from '../API'; +import Log from '../Log'; let timestampData = {}; @@ -26,8 +27,9 @@ function start(eventName, shouldUseFirebase = false) { * * @param {String} eventName - event name used as timestamp key * @param {String} [secondaryName] - optional secondary event name, passed to grafana + * @param {number} [maxExecutionTime] - optional amount of time (ms) to wait before logging a warn */ -function end(eventName, secondaryName = '') { +function end(eventName, secondaryName = '', maxExecutionTime = 0) { if (!timestampData[eventName]) { return; } @@ -51,6 +53,10 @@ function end(eventName, secondaryName = '') { return; } + if (maxExecutionTime && eventTime > maxExecutionTime) { + Log.warn(`${eventName} exceeded max execution time of ${maxExecutionTime}.`, {eventTime, eventName}); + } + API.read( 'SendPerformanceTiming', {