Compare commits

..

86 Commits

Author SHA1 Message Date
akashnimare
7194e788e4 Add a setting option to control auto-updates 2017-09-12 21:43:53 +05:30
Akash Nimare
6dd79b205c Merge pull request #293 from cedricium/shortcut-settings
Adding keyboard shortcuts to Settings page
2017-09-11 20:23:11 +05:30
Cedricium
538c18fa90 Shorten 'Keyboard Shortcuts' to just 'Shortcuts' 2017-09-11 07:39:19 -07:00
Cedricium
29e347c511 List application-specific shortcuts only 2017-09-10 14:07:04 -07:00
Cedricium
ad37a5e0a6 Changed 'Ctrl/Cmd' to appropriate user OS key
If Windows or Linux, variable `userOSKey` will be 'Ctrl'. For Macs, `userOSKey`
will be '⌘' and these values will show up in place of the previous 'Ctrl/Cmd' keys.
2017-09-10 00:32:06 -07:00
Cedricium
352b775e27 Added all keyboard shortcuts
Finished adding all keyboard shortcuts to the Settings page. Styled the tables
such that they are uniform with their columns being the same
width.

At the bottom of the 'Keyboard Shortcuts' settings page, a link to the
complete keyboard shortcuts documentation
(https://chat.zulip.org/help/keyboard-shortcuts) was also added.
2017-09-09 22:39:34 -07:00
Cedricium
38cec25680 Adding keyboard shortcuts in Settings
This is the initial pass at adding keyboard shortcuts to the Settings page. In
this commit, the `ShortcutsSection` class has been created and is applied to a
newly-added 'Keyboard Shortcuts' nav item. The template for ShortcutsSection
is essentially multiple settings cards containing one table of keyboard short-
cuts organized by their underlying functionality.

The HTML `<kbd>` tag was defined in preference.css which styles the element to
look like a keyboard key, similar to StackOverflow or GitHub.
2017-09-09 19:11:19 -07:00
Akash Nimare
f77ab92202 Merge pull request #287 from vbNETonIce/patch-1
Windows set up instructions
2017-09-08 16:51:20 +05:30
Akash Nimare
e843a29316 Update Windows.md 2017-09-08 16:50:59 +05:30
akashnimare
85837242e7 Add back tray icon on windows #289 2017-09-08 05:17:14 +05:30
akashnimare
9f6da5712e Add show/hide desktop notification setting #192 2017-09-08 04:27:15 +05:30
akashnimare
158685a869 code refactor for mouse events 2017-09-07 22:50:20 +05:30
Akash Nimare
288b1cb3f2 Merge pull request #290 from zulip/clear-settings
Added Clear app settings menu item
2017-09-07 22:36:43 +05:30
akashnimare
e24a966d48 Add shortcut for clearing app data 2017-09-07 03:34:30 +05:30
akashnimare
306feb2eff reset window position 2017-09-07 03:29:04 +05:30
akashnimare
f426c932b0 Relaunch app on clearing app data 2017-09-06 16:23:56 +05:30
Akash Nimare
26172d8508 Merge pull request #288 from vbNETonIce/patch-2
tiny change in wording of new reset data option
2017-09-06 16:07:17 +05:30
vbNETonIce
99da0a338f tiny change in wording of new reset data option 2017-09-06 12:36:20 +02:00
akashnimare
9599249b31 Add reset app settings menu item #286 2017-09-06 15:23:22 +05:30
vbNETonIce
4bdd2564b7 Windows set up instructions 2017-09-06 08:57:13 +02:00
Akash Nimare
4dcf22a53c Merge pull request #285 from zulip/reset-data-setting
Add reset app data setting #192
2017-09-06 04:08:37 +05:30
akashnimare
0dc97648a0 code refactor 2017-09-06 03:54:25 +05:30
akashnimare
787f097cf3 style reset data button 2017-09-06 03:42:39 +05:30
akashnimare
5481d55c66 remove clear cache menu items 2017-09-06 03:26:56 +05:30
akashnimare
2a052b2c38 Added reset app data functionality [WIP] 2017-09-06 03:15:23 +05:30
akashnimare
3ed253d2e1 🎉 v1.4.0 2017-09-04 20:12:58 +05:30
akashnimare
5f672fe404 updated dependencies 2017-09-04 20:04:02 +05:30
akashnimare
10372787ac updated dependencies 2017-08-31 01:52:23 +05:30
akashnimare
bcabb615b4 clear app data setting option [WIP] 2017-08-29 04:26:16 +05:30
Akash Nimare
68acf2ec64 Merge pull request #282 from zulip/auto-launch
Added auto launch Zulip on system startup setting option #192
2017-08-29 03:32:59 +05:30
akashnimare
afd24035f4 Don't run start at login script in dev mode 2017-08-29 03:28:07 +05:30
akashnimare
4f28f6b935 Add open at login setting [WIP] 2017-08-29 01:47:19 +05:30
akashnimare
52bd600690 Detailed certificate warning message #126 2017-08-28 23:50:15 +05:30
Akash Nimare
c7ce2a8a99 updated placeholder text
organization >> server
2017-08-28 20:19:36 +05:30
akashnimare
9760b1bf98 Add start app at login template 2017-08-28 17:11:27 +05:30
akashnimare
d579c267f0 code refactor 2017-08-28 16:34:33 +05:30
Akash Nimare
21f01d268a Merge pull request #280 from zulip/badge-setting
Add an option to show/hide badges and taskbar icons #192
2017-08-27 05:13:19 +05:30
akashnimare
33782e0492 disable text-selection in setting page 2017-08-27 05:00:18 +05:30
akashnimare
8d20568b7a Show badge on app load by default 2017-08-27 04:50:44 +05:30
akashnimare
06f38e92ce refactor code 2017-08-27 04:28:03 +05:30
akashnimare
855e96e40e refactoring code for badge/taskbar icon [WIP] 2017-08-27 03:58:08 +05:30
akashnimare
bb68720ab7 add badge-setting constructor [WIP] 2017-08-27 01:50:13 +05:30
akashnimare
044d007a0c Hide/Show badge setting option [WIP] 2017-08-26 05:28:59 +05:30
akashnimare
4ea95fe8e8 Relaunch app when new updated has been downloaded fixes #279 2017-08-26 03:18:06 +05:30
Akash Nimare
d17c685e4d Document windows portable installer fixes #148 2017-08-25 18:26:56 +05:30
Akash Nimare
bb174da59a Merge pull request #278 from geeeeeeeeek/feature/connect-through-proxy
Feature/connect through proxy
2017-08-25 18:15:36 +05:30
Akash Nimare
a028b80adb Add proxy rules examples 2017-08-25 18:08:39 +05:30
akashnimare
3683511e60 updated windows build config 2017-08-24 05:08:14 +05:30
Akash Nimare
bf359db7f4 Updated download instructions 2017-08-24 04:53:43 +05:30
Akash Nimare
949d786f1f installation instructions [WIP] 2017-08-24 04:49:25 +05:30
Zhongyi Tong
5cd0c1ca4e Do not show alert after turning off the proxy. 2017-08-21 11:10:01 -04:00
Zhongyi Tong
86674991c1 Fix incorrect base component. 2017-08-21 11:06:59 -04:00
Zhongyi Tong
8492cda092 Add placeholder for proxy inputs. 2017-08-20 23:04:40 -04:00
Zhongyi Tong
647303c001 Implement proxy. 2017-08-20 23:00:50 -04:00
Zhongyi Tong
fe34f8adad Finish setting section for proxy. 2017-08-20 21:59:31 -04:00
Zhongyi Tong
f142a2eb4e Move help.md outside app folder. 2017-08-20 16:59:48 -04:00
Zhongyi Tong
43eaa3dd63 Initialize user guide. 2017-08-20 16:58:31 -04:00
Zhongyi Tong
a3142713f7 Initialize setting section for proxy. 2017-08-20 16:35:39 -04:00
Zhongyi Tong
876936125a Refactor general setting section. 2017-08-20 16:00:10 -04:00
Zhongyi Tong
7261fb6cef Update settings layout. 2017-08-20 15:21:13 -04:00
akashnimare
fba52e6dff code refactoring 2017-08-20 03:51:31 +05:30
akashnimare
23ac347fb9 Additional app categories [Linux] 2017-08-19 20:52:14 +05:30
akashnimare
082aebb1e0 Add app category [Linux] fixes #277 2017-08-19 20:49:44 +05:30
akashnimare
3b18dc4df0 improve auto-updates logging
From now on, all the logs will be saved within the Zulip application directory.
2017-08-19 02:21:50 +05:30
akashnimare
39d30b92fa Fixing beta updates [WIP] #269 2017-08-18 20:47:33 +05:30
Akash Nimare
733209e04e Merge pull request #274 from geeeeeeeeek/feature/load-all-tabs-on-startup
Load all servers on app start-up.
2017-08-18 15:25:59 +05:30
Zhongyi Tong
a68d697fc5 Fix linting. 2017-08-17 14:16:12 -04:00
Zhongyi Tong
e52ece57df Load all servers on app start-up. 2017-08-17 14:14:23 -04:00
akashnimare
65681c7da9 Add pre-commit test 2017-08-17 16:40:20 +05:30
akashnimare
52b6209905 Allow spacing between curly braces [XO Linter] 2017-08-17 16:27:34 +05:30
akashnimare
0e37925418 Handle crash events 2017-08-17 16:23:18 +05:30
Akash Nimare
1a31d2e431 Merge pull request #273 from geeeeeeeeek/issue/new-server-icon-cache
Issue/new server icon cache
2017-08-16 23:52:58 +05:30
Zhongyi Tong
c2fab2c7bc Fix duplicate checking. 2017-08-16 13:22:59 -04:00
Zhongyi Tong
965b55ba5f Fix regression of avoiding duplicated servers. 2017-08-16 12:48:31 -04:00
akashnimare
2c7bb36f8e Submit new server form when Enter key is pressed 2017-08-16 04:48:41 +05:30
Zhongyi Tong
48b829b771 Lazy update server settings on reload. 2017-08-15 17:45:38 -04:00
Zhongyi Tong
f13c28db73 Generate stored icon path using url hash. 2017-08-15 17:42:40 -04:00
Akash Nimare
19157c77e4 Merge pull request #272 from Anshuman-Verma/es6-index.js
Use ES6 destructuring for grabbing elements
2017-08-15 00:10:51 +05:30
Akash Nimare
161cd80d38 fixed linting errors 2017-08-14 23:07:17 +05:30
Anshuman
65db4c8f39 Use ES6 destructuring for grabbing elements 2017-08-13 23:04:48 +05:30
akashnimare
d600d3a6ed clear autoupdate dialog message 2017-08-12 17:38:50 +05:30
akashnimare
5fdd971966 Check for duplicate domains 2017-08-11 03:26:34 +05:30
Akash Nimare
d77d39d6ad Merge pull request #270 from geeeeeeeeek/issue/refine-add-server
Issue/refine add server
2017-08-10 15:54:45 +05:30
Akash Nimare
bf35db26bd fixed linting errors 2017-08-10 15:44:25 +05:30
Zhongyi Tong
4d8c8e08a0 Shorten loading animation of functional tabs. 2017-08-10 13:04:07 +08:00
Zhongyi Tong
b34681ece8 Show new-server form by default. 2017-08-10 13:00:25 +08:00
28 changed files with 1368 additions and 278 deletions

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
You can download the latest version from the [Releases](https://github.com/zulip/zulip-electron/releases/latest) page.
Please see [installation guide](./how-to-install.md).
# Features
* Sign in to multiple teams

View File

@@ -1,30 +1,52 @@
'use strict';
const {app, dialog} = require('electron');
const {autoUpdater} = require('electron-updater');
const fs = require('fs');
const { app, dialog } = require('electron');
const { autoUpdater } = require('electron-updater');
const isDev = require('electron-is-dev');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
function appUpdater() {
// Don't initiate auto-updates in development and on Linux system
// since autoUpdater doesn't work on Linux
if (isDev || process.platform === 'linux') {
return;
}
// Create Logs directory
const LogsDir = `${app.getPath('userData')}/Logs`;
if (!fs.existsSync(LogsDir)) {
fs.mkdirSync(LogsDir);
}
// Log whats happening
const log = require('electron-log');
log.transports.file.file = `${LogsDir}/updates.log`;
log.transports.file.level = 'info';
autoUpdater.logger = log;
// Handle auto updates for beta/pre releases
autoUpdater.allowPrerelease = ConfigUtil.getConfigItem('betaUpdate') || false;
// Ask the user if update is available
// eslint-disable-next-line no-unused-vars
autoUpdater.on('update-downloaded', (event, info) => {
autoUpdater.on('update-downloaded', event => {
// Ask user to update the app
dialog.showMessageBox({
type: 'question',
buttons: ['Install and Relaunch', 'Later'],
buttons: ['Install and Relaunch', 'Install Later'],
defaultId: 0,
message: 'A new version of ' + app.getName() + ' has been downloaded',
message: `A new update ${event.version} has been downloaded`,
detail: 'It will be installed the next time you restart the application'
}, response => {
if (response === 0) {
setTimeout(() => autoUpdater.quitAndInstall(), 1);
setTimeout(() => {
autoUpdater.quitAndInstall();
// force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit();
}, 1000);
}
});
});

View File

@@ -1,19 +1,22 @@
'use strict';
const path = require('path');
const electron = require('electron');
const {app} = require('electron');
const ipc = require('electron').ipcMain;
const electronLocalshortcut = require('electron-localshortcut');
const isDev = require('electron-is-dev');
const windowStateKeeper = require('electron-window-state');
const appMenu = require('./menu');
const {appUpdater} = require('./autoupdater');
const { appUpdater } = require('./autoupdater');
const { setAutoLaunch } = require('./startup');
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
require('electron-debug')();
// Prevent window being garbage collected
let mainWindow;
let badgeCount;
let isQuitting = false;
@@ -34,10 +37,6 @@ if (isAlreadyRunning) {
return app.quit();
}
function isWindowsOrmacOS() {
return process.platform === 'darwin' || process.platform === 'win32';
}
const APP_ICON = path.join(__dirname, '../resources', 'Icon');
const iconPath = () => {
@@ -50,6 +49,10 @@ function createMainWindow() {
defaultWidth: 1000,
defaultHeight: 600
});
// Let's keep the window position global so that we can access it in other process
global.mainWindowState = mainWindowState;
const win = new electron.BrowserWindow({
// This settings needs to be saved in config
title: 'Zulip',
@@ -167,32 +170,35 @@ app.on('ready', () => {
});
page.once('did-frame-finish-load', () => {
const checkOS = isWindowsOrmacOS();
if (checkOS && !isDev) {
// Initate auto-updates on MacOS and Windows
appUpdater();
}
// Initate auto-updates on MacOS and Windows
appUpdater();
});
electron.powerMonitor.on('resume', () => {
page.send('reload-viewer');
});
ipc.on('focus-app', () => {
ipcMain.on('focus-app', () => {
mainWindow.show();
});
ipc.on('quit-app', () => {
ipcMain.on('quit-app', () => {
app.quit();
});
// Reload full app not just webview, useful in debugging
ipc.on('reload-full-app', () => {
ipcMain.on('reload-full-app', () => {
mainWindow.reload();
page.send('destroytray');
});
ipc.on('toggle-app', () => {
ipcMain.on('clear-app-settings', () => {
global.mainWindowState.unmanage(mainWindow);
app.relaunch();
app.exit();
});
ipcMain.on('toggle-app', () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
@@ -200,50 +206,46 @@ app.on('ready', () => {
}
});
ipc.on('update-badge', (event, messageCount) => {
if (process.platform === 'darwin') {
app.setBadgeCount(messageCount);
}
if (process.platform === 'win32') {
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(true);
}
if (messageCount === 0) {
mainWindow.setOverlayIcon(null, '');
} else {
page.send('render-taskbar-icon', messageCount);
}
}
ipcMain.on('toggle-badge-option', () => {
BadgeSettings.updateBadge(badgeCount, mainWindow);
});
ipcMain.on('update-badge', (event, messageCount) => {
badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow);
page.send('tray', messageCount);
});
ipc.on('update-taskbar-icon', (event, data, text) => {
const img = electron.nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
ipcMain.on('update-taskbar-icon', (event, data, text) => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
});
ipc.on('forward-message', (event, listener, ...params) => {
ipcMain.on('forward-message', (event, listener, ...params) => {
page.send(listener, ...params);
});
ipc.on('update-menu', (event, props) => {
ipcMain.on('update-menu', (event, props) => {
appMenu.setMenu(props);
});
ipc.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
page.send('switch-server-tab', index - 1);
});
});
ipc.on('local-shortcuts', (event, enable) => {
ipcMain.on('local-shortcuts', (event, enable) => {
if (enable) {
registerLocalShortcuts(page);
} else {
electronLocalshortcut.unregisterAll(mainWindow);
}
});
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => {
setAutoLaunch(AutoLaunchValue);
});
});
app.on('will-quit', () => {
@@ -254,3 +256,9 @@ app.on('will-quit', () => {
app.on('before-quit', () => {
isQuitting = true;
});
// Send crash reports
process.on('uncaughtException', err => {
console.error(err);
console.error(err.stack);
});

View File

@@ -1,6 +1,10 @@
'use strict';
const os = require('os');
const {dialog, app, shell, BrowserWindow, Menu} = require('electron');
const path = require('path');
const { app, shell, BrowserWindow, Menu } = require('electron');
const fs = require('fs-extra');
const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');
@@ -86,9 +90,9 @@ class AppMenu {
accelerator: 'CommandOrControl+S',
click(item, focusedWindow) {
if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem('show-sidebar');
const newValue = !ConfigUtil.getConfigItem('showSidebar');
focusedWindow.webContents.send('toggle-sidebar', newValue);
ConfigUtil.setConfigItem('show-sidebar', newValue);
ConfigUtil.setConfigItem('showSidebar', newValue);
}
}
}, {
@@ -164,7 +168,7 @@ class AppMenu {
}
getDarwinTpl(props) {
const {tabs, activeTabIndex} = props;
const { tabs, activeTabIndex } = props;
return [{
label: `${app.getName()}`,
@@ -196,9 +200,10 @@ class AppMenu {
}, {
type: 'separator'
}, {
label: 'Clear Cache',
label: 'Reset App Settings',
accelerator: 'Command+Shift+D',
click() {
AppMenu.clearCache();
AppMenu.resetAppSettings();
}
}, {
label: 'Log Out',
@@ -263,7 +268,7 @@ class AppMenu {
}
getOtherTpl(props) {
const {tabs, activeTabIndex} = props;
const { tabs, activeTabIndex } = props;
return [{
label: 'File',
@@ -297,9 +302,10 @@ class AppMenu {
}, {
type: 'separator'
}, {
label: 'Clear Cache',
label: 'Reset App Settings',
accelerator: 'Ctrl+Shift+D',
click() {
AppMenu.clearCache();
AppMenu.resetAppSettings();
}
}, {
label: 'Log Out',
@@ -363,11 +369,11 @@ class AppMenu {
win.webContents.send(action, ...params);
}
static clearCache() {
const win = BrowserWindow.getAllWindows()[0];
const ses = win.webContents.session;
ses.clearCache(() => {
dialog.showMessageBox({type: 'info', buttons: [], message: 'Cache cleared!'});
static resetAppSettings() {
const getAppPath = path.join(app.getPath('appData'), appName, 'window-state.json');
fs.unlink(getAppPath, () => {
setTimeout(() => AppMenu.sendAction('clear-app-data'), 1000);
});
}

34
app/main/startup.js Normal file
View File

@@ -0,0 +1,34 @@
'use strict';
const { app } = require('electron');
const AutoLaunch = require('auto-launch');
const isDev = require('electron-is-dev');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
const setAutoLaunch = AutoLaunchValue => {
// Don't run this in development
if (isDev) {
return;
}
// On Mac, work around a bug in auto-launch where it opens a Terminal window
// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
const appPath = process.platform === 'darwin' ? app.getPath('exe').replace(/\.app\/Content.*/, '.app') : undefined; // Use the default
const ZulipAutoLauncher = new AutoLaunch({
name: 'Zulip',
path: appPath,
isHidden: false
});
const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue);
if (autoLaunchOption) {
ZulipAutoLauncher.enable();
} else {
ZulipAutoLauncher.disable();
}
};
module.exports = {
setAutoLaunch
};

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "1.3.0-beta",
"version": "1.4.0",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"email": "<svnitakash@gmail.com>",
@@ -32,10 +32,11 @@
"electron-localshortcut": "2.0.2",
"electron-log": "2.2.7",
"electron-spellchecker": "1.2.0",
"electron-updater": "2.8.5",
"electron-updater": "2.8.9",
"node-json-db": "0.7.3",
"request": "2.81.0",
"wurl": "2.5.0",
"electron-window-state": "4.1.1"
"electron-window-state": "4.1.1",
"auto-launch": "5.0.1"
}
}

View File

@@ -3,11 +3,50 @@ body {
height: 100%;
margin: 0;
cursor: default;
user-select: none;
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
color: #333;
background: #efefef;
}
kbd {
padding: 0.1em 0.6em;
border: 1px solid #ccc;
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;
text-shadow: 0 1px 0 #fff;
line-height: 1.4;
white-space: nowrap;
}
table, th, td {
border: 1px solid #ddd;
border-collapse: collapse;
}
table { width: 85%; }
table tr:nth-child(even) { background-color: #f2f2f2; }
td { padding: 5px; }
td:nth-child(odd) {
text-align: right;
width: 50%;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
@@ -88,7 +127,6 @@ body {
}
#new-server-container {
margin: 20px 0;
opacity: 1;
transition: opacity 0.3s;
}
@@ -130,7 +168,17 @@ img.server-info-icon {
cursor: pointer;
}
.server-info-url {
.setting-input-key {
font-size: 14px;
height: 27px;
line-height: 27px;
font-weight: bold;
background: transparent;
flex-wrap: nowrap;
margin-right: 10px;
}
.setting-input-value {
flex-grow: 1;
font-size: 14px;
height: 24px;
@@ -141,8 +189,12 @@ img.server-info-icon {
max-width: 500px;
}
.server-info-value:focus {
border-bottom: #388E3C 1px solid;
.setting-input-value:focus {
border-bottom: #7cb980 1px solid;
}
.setting-block {
width: 100%;
}
.actions-container {
@@ -186,7 +238,8 @@ img.server-info-icon {
.settings-card {
display: flex;
padding: 16px 30px;
flex-wrap: wrap;
padding: 12px 30px;
margin: 10px 0 20px 0;
background: #fff;
border-radius: 2px;
@@ -221,6 +274,7 @@ img.server-info-icon {
display: flex;
justify-content: space-between;
width: 100%;
margin: 6px 0;
}
.code {
@@ -231,4 +285,30 @@ i.open-tab-button {
padding: 0 5px;
font-size: 18px;
cursor: pointer;
}
.reset-data-button {
display: inline-block;
width: 120px;
cursor: pointer;
font-size: 11px;
transition: background-color 0.2s ease;
text-decoration: none;
}
.reset-data-button:hover {
background-color: #32a692;
color: #fff;
}
#open-shortcuts-url {
color: #08c;
cursor: pointer;
text-decoration: none;
}
#open-shortcuts-url:hover {
color: #005580;;
text-decoration: underline;
}

View File

@@ -28,6 +28,7 @@ class WebView extends BaseComponent {
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
disablewebsecurity
${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession"
webpreferences="allowRunningInsecureContent, javascript=yes">
</webview>`;
}
@@ -60,7 +61,9 @@ class WebView extends BaseComponent {
});
this.$el.addEventListener('dom-ready', () => {
this.$el.classList.add('onload');
if (this.props.role === 'server') {
this.$el.classList.add('onload');
}
this.show();
});
@@ -96,11 +99,13 @@ class WebView extends BaseComponent {
this.$el.classList.remove('disabled');
setTimeout(() => {
this.$el.classList.remove('onload');
if (this.props.role === 'server') {
this.$el.classList.remove('onload');
}
}, 1000);
this.focus();
this.loading = false;
this.props.onTitleChange(this.$el.getTitle());
this.props.onTitleChange();
// Injecting preload css in webview to override some css rules
this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8'));
}

View File

@@ -1,7 +1,9 @@
'use strict';
require(__dirname + '/js/tray.js');
const {ipcRenderer} = require('electron');
const { ipcRenderer, remote } = require('electron');
const { session } = remote;
const DomainUtil = require(__dirname + '/js/utils/domain-util.js');
const WebView = require(__dirname + '/js/components/webview.js');
@@ -33,14 +35,35 @@ class ServerManagerView {
}
init() {
this.initSidebar();
this.initTabs();
this.initActions();
this.registerIpcs();
this.loadProxy().then(() => {
this.initSidebar();
this.initTabs();
this.initActions();
this.registerIpcs();
});
}
loadProxy() {
return new Promise(resolve => {
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
if (proxyEnabled) {
session.fromPartition('persist:webviewsession').setProxy({
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
}, resolve);
} else {
session.fromPartition('persist:webviewsession').setProxy({
pacScript: '',
proxyRules: '',
proxyBypassRules: ''
}, resolve);
}
});
}
initSidebar() {
const showSidebar = ConfigUtil.getConfigItem('show-sidebar', true);
const showSidebar = ConfigUtil.getConfigItem('showSidebar', true);
this.toggleSidebar(showSidebar);
}
@@ -49,6 +72,8 @@ class ServerManagerView {
if (servers.length > 0) {
for (let i = 0; i < servers.length; i++) {
this.initServer(servers[i], i);
DomainUtil.updateSavedServer(servers[i].url, i);
this.activateTab(i);
}
this.activateTab(0);
} else {
@@ -91,17 +116,17 @@ class ServerManagerView {
this.$settingsButton.addEventListener('click', () => {
this.openSettings('General');
});
this.$reloadButton.addEventListener('mouseover', () => {
this.$reloadTooltip.removeAttribute('style');
this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip);
this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip);
}
sidebarHoverEvent(SidebarButton, SidebarTooltip) {
SidebarButton.addEventListener('mouseover', () => {
SidebarTooltip.removeAttribute('style');
});
this.$reloadButton.addEventListener('mouseout', () => {
this.$reloadTooltip.style.display = 'none';
});
this.$settingsButton.addEventListener('mouseover', () => {
this.$settingsTooltip.removeAttribute('style');
});
this.$settingsButton.addEventListener('mouseout', () => {
this.$settingsTooltip.style.display = 'none';
SidebarButton.addEventListener('mouseout', () => {
SidebarTooltip.style.display = 'none';
});
}
@@ -276,10 +301,22 @@ class ServerManagerView {
ipcRenderer.send('reload-full-app');
});
ipcRenderer.on('clear-app-data', () => {
ipcRenderer.send('clear-app-settings');
});
ipcRenderer.on('switch-server-tab', (event, index) => {
this.activateTab(index);
});
ipcRenderer.on('reload-proxy', (event, showAlert) => {
this.loadProxy().then(() => {
if (showAlert) {
alert('Proxy settings saved!');
}
});
});
ipcRenderer.on('toggle-sidebar', (event, show) => {
this.toggleSidebar(show);
});

View File

@@ -1,6 +1,6 @@
'use strict';
const {remote} = require('electron');
const { remote } = require('electron');
const ConfigUtil = require(__dirname + '/utils/config-util.js');
@@ -12,11 +12,13 @@ app.setAppUserModelId('org.zulip.zulip-electron');
const NativeNotification = window.Notification;
class SilentNotification extends NativeNotification {
class baseNotification extends NativeNotification {
constructor(title, opts) {
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
ConfigUtil.getConfigItem('showNotification') ? super(title, opts) : super(); // eslint-disable-line no-unused-expressions
}
}
window.Notification = SilentNotification;
window.Notification = baseNotification;

View File

@@ -0,0 +1,63 @@
'use strict';
const electron = require('electron');
const { app } = require('electron');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
let instance = null;
class BadgeSettings {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
return instance;
}
showBadgeCount(messageCount, mainWindow) {
if (process.platform === 'darwin') {
app.setBadgeCount(messageCount);
}
if (process.platform === 'win32') {
this.updateOverlayIcon(messageCount, mainWindow);
}
}
hideBadgeCount(mainWindow) {
if (process.platform === 'darwin') {
app.setBadgeCount(0);
}
if (process.platform === 'win32') {
mainWindow.setOverlayIcon(null, '');
}
}
updateBadge(badgeCount, mainWindow) {
if (ConfigUtil.getConfigItem('badgeOption', true)) {
this.showBadgeCount(badgeCount, mainWindow);
} else {
this.hideBadgeCount(mainWindow);
}
}
updateOverlayIcon(messageCount, mainWindow) {
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(true);
}
if (messageCount === 0) {
mainWindow.setOverlayIcon(null, '');
} else {
mainWindow.webContents.send('render-taskbar-icon', messageCount);
}
}
updateTaskbarIcon(data, text, mainWindow) {
const img = electron.nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
}
}
module.exports = new BadgeSettings();

View File

@@ -0,0 +1,40 @@
'use strict';
const {ipcRenderer} = require('electron');
const BaseComponent = require(__dirname + '/../../components/base.js');
class BaseSection extends BaseComponent {
generateSettingOption(props) {
const {$element, value, clickHandler} = props;
$element.innerHTML = '';
const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value));
$element.appendChild($optionControl);
$optionControl.addEventListener('click', clickHandler);
}
generateOptionTemplate(settingOption) {
if (settingOption) {
return `
<div class="action green">
<span>On</span>
</div>
`;
} else {
return `
<div class="action red">
<span>Off</span>
</div>
`;
}
}
reloadApp() {
ipcRenderer.send('forward-message', 'reload-viewer');
}
}
module.exports = BaseSection;

View File

@@ -1,11 +1,15 @@
'use strict';
const path = require('path');
const {ipcRenderer} = require('electron');
const { ipcRenderer } = require('electron');
const { app, dialog } = require('electron').remote;
const BaseComponent = require(__dirname + '/../../components/base.js');
const fs = require('fs-extra');
const BaseSection = require(__dirname + '/base-section.js');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
class GeneralSection extends BaseComponent {
class GeneralSection extends BaseSection {
constructor(props) {
super();
this.props = props;
@@ -13,144 +17,201 @@ class GeneralSection extends BaseComponent {
template() {
return `
<div class="settings-pane" id="server-settings-pane">
<div class="title">Tray Options</div>
<div id="tray-option-settings" class="settings-card">
<div class="setting-row">
<div class="settings-pane">
<div class="title">Appearance</div>
<div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="tray-option">
<div class="setting-description">Show app icon in system tray</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="sidebar-option">
<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>
<div class="title">Desktop Notification</div>
<div class="settings-card">
<div class="setting-row" id="show-notification-option">
<div class="setting-description">Show Desktop Notifications</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="silent-option">
<div class="setting-description">Mute all sounds from Zulip</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">App Updates</div>
<div id="betaupdate-option-settings" class="settings-card">
<div class="setting-row">
<div class="settings-card">
<div class="setting-row" id="betaupdate-option">
<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 class="title">Desktop Notification</div>
<div id="silent-option-settings" class="settings-card">
<div class="setting-row">
<div class="setting-description">Mute all sounds from Zulip (requires reload)</div>
</div>
<div class="title">Functionality</div>
<div class="settings-card">
<div class="setting-row" id="startAtLogin-option">
<div class="setting-description">Start app at login</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">User Interface</div>
<div id="ui-option-settings" class="settings-card">
<div class="setting-row" id="sidebar-option">
<div class="setting-description">Show sidebar (<span class="code">CmdOrCtrl+S</span>)</div>
<div class="setting-control"></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 green">Reset App Data</button>
</div>
</div>
</div>
`;
}
settingsOptionTemplate(settingOption) {
if (settingOption) {
return `
<div class="action green">
<span>On</span>
</div>
`;
} else {
return `
<div class="action red">
<span>Off</span>
</div>
`;
}
}
trayOptionTemplate(trayOption) {
this.settingsOptionTemplate(trayOption);
}
updateOptionTemplate(updateOption) {
this.settingsOptionTemplate(updateOption);
}
silentOptionTemplate(silentOption) {
this.settingsOptionTemplate(silentOption);
}
sidebarToggleTemplate(toggleOption) {
this.settingsOptionTemplate(toggleOption);
}
init() {
this.props.$root.innerHTML = this.template();
this.initTrayOption();
this.initUpdateOption();
this.initSilentOption();
this.initSidebarToggle();
this.updateTrayOption();
this.updateBadgeOption();
this.updateUpdateOption();
this.updateAutoUpdateOption();
this.updateSilentOption();
this.updateSidebarOption();
this.updateStartAtLoginOption();
this.updateResetDataOption();
this.showDesktopNotification();
}
initTrayOption() {
this.$trayOptionSettings = document.querySelector('#tray-option-settings .setting-control');
this.$trayOptionSettings.innerHTML = '';
const trayOption = ConfigUtil.getConfigItem('trayIcon', true);
const $trayOption = this.generateNodeFromTemplate(this.settingsOptionTemplate(trayOption));
this.$trayOptionSettings.appendChild($trayOption);
$trayOption.addEventListener('click', () => {
const newValue = !ConfigUtil.getConfigItem('trayIcon');
ConfigUtil.setConfigItem('trayIcon', newValue);
ipcRenderer.send('forward-message', 'toggletray');
this.initTrayOption();
updateTrayOption() {
this.generateSettingOption({
$element: document.querySelector('#tray-option .setting-control'),
value: ConfigUtil.getConfigItem('trayIcon', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('trayIcon');
ConfigUtil.setConfigItem('trayIcon', newValue);
ipcRenderer.send('forward-message', 'toggletray');
this.updateTrayOption();
}
});
}
initUpdateOption() {
this.$updateOptionSettings = document.querySelector('#betaupdate-option-settings .setting-control');
this.$updateOptionSettings.innerHTML = '';
const updateOption = ConfigUtil.getConfigItem('betaUpdate', false);
const $updateOption = this.generateNodeFromTemplate(this.settingsOptionTemplate(updateOption));
this.$updateOptionSettings.appendChild($updateOption);
$updateOption.addEventListener('click', () => {
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
ConfigUtil.setConfigItem('betaUpdate', newValue);
this.initUpdateOption();
updateBadgeOption() {
this.generateSettingOption({
$element: document.querySelector('#badge-option .setting-control'),
value: ConfigUtil.getConfigItem('badgeOption', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('badgeOption');
ConfigUtil.setConfigItem('badgeOption', newValue);
ipcRenderer.send('toggle-badge-option', newValue);
this.updateBadgeOption();
}
});
}
initSilentOption() {
this.$silentOptionSettings = document.querySelector('#silent-option-settings .setting-control');
this.$silentOptionSettings.innerHTML = '';
const silentOption = ConfigUtil.getConfigItem('silent', false);
const $silentOption = this.generateNodeFromTemplate(this.settingsOptionTemplate(silentOption));
this.$silentOptionSettings.appendChild($silentOption);
$silentOption.addEventListener('click', () => {
const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue);
this.initSilentOption();
updateUpdateOption() {
this.generateSettingOption({
$element: document.querySelector('#betaupdate-option .setting-control'),
value: ConfigUtil.getConfigItem('betaUpdate', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
ConfigUtil.setConfigItem('betaUpdate', newValue);
this.updateUpdateOption();
}
});
}
initSidebarToggle() {
this.$sidebarOptionSettings = document.querySelector('#ui-option-settings #sidebar-option .setting-control');
this.$sidebarOptionSettings.innerHTML = '';
const sidebarOption = ConfigUtil.getConfigItem('show-sidebar', true);
const $sidebarOption = this.generateNodeFromTemplate(this.settingsOptionTemplate(sidebarOption));
this.$sidebarOptionSettings.appendChild($sidebarOption);
$sidebarOption.addEventListener('click', () => {
const newValue = !ConfigUtil.getConfigItem('show-sidebar');
ConfigUtil.setConfigItem('show-sidebar', newValue);
ipcRenderer.send('forward-message', 'toggle-sidebar', newValue);
this.initSidebarToggle();
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();
}
});
}
handleServerInfoChange() {
ipcRenderer.send('forward-message', 'reload-viewer');
updateSilentOption() {
this.generateSettingOption({
$element: document.querySelector('#silent-option .setting-control'),
value: ConfigUtil.getConfigItem('silent', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue);
this.updateSilentOption();
}
});
}
showDesktopNotification() {
this.generateSettingOption({
$element: document.querySelector('#show-notification-option .setting-control'),
value: ConfigUtil.getConfigItem('showNotification', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('showNotification', true);
ConfigUtil.setConfigItem('showNotification', newValue);
this.showDesktopNotification();
}
});
}
updateSidebarOption() {
this.generateSettingOption({
$element: document.querySelector('#sidebar-option .setting-control'),
value: ConfigUtil.getConfigItem('showSidebar', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('showSidebar');
ConfigUtil.setConfigItem('showSidebar', newValue);
ipcRenderer.send('forward-message', 'toggle-sidebar', newValue);
this.updateSidebarOption();
}
});
}
updateStartAtLoginOption() {
this.generateSettingOption({
$element: document.querySelector('#startAtLogin-option .setting-control'),
value: ConfigUtil.getConfigItem('startAtLogin', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('startAtLogin');
ConfigUtil.setConfigItem('startAtLogin', newValue);
ipcRenderer.send('toggleAutoLauncher', newValue);
this.updateStartAtLoginOption();
}
});
}
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());
dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
message: 'Are you sure',
detail: clearAppDataMessage
}, response => {
if (response === 0) {
fs.remove(getAppPath);
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000);
}
});
}
updateResetDataOption() {
const resetDataButton = document.querySelector('#resetdata-option .reset-data-button');
resetDataButton.addEventListener('click', () => {
this.clearAppDataDialog();
});
}
}
module.exports = GeneralSection;

View File

@@ -8,7 +8,7 @@ class PreferenceNav extends BaseComponent {
this.props = props;
this.navItems = ['General', 'Servers'];
this.navItems = ['General', 'Network', 'Servers', 'Shortcuts'];
this.init();
}

View File

@@ -0,0 +1,102 @@
'use strict';
const {ipcRenderer} = require('electron');
const BaseSection = require(__dirname + '/base-section.js');
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
class NetworkSection extends BaseSection {
constructor(props) {
super();
this.props = props;
}
template() {
return `
<div class="settings-pane">
<div class="title">Proxy</div>
<div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="use-proxy-option">
<div class="setting-description">Connect servers through a proxy</div>
<div class="setting-control"></div>
</div>
<div class="setting-block">
<div class="setting-row" id="proxy-pac-option">
<span class="setting-input-key">PAC script</span>
<input class="setting-input-value" placeholder="e.g. foobar.com/pacfile.js"/>
</div>
<div class="setting-row" id="proxy-rules-option">
<span class="setting-input-key">Proxy rules</span>
<input class="setting-input-value" placeholder="e.g. http=foopy:80;ftp=foopy2"/>
</div>
<div class="setting-row" id="proxy-bypass-option">
<span class="setting-input-key">Proxy bypass rules</span>
<input class="setting-input-value" placeholder="e.g. foobar.com"/>
</div>
<div class="setting-row">
<div class="action green" id="proxy-save-action">
<i class="material-icons">check_box</i>
<span>Save</span>
</div>
</div>
</div>
</div>
</div>
`;
}
init() {
this.props.$root.innerHTML = this.template();
this.$proxyPAC = document.querySelector('#proxy-pac-option .setting-input-value');
this.$proxyRules = document.querySelector('#proxy-rules-option .setting-input-value');
this.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value');
this.$proxySaveAction = document.getElementById('proxy-save-action');
this.$settingBlock = this.props.$root.querySelector('.setting-block');
this.initProxyOption();
this.$proxyPAC.value = ConfigUtil.getConfigItem('proxyPAC', '');
this.$proxyRules.value = ConfigUtil.getConfigItem('proxyRules', '');
this.$proxyBypass.value = ConfigUtil.getConfigItem('proxyBypass', '');
this.$proxySaveAction.addEventListener('click', () => {
ConfigUtil.setConfigItem('proxyPAC', this.$proxyPAC.value);
ConfigUtil.setConfigItem('proxyRules', this.$proxyRules.value);
ConfigUtil.setConfigItem('proxyBypass', this.$proxyBypass.value);
ipcRenderer.send('forward-message', 'reload-proxy', true);
});
}
initProxyOption() {
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
this.toggleProxySettings(proxyEnabled);
this.updateProxyOption();
}
toggleProxySettings(option) {
if (option) {
this.$settingBlock.classList.remove('hidden');
} else {
this.$settingBlock.classList.add('hidden');
}
}
updateProxyOption() {
this.generateSettingOption({
$element: document.querySelector('#use-proxy-option .setting-control'),
value: ConfigUtil.getConfigItem('useProxy', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('useProxy');
ConfigUtil.setConfigItem('useProxy', newValue);
this.toggleProxySettings(newValue);
if (newValue === false) {
// Reload proxy if the proxy is turned off
ipcRenderer.send('forward-message', 'reload-proxy', false);
}
this.updateProxyOption();
}
});
}
}
module.exports = NetworkSection;

View File

@@ -11,10 +11,10 @@ class NewServerForm extends BaseComponent {
template() {
return `
<div class="settings-card" style="border: solid 1px #4CAF50;">
<div class="settings-card">
<div class="server-info-right">
<div class="server-info-row">
<input class="server-info-url" autofocus placeholder="Enter the url of your Zulip server..."/>
<input class="setting-input-value" autofocus placeholder="Entert the URL of your Zulip organization..."/>
</div>
<div class="server-info-row">
<div class="action green server-save-action">
@@ -38,18 +38,29 @@ class NewServerForm extends BaseComponent {
this.props.$root.innerHTML = '';
this.props.$root.appendChild(this.$newServerForm);
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.server-info-url')[0];
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0];
}
submitFormHandler() {
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
DomainUtil.addDomain(serverConf).then(() => {
this.props.onChange(this.props.index);
});
}, errorMessage => {
alert(errorMessage);
});
}
initActions() {
this.$saveServerButton.addEventListener('click', () => {
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
DomainUtil.addDomain(serverConf).then(() => {
this.props.onChange(this.props.index);
});
}, errorMessage => {
alert(errorMessage);
});
this.submitFormHandler();
});
this.$newServerUrl.addEventListener('keypress', event => {
const EnterkeyCode = event.keyCode;
// Submit form when Enter key is pressed
if (EnterkeyCode === 13) {
this.submitFormHandler();
}
});
}
}

View File

@@ -6,6 +6,8 @@ const {ipcRenderer} = require('electron');
const Nav = require(__dirname + '/js/pages/preference/nav.js');
const ServersSection = require(__dirname + '/js/pages/preference/servers-section.js');
const GeneralSection = require(__dirname + '/js/pages/preference/general-section.js');
const NetworkSection = require(__dirname + '/js/pages/preference/network-section.js');
const ShortcutsSection = require(__dirname + '/js/pages/preference/shortcuts-section.js');
class PreferenceView extends BaseComponent {
constructor() {
@@ -49,6 +51,18 @@ class PreferenceView extends BaseComponent {
});
break;
}
case 'Network': {
this.section = new NetworkSection({
$root: this.$settingsContainer
});
break;
}
case 'Shortcuts': {
this.section = new ShortcutsSection({
$root: this.$settingsContainer
});
break;
}
default: break;
}
this.section.init();

View File

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

View File

@@ -1,13 +1,11 @@
'use strict';
const {ipcRenderer} = require('electron');
const BaseComponent = require(__dirname + '/../../components/base.js');
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');
class ServersSection extends BaseComponent {
class ServersSection extends BaseSection {
constructor(props) {
super();
this.props = props;
@@ -16,15 +14,9 @@ class ServersSection extends BaseComponent {
template() {
return `
<div class="settings-pane" id="server-settings-pane">
<div class="title">Manage Servers</div>
<div class="actions-container">
<div class="action green" id="new-server-action">
<i class="material-icons">add_box</i>
<span>Add Server</span>
</div>
</div>
<div id="new-server-container" class="hidden"></div>
<div class="sub-title" id="existing-servers"></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>
`;
@@ -32,7 +24,6 @@ class ServersSection extends BaseComponent {
init() {
this.initServers();
this.initActions();
}
initServers() {
@@ -47,7 +38,7 @@ class ServersSection extends BaseComponent {
this.$serverInfoContainer.innerHTML = servers.length ? '' : 'Add your first server to get started!';
// Show Existing servers if servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Existing servers';
this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Existing Servers';
this.initNewServerForm();
for (let i = 0; i < servers.length; i++) {
@@ -55,7 +46,7 @@ class ServersSection extends BaseComponent {
$root: this.$serverInfoContainer,
server: servers[i],
index: i,
onChange: this.handleServerInfoChange.bind(this)
onChange: this.reloadApp
}).init();
}
}
@@ -63,19 +54,9 @@ class ServersSection extends BaseComponent {
initNewServerForm() {
new NewServerForm({
$root: this.$newServerContainer,
onChange: this.handleServerInfoChange.bind(this)
onChange: this.reloadApp
}).init();
}
initActions() {
this.$newServerContainer.classList.remove('hidden');
this.$newServerButton.classList.remove('green');
this.$newServerButton.classList.add('grey');
}
handleServerInfoChange() {
ipcRenderer.send('forward-message', 'reload-viewer');
}
}
module.exports = ServersSection;

View File

@@ -0,0 +1,311 @@
'use strict';
const BaseSection = require(__dirname + '/base-section.js');
class ShortcutsSection extends BaseSection {
constructor(props) {
super();
this.props = props;
}
templateMac() {
const userOSKey = '⌘';
return `
<div class="settings-pane">
<div class="title">Application Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>,</kbd></td>
<td>Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>D</kbd></td>
<td>Reset App Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>L</kbd></td>
<td>Log Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>Hide Zulip</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>Hide Others</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Q</kbd></td>
<td>Quit Zulip</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Edit Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>Undo</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>Redo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>X</kbd></td>
<td>Cut</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>C</kbd></td>
<td>Copy</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
<td>Paste</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
<td>Paste and Match Style</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>A</kbd></td>
<td>Select All</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
<td>Find</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>G</kbd></td>
<td>Find Next</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>Space</kbd></td>
<td>Emoji & Symbols</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">View Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>Reload</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>Hard Reload</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
<td>Enter Full Screen</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>=</kbd></td>
<td>Zoom In</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>-</kbd></td>
<td>Zoom Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>0</kbd></td>
<td>Actual Size</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>S</kbd></td>
<td>Toggle Sidebar</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>I</kbd></td>
<td>Toggle DevTools for Zulip App</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>U</kbd></td>
<td>Toggle DevTools for Active Tab</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">History Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>←</kbd></td>
<td>Back</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>→</kbd></td>
<td>Forward</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Window Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>M</kbd></td>
<td>Minimize</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>W</kbd></td>
<td>Close</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
</div>
`;
}
templateWinLin() {
const userOSKey = 'Ctrl';
return `
<div class="settings-pane">
<div class="title">Application Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>,</kbd></td>
<td>Settings</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>L</kbd></td>
<td>Log Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Q</kbd></td>
<td>Quit Zulip</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Edit Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Z</kbd></td>
<td>Undo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Y</kbd></td>
<td>Redo</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>X</kbd></td>
<td>Cut</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>C</kbd></td>
<td>Copy</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>V</kbd></td>
<td>Paste</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd></td>
<td>Paste and Match Style</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>A</kbd></td>
<td>Select All</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">View Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>R</kbd></td>
<td>Reload</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd></td>
<td>Hard Reload</td>
</tr>
<tr>
<td><kbd>F11</kbd></td>
<td>Toggle Full Screen</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>=</kbd></td>
<td>Zoom In</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>-</kbd></td>
<td>Zoom Out</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>0</kbd></td>
<td>Actual Size</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>S</kbd></td>
<td>Toggle Sidebar</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd></td>
<td>Toggle DevTools for Zulip App</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>U</kbd></td>
<td>Toggle DevTools for Active Tab</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">History Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>Alt</kbd> + <kbd>←</kbd></td>
<td>Back</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>→</kbd></td>
<td>Forward</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Window Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>M</kbd></td>
<td>Minimize</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>W</kbd></td>
<td>Close</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
</div>
`;
}
init() {
this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin();
}
}
module.exports = ShortcutsSection;

View File

@@ -1,7 +1,7 @@
'use strict';
const {ipcRenderer} = require('electron');
const {spellChecker} = require('./spellchecker');
const { ipcRenderer } = require('electron');
const { spellChecker } = require('./spellchecker');
// eslint-disable-next-line import/no-unassigned-import
require('./notification');
@@ -36,7 +36,10 @@ document.addEventListener('DOMContentLoaded', () => {
spellChecker();
// redirect users to network troubleshooting page
document.querySelector('.restart_get_events_button').addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
const getRestartButton = document.querySelector('.restart_get_events_button');
if (getRestartButton) {
getRestartButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
}
});

View File

@@ -180,11 +180,10 @@ ipcRenderer.on('tray', (event, arg) => {
if (!window.tray) {
return;
}
// We don't want to create tray from unread messages on windows and macOS since these systems already have dock badges and taskbar overlay icon.
if (process.platform === 'linux') {
// We don't want to create tray from unread messages on macOS since it already has dock badges.
if (process.platform === 'linux' || process.platform === 'win32') {
if (arg === 0) {
unread = arg;
// Message Count // console.log("message count is zero.");
window.tray.setImage(iconPath());
window.tray.setToolTip('No unread messages');
} else {
@@ -206,7 +205,7 @@ function toggleTray() {
ConfigUtil.setConfigItem('trayIcon', false);
} else {
createTray();
if (process.platform === 'linux') {
if (process.platform === 'linux' || process.platform === 'win32') {
renderNativeImage(unread).then(image => {
window.tray.setImage(image);
window.tray.setToolTip(unread + ' unread messages');

View File

@@ -26,6 +26,7 @@ class ConfigUtil {
}
getConfigItem(key, defaultValue = null) {
this.reloadDB();
const value = this.db.getData('/')[key];
if (value === undefined) {
this.setConfigItem(key, defaultValue);

View File

@@ -1,6 +1,6 @@
'use strict';
const {app, dialog} = require('electron').remote;
const { app, dialog } = require('electron').remote;
const fs = require('fs');
const path = require('path');
const JsonDB = require('node-json-db');
@@ -45,6 +45,11 @@ class DomainUtil {
return this.db.getData(`/domains[${index}]`);
}
updateDomain(index, server) {
this.reloadDB();
this.db.push(`/domains[${index}]`, server, true);
}
addDomain(server) {
return new Promise(resolve => {
if (server.icon) {
@@ -73,11 +78,26 @@ class DomainUtil {
this.reloadDB();
}
checkDomain(domain) {
const hasPrefix = (domain.indexOf('http') === 0);
if (!hasPrefix) {
domain = (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`;
// Check if domain is already added
duplicateDomain(domain) {
domain = this.formatUrl(domain);
const servers = this.getDomains();
for (const i in servers) {
if (servers[i].url === domain) {
return true;
}
}
return false;
}
checkDomain(domain, silent = false) {
if (!silent && this.duplicateDomain(domain)) {
// Do not check duplicate in silent mode
alert('This server has been added.');
return;
}
domain = this.formatUrl(domain);
const checkDomain = domain + '/static/audio/zulip.ogg';
@@ -101,22 +121,36 @@ class DomainUtil {
resolve(serverConf);
});
} else if (certsError.indexOf(error.toString()) >= 0) {
dialog.showMessageBox({
type: 'question',
buttons: ['Yes', 'No'],
defaultId: 0,
message: `Do you trust certificate from ${domain}? \n ${error}`
}, response => {
if (response === 0) {
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
}, () => {
resolve(serverConf);
});
} else {
reject('Untrusted Certificate.');
}
});
if (silent) {
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
}, () => {
resolve(serverConf);
});
} else {
const certErrorMessage = `Do you trust certificate from ${domain}? \n ${error}`;
const certErrorDetail = `The server you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
\n Unless you have a good reason to believe otherwise, you should not proceed.
\n You can click here if you'd like to proceed with the connection.`;
dialog.showMessageBox({
type: 'warning',
buttons: ['Yes', 'No'],
defaultId: 0,
message: certErrorMessage,
detail: certErrorDetail
}, response => {
if (response === 0) {
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
}, () => {
resolve(serverConf);
});
} else {
reject('Untrusted Certificate.');
}
});
}
} else {
reject('Not a valid Zulip server');
}
@@ -146,14 +180,8 @@ class DomainUtil {
saveServerIcon(url) {
// The save will always succeed. If url is invalid, downgrade to default icon.
const dir = `${app.getPath('userData')}/server-icons`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return new Promise(resolve => {
const filePath = `${dir}/${new Date().getMilliseconds()}${path.extname(url).split('?')[0]}`;
const filePath = this.generateFilePath(url);
const file = fs.createWriteStream(filePath);
try {
request(url).on('response', response => {
@@ -175,9 +203,48 @@ class DomainUtil {
});
}
updateSavedServer(url, index) {
// Does not promise successful update
this.checkDomain(url, true).then(newServerConf => {
this.saveServerIcon(newServerConf.icon).then(localIconUrl => {
newServerConf.icon = localIconUrl;
this.updateDomain(index, newServerConf);
this.reloadDB();
});
});
}
reloadDB() {
this.db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
}
generateFilePath(url) {
const dir = `${app.getPath('userData')}/server-icons`;
const extension = path.extname(url).split('?')[0];
let hash = 5381;
let len = url.length;
while (len) {
hash = (hash * 33) ^ url.charCodeAt(--len);
}
// Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return `${dir}/${hash >>> 0}${extension}`;
}
formatUrl(domain) {
const hasPrefix = (domain.indexOf('http') === 0);
if (hasPrefix) {
return domain;
} else {
return (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`;
}
}
}
module.exports = new DomainUtil();

View File

@@ -1 +1,58 @@
** Windows Set up instructions **
## Prerequisites
* [Git](http://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node.js](https://nodejs.org) >= v6.9.0
* [python](https://www.python.org/downloads/release/python-2713/) (v2.7.x recommended)
* [node-gyp](https://github.com/nodejs/node-gyp#installation) (installed via powershell)
## System specific dependencies
* use only 32bit or 64bit for all of the installers, do not mix architectures
* install using default settings
* open Windows Powershell as Admin
```powershell
C:\Windows\system32> npm install --global --production windows-build-tools
```
## Installation
Clone the source locally:
```sh
$ git clone https://github.com/zulip/zulip-electron
$ cd zulip-electron
```
Install project dependencies:
```sh
$ npm install
```
Start the app:
```sh
$ npm start
```
Start and watch changes
```sh
$ npm run dev
```
### Making a release
To package app into an installer use command:
```
npm run pack
npm run dist
```
It will start the packaging process. The ready for distribution file (e.g. dmg, windows installer, deb package) will be outputted to the `dist` directory.
# Troubleshooting
If you have any problems running the app please see the [most common issues](./troubleshooting.md).

105
help.md Normal file
View File

@@ -0,0 +1,105 @@
# User Guide
> Welcome! This guide will walk you through the basics of using Zulip Desktop.
## Get Zulip Desktop
## Connect to a Server
### Connect through a proxy
It's possible to connect to your server through a proxy.
You can enter the proxy settings in the `Network` section of App Settings.
There are three fields provided:
* `PAC script` - The URL associated with the PAC file.
* `Proxy rules` - Rules indicating which proxies to use.
* `Proxy bypass rules` - Rules indicating which URLs should
bypass the proxy settings.
For a typical setup where internet access is required to use an HTTP proxy,
but URLs on the local network should be accessed directly, configure as follows:
`Proxy rules = proxy.example.com`
Your HTTP proxy server
`Proxy bypass rules = *.example.com;10.0.0.0/8`
Directly connect to your own domain and private IP subnet
for more complex setups, read below to configure complex proxy rules and proxy bypass rules.
### Sets the proxy settings.
When `PAC script` and `Proxy rules` are provided together, the `Proxy rules`
option is ignored and `PAC script` configuration is applied.
The `Proxy rules` has to follow the rules below:
```
proxyRules = schemeProxies[";"<schemeProxies>]
schemeProxies = [<urlScheme>"="]<proxyURIList>
urlScheme = "http" | "https" | "ftp" | "socks"
proxyURIList = <proxyURL>[","<proxyURIList>]
proxyURL = [<proxyScheme>"://"]<proxyHost>[":"<proxyPort>]
```
For example:
* `http=foopy:80;ftp=foopy2` - Use HTTP proxy `foopy:80` for `http://` URLs, and
HTTP proxy `foopy2:80` for `ftp://` URLs.
* `foopy:80` - Use HTTP proxy `foopy:80` for all URLs.
* `foopy:80,bar,direct://` - Use HTTP proxy `foopy:80` for all URLs, failing
over to `bar` if `foopy:80` is unavailable, and after that using no proxy.
* `socks4://foopy` - Use SOCKS v4 proxy `foopy:1080` for all URLs.
* `http=foopy,socks5://bar.com` - Use HTTP proxy `foopy` for http URLs, and fail
over to the SOCKS5 proxy `bar.com` if `foopy` is unavailable.
* `http=foopy,direct://` - Use HTTP proxy `foopy` for http URLs, and use no
proxy if `foopy` is unavailable.
* `http=foopy;socks=foopy2` - Use HTTP proxy `foopy` for http URLs, and use
`socks4://foopy2` for all other URLs.
The `Proxy bypass rules` is a comma separated list of rules described below:
* `[ URL_SCHEME "://" ] HOSTNAME_PATTERN [ ":" <port> ]`
Match all hostnames that match the pattern HOSTNAME_PATTERN.
Examples:
"foobar.com", "*foobar.com", "*.foobar.com", "*foobar.com:99",
"https://x.*.y.com:99"
* `"." HOSTNAME_SUFFIX_PATTERN [ ":" PORT ]`
Match a particular domain suffix.
Examples:
".google.com", ".com", "http://.google.com"
* `[ SCHEME "://" ] IP_LITERAL [ ":" PORT ]`
Match URLs which are IP address literals.
Examples:
"127.0.1", "[0:0::1]", "[::1]", "http://[::1]:99"
* `IP_LITERAL "/" PREFIX_LENGHT_IN_BITS`
Match any URL that is to an IP literal that falls between the
given range. IP range is specified using CIDR notation.
Examples:
"192.168.1.1/16", "fefe:13::abc/33".
* `<local>`
Match local addresses. The meaning of `<local>` is whether the
host matches one of: "127.0.0.1", "::1", "localhost".
## Change App Preferences
## Reporting an Issue

68
how-to-install.md Normal file
View File

@@ -0,0 +1,68 @@
# How to install
**Note:** If you download from the [releases page](https://github.com/zulip/zulip-electron/releases), be careful what version you pick. Releases that end with `-beta` are beta releases and the rest are stable.
- **beta:** these releases are the right balance between getting new features early while staying away from nasty bugs.
- **stable:** these releases are more thoroughly tested; they receive new features later, but there's a lower chance that things will go wrong.
[LR]: https://github.com/zulip/zulip-electron/releases
## OS X
**DMG or zip**:
1. Download [Zulip-x.x.x.dmg][LR] or [Zulip-x.x.x-mac.zip][LR]
2. Open or unzip the file and drag the app into the `Applications` folder
3. Done! The app will update automatically
**Using brew**:
1. Run `brew cask install zulip` in your terminal
2. The app will be installed in your `Applications`
3. Done! The app will update automatically (you can also use `brew update && brew upgrade zulip`)
## Windows
**Installer (recommended)**:
1. Download [Zulip-Web-Setup-x.x.x.exe][LR]
2. Run the installer, wait until it finishes
3. Done! The app will update automatically
**Portable**:
1. Download [zulip-x.x.x-arch.nsis.7z][LR] [*here arch = ia32 (32-bit), x64 (64-bit)*]
2. Extract the zip wherever you want (e.g. a flash drive) and run the app from there
## Linux
**Ubuntu, Debian 8+ (deb package)**:
1. Download [Zulip-x.x.x-amd64.deb][LR]
2. Double click and install, or run `dpkg -i Zulip-x.x.x-amd64.deb` in the terminal
3. Start the app with your app launcher or by running `zulip` in a terminal
4. Done! The app will NOT update automatically, but you can still check for updates
**Other distros (Fedora, CentOS, Arch Linux etc)** :
1. Download Zulip-x.x.x-x86_64.AppImage[LR]
2. Make it executable using chmod a+x Zulip-x.x.x-x86_64.AppImage
3. Start the app with your app launcher
**You can also use `apt-get` (recommended)**:
* First download our signing key to make sure the deb you download is correct:
```
sudo apt-key adv --keyserver pool.sks-keyservers.net --recv 69AD12704E71A4803DCA3A682424BE5AE9BD10D9
```
* Add the repo to your apt source list :
```
echo "deb https://dl.bintray.com/zulip/debian/ beta main" |
sudo tee -a /etc/apt/sources.list.d/zulip.list
```
* Now install the client :
```
sudo apt-get update
sudo apt-get install zulip
```

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "1.3.0-beta",
"version": "1.4.0",
"main": "./app/main",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
@@ -26,9 +26,11 @@
"pack": "electron-builder --dir",
"dist": "electron-builder",
"mas": "electron-builder --mac mas",
"build:win": "electron-builder --win nsis-web --ia32 --x64",
"travis": "cd ./scripts && ./travis-build-test.sh"
},
"pre-commit": [
"test"
],
"build": {
"appId": "org.zulip.zulip-electron",
"asar": true,
@@ -42,7 +44,7 @@
"category": "public.app-category.productivity"
},
"linux": {
"category": "",
"category": "Chat;GNOME;GTK;Network;InstantMessaging",
"packageCategory": "GNOME;GTK;Network;InstantMessaging",
"description": "Zulip Desktop Client for Linux",
"target": [
@@ -74,7 +76,15 @@
]
},
"win": {
"target": "nsis",
"target": [
{
"target": "nsis-web",
"arch": [
"x64",
"ia32"
]
}
],
"icon": "build/icon.ico",
"publisherName": "Kandra Labs, Inc."
},
@@ -95,7 +105,7 @@
"devDependencies": {
"assert": "1.4.1",
"devtron": "1.4.0",
"electron-builder": "19.19.1",
"electron-builder": "19.27.3",
"electron": "1.6.11",
"electron-connect": "0.6.2",
"gulp": "3.9.1",
@@ -103,7 +113,8 @@
"chai-as-promised": "7.1.1",
"chai": "4.1.1",
"spectron": "3.7.2",
"xo": "0.18.2"
"xo": "0.18.2",
"pre-commit": "1.2.2"
},
"xo": {
"parserOptions": {
@@ -122,6 +133,7 @@
500
],
"no-warning-comments": 0,
"object-curly-spacing": 0,
"capitalized-comments": 0,
"no-else-return": 0,
"no-path-concat": 0,