diff --git a/app/package.json b/app/package.json index d75674fe..cfee6f64 100644 --- a/app/package.json +++ b/app/package.json @@ -31,6 +31,7 @@ "@sentry/electron": "0.14.0", "adm-zip": "0.4.11", "auto-launch": "5.0.5", + "backoff": "2.5.0", "dotenv": "8.0.0", "electron-is-dev": "0.3.0", "electron-log": "2.2.14", @@ -39,7 +40,6 @@ "electron-window-state": "5.0.3", "escape-html": "1.0.3", "i18n": "0.8.3", - "is-online": "7.0.0", "node-json-db": "0.9.2", "request": "2.85.0", "semver": "5.4.1", diff --git a/app/renderer/css/network.css b/app/renderer/css/network.css index a6d9b74f..f90fd5b5 100644 --- a/app/renderer/css/network.css +++ b/app/renderer/css/network.css @@ -17,27 +17,43 @@ body { } #title { + text-align: left; font-size: 24px; font-weight: bold; margin: 20px 0; } +#subtitle { + font-size: 20px; + text-align: left; + margin: 12px 0; +} + #description { + text-align: left; font-size: 16px; + list-style-position: inside; } #reconnect { + float: left; +} + +#settings { + margin-left: 116px; +} + +.button { font-size: 16px; background: rgba(0, 150, 136, 1.000); color: rgba(255, 255, 255, 1.000); - width: 84px; + width: 96px; height: 32px; border-radius: 5px; line-height: 32px; - margin: 20px auto 0; cursor: pointer; } -#reconnect:hover { +.button:hover { opacity: 0.8; } \ No newline at end of file diff --git a/app/renderer/js/components/tab.ts b/app/renderer/js/components/tab.ts index 04ded25e..defeeeda 100644 --- a/app/renderer/js/components/tab.ts +++ b/app/renderer/js/components/tab.ts @@ -25,6 +25,10 @@ class Tab extends BaseComponent { this.$el.addEventListener('mouseout', this.props.onHoverOut); } + showNetworkError(): void { + this.webview.forceLoad(); + } + activate(): void { this.$el.classList.add('active'); this.webview.load(); diff --git a/app/renderer/js/components/webview.ts b/app/renderer/js/components/webview.ts index 9824b2ad..2cf6b30b 100644 --- a/app/renderer/js/components/webview.ts +++ b/app/renderer/js/components/webview.ts @@ -125,7 +125,9 @@ class WebView extends BaseComponent { const hasConnectivityErr = SystemUtil.connectivityERR.includes(errorDescription); if (hasConnectivityErr) { console.error('error', errorDescription); - this.props.onNetworkError(); + if (!this.props.url.includes('network.html')) { + this.props.onNetworkError(this.props.index); + } } }); @@ -283,6 +285,10 @@ class WebView extends BaseComponent { this.$el.reload(); } + forceLoad(): void { + this.init(); + } + send(channel: string, ...param: any[]): void { this.$el.send(channel, ...param); } diff --git a/app/renderer/js/main.ts b/app/renderer/js/main.ts index 87a1b064..0a719b95 100644 --- a/app/renderer/js/main.ts +++ b/app/renderer/js/main.ts @@ -23,11 +23,6 @@ import CommonUtil = require('./utils/common-util'); import EnterpriseUtil = require('./utils/enterprise-util'); import Messages = require('./../../resources/messages'); -const logger = new Logger({ - file: 'errors.log', - timestamp: true -}); - interface FunctionalTabProps { name: string; materialIcon: string; @@ -68,6 +63,11 @@ interface SettingsOptions { loading?: AnyObject; } +const logger = new Logger({ + file: 'errors.log', + timestamp: true +}); + const rendererDirectory = path.resolve(__dirname, '..'); type ServerOrFunctionalTab = ServerTab | FunctionalTab; @@ -370,7 +370,7 @@ class ServerManagerView { } this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]); }, - onNetworkError: this.openNetworkTroubleshooting.bind(this), + onNetworkError: (index: number) => this.openNetworkTroubleshooting(index), onTitleChange: this.updateBadge.bind(this), nodeIntegration: false, preload: true @@ -536,7 +536,7 @@ class ServerManagerView { } this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]); }, - onNetworkError: this.openNetworkTroubleshooting.bind(this), + onNetworkError: (index: number) => this.openNetworkTroubleshooting(index), onTitleChange: this.updateBadge.bind(this), nodeIntegration: true, preload: false @@ -568,12 +568,11 @@ class ServerManagerView { }); } - openNetworkTroubleshooting(): void { - this.openFunctionalTab({ - name: 'Network Troubleshooting', - materialIcon: 'network_check', - url: `file://${rendererDirectory}/network.html` - }); + openNetworkTroubleshooting(index: number): void { + const reconnectUtil = new ReconnectUtil(this.tabs[index].webview); + reconnectUtil.pollInternetAndReload(); + this.tabs[index].webview.props.url = `file://${rendererDirectory}/network.html`; + this.tabs[index].showNetworkError(); } activateLastTab(index: number): void { @@ -797,6 +796,10 @@ class ServerManagerView { }); } + ipcRenderer.on('show-network-error', (event: Event, index: number) => { + this.openNetworkTroubleshooting(index); + }); + ipcRenderer.on('open-settings', (event: Event, settingNav: string) => { this.openSettings(settingNav); }); @@ -999,17 +1002,7 @@ class ServerManagerView { window.addEventListener('load', () => { const serverManagerView = new ServerManagerView(); - const reconnectUtil = new ReconnectUtil(serverManagerView); serverManagerView.init(); - window.addEventListener('online', () => { - reconnectUtil.pollInternetAndReload(); - }); - - window.addEventListener('offline', () => { - reconnectUtil.clearState(); - logger.log('No internet connection, you are offline.'); - }); - // only start electron-connect (auto reload on change) when its ran // from `npm run dev` or `gulp dev` and not from `npm start` when // app is started `npm start` main process's proces.argv will have diff --git a/app/renderer/js/pages/network.ts b/app/renderer/js/pages/network.ts index d0a9f0c1..961c67e3 100644 --- a/app/renderer/js/pages/network.ts +++ b/app/renderer/js/pages/network.ts @@ -3,19 +3,14 @@ import { ipcRenderer } from 'electron'; class NetworkTroubleshootingView { - $reconnectButton: Element; - constructor() { - this.$reconnectButton = document.querySelector('#reconnect'); - } - - init(): void { - this.$reconnectButton.addEventListener('click', () => { + init($reconnectButton: Element, $settingsButton: Element): void { + $reconnectButton.addEventListener('click', () => { ipcRenderer.send('forward-message', 'reload-viewer'); }); + $settingsButton.addEventListener('click', () => { + ipcRenderer.send('forward-message', 'open-settings'); + }); } } -window.addEventListener('load', () => { - const networkTroubleshootingView = new NetworkTroubleshootingView(); - networkTroubleshootingView.init(); -}); +export = new NetworkTroubleshootingView(); diff --git a/app/renderer/js/preload.ts b/app/renderer/js/preload.ts index 782f8849..4164b91a 100644 --- a/app/renderer/js/preload.ts +++ b/app/renderer/js/preload.ts @@ -12,6 +12,8 @@ import isDev = require('electron-is-dev'); import LinkUtil = require('./utils/link-util'); import params = require('./utils/params-util'); +import NetworkError = require('./pages/network'); + interface PatchedGlobal extends NodeJS.Global { logout: () => void; shortcut: () => void; @@ -120,6 +122,15 @@ window.addEventListener('beforeunload', (): void => { SetupSpellChecker.unsubscribeSpellChecker(); }); +window.addEventListener('load', (event: any): void => { + if (!event.target.URL.includes('app/renderer/network.html')) { + return; + } + const $reconnectButton = document.querySelector('#reconnect'); + const $settingsButton = document.querySelector('#settings'); + NetworkError.init($reconnectButton, $settingsButton); +}); + // electron's globalShortcut can cause unexpected results // so adding the reload shortcut in the old-school way // Zoom from numpad keys is not supported by electron, so adding it through listeners. diff --git a/app/renderer/js/utils/domain-util.ts b/app/renderer/js/utils/domain-util.ts index b547d3fe..6a8b5cce 100644 --- a/app/renderer/js/utils/domain-util.ts +++ b/app/renderer/js/utils/domain-util.ts @@ -12,6 +12,7 @@ import RequestUtil = require('./request-util'); import EnterpriseUtil = require('./enterprise-util'); import Messages = require('../../../resources/messages'); +const { ipcRenderer } = electron; const { app, dialog } = electron.remote; const logger = new Logger({ @@ -59,6 +60,16 @@ class DomainUtil { return this.db.getData(`/domains[${index}]`); } + shouldIgnoreCerts(url: string): boolean { + const domains = this.getDomains(); + for (const domain of domains) { + if (domain.url === url) { + return domain.ignoreCerts; + } + } + return null; + } + updateDomain(index: number, server: object): void { this.reloadDB(); this.db.push(`/domains[${index}]`, server, true); @@ -250,19 +261,21 @@ class DomainUtil { }); } - updateSavedServer(url: string, index: number): void { + async updateSavedServer(url: string, index: number): Promise { // Does not promise successful update const oldIcon = this.getDomain(index).icon; const { ignoreCerts } = this.getDomain(index); - this.checkDomain(url, ignoreCerts, true).then(newServerConf => { - this.saveServerIcon(newServerConf, ignoreCerts).then(localIconUrl => { - if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') { - newServerConf.icon = localIconUrl; - this.updateDomain(index, newServerConf); - this.reloadDB(); - } - }); - }); + try { + const newServerConf = await this.checkDomain(url, ignoreCerts, true); + const localIconUrl = await this.saveServerIcon(newServerConf, ignoreCerts); + if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') { + newServerConf.icon = localIconUrl; + this.updateDomain(index, newServerConf); + this.reloadDB(); + } + } catch (err) { + ipcRenderer.send('forward-message', 'show-network-error', index); + } } reloadDB(): void { diff --git a/app/renderer/js/utils/reconnect-util.ts b/app/renderer/js/utils/reconnect-util.ts index 90a363b2..873518a5 100644 --- a/app/renderer/js/utils/reconnect-util.ts +++ b/app/renderer/js/utils/reconnect-util.ts @@ -1,5 +1,10 @@ -import isOnline = require('is-online'); +import { ipcRenderer } from 'electron'; + +import backoff = require('backoff'); +import request = require('request'); import Logger = require('./logger-util'); +import RequestUtil = require('./request-util'); +import DomainUtil = require('./domain-util'); const logger = new Logger({ file: `domain-util.log`, @@ -7,43 +12,73 @@ const logger = new Logger({ }); class ReconnectUtil { - // TODO: TypeScript - Figure out how to annotate serverManagerView - // it should be ServerManagerView; maybe make it a generic so we can - // pass the class from main.js - serverManagerView: any; + // TODO: TypeScript - Figure out how to annotate webview + // it should be WebView; maybe make it a generic so we can + // pass the class from main.ts + webview: any; + url: string; alreadyReloaded: boolean; + fibonacciBackoff: any; - constructor(serverManagerView: any) { - this.serverManagerView = serverManagerView; + constructor(webview: any) { + this.webview = webview; + this.url = webview.props.url; this.alreadyReloaded = false; + this.clearState(); } clearState(): void { - this.alreadyReloaded = false; + this.fibonacciBackoff = backoff.fibonacci({ + initialDelay: 5000, + maxDelay: 300000 + }); + } + + isOnline(): Promise { + return new Promise(resolve => { + try { + const ignoreCerts = DomainUtil.shouldIgnoreCerts(this.url); + if (ignoreCerts === null) { + return; + } + request( + { + url: `${this.url}/static/favicon.ico`, + ...RequestUtil.requestOptions(this.url, ignoreCerts) + }, + (error: Error, response: any) => { + const isValidResponse = + !error && response.statusCode >= 200 && response.statusCode < 400; + resolve(isValidResponse); + } + ); + } catch (err) { + logger.log(err); + } + }); } pollInternetAndReload(): void { - const pollInterval = setInterval(() => { - this._checkAndReload() - .then(status => { - if (status) { - this.alreadyReloaded = true; - clearInterval(pollInterval); - } - }); - }, 1500); + this.fibonacciBackoff.backoff(); + this.fibonacciBackoff.on('ready', () => { + this._checkAndReload().then(status => { + if (status) { + this.fibonacciBackoff.reset(); + } else { + this.fibonacciBackoff.backoff(); + } + }); + }); } // TODO: Make this a async function _checkAndReload(): Promise { return new Promise(resolve => { if (!this.alreadyReloaded) { // eslint-disable-line no-negated-condition - isOnline() + this.isOnline() .then((online: boolean) => { if (online) { - if (!this.alreadyReloaded) { - this.serverManagerView.reloadCurrentView(); - } + ipcRenderer.send('forward-message', 'reload-viewer'); logger.log('You\'re back online.'); return resolve(true); } diff --git a/app/renderer/network.html b/app/renderer/network.html index 093274b3..7e2a2fe0 100644 --- a/app/renderer/network.html +++ b/app/renderer/network.html @@ -9,15 +9,21 @@
-
Zulip can't connect
-
-
Your computer seems to be offline.
-
We will keep trying to reconnect, or you can try now.
+
We can't connect to this organization
+
This could be because
+
    +
  • You're not online or your proxy is misconfigured.
  • +
  • There is no Zulip organization hosted at this URL.
  • +
  • This Zulip organization is temporarily unavailable.
  • +
  • This Zulip organization has been moved or deleted.
  • +
+
+
Reconnect
+
Settings
-
Try now
- + diff --git a/typings.d.ts b/typings.d.ts index 705d6a8c..5555c928 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -12,6 +12,7 @@ declare module 'escape-html'; declare module 'fs-extra'; declare module 'wurl'; declare module 'i18n'; +declare module 'backoff'; interface PageParamsObject { realm_uri: string;