Compare commits

..

1 Commits

Author SHA1 Message Date
akashnimare
7194e788e4 Add a setting option to control auto-updates 2017-09-12 21:43:53 +05:30
72 changed files with 483 additions and 13056 deletions

View File

@@ -1,8 +0,0 @@
---
<!-- Please Include: -->
- **Operating System**:
- [ ] Windows
- [ ] Linux/Ubutnu
- [ ] macOS
- **Clear steps to reproduce the issue**:
- **Relevant error messages and/or screenshots**:

View File

@@ -1,16 +0,0 @@
---
<!--
Remove the fields that are not appropriate
Please include:
-->
**What's this PR do?**
**Any background context you want to provide?**
**Screenshots?**
**You have tested this PR on:**
- [ ] Windows
- [ ] Linux/Ubuntu
- [ ] macOS

5
.gitignore vendored
View File

@@ -24,8 +24,3 @@ yarn-error.log*
# miscellaneous # miscellaneous
.idea .idea
config.gypi config.gypi
# Test generated files
tests/package.json
.python-version

1
.python-version Normal file
View File

@@ -0,0 +1 @@
2.7.9

View File

@@ -18,7 +18,6 @@ node_js:
- '6' - '6'
before_install: before_install:
- ./scripts/travis-xvfb.sh
- npm install -g gulp - npm install -g gulp
- npm install - npm install

7
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,7 @@
---
Please include:
- `Operating System`
- `Clear steps to reproduce the issue`
- `Relevant error messages and/or screenshots`

View File

@@ -8,16 +8,23 @@ Desktop client for Zulip. Available for Mac, Linux and Windows.
<img src="http://i.imgur.com/ChzTq4F.png"/> <img src="http://i.imgur.com/ChzTq4F.png"/>
# Download # Download
Please see [installation guide](https://zulipchat.com/help/desktop-app-install-guide). Please see [installation guide](./how-to-install.md).
# Features # Features
* Sign in to multiple teams * Sign in to multiple teams
* Desktop Notifications with inline reply support * Native desktop Notifications
* Multilanguage SpellChecker * SpellChecker
* OSX/Win/Linux installers * OSX/Win/Linux installers
* Automatic Updates (macOS/Windows/Linux) * Automatic Updates (macOS/Windows)
* Keyboard shortcuts * Keyboard shortcuts
Description | Keys
-----------------------| -----------------------
Default shortcuts | <kbd>Cmd/Ctrl</kbd> <kbd>k</kbd>
Manage Zulip Servers | <kbd>Cmd/Ctrl</kbd> <kbd>,</kbd>
Back | <kbd>Cmd/Ctrl</kbd> <kbd>[</kbd>
Forward | <kbd>Cmd/Ctrl</kbd> <kbd>]</kbd>
# Development # Development
Please see our [development guide](./development.md) to get started and run app locally. Please see our [development guide](./development.md) to get started and run app locally.

View File

@@ -1,4 +1,5 @@
'use strict'; 'use strict';
const fs = require('fs');
const { app, dialog } = require('electron'); const { app, dialog } = require('electron');
const { autoUpdater } = require('electron-updater'); const { autoUpdater } = require('electron-updater');
const isDev = require('electron-is-dev'); const isDev = require('electron-is-dev');
@@ -6,20 +7,19 @@ const isDev = require('electron-is-dev');
const ConfigUtil = require('./../renderer/js/utils/config-util.js'); const ConfigUtil = require('./../renderer/js/utils/config-util.js');
function appUpdater() { function appUpdater() {
// Don't initiate auto-updates in development // Don't initiate auto-updates in development and on Linux system
if (isDev) { // since autoUpdater doesn't work on Linux
return; if (isDev || process.platform === 'linux') {
}
if (process.platform === 'linux' && !process.env.APPIMAGE) {
const { linuxUpdateNotification } = require('./linuxupdater');
linuxUpdateNotification();
return; return;
} }
// Create Logs directory // Create Logs directory
const LogsDir = `${app.getPath('userData')}/Logs`; const LogsDir = `${app.getPath('userData')}/Logs`;
if (!fs.existsSync(LogsDir)) {
fs.mkdirSync(LogsDir);
}
// Log whats happening // Log whats happening
const log = require('electron-log'); const log = require('electron-log');

View File

@@ -1,16 +0,0 @@
'use strict';
const { crashReporter } = require('electron');
const crashHandler = () => {
crashReporter.start({
productName: 'zulip-electron',
companyName: 'Kandra Labs, Inc.',
submitURL: 'https://zulip-sentry.herokuapp.com/crashreport',
autoSubmit: true
});
};
module.exports = {
crashHandler
};

View File

@@ -1,24 +1,18 @@
'use strict'; 'use strict';
const path = require('path'); const path = require('path');
const electron = require('electron'); const electron = require('electron');
const electronLocalshortcut = require('electron-localshortcut');
const windowStateKeeper = require('electron-window-state'); const windowStateKeeper = require('electron-window-state');
const isDev = require('electron-is-dev');
const appMenu = require('./menu'); const appMenu = require('./menu');
const { appUpdater } = require('./autoupdater'); const { appUpdater } = require('./autoupdater');
const { crashHandler } = require('./crash-reporter');
const { setAutoLaunch } = require('./startup'); const { setAutoLaunch } = require('./startup');
const { app, ipcMain } = electron; const { app, ipcMain } = electron;
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js'); const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
// Adds debug features like hotkeys for triggering dev tools and reload // Adds debug features like hotkeys for triggering dev tools and reload
// in development mode
if (isDev) {
require('electron-debug')(); require('electron-debug')();
}
// Prevent window being garbage collected // Prevent window being garbage collected
let mainWindow; let mainWindow;
@@ -67,8 +61,8 @@ function createMainWindow() {
y: mainWindowState.y, y: mainWindowState.y,
width: mainWindowState.width, width: mainWindowState.width,
height: mainWindowState.height, height: mainWindowState.height,
minWidth: 300, minWidth: 600,
minHeight: 400, minHeight: 500,
webPreferences: { webPreferences: {
plugins: true, plugins: true,
allowDisplayingInsecureContent: true, allowDisplayingInsecureContent: true,
@@ -82,11 +76,7 @@ function createMainWindow() {
}); });
win.once('ready-to-show', () => { win.once('ready-to-show', () => {
if (ConfigUtil.getConfigItem('startMinimized')) {
win.minimize();
} else {
win.show(); win.show();
}
}); });
win.loadURL(mainURL); win.loadURL(mainURL);
@@ -102,6 +92,9 @@ function createMainWindow() {
win.hide(); win.hide();
} }
} }
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
}); });
win.setTitle('Zulip'); win.setTitle('Zulip');
@@ -129,8 +122,21 @@ function createMainWindow() {
return win; return win;
} }
// Decrease load on GPU (experimental) function registerLocalShortcuts(page) {
app.disableHardwareAcceleration(); // Somehow, reload action cannot be overwritten by the menu item
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
page.send('reload-viewer');
});
// Also adding these shortcuts because some users might want to use it instead of CMD/Left-Right
electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => {
page.send('back');
});
electronLocalshortcut.register(mainWindow, 'CommandOrControl+]', () => {
page.send('forward');
});
}
// eslint-disable-next-line max-params // eslint-disable-next-line max-params
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
@@ -138,6 +144,11 @@ app.on('certificate-error', (event, webContents, url, error, certificate, callba
callback(true); callback(true);
}); });
app.on('window-all-closed', () => {
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
});
app.on('activate', () => { app.on('activate', () => {
if (!mainWindow) { if (!mainWindow) {
mainWindow = createMainWindow(); mainWindow = createMainWindow();
@@ -152,25 +163,20 @@ app.on('ready', () => {
const page = mainWindow.webContents; const page = mainWindow.webContents;
registerLocalShortcuts(page);
page.on('dom-ready', () => { page.on('dom-ready', () => {
if (ConfigUtil.getConfigItem('startMinimized')) {
mainWindow.minimize();
} else {
mainWindow.show(); mainWindow.show();
}
}); });
page.once('did-frame-finish-load', () => { page.once('did-frame-finish-load', () => {
// Initate auto-updates on MacOS and Windows // Initate auto-updates on MacOS and Windows
appUpdater(); appUpdater();
crashHandler();
}); });
// Temporarily remove this event electron.powerMonitor.on('resume', () => {
// electron.powerMonitor.on('resume', () => { page.send('reload-viewer');
// mainWindow.reload(); });
// page.send('destroytray');
// });
ipcMain.on('focus-app', () => { ipcMain.on('focus-app', () => {
mainWindow.show(); mainWindow.show();
@@ -222,11 +228,31 @@ app.on('ready', () => {
appMenu.setMenu(props); appMenu.setMenu(props);
}); });
ipcMain.on('register-server-tab-shortcut', (event, index) => {
electronLocalshortcut.register(mainWindow, `CommandOrControl+${index}`, () => {
// Array index == Shown index - 1
page.send('switch-server-tab', index - 1);
});
});
ipcMain.on('local-shortcuts', (event, enable) => {
if (enable) {
registerLocalShortcuts(page);
} else {
electronLocalshortcut.unregisterAll(mainWindow);
}
});
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => { ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => {
setAutoLaunch(AutoLaunchValue); setAutoLaunch(AutoLaunchValue);
}); });
}); });
app.on('will-quit', () => {
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
});
app.on('before-quit', () => { app.on('before-quit', () => {
isQuitting = true; isQuitting = true;
}); });

View File

@@ -1,42 +0,0 @@
const { app } = require('electron');
const { Notification } = require('electron');
const request = require('request');
const semver = require('semver');
const ConfigUtil = require('../renderer/js/utils/config-util');
const LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util');
function linuxUpdateNotification() {
let url = 'https://api.github.com/repos/zulip/zulip-electron/releases';
url = ConfigUtil.getConfigItem('betaUpdate') ? url : url + '/latest';
const options = {
url,
headers: {'User-Agent': 'request'}
};
request(options, (error, response, body) => {
if (error) {
console.log('Error:', error);
return;
}
if (response.statusCode < 400) {
const data = JSON.parse(body);
const latestVersion = ConfigUtil.getConfigItem('betaUpdate') ? data[0].tag_name : data.tag_name;
if (semver.gt(latestVersion, app.getVersion())) {
const notified = LinuxUpdateUtil.getUpdateItem(latestVersion);
if (notified === null) {
new Notification({title: 'Zulip Update', body: 'A new version ' + latestVersion + ' is available. Please update using your package manager.'}).show();
LinuxUpdateUtil.setUpdateItem(latestVersion, true);
}
}
} else {
console.log('Status:', response.statusCode);
}
});
}
module.exports = {
linuxUpdateNotification
};

View File

@@ -37,7 +37,7 @@ class AppMenu {
accelerator: 'CommandOrControl+R', accelerator: 'CommandOrControl+R',
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('reload-current-viewer'); AppMenu.sendAction('reload-viewer');
} }
} }
}, { }, {
@@ -54,7 +54,7 @@ class AppMenu {
role: 'togglefullscreen' role: 'togglefullscreen'
}, { }, {
label: 'Zoom In', label: 'Zoom In',
accelerator: process.platform === 'darwin' ? 'Command+Plus' : 'Control+=', accelerator: 'CommandOrControl+=',
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('zoomIn'); AppMenu.sendAction('zoomIn');
@@ -115,21 +115,14 @@ class AppMenu {
} }
getHelpSubmenu() { getHelpSubmenu() {
return [ return [{
{ label: `${appName} Website`,
label: `${appName + ' Desktop-'} v${app.getVersion()}`,
enabled: false
},
{
label: `${appName} Help`,
click() { click() {
shell.openExternal('https://zulipchat.com/help/'); shell.openExternal('https://zulipchat.com/help/');
} }
}, { }, {
label: 'Show App Logs', label: `${appName + 'Desktop'} - ${app.getVersion()}`,
click() { enabled: false
shell.openItem(app.getPath('userData'));
}
}, { }, {
label: 'Report an Issue...', label: 'Report an Issue...',
click() { click() {
@@ -157,11 +150,6 @@ class AppMenu {
type: 'separator' type: 'separator'
}); });
for (let i = 0; i < tabs.length; i++) { for (let i = 0; i < tabs.length; i++) {
// Do not add functional tab settings to list of windows in menu bar
if (tabs[i].props.role === 'function' && tabs[i].webview.props.name === 'Settings') {
continue;
}
initialSubmenu.push({ initialSubmenu.push({
label: tabs[i].webview.props.name, label: tabs[i].webview.props.name,
accelerator: tabs[i].props.role === 'function' ? '' : `${ShortcutKey} + ${tabs[i].props.index + 1}`, accelerator: tabs[i].props.role === 'function' ? '' : `${ShortcutKey} + ${tabs[i].props.index + 1}`,
@@ -171,7 +159,7 @@ class AppMenu {
AppMenu.sendAction('switch-server-tab', tabs[i].props.index); AppMenu.sendAction('switch-server-tab', tabs[i].props.index);
} }
}, },
type: 'checkbox' type: 'radio'
}); });
} }
} }
@@ -185,7 +173,7 @@ class AppMenu {
return [{ return [{
label: `${app.getName()}`, label: `${app.getName()}`,
submenu: [{ submenu: [{
label: 'About Zulip', label: 'Zulip Desktop',
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('open-about'); AppMenu.sendAction('open-about');
@@ -194,7 +182,7 @@ class AppMenu {
}, { }, {
type: 'separator' type: 'separator'
}, { }, {
label: 'Desktop App Settings', label: 'Settings',
accelerator: 'Cmd+,', accelerator: 'Cmd+,',
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
@@ -285,7 +273,7 @@ class AppMenu {
return [{ return [{
label: 'File', label: 'File',
submenu: [{ submenu: [{
label: 'About Zulip', label: 'Zulip Desktop',
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('open-about'); AppMenu.sendAction('open-about');
@@ -294,7 +282,7 @@ class AppMenu {
}, { }, {
type: 'separator' type: 'separator'
}, { }, {
label: 'Desktop App Settings', label: 'Settings',
accelerator: 'Ctrl+,', accelerator: 'Ctrl+,',
click(item, focusedWindow) { click(item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
@@ -382,20 +370,10 @@ class AppMenu {
} }
static resetAppSettings() { static resetAppSettings() {
// We save App's settings/configurations in following files const getAppPath = path.join(app.getPath('appData'), appName, 'window-state.json');
const settingFiles = ['window-state.json', 'domain.json', 'settings.json'];
settingFiles.forEach(settingFileName => { fs.unlink(getAppPath, () => {
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName); setTimeout(() => AppMenu.sendAction('clear-app-data'), 1000);
fs.access(getSettingFilesPath, error => {
if (error) {
console.log(error);
} else {
fs.unlink(getSettingFilesPath, () => {
AppMenu.sendAction('clear-app-data');
});
}
});
}); });
} }

1394
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,14 @@
{ {
"name": "zulip", "name": "zulip",
"productName": "Zulip", "productName": "Zulip",
"version": "1.8.1", "version": "1.4.0",
"description": "Zulip Desktop App", "description": "Zulip Desktop App",
"license": "Apache-2.0", "license": "Apache-2.0",
"copyright": "Kandra Labs, Inc.", "email": "<svnitakash@gmail.com>",
"copyright": "©2017 Kandra Labs, Inc.",
"author": { "author": {
"name": "Kandra Labs, Inc.", "name": "Kandra Labs, Inc.",
"email": "support@zulipchat.com" "email": "svnitakash@gmail.com"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -26,19 +27,16 @@
"InstantMessaging" "InstantMessaging"
], ],
"dependencies": { "dependencies": {
"auto-launch": "5.0.1", "electron-debug": "1.4.0",
"electron-is-dev": "0.3.0", "electron-is-dev": "0.3.0",
"electron-localshortcut": "2.0.2",
"electron-log": "2.2.7", "electron-log": "2.2.7",
"electron-spellchecker": "1.1.2", "electron-spellchecker": "1.2.0",
"electron-updater": "2.18.2", "electron-updater": "2.8.9",
"electron-window-state": "4.1.1",
"is-online": "7.0.0",
"node-json-db": "0.7.3", "node-json-db": "0.7.3",
"request": "2.81.0", "request": "2.81.0",
"semver": "5.4.1", "wurl": "2.5.0",
"wurl": "2.5.0" "electron-window-state": "4.1.1",
}, "auto-launch": "5.0.1"
"optionalDependencies": {
"node-mac-notifier": "0.0.13"
} }
} }

View File

@@ -7,37 +7,25 @@
<body> <body>
<div class="about"> <div class="about">
<img class="logo" src="../resources/zulip.png" /> <img class="logo" src="../resources/zulip.png" />
<p class="detail" id="version">v?.?.?</p> <p class="detail" id="version">version ?.?.?</p>
<div class="maintenance-info"> <div class="maintenance-info">
<p class="detail maintainer"> <p class="detail maintainer">Maintained by Zulip</p>
Maintained by <a onclick="linkInBrowser('website')">Zulip</a> <p class="detail license">Available under the Apache License</p>
</p> <a class="bug" onclick="linkInBrowser()" href="#">Found bug?</a>
<p class="detail license">
Available under the <a onclick="linkInBrowser('license')">Apache 2.0 License</a>
</p>
<a class="bug" onclick="linkInBrowser('bug')" href="#">Found bug?</a>
</div> </div>
</div> </div>
<script> <script>
const { app } = require('electron').remote; const app = require('electron').remote.app;
const { shell } = require('electron'); const version_tag = document.getElementById('version');
const version_tag = document.querySelector('#version'); version_tag.innerHTML = 'version ' + app.getVersion();
version_tag.innerHTML = 'v' + app.getVersion();
function linkInBrowser(event) {
const shell = require('electron').shell;
const url = "https://github.com/zulip/zulip-electron/issues/new?body=Please describe your issue and steps to reproduce it."
function linkInBrowser(type) {
let url;
switch (type) {
case 'website':
url = "https://zulipchat.com";
break;
case 'license':
url = "https://github.com/zulip/zulip-electron/blob/master/LICENSE";
break;
default:
url = 'https://github.com/zulip/zulip-electron/issues/new?body=' +
'%3C!--Please%20describe%20your%20issue%20and%20steps%20to%20reproduce%20it.--%3E';
}
shell.openExternal(url); shell.openExternal(url);
} }
</script> </script>

View File

@@ -1,23 +1,21 @@
body { body {
background: #fafafa; background: #fafafa;
font-family: menu, "Helvetica Neue", sans-serif; font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: subpixel-antialiased; -webkit-font-smoothing: antialiased;
} }
.logo { .logo {
display: block; display: block;
margin: -40px auto; margin: 0 auto;
} }
#version { #version {
color: #444343; color: #aaa;
font-size: 1.3em; font-size: 0.9em;
padding-top: 40px;
} }
.about { .about {
margin: 25vh auto; margin-top: 50px;
height: 25vh;
text-align: center; text-align: center;
} }
@@ -44,9 +42,9 @@ body {
} }
.maintenance-info { .maintenance-info {
cursor: pointer;
position: absolute; position: absolute;
width: 100%; width: 100%;
bottom: 20px;
left: 0px; left: 0px;
color: #444; color: #444;
} }
@@ -54,6 +52,7 @@ body {
.maintenance-info p { .maintenance-info p {
margin: 0; margin: 0;
font-size: 1em; font-size: 1em;
width: 100%; width: 100%;
} }
@@ -72,11 +71,3 @@ body {
.maintenance-info .bug:hover { .maintenance-info .bug:hover {
background-color: #32a692; background-color: #32a692;
} }
p.detail a {
color: #355f4c;
}
p.detail a:hover {
text-decoration: underline;
}

View File

@@ -13,15 +13,12 @@ body {
#content { #content {
display: flex; display: flex;
height: 100%; height: 100%;
} background: #eee url(../img/ic_loading.gif) no-repeat;
.loading {
background: #fff url(../img/ic_loading.gif) no-repeat;
background-size: 60px 60px; background-size: 60px 60px;
background-position: center; background-position: center;
} }
.toggle-sidebar { #sidebar {
background: #222c31; background: #222c31;
width: 54px; width: 54px;
padding: 27px 0 20px 0; padding: 27px 0 20px 0;
@@ -30,21 +27,6 @@ body {
flex-direction: column; flex-direction: column;
-webkit-app-region: drag; -webkit-app-region: drag;
overflow: hidden; overflow: hidden;
transition: all 0.5s ease;
}
.toggle-sidebar div {
transition: all 0.5s ease-out;
}
.sidebar-hide {
width: 0;
transition: all 0.8s ease;
}
.sidebar-hide div {
transform: translateX(-100%);
transition: all 0.6s ease-out;
} }
@font-face { @font-face {
@@ -104,26 +86,6 @@ body {
color: #98a9b3; color: #98a9b3;
} }
.action-button.disable {
opacity: 0.6;
}
.action-button.disable:hover {
cursor: not-allowed;
}
.action-button.disable:hover i {
color: #6c8592;
}
.action-button.active {
background-color: rgba(255, 255, 255, 0.25);
}
.action-button.active i {
color: #eee;
}
.tab:first-child { .tab:first-child {
margin-top: 8px; margin-top: 8px;
} }
@@ -269,7 +231,6 @@ webview:focus {
/* Tooltip styling */ /* Tooltip styling */
#back-tooltip,
#reload-tooltip, #reload-tooltip,
#setting-tooltip { #setting-tooltip {
font-family: sans-serif; font-family: sans-serif;
@@ -286,7 +247,6 @@ webview:focus {
font-size: 14px; font-size: 14px;
} }
#back-tooltip:after,
#reload-tooltip:after, #reload-tooltip:after,
#setting-tooltip:after { #setting-tooltip:after {
content: " "; content: " ";
@@ -298,33 +258,6 @@ webview:focus {
right: 68px; right: 68px;
} }
#add-server-tooltip,
.server-tooltip {
font-family: 'arial';
background: #222c31;
left: 56px;
padding: 10px 20px;
position: fixed;
margin-top: 8px;
z-index: 5000 !important;
color: #fff;
border-radius: 4px;
text-align: center;
width: max-content;
font-size: 14px;
}
#add-server-tooltip:after,
.server-tooltip:after {
content: " ";
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid #222c31;
position: absolute;
top: 10px;
left: -5px;
}
#collapse-button { #collapse-button {
bottom: 30px; bottom: 30px;
left: 20px; left: 20px;
@@ -352,9 +285,7 @@ webview:focus {
display: none !important; display: none !important;
} }
/* Full screen Popup container */ /* Full screen Popup container */
.popup .popuptext { .popup .popuptext {
visibility: hidden; visibility: hidden;
background-color: #555; background-color: #555;

View File

@@ -12,45 +12,35 @@ body {
} }
kbd { kbd {
display: inline-block; padding: 0.1em 0.6em;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 3px; font-size: 12px;
font-size: 15px; font-family: Arial,Helvetica,sans-serif;
font-family: Courier New, Courier, monospace;
font-weight: bold;
white-space: nowrap;
background-color: #f7f7f7; background-color: #f7f7f7;
color: #333; color: #333;
-moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2),0 0 0 2px #ffffff inset;
-webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2),0 0 0 2px #ffffff inset;
box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2),0 0 0 2px #ffffff inset;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
display: inline-block;
margin: 0 0.1em; margin: 0 0.1em;
padding: 0.3em 0.8em;
text-shadow: 0 1px 0 #fff; text-shadow: 0 1px 0 #fff;
line-height: 1.4; line-height: 1.4;
white-space: nowrap;
} }
table, table, th, td {
th, border: 1px solid #ddd;
td {
border-collapse: collapse; border-collapse: collapse;
color: #383430;
} }
table { table { width: 85%; }
width: 100%;
margin-top: 18px;
margin-bottom: 18px;
}
table tr:nth-child(even) { table tr:nth-child(even) { background-color: #f2f2f2; }
background-color: #fafafa;
}
table tr:nth-child(odd) { td { padding: 5px; }
background-color: #fff;
}
td {
padding: 5px;
}
td:nth-child(odd) { td:nth-child(odd) {
text-align: right; text-align: right;
@@ -61,12 +51,9 @@ td:nth-child(odd) {
font-family: 'Material Icons'; font-family: 'Material Icons';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Material Icons'), local('MaterialIcons-Regular'), url(../fonts/MaterialIcons-Regular.ttf) format('truetype'); src: local('Material Icons'),
} local('MaterialIcons-Regular'),
url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
@font-face {
font-family: 'Montserrat';
src: url(../fonts/Montserrat-Regular.ttf) format('truetype');
} }
.material-icons { .material-icons {
@@ -91,7 +78,7 @@ td:nth-child(odd) {
#content { #content {
display: flex; display: flex;
height: 100%; height: 100%;
font-family: 'Montserrat'; font-family: sans-serif;
} }
#sidebar { #sidebar {
@@ -129,9 +116,7 @@ td:nth-child(odd) {
#settings-header { #settings-header {
font-size: 22px; font-size: 22px;
color: #222c31; color: #5c6166;
font-weight: bold;
text-transform: uppercase;
} }
#settings-container { #settings-container {
@@ -148,15 +133,8 @@ td:nth-child(odd) {
.title { .title {
padding: 4px 0 6px 0; padding: 4px 0 6px 0;
font-weight: 500;
color: #222c31;
}
.page-title {
color: #222c31;
font-size: 15px;
font-weight: bold; font-weight: bold;
padding: 4px 0 6px 0; color: #1e1e1e;
} }
.sub-title { .sub-title {
@@ -203,18 +181,16 @@ img.server-info-icon {
.setting-input-value { .setting-input-value {
flex-grow: 1; flex-grow: 1;
font-size: 14px; font-size: 14px;
height: 22px; height: 24px;
border-radius: 3px; border: none;
padding: 7px; border-bottom: #ededed 1px solid;
border: #ededed 2px solid;
outline-width: 0; outline-width: 0;
background: transparent; background: transparent;
max-width: 500px; max-width: 500px;
} }
.setting-input-value:focus { .setting-input-value:focus {
border: #7cb980 2px solid; border-bottom: #7cb980 1px solid;
border-radius: 3px;
} }
.setting-block { .setting-block {
@@ -234,13 +210,14 @@ img.server-info-icon {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 10px; padding: 0 10px;
border-radius: 2px;
margin-right: 10px; margin-right: 10px;
} }
.action i { .action i {
margin-right: 5px; margin-right: 5px;
font-size: 18px; font-size: 18px;
line-height: 26px; line-height: 27px;
} }
.settings-pane { .settings-pane {
@@ -265,14 +242,9 @@ img.server-info-icon {
padding: 12px 30px; padding: 12px 30px;
margin: 10px 0 20px 0; margin: 10px 0 20px 0;
background: #fff; background: #fff;
width: 70%; border-radius: 2px;
transition: all 0.2s; width: 540px;
} box-shadow: 1px 2px 4px #bcbcbc;
.settings-card:hover {
border-left: 8px solid #bcbcbc;
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),
0 2px 0px 0px rgba(0,0,0,0.12);
} }
.hidden { .hidden {
@@ -281,19 +253,15 @@ img.server-info-icon {
} }
.red { .red {
color: #ffffff; color: #ef5350;
background: #ef5350; background: #ffebee;
padding: 3px; border: 1px solid #ef5350;
padding-right: 10px;
padding-left: 10px;
} }
.blue { .green {
color: #ffffff; color: #388E3C;
background: #4EBFAC; background: #E8F5E9;
padding: 3px; border: 1px solid #388E3C;
padding-right: 10px;
padding-left: 10px;
} }
.grey { .grey {
@@ -304,10 +272,9 @@ img.server-info-icon {
.setting-row { .setting-row {
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
margin: 6px; margin: 6px 0;
} }
.code { .code {
@@ -322,96 +289,26 @@ i.open-tab-button {
.reset-data-button { .reset-data-button {
display: inline-block; display: inline-block;
border: none;
padding: 10px;
width: 120px; width: 120px;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 11px;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
text-decoration: none; text-decoration: none;
} }
.reset-data-button:hover { .reset-data-button:hover {
background-color: #3c9f8d; background-color: #32a692;
color: #fff; color: #fff;
} }
#server-info-container { #open-shortcuts-url {
min-height: calc(100% - 235px); color: #08c;
}
#create-organization-container {
font-size: 1.15em;
position: fixed;
bottom: 15px;
}
#create-organization-container i {
position: relative;
top: 3px;
}
#open-create-org-link {
color: #666;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
} }
#open-create-org-link:hover { #open-shortcuts-url:hover {
color: #005580; color: #005580;;
;
text-decoration: underline; text-decoration: underline;
} }
.toggle {
position: absolute;
margin-left: -9999px;
visibility: hidden;
}
.toggle+label {
display: block;
position: relative;
cursor: pointer;
outline: none;
user-select: none;
}
input.toggle-round+label {
padding: 2px;
width: 50px;
height: 25px;
background-color: #dddddd;
border-radius: 25px;
}
input.toggle-round+label:before,
input.toggle-round+label:after {
display: block;
position: absolute;
top: 2px;
left: 2px;
bottom: 2px;
content: "";
}
input.toggle-round+label:before {
right: 2px;
background-color: #f1f1f1;
border-radius: 25px;
}
input.toggle-round+label:after {
width: 25px;
height: 25px;
background-color: #fff;
border-radius: 100%;
}
input.toggle-round:checked+label:before {
background-color: #4EBFAC;
}
input.toggle-round:checked+label:after {
margin-left: 25px;
}

View File

@@ -4,7 +4,7 @@ const Tab = require(__dirname + '/../components/tab.js');
class FunctionalTab extends Tab { class FunctionalTab extends Tab {
template() { template() {
return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}"> return `<div class="tab functional-tab">
<div class="server-tab-badge close-button"> <div class="server-tab-badge close-button">
<i class="material-icons">close</i> <i class="material-icons">close</i>
</div> </div>
@@ -16,12 +16,11 @@ class FunctionalTab extends Tab {
init() { init() {
this.$el = this.generateNodeFromTemplate(this.template()); this.$el = this.generateNodeFromTemplate(this.template());
if (this.props.name !== 'Settings') {
this.props.$root.appendChild(this.$el); this.props.$root.appendChild(this.$el);
this.$closeButton = this.$el.getElementsByClassName('server-tab-badge')[0]; this.$closeButton = this.$el.getElementsByClassName('server-tab-badge')[0];
this.registerListeners(); this.registerListeners();
} }
}
registerListeners() { registerListeners() {
super.registerListeners(); super.registerListeners();

View File

@@ -7,8 +7,7 @@ const {ipcRenderer} = require('electron');
class ServerTab extends Tab { class ServerTab extends Tab {
template() { template() {
return `<div class="tab" data-tab-id="${this.props.tabIndex}"> return `<div class="tab">
<div class="server-tooltip" style="display:none"></div>
<div class="server-tab-badge"></div> <div class="server-tab-badge"></div>
<div class="server-tab"> <div class="server-tab">
<img class="server-icons" src='${this.props.icon}'/> <img class="server-icons" src='${this.props.icon}'/>
@@ -50,8 +49,7 @@ class ServerTab extends Tab {
shortcutText = `Ctrl+${shownIndex}`; shortcutText = `Ctrl+${shownIndex}`;
} }
// Array index == Shown index - 1 ipcRenderer.send('register-server-tab-shortcut', shownIndex);
ipcRenderer.send('switch-server-tab', shownIndex - 1);
return shortcutText; return shortcutText;
} }

View File

@@ -21,8 +21,6 @@ class Tab extends BaseComponent {
registerListeners() { registerListeners() {
this.$el.addEventListener('click', this.props.onClick); this.$el.addEventListener('click', this.props.onClick);
this.$el.addEventListener('mouseover', this.props.onHover);
this.$el.addEventListener('mouseout', this.props.onHoverOut);
} }
isLoading() { isLoading() {

View File

@@ -4,14 +4,12 @@ const path = require('path');
const fs = require('fs'); const fs = require('fs');
const DomainUtil = require(__dirname + '/../utils/domain-util.js'); const DomainUtil = require(__dirname + '/../utils/domain-util.js');
const ConfigUtil = require(__dirname + '/../utils/config-util.js');
const SystemUtil = require(__dirname + '/../utils/system-util.js'); const SystemUtil = require(__dirname + '/../utils/system-util.js');
const LinkUtil = require(__dirname + '/../utils/link-util.js'); const LinkUtil = require(__dirname + '/../utils/link-util.js');
const { shell, app } = require('electron').remote; const {shell} = require('electron').remote;
const BaseComponent = require(__dirname + '/../components/base.js'); const BaseComponent = require(__dirname + '/../components/base.js');
const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
class WebView extends BaseComponent { class WebView extends BaseComponent {
constructor(props) { constructor(props) {
super(); super();
@@ -21,13 +19,11 @@ class WebView extends BaseComponent {
this.zoomFactor = 1.0; this.zoomFactor = 1.0;
this.loading = false; this.loading = false;
this.badgeCount = 0; this.badgeCount = 0;
this.loadingIndicator = document.getElementById('content');
} }
template() { template() {
return `<webview return `<webview
class="disabled" class="disabled"
data-tab-id="${this.props.tabIndex}"
src="${this.props.url}" src="${this.props.url}"
${this.props.nodeIntegration ? 'nodeIntegration' : ''} ${this.props.nodeIntegration ? 'nodeIntegration' : ''}
disablewebsecurity disablewebsecurity
@@ -58,43 +54,13 @@ class WebView extends BaseComponent {
} }
}); });
if (shouldSilentWebview) {
this.$el.addEventListener('dom-ready', () => {
this.$el.setAudioMuted(true);
});
}
this.$el.addEventListener('page-title-updated', event => { this.$el.addEventListener('page-title-updated', event => {
const {title} = event; const {title} = event;
this.badgeCount = this.getBadgeCount(title); this.badgeCount = this.getBadgeCount(title);
this.props.onTitleChange(); this.props.onTitleChange();
}); });
this.$el.addEventListener('did-navigate-in-page', event => {
const isSettingPage = event.url.includes('renderer/preference.html');
if (isSettingPage) {
return;
}
this.canGoBackButton();
});
this.$el.addEventListener('did-navigate', () => {
this.canGoBackButton();
});
this.$el.addEventListener('page-favicon-updated', event => {
const { favicons } = event;
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') {
// This api is only supported on macOS
app.dock.setBadge('●');
}
});
this.$el.addEventListener('dom-ready', () => { this.$el.addEventListener('dom-ready', () => {
// const loadingIndicator = document.getElementById('content');
this.loadingIndicator.classList.remove('loading');
if (this.props.role === 'server') { if (this.props.role === 'server') {
this.$el.classList.add('onload'); this.$el.classList.add('onload');
} }
@@ -198,15 +164,6 @@ class WebView extends BaseComponent {
} }
} }
canGoBackButton() {
const $backButton = document.querySelector('#actions-container #back-action');
if (this.$el.canGoBack()) {
$backButton.classList.remove('disable');
} else {
$backButton.classList.add('disable');
}
}
forward() { forward() {
if (this.$el.canGoForward()) { if (this.$el.canGoForward()) {
this.$el.goForward(); this.$el.goForward();

View File

@@ -1,17 +1,15 @@
'use strict'; 'use strict';
require(__dirname + '/js/tray.js');
const { ipcRenderer, remote } = require('electron'); const { ipcRenderer, remote } = require('electron');
const isDev = require('electron-is-dev');
const { session } = remote; const { session } = remote;
require(__dirname + '/js/tray.js');
const DomainUtil = require(__dirname + '/js/utils/domain-util.js'); const DomainUtil = require(__dirname + '/js/utils/domain-util.js');
const WebView = require(__dirname + '/js/components/webview.js'); const WebView = require(__dirname + '/js/components/webview.js');
const ServerTab = require(__dirname + '/js/components/server-tab.js'); const ServerTab = require(__dirname + '/js/components/server-tab.js');
const FunctionalTab = require(__dirname + '/js/components/functional-tab.js'); const FunctionalTab = require(__dirname + '/js/components/functional-tab.js');
const ConfigUtil = require(__dirname + '/js/utils/config-util.js'); const ConfigUtil = require(__dirname + '/js/utils/config-util.js');
const ReconnectUtil = require(__dirname + '/js/utils/reconnect-util.js');
class ServerManagerView { class ServerManagerView {
constructor() { constructor() {
@@ -22,14 +20,9 @@ class ServerManagerView {
this.$reloadButton = $actionsContainer.querySelector('#reload-action'); this.$reloadButton = $actionsContainer.querySelector('#reload-action');
this.$settingsButton = $actionsContainer.querySelector('#settings-action'); this.$settingsButton = $actionsContainer.querySelector('#settings-action');
this.$webviewsContainer = document.getElementById('webviews-container'); this.$webviewsContainer = document.getElementById('webviews-container');
this.$backButton = $actionsContainer.querySelector('#back-action');
this.$addServerTooltip = document.getElementById('add-server-tooltip');
this.$reloadTooltip = $actionsContainer.querySelector('#reload-tooltip'); this.$reloadTooltip = $actionsContainer.querySelector('#reload-tooltip');
this.$settingsTooltip = $actionsContainer.querySelector('#setting-tooltip'); this.$settingsTooltip = $actionsContainer.querySelector('#setting-tooltip');
this.$serverIconTooltip = document.getElementsByClassName('server-tooltip');
this.$backTooltip = $actionsContainer.querySelector('#back-tooltip');
this.$sidebar = document.getElementById('sidebar'); this.$sidebar = document.getElementById('sidebar');
this.$fullscreenPopup = document.getElementById('fullscreen-popup'); this.$fullscreenPopup = document.getElementById('fullscreen-popup');
@@ -39,7 +32,6 @@ class ServerManagerView {
this.activeTabIndex = -1; this.activeTabIndex = -1;
this.tabs = []; this.tabs = [];
this.functionalTabs = {}; this.functionalTabs = {};
this.tabIndex = 0;
} }
init() { init() {
@@ -48,7 +40,6 @@ class ServerManagerView {
this.initTabs(); this.initTabs();
this.initActions(); this.initActions();
this.registerIpcs(); this.registerIpcs();
this.initDefaultSettings();
}); });
} }
@@ -71,39 +62,6 @@ class ServerManagerView {
}); });
} }
// Settings are initialized only when user clicks on General/Server/Network section settings
// In case, user doesn't visit these section, those values set to be null automatically
// This will make sure the default settings are correctly set to either true or false
initDefaultSettings() {
// Default settings which should be respected
const settingOptions = {
trayIcon: true,
useProxy: false,
showSidebar: true,
badgeOption: true,
startAtLogin: false,
startMinimized: false,
enableSpellchecker: true,
showNotification: true,
betaUpdate: false,
silent: false,
lastActiveTab: 0
};
// Platform specific settings
if (process.platform === 'win32') {
// Only available on Windows
settingOptions.flashTaskbarOnMessage = true;
}
for (const i in settingOptions) {
if (ConfigUtil.getConfigItem(i) === null) {
ConfigUtil.setConfigItem(i, settingOptions[i]);
}
}
}
initSidebar() { initSidebar() {
const showSidebar = ConfigUtil.getConfigItem('showSidebar', true); const showSidebar = ConfigUtil.getConfigItem('showSidebar', true);
this.toggleSidebar(showSidebar); this.toggleSidebar(showSidebar);
@@ -117,30 +75,24 @@ class ServerManagerView {
DomainUtil.updateSavedServer(servers[i].url, i); DomainUtil.updateSavedServer(servers[i].url, i);
this.activateTab(i); this.activateTab(i);
} }
// Open last active tab this.activateTab(0);
this.activateTab(ConfigUtil.getConfigItem('lastActiveTab'));
// Remove focus from the settings icon at sidebar bottom
this.$settingsButton.classList.remove('active');
} else { } else {
this.openSettings('Servers'); this.openSettings('Servers');
} }
ipcRenderer.send('local-shortcuts', true);
} }
initServer(server, index) { initServer(server, index) {
const tabIndex = this.getTabIndex();
this.tabs.push(new ServerTab({ this.tabs.push(new ServerTab({
role: 'server', role: 'server',
icon: server.icon, icon: server.icon,
$root: this.$tabsContainer, $root: this.$tabsContainer,
onClick: this.activateLastTab.bind(this, index), onClick: this.activateTab.bind(this, index),
index, index,
tabIndex,
onHover: this.onHover.bind(this, index, server.alias),
onHoverOut: this.onHoverOut.bind(this, index),
webview: new WebView({ webview: new WebView({
$root: this.$webviewsContainer, $root: this.$webviewsContainer,
index, index,
tabIndex,
url: server.url, url: server.url,
name: server.alias, name: server.alias,
isActive: () => { isActive: () => {
@@ -164,27 +116,9 @@ class ServerManagerView {
this.$settingsButton.addEventListener('click', () => { this.$settingsButton.addEventListener('click', () => {
this.openSettings('General'); this.openSettings('General');
}); });
this.$backButton.addEventListener('click', () => {
this.tabs[this.activeTabIndex].webview.back();
});
const $serverImgs = document.querySelectorAll('.server-icons');
$serverImgs.forEach($serverImg => {
$serverImg.addEventListener('error', () => {
$serverImg.src = 'img/icon.png';
});
});
this.sidebarHoverEvent(this.$addServerButton, this.$addServerTooltip);
this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip); this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip);
this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip); this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip);
this.sidebarHoverEvent(this.$backButton, this.$backTooltip);
}
getTabIndex() {
const currentIndex = this.tabIndex;
this.tabIndex++;
return currentIndex;
} }
sidebarHoverEvent(SidebarButton, SidebarTooltip) { sidebarHoverEvent(SidebarButton, SidebarTooltip) {
@@ -196,15 +130,6 @@ class ServerManagerView {
}); });
} }
onHover(index, serverName) {
this.$serverIconTooltip[index].innerHTML = serverName;
this.$serverIconTooltip[index].removeAttribute('style');
}
onHoverOut(index) {
this.$serverIconTooltip[index].style.display = 'none';
}
openFunctionalTab(tabProps) { openFunctionalTab(tabProps) {
if (this.functionalTabs[tabProps.name] !== undefined) { if (this.functionalTabs[tabProps.name] !== undefined) {
this.activateTab(this.functionalTabs[tabProps.name]); this.activateTab(this.functionalTabs[tabProps.name]);
@@ -213,20 +138,16 @@ class ServerManagerView {
this.functionalTabs[tabProps.name] = this.tabs.length; this.functionalTabs[tabProps.name] = this.tabs.length;
const tabIndex = this.getTabIndex();
this.tabs.push(new FunctionalTab({ this.tabs.push(new FunctionalTab({
role: 'function', role: 'function',
materialIcon: tabProps.materialIcon, materialIcon: tabProps.materialIcon,
name: tabProps.name,
$root: this.$tabsContainer, $root: this.$tabsContainer,
index: this.functionalTabs[tabProps.name], index: this.functionalTabs[tabProps.name],
tabIndex,
onClick: this.activateTab.bind(this, this.functionalTabs[tabProps.name]), onClick: this.activateTab.bind(this, this.functionalTabs[tabProps.name]),
onDestroy: this.destroyTab.bind(this, tabProps.name, this.functionalTabs[tabProps.name]), onDestroy: this.destroyTab.bind(this, tabProps.name, this.functionalTabs[tabProps.name]),
webview: new WebView({ webview: new WebView({
$root: this.$webviewsContainer, $root: this.$webviewsContainer,
index: this.functionalTabs[tabProps.name], index: this.functionalTabs[tabProps.name],
tabIndex,
url: tabProps.url, url: tabProps.url,
name: tabProps.name, name: tabProps.name,
isActive: () => { isActive: () => {
@@ -238,6 +159,7 @@ class ServerManagerView {
preload: false preload: false
}) })
})); }));
this.activateTab(this.functionalTabs[tabProps.name]); this.activateTab(this.functionalTabs[tabProps.name]);
} }
@@ -247,7 +169,6 @@ class ServerManagerView {
materialIcon: 'settings', materialIcon: 'settings',
url: `file://${__dirname}/preference.html#${nav}` url: `file://${__dirname}/preference.html#${nav}`
}); });
this.$settingsButton.classList.add('active');
this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav); this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav);
} }
@@ -267,15 +188,8 @@ class ServerManagerView {
}); });
} }
activateLastTab(index) {
// Open last active tab
ConfigUtil.setConfigItem('lastActiveTab', index);
// Open all the tabs in background
this.activateTab(index);
}
activateTab(index, hideOldTab = true) { activateTab(index, hideOldTab = true) {
if (!this.tabs[index]) { if (this.tabs[index].webview.loading) {
return; return;
} }
@@ -283,20 +197,10 @@ class ServerManagerView {
if (this.activeTabIndex === index) { if (this.activeTabIndex === index) {
return; return;
} else if (hideOldTab) { } else if (hideOldTab) {
// If old tab is functional tab Settings, remove focus from the settings icon at sidebar bottom
if (this.tabs[this.activeTabIndex].props.role === 'function' && this.tabs[this.activeTabIndex].props.name === 'Settings') {
this.$settingsButton.classList.remove('active');
}
this.tabs[this.activeTabIndex].deactivate(); this.tabs[this.activeTabIndex].deactivate();
} }
} }
try {
this.tabs[index].webview.canGoBackButton();
} catch (err) {
console.log(err);
}
this.activeTabIndex = index; this.activeTabIndex = index;
this.tabs[index].activate(); this.tabs[index].activate();
@@ -304,27 +208,6 @@ class ServerManagerView {
tabs: this.tabs, tabs: this.tabs,
activeTabIndex: this.activeTabIndex activeTabIndex: this.activeTabIndex
}); });
ipcRenderer.on('toggle-sidebar', (event, state) => {
const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector);
const webContents = webview.getWebContents();
webContents.send('toggle-sidebar', state);
});
ipcRenderer.on('toogle-silent', (event, state) => {
const webviews = document.querySelectorAll('webview');
webviews.forEach(webview => {
try {
webview.setAudioMuted(state);
} catch (err) {
// webview is not ready yet
webview.addEventListener('dom-ready', () => {
webview.isAudioMuted();
});
}
});
});
} }
destroyTab(name, index) { destroyTab(name, index) {
@@ -352,24 +235,16 @@ class ServerManagerView {
// Clear DOM elements // Clear DOM elements
this.$tabsContainer.innerHTML = ''; this.$tabsContainer.innerHTML = '';
this.$webviewsContainer.innerHTML = ''; this.$webviewsContainer.innerHTML = '';
// Destroy shortcuts
ipcRenderer.send('local-shortcuts', false);
} }
reloadView() { reloadView() {
// Save and remember the index of last active tab so that we can use it later
const lastActiveTab = this.tabs[this.activeTabIndex].props.index;
ConfigUtil.setConfigItem('lastActiveTab', lastActiveTab);
// Destroy the current view and re-initiate it
this.destroyView(); this.destroyView();
this.initTabs(); this.initTabs();
} }
// This will trigger when pressed CTRL/CMD + R [WIP]
// It won't reload the current view properly when you add/delete a server.
reloadCurrentView() {
this.$reloadButton.click();
}
updateBadge() { updateBadge() {
let messageCountAll = 0; let messageCountAll = 0;
for (let i = 0; i < this.tabs.length; i++) { for (let i = 0; i < this.tabs.length; i++) {
@@ -385,9 +260,9 @@ class ServerManagerView {
toggleSidebar(show) { toggleSidebar(show) {
if (show) { if (show) {
this.$sidebar.classList.remove('sidebar-hide'); this.$sidebar.classList.remove('hidden');
} else { } else {
this.$sidebar.classList.add('sidebar-hide'); this.$sidebar.classList.add('hidden');
} }
} }
@@ -420,9 +295,7 @@ class ServerManagerView {
ipcRenderer.on('open-about', this.openAbout.bind(this)); ipcRenderer.on('open-about', this.openAbout.bind(this));
ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index)); ipcRenderer.on('reload-viewer', this.reloadView.bind(this));
ipcRenderer.on('reload-current-viewer', this.reloadCurrentView.bind(this));
ipcRenderer.on('hard-reload', () => { ipcRenderer.on('hard-reload', () => {
ipcRenderer.send('reload-full-app'); ipcRenderer.send('reload-full-app');
@@ -433,7 +306,7 @@ class ServerManagerView {
}); });
ipcRenderer.on('switch-server-tab', (event, index) => { ipcRenderer.on('switch-server-tab', (event, index) => {
this.activateLastTab(index); this.activateTab(index);
}); });
ipcRenderer.on('reload-proxy', (event, showAlert) => { ipcRenderer.on('reload-proxy', (event, showAlert) => {
@@ -457,18 +330,6 @@ class ServerManagerView {
this.$fullscreenPopup.classList.remove('show'); this.$fullscreenPopup.classList.remove('show');
}); });
ipcRenderer.on('focus-webview-with-id', (event, webviewId) => {
const webviews = document.querySelectorAll('webview');
webviews.forEach(webview => {
const currentId = webview.getWebContents().id;
const tabId = webview.getAttribute('data-tab-id');
const concurrentTab = document.querySelector(`div[data-tab-id="${tabId}"]`);
if (currentId === webviewId) {
concurrentTab.click();
}
});
});
ipcRenderer.on('render-taskbar-icon', (event, messageCount) => { ipcRenderer.on('render-taskbar-icon', (event, messageCount) => {
// Create a canvas from unread messagecounts // Create a canvas from unread messagecounts
function createOverlayIcon(messageCount) { function createOverlayIcon(messageCount) {
@@ -502,24 +363,9 @@ class ServerManagerView {
window.onload = () => { window.onload = () => {
const serverManagerView = new ServerManagerView(); const serverManagerView = new ServerManagerView();
const reconnectUtil = new ReconnectUtil(serverManagerView);
serverManagerView.init(); serverManagerView.init();
window.addEventListener('online', () => { window.addEventListener('online', () => {
reconnectUtil.pollInternetAndReload(); serverManagerView.reloadView();
}); });
window.addEventListener('offline', () => {
reconnectUtil.clearState();
console.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
// `--no-electron-connect`
const mainProcessArgv = remote.getGlobal('process').argv;
if (isDev && !mainProcessArgv.includes('--no-electron-connect')) {
const electronConnect = require('electron-connect');
electronConnect.client.create();
}
}; };

View File

@@ -0,0 +1,24 @@
'use strict';
const { remote } = require('electron');
const ConfigUtil = require(__dirname + '/utils/config-util.js');
const app = remote.app;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
app.setAppUserModelId('org.zulip.zulip-electron');
const NativeNotification = window.Notification;
class baseNotification extends NativeNotification {
constructor(title, opts) {
opts.silent = ConfigUtil.getConfigItem('silent') || false;
// calling super without passing arguments will fail to constuct Notification
// and will result in no notification. It's a hack (not an ideal way) for deleting the window notification
ConfigUtil.getConfigItem('showNotification') ? super(title, opts) : super(); // eslint-disable-line no-unused-expressions
}
}
window.Notification = baseNotification;

View File

@@ -1,100 +0,0 @@
'use strict';
const { ipcRenderer } = require('electron');
const url = require('url');
const MacNotifier = require('node-mac-notifier');
const ConfigUtil = require('../utils/config-util');
const {
appId, customReply, focusCurrentServer, parseReply, setupReply
} = require('./helpers');
let replyHandler;
let clickHandler;
class DarwinNotification {
constructor(title, opts) {
const silent = ConfigUtil.getConfigItem('silent') || false;
const { host, protocol } = location;
const { icon } = opts;
const profilePic = url.resolve(`${protocol}//${host}`, icon);
this.tag = opts.tag;
const notification = new MacNotifier(title, Object.assign(opts, {
bundleId: appId,
canReply: true,
silent,
icon: profilePic
}));
notification.addEventListener('click', () => {
// focus to the server who sent the
// notification if not focused already
if (clickHandler) {
clickHandler();
}
focusCurrentServer();
ipcRenderer.send('focus-app');
});
notification.addEventListener('reply', this.notificationHandler);
}
static requestPermission() {
return; // eslint-disable-line no-useless-return
}
// Override default Notification permission
static get permission() {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
}
set onreply(handler) {
replyHandler = handler;
}
get onreply() {
return replyHandler;
}
set onclick(handler) {
clickHandler = handler;
}
get onclick() {
return clickHandler;
}
// not something that is common or
// used by zulip server but added to be
// future proff.
addEventListener(event, handler) {
if (event === 'click') {
clickHandler = handler;
}
if (event === 'reply') {
replyHandler = handler;
}
}
notificationHandler({ response }) {
response = parseReply(response);
focusCurrentServer();
setupReply(this.tag);
if (replyHandler) {
replyHandler(response);
return;
}
customReply(response);
}
// method specific to notification api
// used by zulip
close() {
return; // eslint-disable-line no-useless-return
}
}
module.exports = DarwinNotification;

View File

@@ -1,31 +0,0 @@
'use strict';
const { ipcRenderer } = require('electron');
const ConfigUtil = require('../utils/config-util');
const { focusCurrentServer } = require('./helpers');
const NativeNotification = window.Notification;
class BaseNotification extends NativeNotification {
constructor(title, opts) {
opts.silent = true;
super(title, opts);
this.addEventListener('click', () => {
// focus to the server who sent the
// notification if not focused already
focusCurrentServer();
ipcRenderer.send('focus-app');
});
}
static requestPermission() {
return; // eslint-disable-line no-useless-return
}
// Override default Notification permission
static get permission() {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
}
}
module.exports = BaseNotification;

View File

@@ -1,149 +0,0 @@
const { remote } = require('electron');
// Do not change this
const appId = 'org.zulip.zulip-electron';
const botsList = [];
let botsListLoaded = false;
// this function load list of bots from the server
// sync=True for a synchronous getJSON request
// in case botsList isn't already completely loaded when required in parseRely
function loadBots(sync = false) {
const { $ } = window;
botsList.length = 0;
if (sync) {
$.ajaxSetup({async: false});
}
$.getJSON('/json/users')
.done(data => {
const members = data.members;
members.forEach(membersRow => {
if (membersRow.is_bot) {
const bot = `@${membersRow.full_name}`;
const mention = `@**${bot.replace(/^@/, '')}**`;
botsList.push([bot, mention]);
}
});
botsListLoaded = true;
})
.fail(error => {
console.log('Request failed: ', error.responseText);
console.log('Request status: ', error.statusText);
});
if (sync) {
$.ajaxSetup({async: true});
}
}
function checkElements(...elements) {
let status = true;
elements.forEach(element => {
if (element === null || element === undefined) {
status = false;
}
});
return status;
}
function customReply(reply) {
// server does not support notification reply yet.
const buttonSelector = '.messagebox #send_controls button[type=submit]';
const messageboxSelector = '.selected_message .messagebox .messagebox-border .messagebox-content';
const textarea = document.querySelector('#compose-textarea');
const messagebox = document.querySelector(messageboxSelector);
const sendButton = document.querySelector(buttonSelector);
// sanity check for old server versions
const elementsExists = checkElements(textarea, messagebox, sendButton);
if (!elementsExists) {
return;
}
textarea.value = reply;
messagebox.click();
sendButton.click();
}
const currentWindow = remote.getCurrentWindow();
const webContents = remote.getCurrentWebContents();
const webContentsId = webContents.id;
// this function will focus the server that sent
// the notification. Main function implemented in main.js
function focusCurrentServer() {
currentWindow.send('focus-webview-with-id', webContentsId);
}
// this function parses the reply from to notification
// making it easier to reply from notification eg
// @username in reply will be converted to @**username**
// #stream in reply will be converted to #**stream**
// bot mentions are not yet supported
function parseReply(reply) {
const usersDiv = document.querySelectorAll('#user_presences li');
const streamHolder = document.querySelectorAll('#stream_filters li');
const users = [];
const streams = [];
usersDiv.forEach(userRow => {
const anchor = userRow.querySelector('span a');
if (anchor !== null) {
const user = `@${anchor.textContent.trim()}`;
const mention = `@**${user.replace(/^@/, '')}**`;
users.push([user, mention]);
}
});
streamHolder.forEach(stream => {
const streamAnchor = stream.querySelector('div a');
if (streamAnchor !== null) {
const streamName = `#${streamAnchor.textContent.trim()}`;
const streamMention = `#**${streamName.replace(/^#/, '')}**`;
streams.push([streamName, streamMention]);
}
});
users.forEach(([user, mention]) => {
if (reply.includes(user)) {
const regex = new RegExp(user, 'g');
reply = reply.replace(regex, mention);
}
});
streams.forEach(([stream, streamMention]) => {
const regex = new RegExp(stream, 'g');
reply = reply.replace(regex, streamMention);
});
// If botsList isn't completely loaded yet, make a synchronous getJSON request for list
if (botsListLoaded === false) {
loadBots(true);
}
// Iterate for every bot name and replace in reply
// @botname with @**botname**
botsList.forEach(([bot, mention]) => {
if (reply.includes(bot)) {
const regex = new RegExp(bot, 'g');
reply = reply.replace(regex, mention);
}
});
reply = reply.replace(/\\n/, '\n');
return reply;
}
function setupReply(id) {
const { narrow } = window;
narrow.by_subject(id, { trigger: 'notification' });
}
module.exports = {
appId,
checkElements,
customReply,
parseReply,
setupReply,
focusCurrentServer,
loadBots
};

View File

@@ -1,27 +0,0 @@
'use strict';
const {
remote: { app }
} = require('electron');
const DefaultNotification = require('./default-notification');
const { appId, loadBots } = require('./helpers');
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
app.setAppUserModelId(appId);
window.Notification = DefaultNotification;
if (process.platform === 'darwin') {
const DarwinNotification = require('./darwin-notifications');
window.Notification = DarwinNotification;
}
window.addEventListener('load', () => {
// Call this function only when user is logged in
// eslint-disable-next-line no-undef, camelcase
if (page_params.realm_uri) {
loadBots();
}
});

View File

@@ -45,7 +45,7 @@ class BadgeSettings {
updateOverlayIcon(messageCount, mainWindow) { updateOverlayIcon(messageCount, mainWindow) {
if (!mainWindow.isFocused()) { if (!mainWindow.isFocused()) {
mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage')); mainWindow.flashFrame(true);
} }
if (messageCount === 0) { if (messageCount === 0) {
mainWindow.setOverlayIcon(null, ''); mainWindow.setOverlayIcon(null, '');

View File

@@ -19,20 +19,14 @@ class BaseSection extends BaseComponent {
generateOptionTemplate(settingOption) { generateOptionTemplate(settingOption) {
if (settingOption) { if (settingOption) {
return ` return `
<div class="action"> <div class="action green">
<div class="switch"> <span>On</span>
<input class="toggle toggle-round" type="checkbox" checked>
<label></label>
</div>
</div> </div>
`; `;
} else { } else {
return ` return `
<div class="action"> <div class="action red">
<div class="switch"> <span>Off</span>
<input class="toggle toggle-round" type="checkbox">
<label></label>
</div>
</div> </div>
`; `;
} }

View File

@@ -1,37 +0,0 @@
'use strict';
const BaseComponent = require(__dirname + '/../../components/base.js');
const shell = require('electron').shell;
class CreateOrganziation extends BaseComponent {
constructor(props) {
super();
this.props = props;
}
template() {
return `
<div class="setting-row">
<div class="setting-description">
<span id="open-create-org-link">Or create a new organization on zulipchat.com<i class="material-icons open-tab-button">open_in_new</i></span>
</div>
<div class="setting-control"></div>
</div>
`;
}
init() {
this.props.$root.innerHTML = this.template();
this.openCreateNewOrgExternalLink();
}
openCreateNewOrgExternalLink() {
const link = 'https://zulipchat.com/beta/';
const externalCreateNewOrgEl = document.getElementById('open-create-org-link');
externalCreateNewOrgEl.addEventListener('click', () => {
shell.openExternal(link);
});
}
}
module.exports = CreateOrganziation;

View File

@@ -1,10 +1,11 @@
'use strict'; 'use strict';
const path = require('path'); const path = require('path');
const { ipcRenderer, remote } = require('electron');
const { ipcRenderer } = require('electron');
const { app, dialog } = require('electron').remote;
const fs = require('fs-extra'); const fs = require('fs-extra');
const { app, dialog } = remote;
const currentBrowserWindow = remote.getCurrentWindow();
const BaseSection = require(__dirname + '/base-section.js'); const BaseSection = require(__dirname + '/base-section.js');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js'); const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
@@ -31,10 +32,6 @@ class GeneralSection extends BaseSection {
<div class="setting-description">Show app unread badge</div> <div class="setting-description">Show app unread badge</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="flash-taskbar-option" style= "display:${process.platform === 'win32' ? '' : 'none'}">
<div class="setting-description">Flash taskbar on new message</div>
<div class="setting-control"></div>
</div>
</div> </div>
<div class="title">Desktop Notification</div> <div class="title">Desktop Notification</div>
<div class="settings-card"> <div class="settings-card">
@@ -53,6 +50,10 @@ class GeneralSection extends BaseSection {
<div class="setting-description">Get beta updates</div> <div class="setting-description">Get beta updates</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="autoupdate-option">
<div class="setting-description">Automatically install new updates</div>
<div class="setting-control"></div>
</div>
</div> </div>
<div class="title">Functionality</div> <div class="title">Functionality</div>
<div class="settings-card"> <div class="settings-card">
@@ -60,21 +61,13 @@ class GeneralSection extends BaseSection {
<div class="setting-description">Start app at login</div> <div class="setting-description">Start app at login</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="start-minimize-option">
<div class="setting-description">Always start minimized</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="enable-spellchecker-option">
<div class="setting-description">Enable Spellchecker (requires restart)</div>
<div class="setting-control"></div>
</div>
</div> </div>
<div class="title">Reset Application Data</div> <div class="title">Reset Application Data</div>
<div class="settings-card"> <div class="settings-card">
<div class="setting-row" id="resetdata-option"> <div class="setting-row" id="resetdata-option">
<div class="setting-description">This will delete all application data including all added accounts and preferences <div class="setting-description">This will delete all application data including all added accounts and preferences
</div> </div>
<button class="reset-data-button blue">Reset App Data</button> <button class="reset-data-button green">Reset App Data</button>
</div> </div>
</div> </div>
</div> </div>
@@ -85,20 +78,13 @@ class GeneralSection extends BaseSection {
this.props.$root.innerHTML = this.template(); this.props.$root.innerHTML = this.template();
this.updateTrayOption(); this.updateTrayOption();
this.updateBadgeOption(); this.updateBadgeOption();
this.updateSilentOption();
this.updateUpdateOption(); this.updateUpdateOption();
this.updateAutoUpdateOption();
this.updateSilentOption();
this.updateSidebarOption(); this.updateSidebarOption();
this.updateStartAtLoginOption(); this.updateStartAtLoginOption();
this.updateResetDataOption(); this.updateResetDataOption();
this.showDesktopNotification(); this.showDesktopNotification();
this.enableSpellchecker();
this.minimizeOnStart();
// Platform specific settings
// Flashing taskbar on Windows
if (process.platform === 'win32') {
this.updateFlashTaskbar();
}
} }
updateTrayOption() { updateTrayOption() {
@@ -127,18 +113,6 @@ class GeneralSection extends BaseSection {
}); });
} }
updateFlashTaskbar() {
this.generateSettingOption({
$element: document.querySelector('#flash-taskbar-option .setting-control'),
value: ConfigUtil.getConfigItem('flashTaskbarOnMessage', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('flashTaskbarOnMessage');
ConfigUtil.setConfigItem('flashTaskbarOnMessage', newValue);
this.updateFlashTaskbar();
}
});
}
updateUpdateOption() { updateUpdateOption() {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#betaupdate-option .setting-control'), $element: document.querySelector('#betaupdate-option .setting-control'),
@@ -151,6 +125,18 @@ class GeneralSection extends BaseSection {
}); });
} }
updateAutoUpdateOption() {
this.generateSettingOption({
$element: document.querySelector('#autoupdate-option .setting-control'),
value: ConfigUtil.getConfigItem('autoUpdate', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('autoUpdate');
ConfigUtil.setConfigItem('autoUpdate', newValue);
this.updateAutoUpdateOption();
}
});
}
updateSilentOption() { updateSilentOption() {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#silent-option .setting-control'), $element: document.querySelector('#silent-option .setting-control'),
@@ -159,7 +145,6 @@ class GeneralSection extends BaseSection {
const newValue = !ConfigUtil.getConfigItem('silent', true); const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue); ConfigUtil.setConfigItem('silent', newValue);
this.updateSilentOption(); this.updateSilentOption();
currentBrowserWindow.send('toogle-silent', newValue);
} }
}); });
} }
@@ -202,18 +187,6 @@ class GeneralSection extends BaseSection {
}); });
} }
enableSpellchecker() {
this.generateSettingOption({
$element: document.querySelector('#enable-spellchecker-option .setting-control'),
value: ConfigUtil.getConfigItem('enableSpellchecker', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('enableSpellchecker');
ConfigUtil.setConfigItem('enableSpellchecker', newValue);
this.enableSpellchecker();
}
});
}
clearAppDataDialog() { clearAppDataDialog() {
const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.'; const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.';
const getAppPath = path.join(app.getPath('appData'), app.getName()); const getAppPath = path.join(app.getPath('appData'), app.getName());
@@ -239,18 +212,6 @@ class GeneralSection extends BaseSection {
}); });
} }
minimizeOnStart() {
this.generateSettingOption({
$element: document.querySelector('#start-minimize-option .setting-control'),
value: ConfigUtil.getConfigItem('startMinimized', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('startMinimized');
ConfigUtil.setConfigItem('startMinimized', newValue);
this.minimizeOnStart();
}
});
}
} }
module.exports = GeneralSection; module.exports = GeneralSection;

View File

@@ -34,7 +34,7 @@ class NetworkSection extends BaseSection {
<input class="setting-input-value" placeholder="e.g. foobar.com"/> <input class="setting-input-value" placeholder="e.g. foobar.com"/>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="action blue" id="proxy-save-action"> <div class="action green" id="proxy-save-action">
<i class="material-icons">check_box</i> <i class="material-icons">check_box</i>
<span>Save</span> <span>Save</span>
</div> </div>

View File

@@ -13,14 +13,13 @@ class NewServerForm extends BaseComponent {
return ` return `
<div class="settings-card"> <div class="settings-card">
<div class="server-info-right"> <div class="server-info-right">
<div class="title">URL of Zulip organization</div>
<div class="server-info-row"> <div class="server-info-row">
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or chat.your-organization.com"/> <input class="setting-input-value" autofocus placeholder="Entert the URL of your Zulip organization..."/>
</div> </div>
<div class="server-info-row"> <div class="server-info-row">
<div class="action blue server-save-action"> <div class="action green server-save-action">
<i class="material-icons">add_box</i> <i class="material-icons">check_box</i>
<span>Add</span> <span>Save</span>
</div> </div>
</div> </div>
</div> </div>
@@ -43,13 +42,11 @@ class NewServerForm extends BaseComponent {
} }
submitFormHandler() { submitFormHandler() {
this.$saveServerButton.children[1].innerHTML = 'Adding...';
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => { DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
DomainUtil.addDomain(serverConf).then(() => { DomainUtil.addDomain(serverConf).then(() => {
this.props.onChange(this.props.index); this.props.onChange(this.props.index);
}); });
}, errorMessage => { }, errorMessage => {
this.$saveServerButton.children[1].innerHTML = 'Add';
alert(errorMessage); alert(errorMessage);
}); });
} }

View File

@@ -69,27 +69,10 @@ class PreferenceView extends BaseComponent {
window.location.hash = `#${navItem}`; window.location.hash = `#${navItem}`;
} }
// Handle toggling and reflect changes in preference page
handleToggle(elementName, state) {
const inputSelector = `#${elementName} .action .switch input`;
const input = document.querySelector(inputSelector);
if (input) {
input.checked = state;
}
}
registerIpcs() { registerIpcs() {
ipcRenderer.on('switch-settings-nav', (event, navItem) => { ipcRenderer.on('switch-settings-nav', (event, navItem) => {
this.handleNavigation(navItem); this.handleNavigation(navItem);
}); });
ipcRenderer.on('toggle-sidebar', (event, state) => {
this.handleToggle('sidebar-option', state);
});
ipcRenderer.on('toggletray', (event, state) => {
this.handleToggle('tray-option', state);
});
} }
} }

View File

@@ -4,7 +4,6 @@ const BaseSection = require(__dirname + '/base-section.js');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js'); const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
const ServerInfoForm = require(__dirname + '/server-info-form.js'); const ServerInfoForm = require(__dirname + '/server-info-form.js');
const NewServerForm = require(__dirname + '/new-server-form.js'); const NewServerForm = require(__dirname + '/new-server-form.js');
const CreateOrganziation = require(__dirname + '/create-new-org.js');
class ServersSection extends BaseSection { class ServersSection extends BaseSection {
constructor(props) { constructor(props) {
@@ -15,11 +14,10 @@ class ServersSection extends BaseSection {
template() { template() {
return ` return `
<div class="settings-pane" id="server-settings-pane"> <div class="settings-pane" id="server-settings-pane">
<div class="page-title">Register or login to a Zulip organization to get started</div> <div class="title">Add Server</div>
<div id="new-server-container"></div> <div id="new-server-container"></div>
<div class="title" id="existing-servers"></div> <div class="title" id="existing-servers"></div>
<div id="server-info-container"></div> <div id="server-info-container"></div>
<div id="create-organization-container"></div>
</div> </div>
`; `;
} }
@@ -38,14 +36,11 @@ class ServersSection extends BaseSection {
this.$newServerContainer = document.getElementById('new-server-container'); this.$newServerContainer = document.getElementById('new-server-container');
this.$newServerButton = document.getElementById('new-server-action'); this.$newServerButton = document.getElementById('new-server-action');
this.$serverInfoContainer.innerHTML = servers.length ? '' : ''; this.$serverInfoContainer.innerHTML = servers.length ? '' : 'Add your first server to get started!';
// Show Existing servers if servers are there otherwise hide it // Show Existing servers if servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Existing organizations'; this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Existing Servers';
this.initNewServerForm(); this.initNewServerForm();
this.$createOrganizationContainer = document.getElementById('create-organization-container');
this.initCreateNewOrganization();
for (let i = 0; i < servers.length; i++) { for (let i = 0; i < servers.length; i++) {
new ServerInfoForm({ new ServerInfoForm({
$root: this.$serverInfoContainer, $root: this.$serverInfoContainer,
@@ -56,12 +51,6 @@ class ServersSection extends BaseSection {
} }
} }
initCreateNewOrganization() {
new CreateOrganziation({
$root: this.$createOrganizationContainer
}).init();
}
initNewServerForm() { initNewServerForm() {
new NewServerForm({ new NewServerForm({
$root: this.$newServerContainer, $root: this.$newServerContainer,

View File

@@ -109,7 +109,7 @@ class ShortcutsSection extends BaseSection {
<td>Enter Full Screen</td> <td>Enter Full Screen</td>
</tr> </tr>
<tr> <tr>
<td><kbd>${userOSKey}</kbd><kbd>+</kbd></td> <td><kbd>${userOSKey}</kbd><kbd>=</kbd></td>
<td>Zoom In</td> <td>Zoom In</td>
</tr> </tr>
<tr> <tr>

View File

@@ -1,10 +1,7 @@
'use strict'; 'use strict';
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
const SetupSpellChecker = require('./spellchecker'); const { spellChecker } = require('./spellchecker');
const ConfigUtil = require(__dirname + '/utils/config-util.js');
// eslint-disable-next-line import/no-unassigned-import // eslint-disable-next-line import/no-unassigned-import
require('./notification'); require('./notification');
@@ -35,15 +32,8 @@ process.once('loaded', () => {
// To prevent failing this script on linux we need to load it after the document loaded // To prevent failing this script on linux we need to load it after the document loaded
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Get the default language of the server
const serverLanguage = page_params.default_language; // eslint-disable-line no-undef, camelcase
if (serverLanguage) {
// Set spellcheker language
ConfigUtil.setConfigItem('spellcheckerLanguage', serverLanguage);
// Init spellchecker // Init spellchecker
SetupSpellChecker.init(); spellChecker();
}
// redirect users to network troubleshooting page // redirect users to network troubleshooting page
const getRestartButton = document.querySelector('.restart_get_events_button'); const getRestartButton = document.querySelector('.restart_get_events_button');
@@ -53,10 +43,3 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
}); });
// Clean up spellchecker events after you navigate away from this page;
// otherwise, you may experience errors
window.addEventListener('beforeunload', () => {
SetupSpellChecker.unsubscribeSpellChecker();
});

View File

@@ -2,55 +2,28 @@
const {SpellCheckHandler, ContextMenuListener, ContextMenuBuilder} = require('electron-spellchecker'); const {SpellCheckHandler, ContextMenuListener, ContextMenuBuilder} = require('electron-spellchecker');
const ConfigUtil = require(__dirname + '/utils/config-util.js'); function spellChecker() {
// Implement spellcheck using electron api
window.spellCheckHandler = new SpellCheckHandler();
window.spellCheckHandler.attachToInput();
class SetupSpellChecker { // Start off as US English
init() { window.spellCheckHandler.switchLanguage('en-US');
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
this.enableSpellChecker();
}
this.enableContextMenu();
}
enableSpellChecker() { const contextMenuBuilder = new ContextMenuBuilder(window.spellCheckHandler);
try { const contextMenuListener = new ContextMenuListener(info => {
this.SpellCheckHandler = new SpellCheckHandler();
} catch (err) {
console.log(err);
}
}
enableContextMenu() {
if (this.SpellCheckHandler) {
this.SpellCheckHandler.attachToInput();
const userLanguage = ConfigUtil.getConfigItem('spellcheckerLanguage');
// eslint-disable-next-line no-unused-expressions
process.platform === 'darwin' ?
// On macOS, spellchecker fails to auto-detect the lanugage user is typing in
// that's why we need to mention it explicitly
this.SpellCheckHandler.switchLanguage(userLanguage) :
// On Linux and Windows, spellchecker can automatically detects the language the user is typing in
// and silently switches on the fly; thus we can start off as US English
this.SpellCheckHandler.switchLanguage('en-US');
}
const contextMenuBuilder = new ContextMenuBuilder(this.SpellCheckHandler);
this.contextMenuListener = new ContextMenuListener(info => {
contextMenuBuilder.showPopupMenu(info); contextMenuBuilder.showPopupMenu(info);
}); });
// Clean up events after you navigate away from this page;
// otherwise, you may experience errors
window.addEventListener('beforeunload', () => {
// eslint-disable-next-line no-undef
spellCheckHandler.unsubscribe();
contextMenuListener.unsubscribe();
});
} }
unsubscribeSpellChecker() { module.exports = {
// eslint-disable-next-line no-undef spellChecker
if (this.SpellCheckHandler) { };
this.SpellCheckHandler.unsubscribe();
}
if (this.contextMenuListener) {
this.contextMenuListener.unsubscribe();
}
}
}
module.exports = new SetupSpellChecker();

View File

@@ -197,16 +197,13 @@ ipcRenderer.on('tray', (event, arg) => {
}); });
function toggleTray() { function toggleTray() {
let state;
if (window.tray) { if (window.tray) {
state = false;
window.tray.destroy(); window.tray.destroy();
if (window.tray.isDestroyed()) { if (window.tray.isDestroyed()) {
window.tray = null; window.tray = null;
} }
ConfigUtil.setConfigItem('trayIcon', false); ConfigUtil.setConfigItem('trayIcon', false);
} else { } else {
state = true;
createTray(); createTray();
if (process.platform === 'linux' || process.platform === 'win32') { if (process.platform === 'linux' || process.platform === 'win32') {
renderNativeImage(unread).then(image => { renderNativeImage(unread).then(image => {
@@ -216,10 +213,6 @@ function toggleTray() {
} }
ConfigUtil.setConfigItem('trayIcon', true); ConfigUtil.setConfigItem('trayIcon', true);
} }
const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector);
const webContents = webview.getWebContents();
webContents.send('toggletray', state);
} }
ipcRenderer.on('toggletray', toggleTray); ipcRenderer.on('toggletray', toggleTray);

View File

@@ -1,29 +1,16 @@
'use strict'; 'use strict';
const fs = require('fs');
const path = require('path');
const process = require('process'); const process = require('process');
const JsonDB = require('node-json-db'); const JsonDB = require('node-json-db');
const Logger = require('./logger-util');
const logger = new Logger({
file: 'config-util.log',
timestamp: true
});
let instance = null; let instance = null;
let dialog = null;
let app = null; let app = null;
/* To make the util runnable in both main and renderer process */ /* To make the util runnable in both main and renderer process */
if (process.type === 'renderer') { if (process.type === 'renderer') {
const remote = require('electron').remote; app = require('electron').remote.app;
dialog = remote.dialog;
app = remote.app;
} else { } else {
const electron = require('electron'); app = require('electron').app;
dialog = electron.dialog;
app = electron.app;
} }
class ConfigUtil { class ConfigUtil {
@@ -60,22 +47,7 @@ class ConfigUtil {
} }
reloadDB() { reloadDB() {
const settingsJsonPath = path.join(app.getPath('userData'), '/settings.json'); this.db = new JsonDB(app.getPath('userData') + '/settings.json', true, true);
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
'Error saving settings',
'We encountered error while saving current settings.'
);
logger.error('Error while JSON parsing settings.json: ');
logger.error(err);
}
}
this.db = new JsonDB(settingsJsonPath, true, true);
} }
} }

View File

@@ -1,31 +0,0 @@
const fs = require('fs');
let app = null;
let setupCompleted = false;
if (process.type === 'renderer') {
app = require('electron').remote.app;
} else {
app = require('electron').app;
}
const zulipDir = app.getPath('userData');
const logDir = `${zulipDir}/Logs/`;
const initSetUp = () => {
// if it is the first time the app is running
// create zulip dir in userData folder to
// avoid errors
if (!setupCompleted) {
if (!fs.existsSync(zulipDir)) {
fs.mkdirSync(zulipDir);
}
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
setupCompleted = true;
}
};
module.exports = {
initSetUp
};

View File

@@ -5,12 +5,6 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const JsonDB = require('node-json-db'); const JsonDB = require('node-json-db');
const request = require('request'); const request = require('request');
const Logger = require('./logger-util');
const logger = new Logger({
file: `domain-util.log`,
timestamp: true
});
let instance = null; let instance = null;
@@ -99,7 +93,8 @@ class DomainUtil {
checkDomain(domain, silent = false) { checkDomain(domain, silent = false) {
if (!silent && this.duplicateDomain(domain)) { if (!silent && this.duplicateDomain(domain)) {
// Do not check duplicate in silent mode // Do not check duplicate in silent mode
return Promise.reject('This server has been added.'); alert('This server has been added.');
return;
} }
domain = this.formatUrl(domain); domain = this.formatUrl(domain);
@@ -115,28 +110,17 @@ class DomainUtil {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request(checkDomain, (error, response) => { request(checkDomain, (error, response) => {
const certsError = const certsError =
[ ['Error: self signed certificate',
'Error: self signed certificate', 'Error: unable to verify the first certificate'
'Error: unable to verify the first certificate',
'Error: unable to get local issuer certificate'
]; ];
if (!error && response.statusCode !== 404) {
// If the domain contains following strings we just bypass the server
const whitelistDomains = [
'zulipdev.org'
];
// make sure that error is a error or string not undefined
// so validation does not throw error.
error = error || '';
if (!error && response.statusCode < 400) {
// Correct // Correct
this.getServerSettings(domain).then(serverSettings => { this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings); resolve(serverSettings);
}, () => { }, () => {
resolve(serverConf); resolve(serverConf);
}); });
} else if (domain.indexOf(whitelistDomains) >= 0 || certsError.indexOf(error.toString()) >= 0) { } else if (certsError.indexOf(error.toString()) >= 0) {
if (silent) { if (silent) {
this.getServerSettings(domain).then(serverSettings => { this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings); resolve(serverSettings);
@@ -168,9 +152,7 @@ class DomainUtil {
}); });
} }
} else { } else {
const invalidZulipServerError = `${domain} does not appear to be a valid Zulip server. Make sure that \ reject('Not a valid Zulip server');
\n(1) you can connect to that URL in a web browser and \n (2) if you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings \n (3) its a zulip server`;
reject(invalidZulipServerError);
} }
}); });
}); });
@@ -184,9 +166,7 @@ class DomainUtil {
const data = JSON.parse(response.body); const data = JSON.parse(response.body);
if (data.hasOwnProperty('realm_icon') && data.realm_icon) { if (data.hasOwnProperty('realm_icon') && data.realm_icon) {
resolve({ resolve({
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL icon: data.realm_uri + data.realm_icon,
// Following check handles both the cases
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
url: data.realm_uri, url: data.realm_uri,
alias: data.realm_name alias: data.realm_name
}); });
@@ -235,23 +215,7 @@ class DomainUtil {
} }
reloadDB() { reloadDB() {
const domainJsonPath = path.join(app.getPath('userData'), '/domain.json'); this.db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
try {
const file = fs.readFileSync(domainJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(domainJsonPath)) {
fs.unlinkSync(domainJsonPath);
dialog.showErrorBox(
'Error saving new organization',
'There seems to be error while saving new organization, ' +
'you may have to re-add your previous organizations back.'
);
logger.error('Error while JSON parsing domain.json: ');
logger.error(err);
}
}
this.db = new JsonDB(domainJsonPath, true, true);
} }
generateFilePath(url) { generateFilePath(url) {

View File

@@ -1,74 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const process = require('process');
const remote =
process.type === 'renderer' ? require('electron').remote : require('electron');
const JsonDB = require('node-json-db');
const Logger = require('./logger-util');
const logger = new Logger({
file: 'linux-update-util.log',
timestamp: true
});
/* To make the util runnable in both main and renderer process */
const { dialog, app } = remote;
let instance = null;
class LinuxUpdateUtil {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
this.reloadDB();
return instance;
}
getUpdateItem(key, defaultValue = null) {
this.reloadDB();
const value = this.db.getData('/')[key];
if (value === undefined) {
this.setUpdateItem(key, defaultValue);
return defaultValue;
} else {
return value;
}
}
setUpdateItem(key, value) {
this.db.push(`/${key}`, value, true);
this.reloadDB();
}
removeUpdateItem(key) {
this.db.delete(`/${key}`);
this.reloadDB();
}
reloadDB() {
const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/updates.json');
try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
'Error saving update notifications.',
'We encountered error while saving update notifications.'
);
logger.error('Error while JSON parsing updates.json: ');
logger.error(err);
}
}
this.db = new JsonDB(linuxUpdateJsonPath, true, true);
}
}
module.exports = new LinuxUpdateUtil();

View File

@@ -1,87 +0,0 @@
const NodeConsole = require('console').Console;
const fs = require('fs');
const isDev = require('electron-is-dev');
const { initSetUp } = require('./default-util');
initSetUp();
let app = null;
if (process.type === 'renderer') {
app = require('electron').remote.app;
} else {
app = require('electron').app;
}
const browserConsole = console;
const logDir = `${app.getPath('userData')}/Logs`;
class Logger {
constructor(opts = {}) {
let {
timestamp = true,
file = 'console.log',
level = true,
logInDevMode = false
} = opts;
file = `${logDir}/${file}`;
if (timestamp === true) {
timestamp = this.getTimestamp;
}
const fileStream = fs.createWriteStream(file, { flags: 'a' });
const nodeConsole = new NodeConsole(fileStream);
this.nodeConsole = nodeConsole;
this.timestamp = timestamp;
this.level = level;
this.logInDevMode = logInDevMode;
this.setUpConsole();
}
_log(type, ...args) {
const {
nodeConsole, timestamp, level, logInDevMode
} = this;
let nodeConsoleLog;
/* eslint-disable no-fallthrough */
switch (true) {
case typeof timestamp === 'function':
args.unshift(timestamp() + ' |\t');
case (level !== false):
args.unshift(type.toUpperCase() + ' |');
case isDev || logInDevMode:
nodeConsoleLog = nodeConsole[type] || nodeConsole.log;
nodeConsoleLog.apply(null, args);
default: break;
}
/* eslint-enable no-fallthrough */
browserConsole[type].apply(null, args);
}
setUpConsole() {
for (const type in browserConsole) {
this.setupConsoleMethod(type);
}
}
setupConsoleMethod(type) {
this[type] = (...args) => {
this._log(type, ...args);
};
}
getTimestamp() {
const date = new Date();
const timestamp =
`${date.getMonth()}/${date.getDate()} ` +
`${date.getMinutes()}:${date.getSeconds()}`;
return timestamp;
}
}
module.exports = Logger;

View File

@@ -1,52 +0,0 @@
const isOnline = require('is-online');
class ReconnectUtil {
constructor(serverManagerView) {
this.serverManagerView = serverManagerView;
this.alreadyReloaded = false;
}
clearState() {
this.alreadyReloaded = false;
}
pollInternetAndReload() {
const pollInterval = setInterval(() => {
this._checkAndReload()
.then(status => {
if (status) {
this.alreadyReloaded = true;
clearInterval(pollInterval);
}
});
}, 1500);
}
_checkAndReload() {
return new Promise(resolve => {
if (!this.alreadyReloaded) { // eslint-disable-line no-negated-condition
isOnline()
.then(online => {
if (online) {
if (!this.alreadyReloaded) {
this.serverManagerView.reloadView();
}
console.log('You\'re back online.');
return resolve(true);
}
console.log('There is no internet connection, try checking network cables, modem and router.');
const errMsgHolder = document.querySelector('#description');
errMsgHolder.innerHTML = `
<div>You internet connection does't seem to work properly!</div>
</div>Verify that it works and then click try again.</div>`;
return resolve(false);
});
} else {
return resolve(true);
}
});
}
}
module.exports = ReconnectUtil;

View File

@@ -9,18 +9,17 @@
</head> </head>
<body> <body>
<div id="content" class="loading"> <div id="content">
<div class="popup"> <div class="popup">
<span class="popuptext hidden" id="fullscreen-popup"></span> <span class="popuptext hidden" id="fullscreen-popup"></span>
</div> </div>
<div id="sidebar" class="toggle-sidebar"> <div id="sidebar">
<div id="view-controls-container"> <div id="view-controls-container">
<div id="tabs-container"></div> <div id="tabs-container"></div>
<div id="add-tab" class="tab functional-tab"> <div id="add-tab" class="tab functional-tab">
<div class="server-tab" id="add-action"> <div class="server-tab" id="add-action">
<i class="material-icons">add</i> <i class="material-icons">add</i>
</div> </div>
<span id="add-server-tooltip" style="display:none">Add organization</span>
</div> </div>
</div> </div>
<div id="actions-container"> <div id="actions-container">
@@ -28,10 +27,6 @@
<i class="material-icons md-48">refresh</i> <i class="material-icons md-48">refresh</i>
<span id="reload-tooltip" style="display:none">Reload</span> <span id="reload-tooltip" style="display:none">Reload</span>
</div> </div>
<div class="action-button disable" id="back-action">
<i class="material-icons md-48">arrow_back</i>
<span id="back-tooltip" style="display:none">Go Back</span>
</div>
<div class="action-button" id="settings-action"> <div class="action-button" id="settings-action">
<i class="material-icons md-48">settings</i> <i class="material-icons md-48">settings</i>
<span id="setting-tooltip" style="display:none">Settings</span> <span id="setting-tooltip" style="display:none">Settings</span>
@@ -44,4 +39,5 @@
</div> </div>
</body> </body>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</html> </html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -21,4 +21,3 @@ build: off
test_script: test_script:
- npm run test - npm run test
- npm run test-e2e

View File

@@ -1,10 +1,9 @@
'use strict'; 'use strict';
const gulp = require('gulp'); const gulp = require('gulp');
const mocha = require('gulp-mocha');
const electron = require('electron-connect').server.create({ const electron = require('electron-connect').server.create({
verbose: true verbose: true
}); });
const tape = require('gulp-tape');
const tapColorize = require('tap-colorize');
gulp.task('dev', () => { gulp.task('dev', () => {
// Start browser process // Start browser process
@@ -14,7 +13,7 @@ gulp.task('dev', () => {
// Reload renderer process // Reload renderer process
gulp.watch('app/renderer/css/*.css', ['reload:renderer']); gulp.watch('app/renderer/css/*.css', ['reload:renderer']);
gulp.watch('app/renderer/*.html', ['reload:renderer']); gulp.watch('app/renderer/*.html', ['reload:renderer']);
gulp.watch('app/renderer/js/**/*.js', ['reload:renderer']); gulp.watch('app/renderer/js/*.js', ['reload:renderer']);
}); });
gulp.task('restart:browser', done => { gulp.task('restart:browser', done => {
@@ -29,11 +28,9 @@ gulp.task('reload:renderer', done => {
done(); done();
}); });
gulp.task('test-e2e', () => { // Test app using mocha+spectron
return gulp.src('tests/*.js') gulp.task('test', () => {
.pipe(tape({ return gulp.src('tests/index.js').pipe(mocha());
reporter: tapColorize()
}));
}); });
gulp.task('default', ['dev', 'test-e2e']); gulp.task('default', ['dev', 'test']);

9724
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,15 @@
{ {
"name": "zulip", "name": "zulip",
"productName": "Zulip", "productName": "Zulip",
"version": "1.8.1", "version": "1.4.0",
"main": "./app/main", "main": "./app/main",
"description": "Zulip Desktop App", "description": "Zulip Desktop App",
"license": "Apache-2.0", "license": "Apache-2.0",
"copyright": "Kandra Labs, Inc.", "email": "<svnitakash@gmail.com>",
"copyright": "©2017 Kandra Labs, Inc.",
"author": { "author": {
"name": "Kandra Labs, Inc.", "name": "Kandra Labs, Inc.",
"email": "support@zulipchat.com" "email": "svnitakash@gmail.com"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -18,12 +19,10 @@
"url": "https://github.com/zulip/zulip-electron/issues" "url": "https://github.com/zulip/zulip-electron/issues"
}, },
"scripts": { "scripts": {
"start": "electron app --disable-http-cache --no-electron-connect", "start": "electron app --disable-http-cache",
"reinstall": "./tools/reinstall-node-modules",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"test": "xo", "test": "xo",
"test-e2e": "gulp test-e2e", "dev": "gulp dev",
"dev": "gulp dev & nodemon --watch app/main --watch app/renderer --exec 'npm test' -e html,css,js",
"pack": "electron-builder --dir", "pack": "electron-builder --dir",
"dist": "electron-builder", "dist": "electron-builder",
"mas": "electron-builder --mac mas", "mas": "electron-builder --mac mas",
@@ -56,9 +55,7 @@
"maintainer": "Akash Nimare <svnitakash@gmail.com>" "maintainer": "Akash Nimare <svnitakash@gmail.com>"
}, },
"deb": { "deb": {
"synopsis": "Zulip Desktop App", "synopsis": "Zulip Desktop App"
"afterInstall": "./scripts/debian-add-repo.sh",
"afterRemove": "./scripts/debian-uninstaller.sh"
}, },
"dmg": { "dmg": {
"background": "build/appdmg.png", "background": "build/appdmg.png",
@@ -107,21 +104,17 @@
], ],
"devDependencies": { "devDependencies": {
"assert": "1.4.1", "assert": "1.4.1",
"cp-file": "^5.0.0",
"devtron": "1.4.0", "devtron": "1.4.0",
"electron": "1.8.2", "electron-builder": "19.27.3",
"electron-builder": "19.53.6", "electron": "1.6.11",
"electron-connect": "0.6.2", "electron-connect": "0.6.2",
"electron-debug": "1.4.0",
"gulp": "3.9.1", "gulp": "3.9.1",
"gulp-tape": "0.0.9", "gulp-mocha": "4.3.1",
"is-ci": "^1.0.10", "chai-as-promised": "7.1.1",
"nodemon": "^1.14.11", "chai": "4.1.1",
"pre-commit": "1.2.2",
"spectron": "3.7.2", "spectron": "3.7.2",
"tap-colorize": "^1.2.0", "xo": "0.18.2",
"tape": "^4.8.0", "pre-commit": "1.2.2"
"xo": "0.18.2"
}, },
"xo": { "xo": {
"parserOptions": { "parserOptions": {
@@ -137,11 +130,7 @@
"rules": { "rules": {
"max-lines": [ "max-lines": [
"warn", "warn",
{ 500
"max": 500,
"skipBlankLines": true,
"skipComments": true
}
], ],
"no-warning-comments": 0, "no-warning-comments": 0,
"object-curly-spacing": 0, "object-curly-spacing": 0,

View File

@@ -1,10 +0,0 @@
#!/bin/bash
# This script runs when user install the debian package
# Install apt repository source list if it does not exist
if ! grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep zulip.list; then
sudo apt-key adv --keyserver pool.sks-keyservers.net --recv 69AD12704E71A4803DCA3A682424BE5AE9BD10D9
echo "deb https://dl.bintray.com/zulip/debian/ stable main" | \
sudo tee -a /etc/apt/sources.list.d/zulip.list;
fi

View File

@@ -1,31 +0,0 @@
#!/bin/bash
# This script runs when user uninstall the debian package.
# It will remove all the config files and anything which was added by the app.
# Remove apt repository source list when user uninstalls Zulip app
if grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep zulip.list; then
sudo apt-key del 69AD12704E71A4803DCA3A682424BE5AE9BD10D9;
sudo rm /etc/apt/sources.list.d/zulip.list;
fi
# Get the root user
if [ $SUDO_USER ];
then getSudoUser=$SUDO_USER;
else getSudoUser=`whoami`;
fi
# Get the path for Zulip's desktop entry which is created by auto-launch script
getDesktopEntry=/home/$getSudoUser/.config/autostart/zulip.desktop;
# Remove desktop entry if exists
if [ -f $getDesktopEntry ]; then
sudo rm $getDesktopEntry;
fi
# App directory which contains all the config, setting files
appDirectory=/home/$getSudoUser/.config/Zulip/;
if [ -d $appDirectory ]; then
sudo rm -rf $appDirectory;
fi

View File

@@ -1,20 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# exit script if fails
set -e;
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
export {no_proxy,NO_PROXY}="127.0.0.1,localhost" export {no_proxy,NO_PROXY}="127.0.0.1,localhost"
export DISPLAY=:99.0 export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start sh -e /etc/init.d/xvfb start
sleep 3 sleep 3
echo 'Travis Screen Resolution:'
xdpyinfo | grep dimensions
fi fi
npm run test npm run test
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
npm run test-e2e
fi

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env bash
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16
fi

View File

@@ -1,7 +0,0 @@
const path = require('path')
const TEST_APP_PRODUCT_NAME = 'ZulipTest'
module.exports = {
TEST_APP_PRODUCT_NAME
}

View File

@@ -1,13 +1,81 @@
const test = require('tape') const assert = require('assert')
const setup = require('./setup') const Application = require('spectron').Application
const chai = require('chai')
const { expect } = chai
const chaiAsPromised = require('chai-as-promised')
test('app runs', function (t) { chai.should()
t.timeoutAfter(10e3) chai.use(chaiAsPromised)
setup.resetTestDataDir()
const app = setup.createApp() describe('application launch', function () {
setup.waitForLoad(app, t) this.timeout(15000)
.then(() => app.client.windowByIndex(1)) // focus on webview
.then(() => app.client.waitForExist('//*[@id="new-server-container"]/div/div/div[2]/input')) beforeEach(function () {
.then(() => setup.endTest(app, t), this.app = new Application({
(err) => setup.endTest(app, t, err || 'error')) path: require('electron'),
args: [__dirname + '/../app/renderer/main.html']
}) })
return this.app.start()
})
beforeEach(function () {
chaiAsPromised.transferPromiseness = this.app.transferPromiseness
})
afterEach(function () {
if (this.app && this.app.isRunning()) {
return this.app.stop()
}
})
it('shows an initial window', function () {
return this.app.client.waitUntilWindowLoaded(5000)
.getWindowCount().should.eventually.equal(2)
.browserWindow.isMinimized().should.eventually.be.false
.browserWindow.isDevToolsOpened().should.eventually.be.false
.browserWindow.isVisible().should.eventually.be.true
.browserWindow.isFocused().should.eventually.be.true
.browserWindow.getBounds().should.eventually.have.property('width').and.be.above(0)
.browserWindow.getBounds().should.eventually.have.property('height').and.be.above(0)
})
it('sets up a default organization', function () {
let app = this.app
let self = this
app.client.execute(() => {
window.confirm = function () { return true }
})
function createOrg (client, name, url, winIndex) {
return client
// Focus on settings webview
.then(switchToWebviewAtIndex.bind(null, self.app.client, winIndex))
.pause(1000) // wait for settings to load
// Fill settings form
.click('#new-server-action')
.setValue('input[id="server-info-name"]', name)
.setValue('input[id="server-info-url"]', url)
.click('#save-server-action')
.pause(500) // Need to pause while server verification takes place
.then(() => app.browserWindow.reload())
.pause(1500) // Wait for webview of org to load
}
function switchToWebviewAtIndex(client, index) {
return client
.windowHandles()
.then(function (session) {
this.window(session.value[index])
})
}
return this.app.client.waitUntilWindowLoaded(5000)
.then(() => createOrg(self.app.client, 'Zulip 1', 'chat.zulip.org', 1))
.then(switchToWebviewAtIndex.bind(null, self.app.client, 0))
.click('#add-action > i').pause(500)
.then(switchToWebviewAtIndex.bind(null, self.app.client, 2))
.then(() => createOrg(self.app.client, 'Zulip 2', 'chat.zulip.org', 2))
})
})

View File

@@ -1,99 +0,0 @@
const Application = require('spectron').Application
const cpFile = require('cp-file')
const fs = require('fs')
const isCI = require('is-ci')
const mkdirp = require('mkdirp')
const path = require('path')
const rimraf = require('rimraf')
const config = require('./config')
module.exports = {
createApp,
endTest,
waitForLoad,
wait,
resetTestDataDir
}
// Runs Zulip Desktop.
// Returns a promise that resolves to a Spectron Application once the app has loaded.
// Takes a Tape test. Makes some basic assertions to verify that the app loaded correctly.
function createApp (t) {
generateTestAppPackageJson()
return new Application({
path: path.join(__dirname, '..', 'node_modules', '.bin',
'electron' + (process.platform === 'win32' ? '.cmd' : '')),
args: [path.join(__dirname)], // Ensure this dir has a package.json file with a 'main' entry piont
env: {NODE_ENV: 'test'},
waitTimeout: 10e3
})
}
// Generates package.json for test app
// Reads app package.json and updates the productName to config.TEST_APP_PRODUCT_NAME
// We do this so that the app integration doesn't doesn't share the same appDataDir as the dev application
function generateTestAppPackageJson () {
let packageJson = require(path.join(__dirname, '../package.json'))
packageJson.productName = config.TEST_APP_PRODUCT_NAME
packageJson.main = '../app/main'
const testPackageJsonPath = path.join(__dirname, 'package.json')
fs.writeFileSync(testPackageJsonPath, JSON.stringify(packageJson, null, ' '), 'utf-8')
}
// Starts the app, waits for it to load, returns a promise
function waitForLoad (app, t, opts) {
if (!opts) opts = {}
return app.start().then(function () {
return app.client.waitUntilWindowLoaded()
})
.then(function() {
return app.client.pause(2000);
})
.then(function () {
return app.webContents.getTitle()
}).then(function (title) {
t.equal(title, 'Zulip', 'html title')
})
}
// Returns a promise that resolves after 'ms' milliseconds. Default: 1 second
function wait (ms) {
if (ms === undefined) ms = 1000 // Default: wait long enough for the UI to update
return new Promise(function (resolve, reject) {
setTimeout(resolve, ms)
})
}
// Quit the app, end the test, either in success (!err) or failure (err)
function endTest (app, t, err) {
return app.stop().then(function () {
t.end(err)
})
}
function getAppDataDir () {
let base
if (process.platform === 'darwin') {
base = path.join(process.env.HOME, 'Library', 'Application Support')
} else if (process.platform === 'linux') {
base = process.env.XDG_CONFIG_HOME ?
process.env.XDG_CONFIG_HOME : path.join(process.env.HOME, '.config')
} else if (process.platform === 'win32') {
base = process.env.APPDATA
} else {
console.log('Could not detect app data dir base. Exiting...')
process.exit(1)
}
console.log('Detected App Data Dir base:', base)
return path.join(base, config.TEST_APP_PRODUCT_NAME)
}
// Resets the test directory, containing domain.json, window-state.json, etc
function resetTestDataDir () {
appDataDir = getAppDataDir()
rimraf.sync(appDataDir)
rimraf.sync(path.join(__dirname, 'package.json'))
}

View File

@@ -1,19 +0,0 @@
const test = require('tape')
const setup = require('./setup')
test('add-organization', function (t) {
t.timeoutAfter(50e3)
setup.resetTestDataDir()
const app = setup.createApp()
setup.waitForLoad(app, t)
.then(() => app.client.windowByIndex(1)) // focus on webview
.then(() => app.client.setValue('.setting-input-value', 'chat.zulip.org'))
.then(() => app.client.click('.server-save-action'))
.then(() => setup.wait(5000))
.then(() => app.client.windowByIndex(0)) // Switch focus back to main win
.then(() => app.client.windowByIndex(1)) // Switch focus back to org webview
.then(() => app.client.waitForExist('//*[@id="id_username"]'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})

View File

@@ -1,16 +0,0 @@
#!/bin/bash
set -e
set -x
if ! git diff-index --quiet HEAD; then
set +x
echo "There are uncommitted changes:"
git status --short
echo "Doing nothing to avoid losing your work."
exit 1
fi
request_id="$1"
remote=${2:-"upstream"}
git fetch "$remote" "pull/$request_id/head"
git checkout -B "review-original-${request_id}"
git reset --hard FETCH_HEAD

View File

@@ -1,23 +0,0 @@
@echo off
git diff-index --quiet HEAD
if %ERRORLEVEL% NEQ 0 (
echo "There are uncommitted changes:"
git status --short
echo "Doing nothing to avoid losing your work."
exit /B 1
)
if "%~1"=="" (
echo "Error you must specify the PR number"
)
if "%~2"=="" (
set remote="upstream"
) else (
set remote=%2
)
set request_id="%1"
git fetch "%remote%" "pull/%request_id%/head"
git checkout -B "review-%request_id%"
git reset --hard FETCH_HEAD

View File

@@ -1,17 +0,0 @@
#!/bin/bash
set -e
set -x
if ! git diff-index --quiet HEAD; then
set +x
echo "There are uncommitted changes:"
git status --short
echo "Doing nothing to avoid losing your work."
exit 1
fi
request_id="$1"
remote=${2:-"upstream"}
git fetch "$remote" "pull/$request_id/head"
git checkout -B "review-${request_id}" $remote/master
git reset --hard FETCH_HEAD
git pull --rebase

View File

@@ -1,24 +0,0 @@
@echo off
git diff-index --quiet HEAD
if %errorlevel% neq 0 (
echo "There are uncommitted changes:"
git status --short
echo "Doing nothing to avoid losing your work."
exit \B 1
)
if "%~1"=="" (
echo "Error you must specify the PR number"
)
if "%~2"=="" (
set remote="upstream"
) else (
set remote=%2
)
set request_id="%1"
git fetch "%remote%" "pull/%request_id%/head"
git checkout -B "review-%request_id%" %remote%/master
git reset --hard FETCH_HEAD
git pull --rebase

View File

@@ -1,10 +0,0 @@
#!/bin/bash
set -e
set -x
echo "Removing node_modules and app/node_modules"
rm -rf node_modules
rm -rf app/node_modules
echo "node_modules removed reinstalling npm packages"
npm i

View File

@@ -1,8 +0,0 @@
@echo off
echo "Removing node_modules and app/node_modules"
rmdir /s /q node_modules
rmdir /s /q app/node_modules
echo "node_modules removed reinstalling npm packages"
npm i

View File

@@ -5,11 +5,7 @@
* Electron is more or less Chrome, you can get developer tools using `CMD+ALT+I` * Electron is more or less Chrome, you can get developer tools using `CMD+ALT+I`
### Error : ChecksumMismatchError ### Error : ChecksumMismatchError
- Try deleteing `node_modules` && `app/node_modules` directories. Re-install dependencies using `npm install` - Try deleteing `node_modules` && `app/node_modules` directories. Re-install dependencies using `npm install`.
### Error : Module version mismatch. Expected 50, got 51 ### Error : Module version mismatch. Expected 50, got 51
- Make sure you have installed [node-gyp](https://github.com/nodejs/node-gyp#installation) dependencies properly - Make sure you have installed [node-gyp](https://github.com/nodejs/node-gyp#installation) dependencies properly.
### Error: Desktop Notifications not working
- Make sure the **Show Desktop Notifications** setting option is set to be true
- Check your OS notifications center settings