diff --git a/src/lib/API.js b/src/lib/API.js index 5553fd4d5ef8e..016468b555d67 100644 --- a/src/lib/API.js +++ b/src/lib/API.js @@ -1,17 +1,14 @@ import _ from 'underscore'; import Ion from './Ion'; import IONKEYS from '../IONKEYS'; -import {xhr, setOfflineStatus} from './Network'; +import xhr from './xhr'; +import NetworkConnection from './NetworkConnection'; import CONFIG from '../CONFIG'; import * as Pusher from './Pusher/pusher'; import ROUTES from '../ROUTES'; import Str from './Str'; import Guid from './Guid'; import redirectToSignIn from './actions/SignInRedirect'; -import Activity from './Activity'; - -// Holds all of the callbacks that need to be triggered when the network reconnects -const reconnectionCallbacks = []; // Queue for network requests so we don't lose actions done by the user while offline let networkRequestQueue = []; @@ -27,39 +24,12 @@ Ion.connect({ callback: val => authToken = val ? val.authToken : null, }); -/** - * Loop over all reconnection callbacks and fire each one - */ -function triggerReconnectionCallbacks() { - _.each(reconnectionCallbacks, callback => callback()); -} - // We subscribe to changes to the online/offline status of the network to determine when we should fire off API calls -// vs queueing them for later. When reconnecting, ie, going from offline to online, all the reconnection callbacks -// are triggered (this is usually Actions that need to re-download data from the server) +// vs queueing them for later. let isOffline; Ion.connect({ key: IONKEYS.NETWORK, - callback: (val) => { - if (isOffline && !val.isOffline) { - triggerReconnectionCallbacks(); - } - isOffline = val && val.isOffline; - } -}); - -// When the app is in the background Pusher can still receive realtime updates -// for a few minutes, but eventually disconnects causing a delay when the app -// returns from the background. So, if we are returning from the background -// and we are online we should trigger our reconnection callbacks. -Activity.registerOnAppBecameActiveCallback(() => { - // If we know we are offline and we have no authToken then there's - // no reason to trigger the reconnection callbacks as they will fail. - if (isOffline || !authToken) { - return; - } - - triggerReconnectionCallbacks(); + callback: val => isOffline = val && val.isOffline, }); // When the user authenticates for the first time we create a login and store credentials in Ion. @@ -264,7 +234,7 @@ function processNetworkRequestQueue() { // Make a simple request every second to see if the API is online again xhr('Get', {doNotRetry: true}) - .then(() => setOfflineStatus(false)); + .then(() => NetworkConnection.setOfflineStatus(false)); return; } @@ -340,16 +310,6 @@ Pusher.registerSocketEventCallback((eventName) => { } }); -/** - * Register a callback function to be called when the network reconnects - * - * @public - * @param {function} cb - */ -function onReconnect(cb) { - reconnectionCallbacks.push(cb); -} - /** * Get the authToken that the network uses * @returns {string} @@ -493,6 +453,5 @@ export { getAuthToken, getPersonalDetails, getReportHistory, - onReconnect, setLastReadActionID, }; diff --git a/src/lib/Activity/index.js b/src/lib/Activity/index.js deleted file mode 100644 index 7667948125b97..0000000000000 --- a/src/lib/Activity/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * On web/desktop we don't really care if the app becomes - * active or inactive as long as Pusher is always connected - */ -export default { - registerOnAppBecameActiveCallback: () => {}, -}; diff --git a/src/lib/Activity/index.native.js b/src/lib/Activity/index.native.js deleted file mode 100644 index 6d5505089c545..0000000000000 --- a/src/lib/Activity/index.native.js +++ /dev/null @@ -1,22 +0,0 @@ -import {AppState} from 'react-native'; - -let onAppBecameActiveCallback; - -AppState.addEventListener('change', (state) => { - if (state === 'active') { - onAppBecameActiveCallback(); - } -}); - -/** - * Register active state change callback - * - * @param {Function} callback - */ -function registerOnAppBecameActiveCallback(callback) { - onAppBecameActiveCallback = callback; -} - -export default { - registerOnAppBecameActiveCallback, -}; diff --git a/src/lib/NetworkConnection.js b/src/lib/NetworkConnection.js new file mode 100644 index 0000000000000..0e20f2f76a756 --- /dev/null +++ b/src/lib/NetworkConnection.js @@ -0,0 +1,111 @@ +import _ from 'underscore'; +import {AppState} from 'react-native'; +import NetInfo from '@react-native-community/netinfo'; +import Ion from './Ion'; +import IONKEYS from '../IONKEYS'; + +// NetInfo.addEventListener() returns a function used to unsubscribe the +// listener so we must create a reference to it and call it in stopListeningForReconnect() +let unsubscribeFromNetInfo; +let sleepTimer; +let lastTime; +let isActive = false; +let isOffline = false; + +// Holds all of the callbacks that need to be triggered when the network reconnects +const reconnectionCallbacks = []; + +/** + * Loop over all reconnection callbacks and fire each one + */ +const triggerReconnectionCallbacks = _.throttle(() => { + _.each(reconnectionCallbacks, callback => callback()); +}, 5000, {trailing: false}); + +/** + * Called when the offline status of the app changes and if the network is "reconnecting" (going from offline to online) + * then all of the reconnection callbacks are triggered + * + * @param {boolean} isCurrentlyOffline + */ +function setOfflineStatus(isCurrentlyOffline) { + Ion.merge(IONKEYS.NETWORK, {isOffline: isCurrentlyOffline}); + + // When reconnecting, ie, going from offline to online, all the reconnection callbacks + // are triggered (this is usually Actions that need to re-download data from the server) + if (isOffline && !isCurrentlyOffline) { + triggerReconnectionCallbacks(); + } + + isOffline = isCurrentlyOffline; +} + +/** + * Set up the event listener for NetInfo to tell whether the user has + * internet connectivity or not. This is more reliable than the Pusher + * `disconnected` event which takes about 10-15 seconds to emit. + */ +function listenForReconnect() { + // Subscribe to the state change event via NetInfo so we can update + // whether a user has internet connectivity or not. + unsubscribeFromNetInfo = NetInfo.addEventListener((state) => { + console.debug('[NetInfo] isConnected:', state && state.isConnected); + setOfflineStatus(!state.isConnected); + }); + + // When the app is in the background Pusher can still receive realtime updates + // for a few minutes, but eventually disconnects causing a delay when the app + // returns from the background. So, if we are returning from the background + // and we are online we should trigger our reconnection callbacks. + AppState.addEventListener('change', (state) => { + console.debug('[AppState] state changed:', state); + const nextStateIsActive = state === 'active'; + + // We are moving from not active to active and we are online so fire callbacks + if (!isOffline && nextStateIsActive && !isActive) { + triggerReconnectionCallbacks(); + } + + isActive = nextStateIsActive; + }); + + // When a device is put to sleep, NetInfo is not always able to detect + // when connectivity has been lost. As a failsafe we will capture the time + // every two seconds and if the last time recorded is greater than 5 seconds + // we know that the computer has been asleep. + lastTime = (new Date()).getTime(); + sleepTimer = setInterval(() => { + const currentTime = (new Date()).getTime(); + if (currentTime > (lastTime + 5000)) { + triggerReconnectionCallbacks(); + } + lastTime = currentTime; + }, 2000); +} + +/** + * Tear down the event listeners when we are finished with them. + */ +function stopListeningForReconnect() { + clearInterval(sleepTimer); + if (unsubscribeFromNetInfo) { + unsubscribeFromNetInfo(); + } + AppState.removeEventListener('change', () => {}); +} + +/** + * Register callback to fire when we reconnect + * + * @param {Function} callback + */ +function onReconnect(callback) { + reconnectionCallbacks.push(callback); +} + +export default { + setOfflineStatus, + listenForReconnect, + stopListeningForReconnect, + onReconnect, +}; diff --git a/src/lib/actions/PersonalDetails.js b/src/lib/actions/PersonalDetails.js index f22c00c873f81..c1829f768b26a 100644 --- a/src/lib/actions/PersonalDetails.js +++ b/src/lib/actions/PersonalDetails.js @@ -5,6 +5,7 @@ import * as API from '../API'; import IONKEYS from '../../IONKEYS'; import md5 from '../md5'; import CONST from '../../CONST'; +import NetworkConnection from '../NetworkConnection'; let currentUserEmail; Ion.connect({ @@ -138,7 +139,7 @@ function getForEmails(emailList) { } // When the app reconnects from being offline, fetch all of the personal details -API.onReconnect(fetch); +NetworkConnection.onReconnect(fetch); export { fetch, diff --git a/src/lib/actions/Report.js b/src/lib/actions/Report.js index 08149e09e8325..f9608a3d73401 100644 --- a/src/lib/actions/Report.js +++ b/src/lib/actions/Report.js @@ -13,6 +13,7 @@ import * as PersonalDetails from './PersonalDetails'; import {redirect} from './App'; import * as ActiveClientManager from '../ActiveClientManager'; import Visibility from '../Visibility'; +import NetworkConnection from '../NetworkConnection'; let currentUserEmail; let currentUserAccountID; @@ -499,7 +500,7 @@ Ion.connect({ }); // When the app reconnects from being offline, fetch all of the reports and their actions -API.onReconnect(() => { +NetworkConnection.onReconnect(() => { fetchAll(false, true); }); diff --git a/src/lib/actions/SignInRedirect.js b/src/lib/actions/SignInRedirect.js index 3acce8dfc1e45..8eef125029a1c 100644 --- a/src/lib/actions/SignInRedirect.js +++ b/src/lib/actions/SignInRedirect.js @@ -3,6 +3,7 @@ import IONKEYS from '../../IONKEYS'; import ROUTES from '../../ROUTES'; import {redirect} from './App'; import * as Pusher from '../Pusher/pusher'; +import NetworkConnection from '../NetworkConnection'; let currentURL; Ion.connect({ @@ -17,6 +18,7 @@ Ion.connect({ * @param {String} [errorMessage] error message to be displayed on the sign in page */ function redirectToSignIn(errorMessage) { + NetworkConnection.stopListeningForReconnect(); Pusher.disconnect(); Ion.clear() .then(() => { diff --git a/src/lib/Network.js b/src/lib/xhr.js similarity index 60% rename from src/lib/Network.js rename to src/lib/xhr.js index 01ba8544da007..1fdb73b932da7 100644 --- a/src/lib/Network.js +++ b/src/lib/xhr.js @@ -1,25 +1,8 @@ import _ from 'underscore'; -import NetInfo from '@react-native-community/netinfo'; import Ion from './Ion'; import CONFIG from '../CONFIG'; import IONKEYS from '../IONKEYS'; - -/** - * Called when the offline status of the app changes and if the network is "reconnecting" (going from offline to online) - * then all of the reconnection callbacks are triggered - * - * @param {boolean} isCurrentlyOffline - */ -function setOfflineStatus(isCurrentlyOffline) { - Ion.merge(IONKEYS.NETWORK, {isOffline: isCurrentlyOffline}); -} - -// Subscribe to the state change event via NetInfo so we can update -// whether a user has internet connectivity or not. This is more reliable -// than the Pusher `disconnected` event which takes about 10-15 seconds to emit -NetInfo.addEventListener((state) => { - setOfflineStatus(!state.isConnected); -}); +import NetworkConnection from './NetworkConnection'; /** * Makes XHR request @@ -41,7 +24,7 @@ function xhr(command, data, type = 'post') { // This will catch any HTTP network errors (like 404s and such), not to be confused with jsonCode which this // does NOT catch .catch(() => { - setOfflineStatus(true); + NetworkConnection.setOfflineStatus(true); // Set an error state and signify we are done loading Ion.merge(IONKEYS.SESSION, {loading: false, error: 'Cannot connect to server'}); @@ -52,7 +35,4 @@ function xhr(command, data, type = 'post') { }); } -export { - xhr, - setOfflineStatus, -}; +export default xhr; diff --git a/src/page/home/HomePage.js b/src/page/home/HomePage.js index 999e67f5f2f3a..015f04b86205a 100644 --- a/src/page/home/HomePage.js +++ b/src/page/home/HomePage.js @@ -15,6 +15,7 @@ import Main from './MainView'; import {subscribeToReportCommentEvents, fetchAll as fetchAllReports} from '../../lib/actions/Report'; import {fetch as fetchPersonalDetails} from '../../lib/actions/PersonalDetails'; import * as Pusher from '../../lib/Pusher/pusher'; +import NetworkConnection from '../../lib/NetworkConnection'; const windowSize = Dimensions.get('window'); const widthBreakPoint = 1000; @@ -36,6 +37,7 @@ export default class App extends React.Component { } componentDidMount() { + NetworkConnection.listenForReconnect(); Pusher.init().then(subscribeToReportCommentEvents); // Fetch all the personal details