mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-23 03:31:56 +00:00
network: Tackle network issues independently.
Few changes - * webview: Show connection failure per server. * network: Try to reconnect diff servers. * Fixes concern that some proxy networks may allow only specific servers to be reachable. * domains: Show network error on server invalidation. * webview: Handle network errors in preload script. Fixes: #591, #312.
This commit is contained in:
committed by
Akash Nimare
parent
77044fd9fa
commit
d4b9663257
@@ -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",
|
||||
|
@@ -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;
|
||||
}
|
@@ -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();
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
|
@@ -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.
|
||||
|
@@ -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<void> {
|
||||
// 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 {
|
||||
|
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
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);
|
||||
}
|
||||
|
@@ -9,15 +9,21 @@
|
||||
<body>
|
||||
<div id="content">
|
||||
<div id="picture"><img src="img/zulip_network.png"></div>
|
||||
<div id="title">Zulip can't connect</div>
|
||||
<div id="description">
|
||||
<div>Your computer seems to be offline.</div>
|
||||
<div>We will keep trying to reconnect, or you can try now.</div>
|
||||
<div id="title">We can't connect to this organization</div>
|
||||
<div id="subtitle">This could be because</div>
|
||||
<ul id="description">
|
||||
<li>You're not online or your proxy is misconfigured.</li>
|
||||
<li>There is no Zulip organization hosted at this URL.</li>
|
||||
<li>This Zulip organization is temporarily unavailable.</li>
|
||||
<li>This Zulip organization has been moved or deleted.</li>
|
||||
</ul>
|
||||
<div id="buttons">
|
||||
<div id="reconnect" class="button">Reconnect</div>
|
||||
<div id="settings" class="button">Settings</div>
|
||||
</div>
|
||||
<div id="reconnect">Try now</div>
|
||||
</div>
|
||||
</body>
|
||||
<script>var exports = {};</script>
|
||||
<script src="js/pages/network.js"></script>
|
||||
<script src="js/preload.js"></script>
|
||||
<script>require('./js/shared/preventdrag.js')</script>
|
||||
</html>
|
||||
|
1
typings.d.ts
vendored
1
typings.d.ts
vendored
@@ -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;
|
||||
|
Reference in New Issue
Block a user