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

7
.gitignore vendored
View File

@@ -23,9 +23,4 @@ yarn-error.log*
# miscellaneous
.idea
config.gypi
# Test generated files
tests/package.json
.python-version
config.gypi

1
.python-version Normal file
View File

@@ -0,0 +1 @@
2.7.9

View File

@@ -18,9 +18,8 @@ node_js:
- '6'
before_install:
- ./scripts/travis-xvfb.sh
- npm install -g gulp
- npm install
- npm install -g gulp
- npm install
cache:
directories:
@@ -34,4 +33,4 @@ notifications:
urls:
- https://zulip.org/zulipbot/travis
on_success: always
on_failure: always
on_failure: always

View File

@@ -8,7 +8,7 @@ Desktop client for Zulip. Available for Mac, Linux and Windows.
<img src="http://i.imgur.com/ChzTq4F.png"/>
# Download
Please see [installation guide](https://zulipchat.com/help/desktop-app-install-guide).
Please see [installation guide](./how-to-install.md).
# Features
* 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');
function appUpdater() {
// Don't initiate auto-updates in development
if (isDev) {
// Don't initiate auto-updates in development and on Linux system
// since autoUpdater doesn't work on Linux
if (isDev || process.platform === 'linux') {
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';
const path = require('path');
const electron = require('electron');
const electronLocalshortcut = require('electron-localshortcut');
const windowStateKeeper = require('electron-window-state');
const isDev = require('electron-is-dev');
const appMenu = require('./menu');
const { appUpdater } = require('./autoupdater');
const { crashHandler } = require('./crash-reporter');
const { setAutoLaunch } = require('./startup');
const { app, ipcMain } = electron;
@@ -14,10 +12,7 @@ const { app, ipcMain } = electron;
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js');
// 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
let mainWindow;
@@ -97,6 +92,9 @@ function createMainWindow() {
win.hide();
}
}
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
});
win.setTitle('Zulip');
@@ -124,12 +122,33 @@ function createMainWindow() {
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
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
event.preventDefault();
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', () => {
if (!mainWindow) {
mainWindow = createMainWindow();
@@ -144,6 +163,8 @@ app.on('ready', () => {
const page = mainWindow.webContents;
registerLocalShortcuts(page);
page.on('dom-ready', () => {
mainWindow.show();
});
@@ -151,7 +172,6 @@ app.on('ready', () => {
page.once('did-frame-finish-load', () => {
// Initate auto-updates on MacOS and Windows
appUpdater();
crashHandler();
});
electron.powerMonitor.on('resume', () => {
@@ -209,8 +229,18 @@ app.on('ready', () => {
});
ipcMain.on('register-server-tab-shortcut', (event, index) => {
// Array index == Shown index - 1
page.send('switch-server-tab', index - 1);
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) => {
@@ -218,6 +248,11 @@ app.on('ready', () => {
});
});
app.on('will-quit', () => {
// Unregister all the shortcuts so that they don't interfare with other apps
electronLocalshortcut.unregisterAll(mainWindow);
});
app.on('before-quit', () => {
isQuitting = true;
});

View File

@@ -37,7 +37,7 @@ class AppMenu {
accelerator: 'CommandOrControl+R',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('reload-current-viewer');
AppMenu.sendAction('reload-viewer');
}
}
}, {
@@ -173,7 +173,7 @@ class AppMenu {
return [{
label: `${app.getName()}`,
submenu: [{
label: 'About Zulip',
label: 'Zulip Desktop',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('open-about');
@@ -273,7 +273,7 @@ class AppMenu {
return [{
label: 'File',
submenu: [{
label: 'About Zulip',
label: 'Zulip Desktop',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('open-about');
@@ -370,20 +370,10 @@ class AppMenu {
}
static resetAppSettings() {
// We save App's settings/configurations in following files
const settingFiles = ['window-state.json', 'domain.json', 'settings.json'];
const getAppPath = path.join(app.getPath('appData'), appName, 'window-state.json');
settingFiles.forEach(settingFileName => {
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
fs.access(getSettingFilesPath, error => {
if (error) {
console.log(error);
} else {
fs.unlink(getSettingFilesPath, () => {
AppMenu.sendAction('clear-app-data');
});
}
});
fs.unlink(getAppPath, () => {
setTimeout(() => AppMenu.sendAction('clear-app-data'), 1000);
});
}

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "1.7.0",
"version": "1.4.0",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"email": "<svnitakash@gmail.com>",
@@ -27,10 +27,12 @@
"InstantMessaging"
],
"dependencies": {
"electron-debug": "1.4.0",
"electron-is-dev": "0.3.0",
"electron-localshortcut": "2.0.2",
"electron-log": "2.2.7",
"electron-spellchecker": "1.1.2",
"electron-updater": "2.16.2",
"electron-spellchecker": "1.2.0",
"electron-updater": "2.8.9",
"node-json-db": "0.7.3",
"request": "2.81.0",
"wurl": "2.5.0",

View File

@@ -18,7 +18,7 @@ body {
background-position: center;
}
.toggle-sidebar {
#sidebar {
background: #222c31;
width: 54px;
padding: 27px 0 20px 0;
@@ -27,21 +27,6 @@ body {
flex-direction: column;
-webkit-app-region: drag;
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 {
@@ -53,8 +38,8 @@ body {
/*******************
* Left Sidebar *
*******************/
* Left Sidebar *
*******************/
#tabs-container {
display: flex;
@@ -212,8 +197,8 @@ body {
/*******************
* Webview Area *
*******************/
* Webview Area *
*******************/
#webviews-container {
display: flex;
@@ -273,33 +258,6 @@ webview:focus {
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 {
bottom: 30px;
left: 20px;
@@ -327,9 +285,7 @@ webview:focus {
display: none !important;
}
/* Full screen Popup container */
.popup .popuptext {
visibility: hidden;
background-color: #555;
@@ -362,4 +318,4 @@ webview:focus {
overflow: hidden;
opacity: 1;
}
}
}

View File

@@ -12,33 +12,33 @@ body {
}
kbd {
padding: 0.3em 0.8em;
padding: 0.1em 0.6em;
border: 1px solid #ccc;
font-size: 15px;
font-family: Courier New, Courier, monospace;
background-color: #383430;
color: #ededed;
font-size: 12px;
font-family: Arial,Helvetica,sans-serif;
background-color: #f7f7f7;
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;
font-weight: bold;
text-shadow: 0 1px 0 #fff;
line-height: 1.4;
white-space: nowrap;
}
table, th, td {
border: 1px solid #ddd;
border-collapse: collapse;
color: #383430;
}
table {
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 { width: 85%; }
table tr:nth-child(even) { background-color: #f2f2f2; }
td { padding: 5px; }
@@ -56,11 +56,6 @@ td:nth-child(odd) {
url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
}
@font-face {
font-family: 'Montserrat';
src: url(../fonts/Montserrat-Regular.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
@@ -83,7 +78,7 @@ td:nth-child(odd) {
#content {
display: flex;
height: 100%;
font-family: 'Montserrat';
font-family: sans-serif;
}
#sidebar {
@@ -121,9 +116,7 @@ td:nth-child(odd) {
#settings-header {
font-size: 22px;
color: #222c31;
font-weight: bold;
text-transform: uppercase;
color: #5c6166;
}
#settings-container {
@@ -140,8 +133,8 @@ td:nth-child(odd) {
.title {
padding: 4px 0 6px 0;
font-weight: 500;
color: #222c31;
font-weight: bold;
color: #1e1e1e;
}
.sub-title {
@@ -217,13 +210,14 @@ img.server-info-icon {
display: flex;
align-items: center;
padding: 0 10px;
border-radius: 2px;
margin-right: 10px;
}
.action i {
margin-right: 5px;
font-size: 18px;
line-height: 26px;
line-height: 27px;
}
.settings-pane {
@@ -248,8 +242,9 @@ img.server-info-icon {
padding: 12px 30px;
margin: 10px 0 20px 0;
background: #fff;
width: 70%;
border-left: 8px solid #bcbcbc;
border-radius: 2px;
width: 540px;
box-shadow: 1px 2px 4px #bcbcbc;
}
.hidden {
@@ -258,19 +253,15 @@ img.server-info-icon {
}
.red {
color: #ffffff;
background: #ef5350;
padding: 3px;
padding-right: 10px;
padding-left: 10px;
color: #ef5350;
background: #ffebee;
border: 1px solid #ef5350;
}
.blue {
color: #ffffff;
background: #4EBFAC;
padding: 3px;
padding-right: 10px;
padding-left: 10px;
.green {
color: #388E3C;
background: #E8F5E9;
border: 1px solid #388E3C;
}
.grey {
@@ -281,10 +272,9 @@ img.server-info-icon {
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin: 6px;
margin: 6px 0;
}
.code {
@@ -299,90 +289,26 @@ i.open-tab-button {
.reset-data-button {
display: inline-block;
border: none;
padding: 10px;
width: 120px;
cursor: pointer;
font-size: 13px;
font-size: 11px;
transition: background-color 0.2s ease;
text-decoration: none;
}
.reset-data-button:hover {
background-color: #3c9f8d;
background-color: #32a692;
color: #fff;
}
#server-info-container {
min-height: calc(100% - 235px);
}
#create-organization-container {
font-size: 1.15em;
margin-bottom: 15px;
}
#create-organization-container i {
position: relative;
top: 3px;
}
#open-create-org-link {
color: #666;
#open-shortcuts-url {
color: #08c;
cursor: pointer;
text-decoration: none;
}
#open-create-org-link:hover {
#open-shortcuts-url:hover {
color: #005580;;
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 {
template() {
return `<div class="tab">
<div class="server-tooltip" style="display:none"></div>
<div class="server-tab-badge"></div>
<div class="server-tab">
<img class="server-icons" src='${this.props.icon}'/>

View File

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

View File

@@ -6,7 +6,7 @@ const fs = require('fs');
const DomainUtil = require(__dirname + '/../utils/domain-util.js');
const SystemUtil = require(__dirname + '/../utils/system-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');
@@ -42,7 +42,7 @@ class WebView extends BaseComponent {
registerListeners() {
this.$el.addEventListener('new-window', event => {
const { url } = event;
const {url} = event;
const domainPrefix = DomainUtil.getDomain(this.props.index).url;
if (LinkUtil.isInternal(domainPrefix, url) || url === (domainPrefix + '/')) {
@@ -55,21 +55,11 @@ class WebView extends BaseComponent {
});
this.$el.addEventListener('page-title-updated', event => {
const { title } = event;
const {title} = event;
this.badgeCount = this.getBadgeCount(title);
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', () => {
if (this.props.role === 'server') {
this.$el.classList.add('onload');
@@ -78,7 +68,7 @@ class WebView extends BaseComponent {
});
this.$el.addEventListener('did-fail-load', event => {
const { errorDescription } = event;
const {errorDescription} = event;
const hasConnectivityErr = (SystemUtil.connectivityERR.indexOf(errorDescription) >= 0);
if (hasConnectivityErr) {
console.error('error', errorDescription);

View File

@@ -21,11 +21,8 @@ class ServerManagerView {
this.$settingsButton = $actionsContainer.querySelector('#settings-action');
this.$webviewsContainer = document.getElementById('webviews-container');
this.$addServerTooltip = document.getElementById('add-server-tooltip');
this.$reloadTooltip = $actionsContainer.querySelector('#reload-tooltip');
this.$settingsTooltip = $actionsContainer.querySelector('#setting-tooltip');
this.$serverIconTooltip = document.getElementsByClassName('server-tooltip');
this.$sidebar = document.getElementById('sidebar');
this.$fullscreenPopup = document.getElementById('fullscreen-popup');
@@ -78,11 +75,12 @@ class ServerManagerView {
DomainUtil.updateSavedServer(servers[i].url, i);
this.activateTab(i);
}
// Open last active tab
this.activateTab(ConfigUtil.getConfigItem('lastActiveTab'));
this.activateTab(0);
} else {
this.openSettings('Servers');
}
ipcRenderer.send('local-shortcuts', true);
}
initServer(server, index) {
@@ -90,10 +88,8 @@ class ServerManagerView {
role: 'server',
icon: server.icon,
$root: this.$tabsContainer,
onClick: this.activateLastTab.bind(this, index),
onClick: this.activateTab.bind(this, index),
index,
onHover: this.onHover.bind(this, index, server.alias),
onHoverOut: this.onHoverOut.bind(this, index),
webview: new WebView({
$root: this.$webviewsContainer,
index,
@@ -121,7 +117,6 @@ class ServerManagerView {
this.openSettings('General');
});
this.sidebarHoverEvent(this.$addServerButton, this.$addServerTooltip);
this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip);
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) {
if (this.functionalTabs[tabProps.name] !== undefined) {
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) {
if (!this.tabs[index]) {
if (this.tabs[index].webview.loading) {
return;
}
@@ -256,24 +235,16 @@ class ServerManagerView {
// Clear DOM elements
this.$tabsContainer.innerHTML = '';
this.$webviewsContainer.innerHTML = '';
// Destroy shortcuts
ipcRenderer.send('local-shortcuts', false);
}
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.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() {
let messageCountAll = 0;
for (let i = 0; i < this.tabs.length; i++) {
@@ -289,9 +260,9 @@ class ServerManagerView {
toggleSidebar(show) {
if (show) {
this.$sidebar.classList.remove('sidebar-hide');
this.$sidebar.classList.remove('hidden');
} 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('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index));
ipcRenderer.on('reload-current-viewer', this.reloadCurrentView.bind(this));
ipcRenderer.on('reload-viewer', this.reloadView.bind(this));
ipcRenderer.on('hard-reload', () => {
ipcRenderer.send('reload-full-app');

View File

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

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

View File

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

@@ -25,17 +25,13 @@ class GeneralSection extends BaseSection {
<div class="setting-control"></div>
</div>
<div class="setting-row" id="sidebar-option">
<div class="setting-description">Show sidebar (<span class="code">Cmd Or Ctrl+S</span>)</div>
<div class="setting-description">Show sidebar (<span class="code">CmdOrCtrl+S</span>)</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="badge-option">
<div class="setting-description">Show app unread badge</div>
<div class="setting-control"></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 class="setting-description">Show app unread badge</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">Desktop Notification</div>
<div class="settings-card">
@@ -54,6 +50,10 @@ class GeneralSection extends BaseSection {
<div class="setting-description">Get beta updates</div>
<div class="setting-control"></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 class="title">Functionality</div>
<div class="settings-card">
@@ -61,17 +61,13 @@ class GeneralSection extends BaseSection {
<div class="setting-description">Start app at login</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 class="title">Reset Application Data</div>
<div class="settings-card">
<div class="setting-row" id="resetdata-option">
<div class="setting-description">This will delete all application data including all added accounts and preferences
</div>
<button class="reset-data-button blue">Reset App Data</button>
<button class="reset-data-button green">Reset App Data</button>
</div>
</div>
</div>
@@ -82,19 +78,13 @@ class GeneralSection extends BaseSection {
this.props.$root.innerHTML = this.template();
this.updateTrayOption();
this.updateBadgeOption();
this.updateSilentOption();
this.updateUpdateOption();
this.updateAutoUpdateOption();
this.updateSilentOption();
this.updateSidebarOption();
this.updateStartAtLoginOption();
this.updateResetDataOption();
this.showDesktopNotification();
this.enableSpellchecker();
// Platform specific settings
// Flashing taskbar on Windows
if (process.platform === 'win32') {
this.updateFlashTaskbar();
}
}
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() {
this.generateSettingOption({
$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() {
this.generateSettingOption({
$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() {
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());

View File

@@ -34,7 +34,7 @@ class NetworkSection extends BaseSection {
<input class="setting-input-value" placeholder="e.g. foobar.com"/>
</div>
<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>
<span>Save</span>
</div>

View File

@@ -14,12 +14,12 @@ class NewServerForm extends BaseComponent {
<div class="settings-card">
<div class="server-info-right">
<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 class="server-info-row">
<div class="action blue server-save-action">
<i class="material-icons">add_box</i>
<span>Add</span>
<div class="action green server-save-action">
<i class="material-icons">check_box</i>
<span>Save</span>
</div>
</div>
</div>

View File

@@ -1,9 +1,7 @@
'use strict';
const BaseComponent = require(__dirname + '/js/components/base.js');
const { ipcRenderer } = require('electron');
const ConfigUtil = require(__dirname + '/js/utils/config-util.js');
const {ipcRenderer} = require('electron');
const Nav = require(__dirname + '/js/pages/preference/nav.js');
const ServersSection = require(__dirname + '/js/pages/preference/servers-section.js');
@@ -27,7 +25,6 @@ class PreferenceView extends BaseComponent {
this.setDefaultView();
this.registerIpcs();
this.setDefaultSettings();
}
setDefaultView() {
@@ -39,30 +36,6 @@ class PreferenceView extends BaseComponent {
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) {
this.nav.select(navItem);
switch (navItem) {

View File

@@ -20,7 +20,7 @@ class ServerInfoForm extends BaseComponent {
<div class="server-info-right">
<div class="server-info-row">
<span class="server-info-alias">${this.props.server.alias}</span>
<i class="material-icons open-tab-button">open_in_new</i>
<i class="material-icons open-tab-button">open_in_new</i>
</div>
<div class="server-info-row">
<input class="setting-input-value" disabled value="${this.props.server.url}"/>

View File

@@ -4,7 +4,6 @@ const BaseSection = require(__dirname + '/base-section.js');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
const ServerInfoForm = require(__dirname + '/server-info-form.js');
const NewServerForm = require(__dirname + '/new-server-form.js');
const CreateOrganziation = require(__dirname + '/create-new-org.js');
class ServersSection extends BaseSection {
constructor(props) {
@@ -15,11 +14,10 @@ class ServersSection extends BaseSection {
template() {
return `
<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 class="title" id="existing-servers"></div>
<div id="server-info-container"></div>
<div id="create-organization-container"></div>
</div>
`;
}
@@ -43,9 +41,6 @@ class ServersSection extends BaseSection {
this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Existing Servers';
this.initNewServerForm();
this.$createOrganizationContainer = document.getElementById('create-organization-container');
this.initCreateNewOrganization();
for (let i = 0; i < servers.length; i++) {
new ServerInfoForm({
$root: this.$serverInfoContainer,
@@ -56,12 +51,6 @@ class ServersSection extends BaseSection {
}
}
initCreateNewOrganization() {
new CreateOrganziation({
$root: this.$createOrganizationContainer
}).init();
}
initNewServerForm() {
new NewServerForm({
$root: this.$newServerContainer,

View File

@@ -1,10 +1,7 @@
'use strict';
const { ipcRenderer } = require('electron');
const SetupSpellChecker = require('./spellchecker');
const ConfigUtil = require(__dirname + '/utils/config-util.js');
const { spellChecker } = require('./spellchecker');
// eslint-disable-next-line import/no-unassigned-import
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
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
SetupSpellChecker.init();
}
// Init spellchecker
spellChecker();
// redirect users to network troubleshooting page
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

@@ -1,56 +1,29 @@
'use strict';
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 {
init() {
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
this.enableSpellChecker();
}
this.enableContextMenu();
}
// Start off as US English
window.spellCheckHandler.switchLanguage('en-US');
enableSpellChecker() {
try {
this.SpellCheckHandler = new SpellCheckHandler();
} catch (err) {
console.log(err);
}
}
const contextMenuBuilder = new ContextMenuBuilder(window.spellCheckHandler);
const contextMenuListener = new ContextMenuListener(info => {
contextMenuBuilder.showPopupMenu(info);
});
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);
});
}
unsubscribeSpellChecker() {
// eslint-disable-next-line no-undef
if (this.SpellCheckHandler) {
this.SpellCheckHandler.unsubscribe();
}
if (this.contextMenuListener) {
this.contextMenuListener.unsubscribe();
}
}
// 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();
});
}
module.exports = new SetupSpellChecker();
module.exports = {
spellChecker
};

View File

@@ -110,15 +110,9 @@ class DomainUtil {
return new Promise((resolve, reject) => {
request(checkDomain, (error, response) => {
const certsError =
[
'Error: self signed certificate',
'Error: unable to verify the first certificate',
'Error: unable to get local issuer certificate'
['Error: self signed certificate',
'Error: unable to verify the first certificate'
];
// If the domain contains following strings we just bypass the server
const whitelistDomains = [
'zulipdev.org'
];
if (!error && response.statusCode !== 404) {
// Correct
this.getServerSettings(domain).then(serverSettings => {
@@ -126,7 +120,7 @@ class DomainUtil {
}, () => {
resolve(serverConf);
});
} else if (domain.indexOf(whitelistDomains) >= 0 || certsError.indexOf(error.toString()) >= 0) {
} else if (certsError.indexOf(error.toString()) >= 0) {
if (silent) {
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
@@ -158,9 +152,7 @@ class DomainUtil {
});
}
} else {
const invalidZulipServerError = `${domain} does not appear to be a valid Zulip server. Make sure that \
\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);
reject('Not a valid Zulip server');
}
});
});
@@ -174,9 +166,7 @@ class DomainUtil {
const data = JSON.parse(response.body);
if (data.hasOwnProperty('realm_icon') && data.realm_icon) {
resolve({
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
icon: data.realm_uri + data.realm_icon,
url: data.realm_uri,
alias: data.realm_name
});

View File

@@ -13,14 +13,13 @@
<div class="popup">
<span class="popuptext hidden" id="fullscreen-popup"></span>
</div>
<div id="sidebar" class="toggle-sidebar">
<div id="sidebar">
<div id="view-controls-container">
<div id="tabs-container"></div>
<div id="add-tab" class="tab functional-tab">
<div class="server-tab" id="add-action">
<i class="material-icons">add</i>
</div>
<span id="add-server-tooltip" style="display:none">Add organization</span>
</div>
</div>
<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

@@ -17,8 +17,7 @@ install:
- npm install
- npm install -g gulp
build: off
build: off
test_script:
- npm run test
- npm run test-e2e

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "1.7.0",
"version": "1.4.0",
"main": "./app/main",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
@@ -20,10 +20,8 @@
},
"scripts": {
"start": "electron app --disable-http-cache",
"reinstall": "rm -rf node_modules; rm -rf app/node_modules; npm install",
"postinstall": "electron-builder install-app-deps",
"test": "xo",
"test-e2e": "gulp test-e2e",
"dev": "gulp dev",
"pack": "electron-builder --dir",
"dist": "electron-builder",
@@ -106,20 +104,17 @@
],
"devDependencies": {
"assert": "1.4.1",
"cp-file": "^5.0.0",
"devtron": "1.4.0",
"electron-builder": "19.46.4",
"electron": "1.6.15",
"electron-builder": "19.27.3",
"electron": "1.6.11",
"electron-connect": "0.6.2",
"gulp": "3.9.1",
"gulp-tape": "0.0.9",
"is-ci": "^1.0.10",
"pre-commit": "1.2.2",
"gulp-mocha": "4.3.1",
"chai-as-promised": "7.1.1",
"chai": "4.1.1",
"spectron": "3.7.2",
"tap-colorize": "^1.2.0",
"tape": "^4.8.0",
"xo": "0.18.2",
"electron-debug": "1.4.0"
"pre-commit": "1.2.2"
},
"xo": {
"parserOptions": {
@@ -159,8 +154,5 @@
"browser",
"mocha"
]
},
"dependencies": {
"resemblejs": "^2.2.6"
}
}
}

View File

@@ -1,17 +1,10 @@
#!/usr/bin/env bash
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
export {no_proxy,NO_PROXY}="127.0.0.1,localhost"
export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start
sleep 3
echo 'Travis Screen Resolution:'
xdpyinfo | grep dimensions
export {no_proxy,NO_PROXY}="127.0.0.1,localhost"
export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start
sleep 3
fi
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 setup = require('./setup')
const assert = require('assert')
const Application = require('spectron').Application
const chai = require('chai')
const { expect } = chai
const chaiAsPromised = require('chai-as-promised')
chai.should()
chai.use(chaiAsPromised)
describe('application launch', function () {
this.timeout(15000)
beforeEach(function () {
this.app = new Application({
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))
})
})
test('app runs', function (t) {
t.timeoutAfter(10e3)
setup.resetTestDataDir()
const app = setup.createApp()
setup.waitForLoad(app, t)
.then(() => app.client.windowByIndex(1)) // focus on webview
.then(() => app.client.waitForExist('//*[@id="new-server-container"]/div/div/div[1]/input'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})

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