diff --git a/.node-version b/.node-version
new file mode 100644
index 00000000..c250d84b
--- /dev/null
+++ b/.node-version
@@ -0,0 +1 @@
+6.9.4
diff --git a/.python-version b/.python-version
new file mode 100644
index 00000000..3e651609
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+2.7.9
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..20af2f68
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+// Place your settings in this file to overwrite default and user settings.
+{
+}
\ No newline at end of file
diff --git a/app/main/index.js b/app/main/index.js
index 0707188d..212008ea 100644
--- a/app/main/index.js
+++ b/app/main/index.js
@@ -1,24 +1,16 @@
'use strict';
const path = require('path');
-const fs = require('fs');
const os = require('os');
const electron = require('electron');
const {app} = require('electron');
const ipc = require('electron').ipcMain;
const {dialog} = require('electron');
-const https = require('https');
-const http = require('http');
const electronLocalshortcut = require('electron-localshortcut');
const Configstore = require('electron-config');
-const JsonDB = require('node-json-db');
const isDev = require('electron-is-dev');
const appMenu = require('./menu');
-const {linkIsInternal, skipImages} = require('./link-helper');
const {appUpdater} = require('./autoupdater');
-const db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
-const data = db.getData('/');
-
// Adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')();
@@ -45,48 +37,9 @@ const isUserAgent = 'ZulipElectron/' + app.getVersion() + ' ' + userOS();
// Prevent window being garbage collected
let mainWindow;
-let targetLink;
// Load this url in main window
-const staticURL = 'file://' + path.join(__dirname, '../renderer', 'index.html');
-
-const targetURL = function () {
- if (data.domain === undefined) {
- return staticURL;
- }
- return data.domain;
-};
-
-function serverError(targetURL) {
- if (targetURL.indexOf('localhost:') < 0 && data.domain) {
- const req = https.request(targetURL + '/static/audio/zulip.ogg', res => {
- console.log('Server StatusCode:', res.statusCode);
- console.log('You are connected to:', res.req._headers.host);
- if (res.statusCode >= 500 && res.statusCode <= 599) {
- return dialog.showErrorBox('SERVER IS DOWN!', 'We are getting a ' + res.statusCode + ' error status from the server ' + res.req._headers.host + '. Please try again after some time or you may switch server.');
- }
- });
- req.on('error', e => {
- if (e.toString().indexOf('Error: self signed certificate') >= 0) {
- const url = targetURL.replace(/^https?:\/\//, '');
- console.log('Server StatusCode:', 200);
- console.log('You are connected to:', url);
- } else {
- console.error(e);
- }
- });
- req.end();
- } else if (data.domain) {
- const req = http.request(targetURL + '/static/audio/zulip.ogg', res => {
- console.log('Server StatusCode:', res.statusCode);
- console.log('You are connected to:', res.req._headers.host);
- });
- req.on('error', e => {
- console.error(e);
- });
- req.end();
- }
-}
+const mainURL = 'file://' + path.join(__dirname, '../renderer', 'main.html');
function checkConnectivity() {
return dialog.showMessageBox({
@@ -113,6 +66,7 @@ const connectivityERR = [
'ERR_NAME_NOT_RESOLVED'
];
+// TODO
function checkConnection() {
// eslint-disable-next-line no-unused-vars
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => {
@@ -138,13 +92,6 @@ if (isAlreadyRunning) {
app.quit();
}
-function checkWindowURL() {
- if (data.domain !== undefined) {
- return data.domain;
- }
- return targetLink;
-}
-
function isWindowsOrmacOS() {
return process.platform === 'darwin' || process.platform === 'win32';
}
@@ -161,20 +108,6 @@ function onClosed() {
mainWindow = null;
}
-function updateDockBadge(title) {
- if (title.indexOf('Zulip') === -1) {
- return;
- }
-
- let messageCount = (/\(([0-9]+)\)/).exec(title);
- messageCount = messageCount ? Number(messageCount[1]) : 0;
-
- if (process.platform === 'darwin') {
- app.setBadgeCount(messageCount);
- }
- mainWindow.webContents.send('tray', messageCount);
-}
-
function createMainWindow() {
const win = new electron.BrowserWindow({
// This settings needs to be saved in config
@@ -184,11 +117,11 @@ function createMainWindow() {
icon: iconPath(),
minWidth: 600,
minHeight: 400,
+ titleBarStyle: 'hidden-inset',
webPreferences: {
- preload: path.join(__dirname, '../renderer/js/preload.js'),
plugins: true,
allowDisplayingInsecureContent: true,
- nodeIntegration: false
+ nodeIntegration: true
},
show: false
});
@@ -197,9 +130,7 @@ function createMainWindow() {
win.show();
});
- serverError(targetURL());
-
- win.loadURL(targetURL(), {
+ win.loadURL(mainURL, {
userAgent: isUserAgent + ' ' + win.webContents.getUserAgent()
});
@@ -241,12 +172,6 @@ function createMainWindow() {
});
});
- // Stop page to update it's title
- win.on('page-title-updated', (e, title) => {
- e.preventDefault();
- updateDockBadge(title);
- });
-
// To destroy tray icon when navigate to a new URL
win.webContents.on('will-navigate', e => {
if (e) {
@@ -257,30 +182,6 @@ function createMainWindow() {
return win;
}
-// TODO - fix certificate errors
-
-// app.commandLine.appendSwitch('ignore-certificate-errors', 'true');
-
-// For self-signed certificate
-ipc.on('certificate-err', (e, domain) => {
- const detail = `URL: ${domain} \n Error: Self-Signed Certificate`;
- dialog.showMessageBox(mainWindow, {
- title: 'Certificate error',
- message: `Do you trust certificate from ${domain}?`,
- // eslint-disable-next-line object-shorthand
- detail: detail,
- type: 'warning',
- buttons: ['Yes', 'No'],
- cancelId: 1
- // eslint-disable-next-line object-shorthand
- }, response => {
- if (response === 0) {
- // eslint-disable-next-line object-shorthand
- db.push('/domain', domain);
- mainWindow.loadURL(domain);
- }
- });
-});
// eslint-disable-next-line max-params
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
event.preventDefault();
@@ -310,36 +211,22 @@ app.on('ready', () => {
// TODO - use global shortcut instead
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
- mainWindow.reload();
- mainWindow.webContents.send('destroytray');
+ page.send('reload');
+ // page.send('destroytray');
});
electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => {
- if (page.canGoBack()) {
- page.goBack();
- }
+ page.send('back');
});
electronLocalshortcut.register(mainWindow, 'CommandOrControl+]', () => {
- if (page.canGoForward()) {
- page.goForward();
- }
+ page.send('forward');
});
page.on('dom-ready', () => {
- page.insertCSS(fs.readFileSync(path.join(__dirname, '../renderer/css/preload.css'), 'utf8'));
mainWindow.show();
});
- page.on('new-window', (event, url) => {
- if (linkIsInternal(checkWindowURL(), url) && url.match(skipImages) === null) {
- event.preventDefault();
- return mainWindow.loadURL(url);
- }
- event.preventDefault();
- electron.shell.openExternal(url);
- });
-
page.once('did-frame-finish-load', () => {
const checkOS = isWindowsOrmacOS();
if (checkOS && !isDev) {
@@ -352,27 +239,20 @@ app.on('ready', () => {
mainWindow.webContents.send('destroytray');
});
checkConnection();
+
+ ipc.on('reload-main', () => {
+ page.reload();
+ });
+
+ ipc.on('update-badge', (event, messageCount) => {
+ if (process.platform === 'darwin') {
+ app.setBadgeCount(messageCount);
+ }
+ page.send('tray', messageCount);
+ });
});
app.on('will-quit', () => {
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
});
-
-ipc.on('new-domain', (e, domain) => {
- // MainWindow.loadURL(domain);
- if (!mainWindow) {
- mainWindow = createMainWindow();
- mainWindow.loadURL(domain);
- mainWindow.webContents.send('destroytray');
- } else if (mainWindow.isMinimized()) {
- mainWindow.webContents.send('destroytray');
- mainWindow.loadURL(domain);
- mainWindow.show();
- } else {
- mainWindow.webContents.send('destroytray');
- mainWindow.loadURL(domain);
- serverError(domain);
- }
- targetLink = domain;
-});
diff --git a/app/main/menu.js b/app/main/menu.js
index ddc81051..35710916 100644
--- a/app/main/menu.js
+++ b/app/main/menu.js
@@ -8,9 +8,8 @@ const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const shell = electron.shell;
const appName = app.getName();
-// Const tray = require('./tray');
-const {addDomain, about} = require('./windowmanager');
+const {about} = require('./windowmanager');
function sendAction(action) {
const win = BrowserWindow.getAllWindows()[0];
@@ -35,8 +34,7 @@ const viewSubmenu = [
label: 'Reload',
click(item, focusedWindow) {
if (focusedWindow) {
- focusedWindow.reload();
- focusedWindow.webContents.send('destroytray');
+ sendAction('reload');
}
}
},
@@ -136,10 +134,12 @@ const darwinTpl = [
type: 'separator'
},
{
- label: 'Change Zulip Server',
+ label: 'Manage Zulip Servers',
accelerator: 'Cmd+,',
- click() {
- addDomain();
+ click(item, focusedWindow) {
+ if (focusedWindow) {
+ sendAction('open-settings');
+ }
}
},
{
@@ -269,10 +269,12 @@ const otherTpl = [
type: 'separator'
},
{
- label: 'Change Zulip Server',
+ label: 'Manage Zulip Servers',
accelerator: 'Ctrl+,',
- click() {
- addDomain();
+ click(item, focusedWindow) {
+ if (focusedWindow) {
+ sendAction('open-settings');
+ }
}
},
{
diff --git a/app/main/windowmanager.js b/app/main/windowmanager.js
index a730aa01..005d8a6f 100644
--- a/app/main/windowmanager.js
+++ b/app/main/windowmanager.js
@@ -89,11 +89,6 @@ ipc.on('trayabout', event => {
}
});
-ipc.on('traychangeserver', event => {
- if (event) {
- addDomain();
- }
-});
module.exports = {
addDomain,
about
diff --git a/app/renderer/css/pref.css b/app/renderer/css/pref.css
deleted file mode 100644
index fcd67827..00000000
--- a/app/renderer/css/pref.css
+++ /dev/null
@@ -1,74 +0,0 @@
-body{
- background-color: #6BB6C7;
-}
-
-.form {
- position: absolute;
- top: 35%;
- width: 300px;
- left: 9%;
-}
-
-.close {
- background: transparent url('../img/close.png') no-repeat 4px 4px;
- background-size: 24px 24px;
- cursor: pointer;
- display: inline-block;
- height: 32px;
- position: absolute;
- right: 6px;
- text-indent: -10000px;
- top: 6px;
- width: 32px;
- z-index: 1;
- -webkit-app-region: no-drag;
-}
-
-input[type="text"] {
- display: block;
- margin: 0;
- width: 100%;
- font-family: sans-serif;
- font-size: 18px;
- appearance: none;
- box-shadow: none;
- border-radius: none;
- color: #646464;
-}
-input[type="text"]:focus {
- outline: none;
-}
-
-.form input[type="text"] {
- padding: 10px;
- border: solid 1px #dcdcdc;
- transition: box-shadow 0.3s, border 0.3s;
-}
-.form input[type="text"]:focus,
-.form input[type="text"].focus {
- border: solid 1px #707070;
- box-shadow: 0 0 5px 1px #969696;
-}
-button {
- border: none;
- color: #fff;
- padding: 12px 32px;
- text-align: center;
- text-decoration: none;
- margin-left: 107px;
- margin-top: 24px;
- display: inline-block;
- font-size: 16px;
- background: #137b86;
-}
-button:focus {
- outline: 0;
-}
-#urladded {
- font-size: 20px;
- position: absolute;
- font-family: 'opensans';
- top: -61%;
- left: 25%;
- text-align: center;
-}
\ No newline at end of file
diff --git a/app/renderer/css/preference.css b/app/renderer/css/preference.css
new file mode 100644
index 00000000..31a34ffb
--- /dev/null
+++ b/app/renderer/css/preference.css
@@ -0,0 +1,162 @@
+html, body {
+ height: 100%;
+ margin: 0;
+ cursor: default;
+ font-size: 14px;
+ color: #333;
+ background: #fff;
+}
+
+#content {
+ display: flex;
+ height: 100%;
+ font-family: sans-serif;
+}
+
+#sidebar {
+ width: 80px;
+ padding: 30px;
+ display: flex;
+ flex-direction: column;
+ font-size: 16px;
+}
+
+#tabs-container {
+ padding: 20px 0;
+}
+
+.tab {
+ padding: 5px 0;
+ color: #999;
+ cursor: pointer;
+}
+
+.tab.active {
+ color: #464e5a;
+ cursor: default;
+ position: relative;
+}
+
+.tab.active::before {
+ background: #464e5a;
+ width: 3px;
+ height: 16px;
+ position: absolute;
+ left: -8px;
+ content: '';
+}
+
+#settings-header {
+ font-size: 22px;
+ color: #5c6166;
+}
+
+#settings-container {
+ width: 100%;
+ display: flex;
+ padding: 30px;
+ overflow-y: scroll;
+}
+
+.server-info-container {
+ margin: 20px 0;
+}
+
+.title {
+ padding: 4px 0 6px 0;
+ font-size: 18px;
+ color: #000;
+}
+
+img.server-info-icon {
+ background: #a4d3c4;
+ background-size: 100%;
+ border-radius: 4px;
+ width: 44px;
+ height: 44px;
+}
+
+.server-info-left {
+ margin-right: 20px;
+}
+
+.server-info-right {
+ flex-grow: 1;
+}
+
+.server-info-row {
+ display: flex;
+ line-height: 26px;
+ height: 40px;
+}
+
+.server-info-key {
+ width: 40px;
+ margin-right: 20px;
+ text-align: right;
+}
+
+.server-info-value {
+ flex-grow: 1;
+ font-size: 14px;
+ height: 24px;
+ border: none;
+ border-bottom: #ddd 1px solid;
+ outline-width: 0;
+ background: transparent;
+ max-width: 500px;
+}
+
+.server-info-value:focus {
+ border-bottom: #b0d8ce 2px solid;
+}
+
+.actions-container {
+ display: flex;
+ font-size: 14px;
+ color: #235d3a;
+ vertical-align: middle;
+ margin: 10px 0;
+ flex-wrap: wrap;
+}
+
+.action {
+ display: flex;
+ align-items: center;
+ margin-right: 20px;
+}
+
+.action i {
+ margin-right: 5px;
+ font-size: 18px;
+}
+
+.settings-pane {
+ flex-grow: 1;
+}
+
+.action:hover {
+ cursor: pointer;
+}
+
+.action.disabled:hover {
+ cursor: default;
+}
+
+.action.disabled {
+ color: #999;
+}
+
+.server-info.active {
+ background: #ecf4ef;
+}
+
+.server-info {
+ display: flex;
+ padding: 10px;
+ margin: 10px 0 10px 0;
+}
+
+.hidden {
+ display: none;
+}
\ No newline at end of file
diff --git a/app/renderer/css/preload.css b/app/renderer/css/preload.css
deleted file mode 100644
index 1fa30f28..00000000
--- a/app/renderer/css/preload.css
+++ /dev/null
@@ -1 +0,0 @@
-/* We'll be overriding default styling so that app look more native * /
diff --git a/app/renderer/css/servermanager.css b/app/renderer/css/servermanager.css
new file mode 100644
index 00000000..563b19af
--- /dev/null
+++ b/app/renderer/css/servermanager.css
@@ -0,0 +1,115 @@
+html, body {
+ height: 100%;
+ margin: 0;
+ cursor: default;
+}
+
+#content {
+ display: flex;
+ height: 100%;
+ background: #eee url(../img/loading.gif) no-repeat;
+ background-size: 60px 60px;
+ background-position: center;
+}
+
+#sidebar {
+ background: #222c31;
+ width: 50px;
+ padding: 40px 0 20px 0;
+ justify-content: space-between;
+ display: flex;
+ flex-direction: column;
+ -webkit-app-region: drag;
+}
+
+#webview {
+ flex-grow: 1;
+}
+
+.action-button {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 10px;
+}
+
+.action-button i {
+ color: #6c8592;
+ font-size: 28px;
+}
+
+.action-button:hover i {
+ color: #98a9b3;
+}
+
+#tabs-container {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+}
+
+.tab {
+ position: relative;
+ margin: 5px 0;
+}
+
+.tab.active::before{
+ content: "";
+ background: #fff;
+ border-radius: 0 3px 3px 0;
+ width: 4px;
+ position: absolute;
+ height: 31px;
+ left: -8px;
+ top: 5px;
+}
+
+.tab .server-tab{
+ background: #a4d3c4;
+ background-size: 100%;
+ border-radius: 4px;
+ width: 31px;
+ height: 31px;
+ position: relative;
+ margin: 5px 0;
+ z-index: 11;
+ line-height: 31px;
+ font-size: 32px;
+ font-family: sans-serif;
+ color: #194a2b;
+ text-align: center;
+ overflow: hidden;
+ opacity: 0.6;
+}
+
+.tab .server-tab:hover{
+ opacity: 0.8;
+}
+
+.tab .settings-tab{
+ background: #eee;
+}
+
+.tab .settings-tab i{
+ font-size: 28px;
+ line-height: 44px;
+}
+
+.tab.active .server-tab{
+ opacity: 1;
+}
+
+webview {
+ opacity: 1;
+ transition: opacity 0.3s;
+ flex-grow: 1;
+}
+
+webview.loading {
+ opacity: 0;
+ transition: opacity 0.3s;
+}
+
+webview.disabled {
+ display: none;
+}
diff --git a/app/renderer/img/loading.gif b/app/renderer/img/loading.gif
new file mode 100644
index 00000000..60d63d36
Binary files /dev/null and b/app/renderer/img/loading.gif differ
diff --git a/app/renderer/img/topography.png b/app/renderer/img/topography.png
deleted file mode 100644
index a009e120..00000000
Binary files a/app/renderer/img/topography.png and /dev/null differ
diff --git a/app/renderer/index.html b/app/renderer/index.html
deleted file mode 100644
index 8c9c6adb..00000000
--- a/app/renderer/index.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
- Login - Zulip
-
-
-
-
-
-
-
- Zulip Login
-
-
-
-
-
-
diff --git a/app/renderer/js/domain.js b/app/renderer/js/domain.js
deleted file mode 100644
index dd45b6d7..00000000
--- a/app/renderer/js/domain.js
+++ /dev/null
@@ -1,80 +0,0 @@
-const {app} = require('electron').remote;
-const ipcRenderer = require('electron').ipcRenderer;
-const JsonDB = require('node-json-db');
-const request = require('request');
-
-const db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
-
-window.addDomain = function () {
- const el = sel => {
- return document.querySelector(sel);
- };
-
- const $el = {
- error: el('#error'),
- main: el('#main'),
- section: el('section')
- };
-
- const event = sel => {
- return {
- on: (event, callback) => {
- document.querySelector(sel).addEventListener(event, callback);
- }
- };
- };
-
- const displayError = msg => {
- $el.error.innerText = msg;
- $el.error.classList.add('show');
- $el.section.classList.add('shake');
- };
-
- let newDomain = document.getElementById('url').value;
- newDomain = newDomain.replace(/^https?:\/\//, '');
- if (newDomain === '') {
- displayError('Please input a valid URL.');
- } else {
- el('#main').innerHTML = 'Checking...';
- if (newDomain.indexOf('localhost:') >= 0) {
- const domain = 'http://' + newDomain;
- const checkDomain = domain + '/static/audio/zulip.ogg';
- request(checkDomain, (error, response) => {
- if (!error && response.statusCode !== 404) {
- document.getElementById('main').innerHTML = 'Connect';
- db.push('/domain', domain);
- ipcRenderer.send('new-domain', domain);
- } else {
- $el.main.innerHTML = 'Connect';
- displayError('Not a valid Zulip local server');
- }
- });
- // });
- } else {
- const domain = 'https://' + newDomain;
- const checkDomain = domain + '/static/audio/zulip.ogg';
-
- request(checkDomain, (error, response) => {
- if (!error && response.statusCode !== 404) {
- $el.main.innerHTML = 'Connect';
- db.push('/domain', domain);
- ipcRenderer.send('new-domain', domain);
- } else if (error.toString().indexOf('Error: self signed certificate') >= 0) {
- $el.main.innerHTML = 'Connect';
- ipcRenderer.send('certificate-err', domain);
- } else {
- $el.main.innerHTML = 'Connect';
- displayError('Not a valid Zulip server');
- }
- });
- }
- }
-
- event('#url').on('input', () => {
- el('#error').classList.remove('show');
- });
-
- event('section').on('animationend', function () {
- this.classList.remove('shake');
- });
-};
diff --git a/app/renderer/js/main.js b/app/renderer/js/main.js
new file mode 100644
index 00000000..e064103d
--- /dev/null
+++ b/app/renderer/js/main.js
@@ -0,0 +1,258 @@
+'use strict';
+
+require(__dirname + '/js/tray.js');
+
+const DomainUtil = require(__dirname + '/js/utils/domain-util.js');
+const {linkIsInternal, skipImages} = require(__dirname + '/../main/link-helper');
+const {shell, ipcRenderer} = require('electron');
+
+class ServerManagerView {
+ constructor() {
+ this.$tabsContainer = document.getElementById('tabs-container');
+
+ const $actionsContainer = document.getElementById('actions-container');
+ this.$addServerButton = $actionsContainer.querySelector('#add-action');
+ this.$settingsButton = $actionsContainer.querySelector('#settings-action');
+ this.$content = document.getElementById('content');
+
+ this.isLoading = false;
+ this.settingsTabIndex = -1;
+ this.activeTabIndex = -1;
+ this.zoomFactors = [];
+ }
+
+ init() {
+ this.domainUtil = new DomainUtil();
+ this.initTabs();
+ this.initActions();
+ this.registerIpcs();
+ }
+
+ initTabs() {
+ const servers = this.domainUtil.getDomains();
+ if (servers.length > 0) {
+ for (const server of servers) {
+ this.initTab(server);
+ }
+
+ this.activateTab(0);
+ } else {
+ this.openSettings();
+ }
+ }
+
+ initTab(tab) {
+ const {
+ url,
+ icon
+ } = tab;
+ const tabTemplate = tab.template || `
+ `;
+ const $tab = this.insertNode(tabTemplate);
+ const index = this.$tabsContainer.childNodes.length;
+ this.$tabsContainer.appendChild($tab);
+ $tab.addEventListener('click', this.activateTab.bind(this, index));
+ }
+
+ initWebView(url, index, nodeIntegration = false) {
+ const webViewTemplate = `
+
+
+ `;
+ const $webView = this.insertNode(webViewTemplate);
+ this.$content.appendChild($webView);
+ this.isLoading = true;
+ $webView.addEventListener('dom-ready', this.endLoading.bind(this, index));
+
+ $webView.addEventListener('dom-ready', () => {
+ // We need to wait until the page title is ready to get badge count
+ setTimeout(() => this.updateBadge(index), 1000);
+ });
+ $webView.addEventListener('dom-ready', () => {
+ $webView.focus()
+ });
+
+ this.registerListeners($webView, index);
+ this.zoomFactors[index] = 1;
+ }
+
+ startLoading(url, index) {
+ const $activeWebView = document.getElementById(`webview-${this.activeTabIndex}`);
+ if ($activeWebView) {
+ $activeWebView.classList.add('disabled');
+ }
+ const $webView = document.getElementById(`webview-${index}`);
+ if ($webView === null) {
+ this.initWebView(url, index, this.settingsTabIndex === index);
+ } else {
+ this.updateBadge(index);
+ $webView.classList.remove('disabled');
+ $webView.focus()
+ }
+ }
+
+ endLoading(index) {
+ const $webView = document.getElementById(`webview-${index}`);
+ this.isLoading = false;
+ $webView.classList.remove('loading');
+ }
+
+ initActions() {
+ this.$addServerButton.addEventListener('click', this.openSettings.bind(this));
+ this.$settingsButton.addEventListener('click', this.openSettings.bind(this));
+ }
+
+ openSettings() {
+ if (this.settingsTabIndex !== -1) {
+ this.activateTab(this.settingsTabIndex);
+ return;
+ }
+ const url = 'file://' + __dirname + '/preference.html';
+
+ const settingsTabTemplate = `
+ `;
+ this.initTab({
+ alias: 'Settings',
+ url,
+ template: settingsTabTemplate
+ });
+
+ this.settingsTabIndex = this.$tabsContainer.childNodes.length - 1;
+ this.activateTab(this.settingsTabIndex);
+ }
+
+ activateTab(index) {
+ if (this.isLoading) {
+ return;
+ }
+
+ if (this.activeTabIndex !== -1) {
+ if (this.activeTabIndex === index) {
+ return;
+ } else {
+ this.getTabAt(this.activeTabIndex).classList.remove('active');
+ }
+ }
+
+ const $tab = this.getTabAt(index);
+ $tab.classList.add('active');
+
+ const domain = $tab.getAttribute('domain');
+ this.startLoading(domain, index);
+ this.activeTabIndex = index;
+ }
+
+ insertNode(html) {
+ const wrapper = document.createElement('div');
+ wrapper.innerHTML = html;
+ return wrapper.firstElementChild;
+ }
+
+ getTabAt(index) {
+ return this.$tabsContainer.childNodes[index];
+ }
+
+ updateBadge (index) {
+ const $activeWebView = document.getElementById(`webview-${index}`);
+ const title = $activeWebView.getTitle();
+ let messageCount = (/\(([0-9]+)\)/).exec(title);
+ messageCount = messageCount ? Number(messageCount[1]) : 0;
+ ipcRenderer.send('update-badge', messageCount);
+ }
+
+ registerListeners($webView, index) {
+ $webView.addEventListener('new-window', event => {
+ const {url} = event;
+ const domainPrefix = this.domainUtil.getDomain(this.activeTabIndex).url;
+ if (linkIsInternal(domainPrefix, url) && url.match(skipImages) === null) {
+ event.preventDefault();
+ return $webView.loadURL(url);
+ }
+ event.preventDefault();
+ shell.openExternal(url);
+ });
+
+ $webView.addEventListener('page-title-updated', event => {
+ const {title} = event;
+ if (title.indexOf('Zulip') === -1) {
+ return;
+ }
+ });
+ }
+
+ registerIpcs() {
+ ipcRenderer.on('reload', () => {
+ const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`);
+ activeWebview.reload();
+ });
+
+ ipcRenderer.on('back', () => {
+ const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`);
+ if (activeWebview.canGoBack()) {
+ activeWebview.goBack();
+ }
+ });
+
+ ipcRenderer.on('forward', () => {
+ const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`);
+ if (activeWebview.canGoForward()) {
+ activeWebview.goForward();
+ }
+ });
+
+ // Handle zooming functionality
+ ipcRenderer.on('zoomIn', () => {
+ const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`);
+ this.zoomFactors[this.activeTabIndex] += 0.1;
+ activeWebview.setZoomFactor(this.zoomFactors[this.activeTabIndex]);
+ });
+
+ ipcRenderer.on('zoomOut', () => {
+ const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`);
+ this.zoomFactors[this.activeTabIndex] -= 0.1;
+ activeWebview.setZoomFactor(this.zoomFactors[this.activeTabIndex]);
+ });
+
+ ipcRenderer.on('zoomActualSize', () => {
+ const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`);
+ this.zoomFactors[this.activeTabIndex] = 1;
+ activeWebview.setZoomFactor(this.zoomFactors[this.activeTabIndex]);
+ });
+
+ ipcRenderer.on('log-out', () => {
+ const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`);
+ activeWebview.executeJavaScript('logout()');
+ });
+
+ ipcRenderer.on('shortcut', () => {
+ const activeWebview = document.getElementById(`webview-${this.activeTabIndex}`);
+ activeWebview.executeJavaScript('shortcut()');
+ });
+
+ ipcRenderer.on('open-settings', () => {
+ if (this.settingsTabIndex === -1) {
+ this.openSettings();
+ } else {
+ this.activateTab(this.settingsTabIndex);
+ }
+ });
+ }
+}
+
+window.onload = () => {
+ const serverManagerView = new ServerManagerView();
+ serverManagerView.init();
+};
diff --git a/app/renderer/js/pref.js b/app/renderer/js/pref.js
deleted file mode 100644
index 7624ad52..00000000
--- a/app/renderer/js/pref.js
+++ /dev/null
@@ -1,69 +0,0 @@
-'use strict';
-// eslint-disable-next-line import/no-extraneous-dependencies
-const {remote} = require('electron');
-
-const prefWindow = remote.getCurrentWindow();
-
-document.getElementById('close-button').addEventListener('click', () => {
- prefWindow.close();
-});
-
-document.addEventListener('keydown', event => {
- if (event.key === 'Escape' || event.keyCode === 27) {
- prefWindow.close();
- }
-});
-// eslint-disable-next-line no-unused-vars
-window.prefDomain = function () {
- const request = require('request');
- // eslint-disable-next-line import/no-extraneous-dependencies
- const ipcRenderer = require('electron').ipcRenderer;
- const JsonDB = require('node-json-db');
- // eslint-disable-next-line import/no-extraneous-dependencies
- const {app} = require('electron').remote;
-
- const db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
-
- let newDomain = document.getElementById('url').value;
- newDomain = newDomain.replace(/^https?:\/\//, '');
- newDomain = newDomain.replace(/^http?:\/\//, '');
-
- if (newDomain === '') {
- document.getElementById('urladded').innerHTML = 'Please input a value';
- } else {
- document.getElementById('main').innerHTML = 'Checking...';
- if (newDomain.indexOf('localhost:') >= 0) {
- const domain = 'http://' + newDomain;
- const checkDomain = domain + '/static/audio/zulip.ogg';
- request(checkDomain, (error, response) => {
- if (!error && response.statusCode !== 404) {
- document.getElementById('main').innerHTML = 'Switch';
- document.getElementById('urladded').innerHTML = 'Switched to ' + newDomain;
- db.push('/domain', domain);
- ipcRenderer.send('new-domain', domain);
- } else {
- document.getElementById('main').innerHTML = 'Switch';
- document.getElementById('urladded').innerHTML = 'Not a valid Zulip Local Server.';
- }
- });
- } else {
- const domain = 'https://' + newDomain;
- const checkDomain = domain + '/static/audio/zulip.ogg';
- request(checkDomain, (error, response) => {
- if (!error && response.statusCode !== 404) {
- document.getElementById('main').innerHTML = 'Switch';
- document.getElementById('urladded').innerHTML = 'Switched to ' + newDomain;
- db.push('/domain', domain);
- ipcRenderer.send('new-domain', domain);
- } else if (error.toString().indexOf('Error: self signed certificate') >= 0) {
- document.getElementById('main').innerHTML = 'Switch';
- ipcRenderer.send('certificate-err', domain);
- document.getElementById('urladded').innerHTML = 'Switched to ' + newDomain;
- } else {
- document.getElementById('main').innerHTML = 'Switch';
- document.getElementById('urladded').innerHTML = 'Not a valid Zulip Server.';
- }
- });
- }
- }
-};
diff --git a/app/renderer/js/preference.js b/app/renderer/js/preference.js
new file mode 100644
index 00000000..e35cbb4f
--- /dev/null
+++ b/app/renderer/js/preference.js
@@ -0,0 +1,143 @@
+'use strict';
+
+const {ipcRenderer} = require('electron');
+
+const DomainUtil = require(__dirname + '/js/utils/domain-util.js');
+
+class PreferenceView {
+ constructor() {
+ this.$newServerButton = document.getElementById('new-server-action');
+ this.$saveServerButton = document.getElementById('save-server-action');
+ this.$reloadServerButton = document.getElementById('reload-server-action');
+ this.$serverInfoContainer = document.querySelector('.server-info-container');
+ }
+
+ init() {
+ this.domainUtil = new DomainUtil();
+ this.initServers();
+ this.initActions();
+ }
+
+ initServers() {
+ const servers = this.domainUtil.getDomains();
+ this.$serverInfoContainer.innerHTML = servers.length ? '' : 'Add your first server to get started!';
+
+ this.initNewServerForm();
+
+ for (const i in servers) {
+ this.initServer(servers[i], i);
+ }
+ }
+
+ initServer(server, index) {
+ const {
+ alias,
+ url,
+ icon
+ } = server;
+ const serverInfoTemplate = `
+
+
+

+
+
+
+ Name
+
+
+
+ Url
+
+
+
+ Icon
+
+
+
+
Actions
+
+ indeterminate_check_box
+ Delete
+
+
+
+
`;
+ this.$serverInfoContainer.appendChild(this.insertNode(serverInfoTemplate));
+ document.getElementById(`delete-server-action-${index}`).addEventListener('click', () => {
+ this.domainUtil.removeDomain(index);
+ this.initServers();
+ alert('Success. Reload to apply changes.');
+ this.$reloadServerButton.classList.remove('hidden');
+ });
+ }
+
+ initNewServerForm() {
+ const newServerFormTemplate = `
+
+
+

+
+
+
+ `;
+ this.$serverInfoContainer.appendChild(this.insertNode(newServerFormTemplate));
+
+ this.$newServerForm = document.querySelector('.server-info.active');
+ this.$newServerAlias = this.$newServerForm.querySelectorAll('input.server-info-value')[0];
+ this.$newServerUrl = this.$newServerForm.querySelectorAll('input.server-info-value')[1];
+ this.$newServerIcon = this.$newServerForm.querySelectorAll('input.server-info-value')[2];
+ }
+
+ initActions() {
+ this.$newServerButton.addEventListener('click', () => {
+ this.$newServerForm.classList.remove('hidden');
+ this.$saveServerButton.classList.remove('hidden');
+ this.$newServerButton.classList.add('hidden');
+ });
+ this.$saveServerButton.addEventListener('click', () => {
+ this.domainUtil.checkDomain(this.$newServerUrl.value).then(domain => {
+ const server = {
+ alias: this.$newServerAlias.value,
+ url: domain,
+ icon: this.$newServerIcon.value
+ };
+ this.domainUtil.addDomain(server);
+ this.$saveServerButton.classList.add('hidden');
+ this.$newServerButton.classList.remove('hidden');
+ this.$newServerForm.classList.add('hidden');
+
+ this.initServers();
+ alert('Success. Reload to apply changes.');
+ this.$reloadServerButton.classList.remove('hidden');
+ }, errorMessage => {
+ alert(errorMessage);
+ });
+ });
+ this.$reloadServerButton.addEventListener('click', () => {
+ ipcRenderer.send('reload-main');
+ });
+ }
+ insertNode(html) {
+ const wrapper = document.createElement('div');
+ wrapper.innerHTML = html;
+ return wrapper.firstElementChild;
+ }
+}
+
+window.onload = () => {
+ const preferenceView = new PreferenceView();
+ preferenceView.init();
+};
diff --git a/app/renderer/js/preload.js b/app/renderer/js/preload.js
index 355bc41e..794a446b 100644
--- a/app/renderer/js/preload.js
+++ b/app/renderer/js/preload.js
@@ -1,56 +1,15 @@
'use strict';
-const ipcRenderer = require('electron').ipcRenderer;
-const {webFrame} = require('electron');
const {spellChecker} = require('./spellchecker');
-const _setImmediate = setImmediate;
-const _clearImmediate = clearImmediate;
-process.once('loaded', () => {
- global.setImmediate = _setImmediate;
- global.clearImmediate = _clearImmediate;
-});
-
-// eslint-disable-next-line import/no-unassigned-import
-require('./domain');
-// eslint-disable-next-line import/no-unassigned-import
-require('./tray.js');
-// Calling Tray.js in renderer process everytime app window loads
-
-// Handle zooming functionality
-const zoomIn = () => {
- webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
-};
-
-const zoomOut = () => {
- webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
-};
-
-const zoomActualSize = () => {
- webFrame.setZoomFactor(1);
-};
-
-// Get zooming actions from main process
-ipcRenderer.on('zoomIn', () => {
- zoomIn();
-});
-
-ipcRenderer.on('zoomOut', () => {
- zoomOut();
-});
-
-ipcRenderer.on('zoomActualSize', () => {
- zoomActualSize();
-});
-
-ipcRenderer.on('log-out', () => {
+const logout = () => {
// Create the menu for the below
document.querySelector('.dropdown-toggle').click();
const nodes = document.querySelectorAll('.dropdown-menu li:last-child a');
nodes[nodes.length - 1].click();
-});
+};
-ipcRenderer.on('shortcut', () => {
+const shortcut = () => {
// Create the menu for the below
const node = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
// Additional check
@@ -60,6 +19,11 @@ ipcRenderer.on('shortcut', () => {
// Atleast click the dropdown
document.querySelector('.dropdown-toggle').click();
}
+};
+
+process.once('loaded', () => {
+ global.logout = logout;
+ global.shortcut = shortcut;
});
// To prevent failing this script on linux we need to load it after the document loaded
diff --git a/app/renderer/js/tray.js b/app/renderer/js/tray.js
index 50311e63..8c7aade7 100644
--- a/app/renderer/js/tray.js
+++ b/app/renderer/js/tray.js
@@ -5,7 +5,7 @@ const electron = require('electron');
const {ipcRenderer, remote} = electron;
-const {Tray, Menu, nativeImage} = remote;
+const {Tray, Menu, nativeImage, BrowserWindow} = remote;
const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray');
@@ -102,6 +102,16 @@ const renderNativeImage = function (arg) {
});
};
+function sendAction(action) {
+ const win = BrowserWindow.getAllWindows()[0];
+
+ if (process.platform === 'darwin') {
+ win.restore();
+ }
+
+ win.webContents.send(action);
+}
+
const createTray = function () {
window.tray = new Tray(iconPath());
const contextMenu = Menu.buildFromTemplate([{
@@ -114,9 +124,9 @@ const createTray = function () {
type: 'separator'
},
{
- label: 'Change Zulip server',
+ label: 'Manage Zulip servers',
click() {
- ipcRenderer.send('traychangeserver');
+ sendAction('open-settings');
}
},
{
@@ -125,8 +135,7 @@ const createTray = function () {
{
label: 'Reload',
click() {
- remote.getCurrentWindow().reload();
- window.tray.destroy();
+ sendAction('reload');
}
},
{
diff --git a/app/renderer/js/utils/domain-util.js b/app/renderer/js/utils/domain-util.js
new file mode 100644
index 00000000..a6ca4592
--- /dev/null
+++ b/app/renderer/js/utils/domain-util.js
@@ -0,0 +1,72 @@
+'use strict';
+
+const {app} = require('electron').remote;
+const JsonDB = require('node-json-db');
+const request = require('request');
+
+const defaultIconUrl = 'https://chat.zulip.org/static/images/logo/zulip-icon-128x128.271d0f6a0ca2.png';
+class DomainUtil {
+ constructor() {
+ this.db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
+ // Migrate from old schema
+ if (this.db.getData('/').domain) {
+ this.addDomain({
+ alias: 'Zulip',
+ url: this.db.getData('/domain')
+ });
+ this.db.delete('/domain');
+ }
+ }
+
+ getDomains() {
+ if (this.db.getData('/').domains === undefined) {
+ return [];
+ } else {
+ return this.db.getData('/domains');
+ }
+ }
+
+ getDomain(index) {
+ return this.db.getData(`/domains[${index}]`);
+ }
+
+ addDomain(server) {
+ server.icon = server.icon || defaultIconUrl;
+ this.db.push('/domains[]', server, true);
+ }
+
+ removeDomains() {
+ this.db.delete('/domains');
+ }
+
+ removeDomain(index) {
+ this.db.delete(`/domains[${index}]`);
+ }
+
+ checkDomain(domain) {
+ const hasPrefix = (domain.indexOf('http') === 0);
+ if (!hasPrefix) {
+ domain = (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`;
+ }
+
+ const checkDomain = domain + '/static/audio/zulip.ogg';
+
+ return new Promise((resolve, reject) => {
+ request(checkDomain, (error, response) => {
+ if (!error && response.statusCode !== 404) {
+ resolve(domain);
+ } else if (error.toString().indexOf('Error: self signed certificate') >= 0) {
+ if (window.confirm(`Do you trust certificate from ${domain}?`)) {
+ resolve(domain);
+ } else {
+ reject('Untrusted Certificate.');
+ }
+ } else {
+ reject('Not a valid Zulip server');
+ }
+ });
+ });
+ }
+}
+
+module.exports = DomainUtil;
diff --git a/app/renderer/main.html b/app/renderer/main.html
new file mode 100644
index 00000000..f15e2709
--- /dev/null
+++ b/app/renderer/main.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ Zulip
+
+
+
+
+
+
+
+
+
+
diff --git a/app/renderer/pref.html b/app/renderer/pref.html
deleted file mode 100644
index 954c66e8..00000000
--- a/app/renderer/pref.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
- Close
-
-
-
-
diff --git a/app/renderer/preference.html b/app/renderer/preference.html
new file mode 100644
index 00000000..dd2588c8
--- /dev/null
+++ b/app/renderer/preference.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+ Zulip - Settings
+
+
+
+
+
+
+
+
+
Manage Servers
+
+
+ add_box
+ New Server
+
+
+ check_box
+ Save
+
+
+ refresh
+ Reload to Apply Changes
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index f6a2bdb7..b7d03c08 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"url": "https://github.com/zulip/zulip-electron/issues"
},
"scripts": {
- "start": "electron ./app/main",
+ "start": "electron app --disable-http-cache",
"postinstall": "install-app-deps",
"test": "gulp test && xo",
"dev": "gulp dev",
@@ -75,7 +75,7 @@
"win": {
"target": "nsis",
"icon": "build/icon.ico"
- },
+ },
"nsis": {
"perMachine": true,
"oneClick": false
@@ -104,7 +104,7 @@
"esnext": true,
"overrides": [
{
- "files": "app/main/*.js",
+ "files": "app*/**/*.js",
"rules": {
"max-lines": [
"warn",
@@ -113,6 +113,10 @@
"no-warning-comments": 0,
"capitalized-comments": 0,
"no-else-return": 0,
+ "no-path-concat": 0,
+ "no-alert": 0,
+ "guard-for-in": 0,
+ "prefer-promise-reject-errors": 0,
"import/no-unresolved": 0,
"import/no-extraneous-dependencies": 0
}
diff --git a/tests/index.js b/tests/index.js
index 115672af..9701c524 100644
--- a/tests/index.js
+++ b/tests/index.js
@@ -7,7 +7,7 @@ describe('application launch', function () {
beforeEach(function () {
this.app = new Application({
path: require('electron'),
- args: [__dirname + '/../app/renderer/index.html']
+ args: [__dirname + '/../app/renderer/main.html']
})
return this.app.start()
})
@@ -20,7 +20,7 @@ describe('application launch', function () {
it('shows an initial window', function () {
return this.app.client.getWindowCount().then(function (count) {
- assert.equal(count, 1)
+ assert.equal(count, 2)
})
})
})