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
42 changed files with 292 additions and 692 deletions

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

View File

@@ -8,7 +8,7 @@ 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

View File

@@ -7,8 +7,9 @@ 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
if (isDev || process.platform === 'linux') {
return; return;
} }

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,12 +1,10 @@
'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;
@@ -14,10 +12,7 @@ const { app, ipcMain } = electron;
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js'); const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.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;
@@ -97,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');
@@ -124,12 +122,33 @@ function createMainWindow() {
return win; return win;
} }
function registerLocalShortcuts(page) {
// 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) => {
event.preventDefault(); event.preventDefault();
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();
@@ -144,6 +163,8 @@ app.on('ready', () => {
const page = mainWindow.webContents; const page = mainWindow.webContents;
registerLocalShortcuts(page);
page.on('dom-ready', () => { page.on('dom-ready', () => {
mainWindow.show(); mainWindow.show();
}); });
@@ -151,7 +172,6 @@ app.on('ready', () => {
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();
}); });
electron.powerMonitor.on('resume', () => { electron.powerMonitor.on('resume', () => {
@@ -209,15 +229,30 @@ app.on('ready', () => {
}); });
ipcMain.on('register-server-tab-shortcut', (event, index) => { ipcMain.on('register-server-tab-shortcut', (event, index) => {
electronLocalshortcut.register(mainWindow, `CommandOrControl+${index}`, () => {
// Array index == Shown index - 1 // Array index == Shown index - 1
page.send('switch-server-tab', 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

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

View File

@@ -1,7 +1,7 @@
{ {
"name": "zulip", "name": "zulip",
"productName": "Zulip", "productName": "Zulip",
"version": "1.7.0", "version": "1.4.0",
"description": "Zulip Desktop App", "description": "Zulip Desktop App",
"license": "Apache-2.0", "license": "Apache-2.0",
"email": "<svnitakash@gmail.com>", "email": "<svnitakash@gmail.com>",
@@ -27,10 +27,12 @@
"InstantMessaging" "InstantMessaging"
], ],
"dependencies": { "dependencies": {
"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.16.2", "electron-updater": "2.8.9",
"node-json-db": "0.7.3", "node-json-db": "0.7.3",
"request": "2.81.0", "request": "2.81.0",
"wurl": "2.5.0", "wurl": "2.5.0",

View File

@@ -18,7 +18,7 @@ body {
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;
@@ -27,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 {
@@ -273,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;
@@ -327,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,33 +12,33 @@ body {
} }
kbd { kbd {
padding: 0.3em 0.8em; padding: 0.1em 0.6em;
border: 1px solid #ccc; border: 1px solid #ccc;
font-size: 15px; font-size: 12px;
font-family: Courier New, Courier, monospace; font-family: Arial,Helvetica,sans-serif;
background-color: #383430; background-color: #f7f7f7;
color: #ededed; 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; display: inline-block;
margin: 0 0.1em; margin: 0 0.1em;
font-weight: bold; text-shadow: 0 1px 0 #fff;
line-height: 1.4;
white-space: nowrap; white-space: nowrap;
} }
table, th, td { table, th, td {
border: 1px solid #ddd;
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) { background-color: #f7eee6; }
table tr:nth-child(odd) { background-color: #fff8ef; }
table tr:nth-child(even) { background-color: #f2f2f2; }
td { padding: 5px; } td { padding: 5px; }
@@ -56,11 +56,6 @@ td:nth-child(odd) {
url(../fonts/MaterialIcons-Regular.ttf) format('truetype'); url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
} }
@font-face {
font-family: 'Montserrat';
src: url(../fonts/Montserrat-Regular.ttf) format('truetype');
}
.material-icons { .material-icons {
font-family: 'Material Icons'; font-family: 'Material Icons';
font-weight: normal; font-weight: normal;
@@ -83,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 {
@@ -121,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 {
@@ -140,8 +133,8 @@ td:nth-child(odd) {
.title { .title {
padding: 4px 0 6px 0; padding: 4px 0 6px 0;
font-weight: 500; font-weight: bold;
color: #222c31; color: #1e1e1e;
} }
.sub-title { .sub-title {
@@ -217,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 {
@@ -248,8 +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;
border-left: 8px solid #bcbcbc; width: 540px;
box-shadow: 1px 2px 4px #bcbcbc;
} }
.hidden { .hidden {
@@ -258,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 {
@@ -281,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 {
@@ -299,90 +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;
margin-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;
transition: background 0.4s;
}
input.toggle-round + label:after {
width: 25px;
height: 25px;
background-color: #fff;
border-radius: 100%;
transition: margin 0.4s;
}
input.toggle-round:checked + label:before {
background-color: #4EBFAC;
}
input.toggle-round:checked + label:after {
margin-left: 25px;
}

View File

@@ -8,7 +8,6 @@ const {ipcRenderer} = require('electron');
class ServerTab extends Tab { class ServerTab extends Tab {
template() { template() {
return `<div class="tab"> 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}'/>

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

@@ -6,7 +6,7 @@ const fs = require('fs');
const DomainUtil = require(__dirname + '/../utils/domain-util.js'); const DomainUtil = require(__dirname + '/../utils/domain-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');
@@ -60,16 +60,6 @@ class WebView extends BaseComponent {
this.props.onTitleChange(); this.props.onTitleChange();
}); });
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', () => {
if (this.props.role === 'server') { if (this.props.role === 'server') {
this.$el.classList.add('onload'); this.$el.classList.add('onload');

View File

@@ -21,11 +21,8 @@ class ServerManagerView {
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.$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.$sidebar = document.getElementById('sidebar'); this.$sidebar = document.getElementById('sidebar');
this.$fullscreenPopup = document.getElementById('fullscreen-popup'); this.$fullscreenPopup = document.getElementById('fullscreen-popup');
@@ -78,11 +75,12 @@ 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'));
} else { } else {
this.openSettings('Servers'); this.openSettings('Servers');
} }
ipcRenderer.send('local-shortcuts', true);
} }
initServer(server, index) { initServer(server, index) {
@@ -90,10 +88,8 @@ class ServerManagerView {
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,
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,
@@ -121,7 +117,6 @@ class ServerManagerView {
this.openSettings('General'); this.openSettings('General');
}); });
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);
} }
@@ -135,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]);
@@ -202,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;
} }
@@ -256,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++) {
@@ -289,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');
} }
} }
@@ -324,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');

View File

@@ -15,16 +15,10 @@ const NativeNotification = window.Notification;
class baseNotification extends NativeNotification { class baseNotification extends NativeNotification {
constructor(title, opts) { constructor(title, opts) {
opts.silent = ConfigUtil.getConfigItem('silent') || false; opts.silent = ConfigUtil.getConfigItem('silent') || false;
super(title, opts); // 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
static requestPermission() { ConfigUtil.getConfigItem('showNotification') ? super(title, opts) : super(); // eslint-disable-line no-unused-expressions
return; // eslint-disable-line no-useless-return
}
// Override default Notification permission
static get permission() {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
} }
} }
window.Notification = baseNotification; window.Notification = baseNotification;

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

@@ -32,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">
@@ -54,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">
@@ -61,17 +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="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>
@@ -82,19 +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();
// Platform specific settings
// Flashing taskbar on Windows
if (process.platform === 'win32') {
this.updateFlashTaskbar();
}
} }
updateTrayOption() { updateTrayOption() {
@@ -123,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'),
@@ -147,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'),
@@ -197,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());

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

@@ -14,12 +14,12 @@ class NewServerForm extends BaseComponent {
<div class="settings-card"> <div class="settings-card">
<div class="server-info-right"> <div class="server-info-right">
<div class="server-info-row"> <div class="server-info-row">
<input class="setting-input-value" autofocus placeholder="example.zulipchat.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>

View File

@@ -3,8 +3,6 @@
const BaseComponent = require(__dirname + '/js/components/base.js'); const BaseComponent = require(__dirname + '/js/components/base.js');
const {ipcRenderer} = require('electron'); const {ipcRenderer} = require('electron');
const ConfigUtil = require(__dirname + '/js/utils/config-util.js');
const Nav = require(__dirname + '/js/pages/preference/nav.js'); const Nav = require(__dirname + '/js/pages/preference/nav.js');
const ServersSection = require(__dirname + '/js/pages/preference/servers-section.js'); const ServersSection = require(__dirname + '/js/pages/preference/servers-section.js');
const GeneralSection = require(__dirname + '/js/pages/preference/general-section.js'); const GeneralSection = require(__dirname + '/js/pages/preference/general-section.js');
@@ -27,7 +25,6 @@ class PreferenceView extends BaseComponent {
this.setDefaultView(); this.setDefaultView();
this.registerIpcs(); this.registerIpcs();
this.setDefaultSettings();
} }
setDefaultView() { setDefaultView() {
@@ -39,30 +36,6 @@ class PreferenceView extends BaseComponent {
this.handleNavigation(nav); this.handleNavigation(nav);
} }
// 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
setDefaultSettings() {
// Default settings which should be respected
const settingOptions = {
trayIcon: true,
useProxy: false,
showSidebar: true,
badgeOption: true,
startAtLogin: false,
enableSpellchecker: true,
showNotification: true,
betaUpdate: false,
silent: false
};
for (const i in settingOptions) {
if (ConfigUtil.getConfigItem(i) === null) {
ConfigUtil.setConfigItem(i, settingOptions[i]);
}
}
}
handleNavigation(navItem) { handleNavigation(navItem) {
this.nav.select(navItem); this.nav.select(navItem);
switch (navItem) { switch (navItem) {

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="title">Enter URL of your Zulip organization</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>
`; `;
} }
@@ -43,9 +41,6 @@ class ServersSection extends BaseSection {
this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Existing Servers'; 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

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

@@ -110,14 +110,8 @@ 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 the domain contains following strings we just bypass the server
const whitelistDomains = [
'zulipdev.org'
]; ];
if (!error && response.statusCode !== 404) { if (!error && response.statusCode !== 404) {
// Correct // Correct
@@ -126,7 +120,7 @@ class DomainUtil {
}, () => { }, () => {
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);
@@ -158,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`;
reject(invalidZulipServerError);
} }
}); });
}); });
@@ -174,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
}); });

View File

@@ -13,14 +13,13 @@
<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">

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

View File

@@ -1,7 +1,7 @@
{ {
"name": "zulip", "name": "zulip",
"productName": "Zulip", "productName": "Zulip",
"version": "1.7.0", "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",
@@ -20,10 +20,8 @@
}, },
"scripts": { "scripts": {
"start": "electron app --disable-http-cache", "start": "electron app --disable-http-cache",
"reinstall": "rm -rf node_modules; rm -rf app/node_modules; npm install",
"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",
"pack": "electron-builder --dir", "pack": "electron-builder --dir",
"dist": "electron-builder", "dist": "electron-builder",
@@ -106,20 +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-builder": "19.46.4", "electron-builder": "19.27.3",
"electron": "1.6.15", "electron": "1.6.11",
"electron-connect": "0.6.2", "electron-connect": "0.6.2",
"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",
"pre-commit": "1.2.2", "chai": "4.1.1",
"spectron": "3.7.2", "spectron": "3.7.2",
"tap-colorize": "^1.2.0",
"tape": "^4.8.0",
"xo": "0.18.2", "xo": "0.18.2",
"electron-debug": "1.4.0" "pre-commit": "1.2.2"
}, },
"xo": { "xo": {
"parserOptions": { "parserOptions": {
@@ -159,8 +154,5 @@
"browser", "browser",
"mocha" "mocha"
] ]
},
"dependencies": {
"resemblejs": "^2.2.6"
} }
} }

View File

@@ -5,13 +5,6 @@ if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
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[1]/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'))
})