diff --git a/internal_packages/notifications/lib/items/offline-notification.jsx b/internal_packages/notifications/lib/items/offline-notification.jsx new file mode 100644 index 000000000..d72f108b0 --- /dev/null +++ b/internal_packages/notifications/lib/items/offline-notification.jsx @@ -0,0 +1,42 @@ +import {OnlineStatusStore, React, Actions} from 'nylas-exports'; +import {Notification, ListensToFluxStore} from 'nylas-component-kit'; + + +function OfflineNotification({isOnline, retryingInSeconds}) { + if (isOnline) { + return false + } + const subtitle = retryingInSeconds ? + `Retrying in ${retryingInSeconds} second${retryingInSeconds > 1 ? 's' : ''}` : + `Retrying now...`; + + return ( + Actions.checkOnlineStatus(), + }]} + /> + ) +} +OfflineNotification.displayName = 'OfflineNotification' +OfflineNotification.propTypes = { + isOnline: React.PropTypes.bool, + retryingInSeconds: React.PropTypes.number, +} + +export default ListensToFluxStore(OfflineNotification, { + stores: [OnlineStatusStore], + getStateFromStores() { + return { + isOnline: OnlineStatusStore.isOnline(), + retryingInSeconds: OnlineStatusStore.retryingInSeconds(), + } + }, +}) diff --git a/internal_packages/notifications/lib/main.es6 b/internal_packages/notifications/lib/main.es6 index 023544110..3254f7e57 100644 --- a/internal_packages/notifications/lib/main.es6 +++ b/internal_packages/notifications/lib/main.es6 @@ -9,6 +9,7 @@ import DefaultClientNotification from "./items/default-client-notif"; import UnstableChannelNotification from "./items/unstable-channel-notif"; import DevModeNotification from "./items/dev-mode-notif"; import DisabledMailRulesNotification from "./items/disabled-mail-rules-notif"; +import OfflineNotification from "./items/offline-notification"; import UpdateNotification from "./items/update-notification"; const notifications = [ @@ -17,6 +18,7 @@ const notifications = [ UnstableChannelNotification, DevModeNotification, DisabledMailRulesNotification, + OfflineNotification, UpdateNotification, ] diff --git a/package.json b/package.json index 09ccd72ca..d59871dab 100644 --- a/package.json +++ b/package.json @@ -45,10 +45,11 @@ "guid": "0.0.10", "imap-provider-settings": "nylas/imap-provider-settings", "immutable": "3.7.5", + "is-online": "6.1.0", "jasmine-json": "~0.0", "jasmine-react-helpers": "^0.2", - "jasmine-tagged": "^1.1.2", "jasmine-reporters": "1.x.x", + "jasmine-tagged": "^1.1.2", "jsx-transform": "^2.3.0", "juice": "^1.4", "kbpgp": "^2.0.52", diff --git a/src/flux/actions.es6 b/src/flux/actions.es6 index a3c2adddf..5fd10f395 100644 --- a/src/flux/actions.es6 +++ b/src/flux/actions.es6 @@ -114,6 +114,7 @@ class Actions { static longPollProcessedDeltas = ActionScopeWorkWindow; static willMakeAPIRequest = ActionScopeWorkWindow; static didMakeAPIRequest = ActionScopeWorkWindow; + static checkOnlineStatus = ActionScopeWindow; static wakeLocalSyncWorkerForAccount = ActionScopeGlobal; diff --git a/src/flux/stores/online-status-store.es6 b/src/flux/stores/online-status-store.es6 new file mode 100644 index 000000000..3db999e9d --- /dev/null +++ b/src/flux/stores/online-status-store.es6 @@ -0,0 +1,111 @@ +import isOnline from 'is-online' +import NylasStore from 'nylas-store' +import Actions from '../actions' +import {ExponentialBackoffScheduler} from '../../services/backoff-schedulers' + + +const CHECK_ONLINE_INTERVAL = 30 * 1000 + +class OnlineStatusStore extends NylasStore { + + constructor() { + super() + this._isOnline = true + this._retryingInSeconds = 0 + this._countdownInterval = null + this._checkOnlineTimeout = null + this._backoffScheduler = new ExponentialBackoffScheduler({jitter: false}) + + this.setupEmitter() + + if (NylasEnv.isMainWindow()) { + Actions.checkOnlineStatus.listen(() => this._checkOnlineStatus()) + this._checkOnlineStatus() + } + } + + isOnline() { + return this._isOnline + } + + retryingInSeconds() { + return this._retryingInSeconds + } + + async _setNextOnlineState() { + const nextIsOnline = await isOnline() + if (this._isOnline !== nextIsOnline) { + this._isOnline = nextIsOnline + this.trigger() + } + } + + async _checkOnlineStatus() { + this._clearCheckOnlineInterval() + this._clearRetryCountdown() + + // If we are currently offline, this trigger will show the `Retrying now...` + // message + this._retryingInSeconds = 0 + this.trigger() + + await this._setNextOnlineState() + + if (!this._isOnline) { + this._checkOnlineStatusAfterBackoff() + } else { + this._backoffScheduler.reset() + this._checkOnlineTimeout = setTimeout(() => { + this._checkOnlineStatus() + }, CHECK_ONLINE_INTERVAL) + } + } + + async _checkOnlineStatusAfterBackoff() { + const nextDelayMs = this._backoffScheduler.nextDelay() + try { + await this._countdownRetryingInSeconds(nextDelayMs) + this._checkOnlineStatus() + } catch (err) { + // This means the retry countdown was cleared, in which case we don't + // want to do anything + } + } + + async _countdownRetryingInSeconds(nextDelayMs) { + this._retryingInSeconds = Math.ceil(nextDelayMs / 1000) + this.trigger() + + return new Promise((resolve, reject) => { + this._clearRetryCountdown() + this._emitter.once('clear-retry-countdown', () => reject(new Error('Retry countdown cleared'))) + + this._countdownInterval = setInterval(() => { + this._retryingInSeconds = Math.max(0, this._retryingInSeconds - 1) + this.trigger() + + if (this._retryingInSeconds === 0) { + this._clearCountdownInterval() + resolve() + } + }, 1000) + }) + } + + _clearCheckOnlineInterval() { + clearInterval(this._checkOnlineTimeout) + this._checkOnlineTimeout = null + } + + _clearCountdownInterval() { + clearInterval(this._countdownInterval) + this._countdownInterval = null + } + + _clearRetryCountdown() { + this._clearCountdownInterval() + this._emitter.emit('clear-retry-countdown') + } +} + +export default new OnlineStatusStore() diff --git a/src/global/nylas-exports.es6 b/src/global/nylas-exports.es6 index 1cc004dcd..cb9b8400e 100644 --- a/src/global/nylas-exports.es6 +++ b/src/global/nylas-exports.es6 @@ -157,6 +157,7 @@ lazyLoadAndRegisterStore(`SendActionsStore`, 'send-actions-store'); lazyLoadAndRegisterStore(`FeatureUsageStore`, 'feature-usage-store'); lazyLoadAndRegisterStore(`ThreadCountsStore`, 'thread-counts-store'); lazyLoadAndRegisterStore(`FileDownloadStore`, 'file-download-store'); +lazyLoadAndRegisterStore(`OnlineStatusStore`, 'online-status-store'); lazyLoadAndRegisterStore(`UpdateChannelStore`, 'update-channel-store'); lazyLoadAndRegisterStore(`PreferencesUIStore`, 'preferences-ui-store'); lazyLoadAndRegisterStore(`FocusedContentStore`, 'focused-content-store');