Merge pull request #151 from zulip/140-fixes

💥 Added multiple server support feature
This commit is contained in:
Akash Nimare
2017-05-22 18:53:21 -07:00
committed by GitHub
25 changed files with 889 additions and 482 deletions

1
.node-version Normal file
View File

@@ -0,0 +1 @@
6.9.4

1
.python-version Normal file
View File

@@ -0,0 +1 @@
2.7.9

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
// Place your settings in this file to overwrite default and user settings.
{
}

View File

@@ -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;
});

View File

@@ -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');
}
}
},
{

View File

@@ -89,11 +89,6 @@ ipc.on('trayabout', event => {
}
});
ipc.on('traychangeserver', event => {
if (event) {
addDomain();
}
});
module.exports = {
addDomain,
about

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1 +0,0 @@
/* We'll be overriding default styling so that app look more native * /

View File

@@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,30 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="responsive desktop">
<!--<![endif]-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width">
<title>Login - Zulip</title>
<link rel="stylesheet" href="css/main.css" type="text/css" media="screen">
</head>
<body>
<div class="center">
<section class="server">
<header>
<img src="../resources/zulip.png" id="logo"/>
<h1>Zulip Login</h1>
</header>
<div class="container">
<form id="frm-signInForm" class="form-large" onsubmit="addDomain(); return false">
<label for="url">
Zulip Server URL
</label>
<input type="text" id="url" autofocus="autofocus" spellcheck="false" placeholder="Server URL">
<button type="submit" id="main" class="btn-primary btn-large" value="Submit">Connect</button>
<p id="error"></p>
</form>
</div>
</section>
</div>
</body>
</html>

View File

@@ -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');
});
};

258
app/renderer/js/main.js Normal file
View File

@@ -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 || `
<div class="tab" domain="${url}">
<div class="server-tab" style="background-image: url(${icon});"></div>
</div>`;
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 = `
<webview
id="webview-${index}"
class="loading"
src="${url}"
${nodeIntegration ? 'nodeIntegration' : ''}
disablewebsecurity
preload="js/preload.js"
webpreferences="allowRunningInsecureContent, javascript=yes">
</webview>
`;
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 = `
<div class="tab" domain="${url}">
<div class="server-tab settings-tab">
<i class="material-icons md-48">settings</i>
</div>
</div>`;
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();
};

View File

@@ -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.';
}
});
}
}
};

View File

@@ -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 = `
<div class="server-info">
<div class="server-info-left">
<img class="server-info-icon" src="${icon}"/>
</div>
<div class="server-info-right">
<div class="server-info-row">
<span class="server-info-key">Name</span>
<input class="server-info-value" disabled value="${alias}"/>
</div>
<div class="server-info-row">
<span class="server-info-key">Url</span>
<input class="server-info-value" disabled value="${url}"/>
</div>
<div class="server-info-row">
<span class="server-info-key">Icon</span>
<input class="server-info-value" disabled value="${icon}"/>
</div>
<div class="server-info-row">
<span class="server-info-key">Actions</span>
<div class="action server-info-value" id="delete-server-action-${index}">
<i class="material-icons">indeterminate_check_box</i>
<span>Delete</span>
</div>
</div>
</div>
</div>`;
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 = `
<div class="server-info active hidden">
<div class="server-info-left">
<img class="server-info-icon" src="https://chat.zulip.org/static/images/logo/zulip-icon-128x128.271d0f6a0ca2.png"/>
</div>
<div class="server-info-right">
<div class="server-info-row">
<span class="server-info-key">Name</span>
<input class="server-info-value" placeholder="(Required)"/>
</div>
<div class="server-info-row">
<span class="server-info-key">Url</span>
<input class="server-info-value" placeholder="(Required)"/>
</div>
<div class="server-info-row">
<span class="server-info-key">Icon</span>
<input class="server-info-value" placeholder="(Optional)"/>
</div>
</div>
</div>
`;
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();
};

View File

@@ -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

View File

@@ -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');
}
},
{

View File

@@ -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;

26
app/renderer/main.html Normal file
View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en" class="responsive desktop">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width">
<title>Zulip</title>
<link rel="stylesheet" href="css/servermanager.css" type="text/css" media="screen">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<div id="content">
<div id="sidebar">
<div id="tabs-container"></div>
<div id="actions-container">
<div class="action-button" id="add-action">
<i class="material-icons md-48">add_circle</i>
</div>
<div class="action-button" id="settings-action">
<i class="material-icons md-48">settings</i>
</div>
</div>
</div>
</div>
</body>
<script src="js/main.js"></script>
</html>

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/pref.css">
</head>
<body>
<div class="close" id="close-button">Close</div>
<div class="form">
<form onsubmit="prefDomain(); return false">
<input id="url" type="text" placeholder="Server URL">
<button type="submit" id="main" value="Submit">
Switch</button>
</form>
<p id="urladded"><p>
</div>
<script src="js/pref.js"></script>
</body>
</html>

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en" class="responsive desktop">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width">
<title>Zulip - Settings</title>
<link rel="stylesheet" href="css/preference.css" type="text/css" media="screen">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<div id="content">
<div id="sidebar">
<div id="settings-header">Settings</div>
<div id="tabs-container">
<div class="tab" id="general-settings">General</div>
<div class="tab active" id="server-settings">Servers</div>
<div class="tab" id="about-settings">About</div>
</div>
</div>
<div id="settings-container">
<div class="settings-pane" id="server-settings-pane">
<div class="title">Manage Servers</div>
<div class="actions-container">
<div class="action" id="new-server-action">
<i class="material-icons">add_box</i>
<span>New Server</span>
</div>
<div class="action hidden" id="save-server-action">
<i class="material-icons">check_box</i>
<span>Save</span>
</div>
<div class="action hidden" id="reload-server-action">
<i class="material-icons">refresh</i>
<span>Reload to Apply Changes</span>
</div>
</div>
<div class="server-info-container">
</div>
</div>
</div>
</div>
</body>
<script src="js/preference.js"></script>
</html>

View File

@@ -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
}

View File

@@ -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)
})
})
})