mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-23 16:13:37 +00:00
Compare commits
354 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4adba0f4b4 | ||
|
ed590c26e3 | ||
|
6d10291a87 | ||
|
3fe3a3da85 | ||
|
ba64438a99 | ||
|
bcc27894c4 | ||
|
0dd0f593d1 | ||
|
f5e9342f78 | ||
|
083ccdf229 | ||
|
1261786db2 | ||
|
7f567f55c3 | ||
|
dcd2abca6e | ||
|
2fb9efb981 | ||
|
7245b6a110 | ||
|
bcb8ffb55f | ||
|
77094596a5 | ||
|
06ad44bdd7 | ||
|
e719ba139c | ||
|
9853e9226c | ||
|
f2c76b5ca3 | ||
|
e6dbff995b | ||
|
4578d4a5f7 | ||
|
4b895a2312 | ||
|
53c0428a3a | ||
|
0a1866abb5 | ||
|
ce862a4890 | ||
|
1b1ad2cd61 | ||
|
ead7a06308 | ||
|
6659dd5097 | ||
|
ed1f0f6d5b | ||
|
79acf8a6e1 | ||
|
8e0033f03e | ||
|
9144c2630d | ||
|
fae05fc3b1 | ||
|
73603a4fd2 | ||
|
a498ffc7d6 | ||
|
7afcf13401 | ||
|
89a292559d | ||
|
be14517caf | ||
|
3b6c5ae532 | ||
|
40e3ed0f2f | ||
|
5d988858b0 | ||
|
3a974136a3 | ||
|
6ed5a5309c | ||
|
80c37fabb8 | ||
|
79366e19df | ||
|
f409bb0449 | ||
|
45bdde951f | ||
|
6b627780f0 | ||
|
6f67553da5 | ||
|
2e710a9322 | ||
|
91f3afa8fe | ||
|
f784345495 | ||
|
67da435154 | ||
|
c89733610d | ||
|
8f272a67b5 | ||
|
f6c4a76138 | ||
|
b90a4c5254 | ||
|
a06e09e565 | ||
|
ad5bef821e | ||
|
58bbd7bf30 | ||
|
90d080dc96 | ||
|
ad3fcf585e | ||
|
4b8f216bab | ||
|
e620e0c428 | ||
|
50b3151b5d | ||
|
0c32756485 | ||
|
0c0835e364 | ||
|
9e962a5c44 | ||
|
a218f7ea64 | ||
|
13a7f7475a | ||
|
48b17a1549 | ||
|
653598fd9e | ||
|
ddbc282f49 | ||
|
992d92b06d | ||
|
45867ef15e | ||
|
6572c90d49 | ||
|
1ed0011c88 | ||
|
6dd79b205c | ||
|
538c18fa90 | ||
|
29e347c511 | ||
|
ad37a5e0a6 | ||
|
352b775e27 | ||
|
38cec25680 | ||
|
f77ab92202 | ||
|
e843a29316 | ||
|
85837242e7 | ||
|
9f6da5712e | ||
|
158685a869 | ||
|
288b1cb3f2 | ||
|
e24a966d48 | ||
|
306feb2eff | ||
|
f426c932b0 | ||
|
26172d8508 | ||
|
99da0a338f | ||
|
9599249b31 | ||
|
4bdd2564b7 | ||
|
4dcf22a53c | ||
|
0dc97648a0 | ||
|
787f097cf3 | ||
|
5481d55c66 | ||
|
2a052b2c38 | ||
|
3ed253d2e1 | ||
|
5f672fe404 | ||
|
10372787ac | ||
|
bcabb615b4 | ||
|
68acf2ec64 | ||
|
afd24035f4 | ||
|
4f28f6b935 | ||
|
52bd600690 | ||
|
c7ce2a8a99 | ||
|
9760b1bf98 | ||
|
d579c267f0 | ||
|
21f01d268a | ||
|
33782e0492 | ||
|
8d20568b7a | ||
|
06f38e92ce | ||
|
855e96e40e | ||
|
bb68720ab7 | ||
|
044d007a0c | ||
|
4ea95fe8e8 | ||
|
d17c685e4d | ||
|
bb174da59a | ||
|
a028b80adb | ||
|
3683511e60 | ||
|
bf359db7f4 | ||
|
949d786f1f | ||
|
5cd0c1ca4e | ||
|
86674991c1 | ||
|
8492cda092 | ||
|
647303c001 | ||
|
fe34f8adad | ||
|
f142a2eb4e | ||
|
43eaa3dd63 | ||
|
a3142713f7 | ||
|
876936125a | ||
|
7261fb6cef | ||
|
fba52e6dff | ||
|
23ac347fb9 | ||
|
082aebb1e0 | ||
|
3b18dc4df0 | ||
|
39d30b92fa | ||
|
733209e04e | ||
|
a68d697fc5 | ||
|
e52ece57df | ||
|
65681c7da9 | ||
|
52b6209905 | ||
|
0e37925418 | ||
|
1a31d2e431 | ||
|
c2fab2c7bc | ||
|
965b55ba5f | ||
|
2c7bb36f8e | ||
|
48b829b771 | ||
|
f13c28db73 | ||
|
19157c77e4 | ||
|
161cd80d38 | ||
|
65db4c8f39 | ||
|
d600d3a6ed | ||
|
5fdd971966 | ||
|
d77d39d6ad | ||
|
bf35db26bd | ||
|
4d8c8e08a0 | ||
|
b34681ece8 | ||
|
58f97038b1 | ||
|
6030d4c62a | ||
|
206f726e4a | ||
|
9a36fffaac | ||
|
30af4277e0 | ||
|
98d23aaae1 | ||
|
20feb9bd38 | ||
|
56090151c2 | ||
|
84a69ce455 | ||
|
2424f7a995 | ||
|
c3c60c98d6 | ||
|
24017631c0 | ||
|
82a9d13a3c | ||
|
c064322234 | ||
|
32542d500a | ||
|
5e8a971789 | ||
|
a7a9e96a58 | ||
|
347f6e50eb | ||
|
b873b358fe | ||
|
caf4545902 | ||
|
9c2b7eeb27 | ||
|
d25d71cb91 | ||
|
289417e5a9 | ||
|
fd549e44a6 | ||
|
ae1ffe7ccc | ||
|
a04e14545e | ||
|
9b928a16b7 | ||
|
b54cedbdbb | ||
|
3f9ba0a2bb | ||
|
9e8ec3b6d4 | ||
|
c3072854fd | ||
|
197e9a0520 | ||
|
ff84792374 | ||
|
a0ae29b34a | ||
|
f880683d9c | ||
|
d80b4a813c | ||
|
3567f6be6c | ||
|
03358b1e50 | ||
|
b098f9e616 | ||
|
d2fc5bd5e8 | ||
|
f5e15e3c85 | ||
|
f7002ecdf3 | ||
|
48e5396092 | ||
|
6901b5f128 | ||
|
df0adb373d | ||
|
1c0cf148f8 | ||
|
c644fa2778 | ||
|
7962ccf19a | ||
|
3ad73a1eaa | ||
|
4445baafa9 | ||
|
63cfcbbaf1 | ||
|
769972fc4b | ||
|
2c8cf2b959 | ||
|
068fe249ea | ||
|
d8c08c1c5b | ||
|
56189806a9 | ||
|
0f6e48c65f | ||
|
fa50243dbb | ||
|
9e4e5e9bfd | ||
|
c413a65f07 | ||
|
53c91e890a | ||
|
6e3017d5e7 | ||
|
1e57daa8bf | ||
|
f1ed6695fb | ||
|
c5d9eceb6d | ||
|
f2a7ce188d | ||
|
3dd9c89a0e | ||
|
697948c9d8 | ||
|
5150b7c57c | ||
|
ce88da3de9 | ||
|
0617f41a2d | ||
|
975bcbbe31 | ||
|
e44af311ce | ||
|
297ada5565 | ||
|
1fe64cb8d7 | ||
|
6557a7d606 | ||
|
9a0f6648ff | ||
|
3cd4890e60 | ||
|
dc287e7e57 | ||
|
ac0d998804 | ||
|
1d38ebdf05 | ||
|
d70783600d | ||
|
527196cdbb | ||
|
ffeb960851 | ||
|
a36b39ec73 | ||
|
2deb63b557 | ||
|
fcd97f3a32 | ||
|
67dc1e2a11 | ||
|
c37ae73392 | ||
|
9e667e3cb0 | ||
|
505fea0e91 | ||
|
70ff8db756 | ||
|
d6975f7b2a | ||
|
6cbea1acba | ||
|
d4decfb6af | ||
|
5bbf710529 | ||
|
b057bffe42 | ||
|
4e04c85258 | ||
|
bfbd9d4578 | ||
|
6594da6ddd | ||
|
38abf2deab | ||
|
0004152e18 | ||
|
e8be57d710 | ||
|
64e8419410 | ||
|
f35a3df63b | ||
|
9e15ed2699 | ||
|
cdc99cda26 | ||
|
7e08af5ced | ||
|
0ee85bea16 | ||
|
592584fcf4 | ||
|
8e1b7a0289 | ||
|
715cf8d86f | ||
|
0dc20cc66c | ||
|
c860832a73 | ||
|
27a29aeba6 | ||
|
d0bd7e1f1c | ||
|
72974b075f | ||
|
947bab657f | ||
|
5a2975ca4d | ||
|
499743e99d | ||
|
6bd4c44893 | ||
|
5b82a82313 | ||
|
0208e407f0 | ||
|
e3a622fc07 | ||
|
f39839618d | ||
|
3f93a3346f | ||
|
9ab75b9800 | ||
|
f600c4db0e | ||
|
4e5816697e | ||
|
dc3c446a46 | ||
|
4cd8efa396 | ||
|
541ba335ae | ||
|
3e226400c4 | ||
|
890d7caea5 | ||
|
45e7993d0c | ||
|
8210a7c472 | ||
|
9e6bb1b48f | ||
|
d3c2da7961 | ||
|
a5017456f2 | ||
|
76cd62d0c8 | ||
|
6b68217494 | ||
|
8148d83448 | ||
|
a55cda3b1f | ||
|
e381960206 | ||
|
091b641abb | ||
|
af0ec80998 | ||
|
5653a38d9b | ||
|
01ea3beb99 | ||
|
cd387aaf9c | ||
|
24b5e0412b | ||
|
8174f7b37e | ||
|
d3a5eceaf9 | ||
|
31f285eba9 | ||
|
b1365f9669 | ||
|
21351125fa | ||
|
50b9ec7220 | ||
|
46cf5c9a5c | ||
|
35a42f3873 | ||
|
14b9fcf8d7 | ||
|
47cfe86a06 | ||
|
57ad2d63e0 | ||
|
2fcc5d9649 | ||
|
45523f41aa | ||
|
15b3af7b97 | ||
|
81d3aa8a1b | ||
|
321860a232 | ||
|
1c290fc2cd | ||
|
a74b17b989 | ||
|
ffba6b68f8 | ||
|
b553b29328 | ||
|
d7b44b23d1 | ||
|
b991fac136 | ||
|
cda8aa3b09 | ||
|
1e60643ae9 | ||
|
d9fbcaf38e | ||
|
9e5a67c36b | ||
|
a16181be33 | ||
|
cd0a7741b1 | ||
|
6da523a18b | ||
|
86302308a9 | ||
|
b0294db133 | ||
|
252586cf71 | ||
|
52ea1a48fd | ||
|
98e73f807c | ||
|
f96dd6e6bc | ||
|
1511ce4610 | ||
|
c2db6fc0f0 | ||
|
937a193a61 | ||
|
3823ac7f78 | ||
|
878cc3fe82 | ||
|
eace637f29 |
31
.gitignore
vendored
31
.gitignore
vendored
@@ -1,10 +1,31 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
domain.json
|
||||
dist
|
||||
config.gypi
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# npm cache directory
|
||||
.npm
|
||||
|
||||
# Compiled binary build directory
|
||||
dist/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
// osx garbage
|
||||
*.DS_Store
|
||||
.DS_Store
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# miscellaneous
|
||||
.idea
|
||||
config.gypi
|
||||
|
||||
# Test generated files
|
||||
tests/package.json
|
||||
|
||||
.python-version
|
||||
|
@@ -1 +0,0 @@
|
||||
2.7.9
|
@@ -18,8 +18,9 @@ node_js:
|
||||
- '6'
|
||||
|
||||
before_install:
|
||||
- npm install -g gulp
|
||||
- npm install
|
||||
- ./scripts/travis-xvfb.sh
|
||||
- npm install -g gulp
|
||||
- npm install
|
||||
|
||||
cache:
|
||||
directories:
|
||||
@@ -33,4 +34,4 @@ notifications:
|
||||
urls:
|
||||
- https://zulip.org/zulipbot/travis
|
||||
on_success: always
|
||||
on_failure: always
|
||||
on_failure: always
|
||||
|
@@ -5,10 +5,10 @@
|
||||
|
||||
Desktop client for Zulip. Available for Mac, Linux and Windows.
|
||||
|
||||
<img src="http://i.imgur.com/bDtK47q.png"/>
|
||||
<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](https://zulipchat.com/help/desktop-app-install-guide).
|
||||
|
||||
# Features
|
||||
* Sign in to multiple teams
|
||||
|
@@ -1,27 +1,51 @@
|
||||
'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
|
||||
if (isDev) {
|
||||
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;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
16
app/main/crash-reporter.js
Normal file
16
app/main/crash-reporter.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'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
|
||||
};
|
@@ -1,23 +1,27 @@
|
||||
'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 Configstore = require('electron-config');
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
const isDev = require('electron-is-dev');
|
||||
const appMenu = require('./menu');
|
||||
const {appUpdater} = require('./autoupdater');
|
||||
const { appUpdater } = require('./autoupdater');
|
||||
const { crashHandler } = require('./crash-reporter');
|
||||
|
||||
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')();
|
||||
|
||||
const conf = new Configstore();
|
||||
|
||||
// Setting userAgent so that server-side code can identify the desktop app
|
||||
// in development mode
|
||||
if (isDev) {
|
||||
require('electron-debug')();
|
||||
}
|
||||
|
||||
// Prevent window being garbage collected
|
||||
let mainWindow;
|
||||
let badgeCount;
|
||||
|
||||
let isQuitting = false;
|
||||
|
||||
@@ -35,11 +39,7 @@ const isAlreadyRunning = app.makeSingleInstance(() => {
|
||||
});
|
||||
|
||||
if (isAlreadyRunning) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
function isWindowsOrmacOS() {
|
||||
return process.platform === 'darwin' || process.platform === 'win32';
|
||||
return app.quit();
|
||||
}
|
||||
|
||||
const APP_ICON = path.join(__dirname, '../resources', 'Icon');
|
||||
@@ -49,12 +49,23 @@ const iconPath = () => {
|
||||
};
|
||||
|
||||
function createMainWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
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',
|
||||
width: conf.get('width') || 1000,
|
||||
height: conf.get('height') || 600,
|
||||
icon: iconPath(),
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 600,
|
||||
minHeight: 500,
|
||||
webPreferences: {
|
||||
@@ -90,39 +101,12 @@ function createMainWindow() {
|
||||
|
||||
win.setTitle('Zulip');
|
||||
|
||||
// Let's save browser window position
|
||||
if (conf.get('x') || conf.get('y')) {
|
||||
win.setPosition(conf.get('x'), conf.get('y'));
|
||||
}
|
||||
|
||||
if (conf.get('maximize')) {
|
||||
win.maximize();
|
||||
}
|
||||
|
||||
// Handle sizing events so we can persist them.
|
||||
win.on('maximize', () => {
|
||||
conf.set('maximize', true);
|
||||
win.on('enter-full-screen', () => {
|
||||
win.webContents.send('enter-fullscreen');
|
||||
});
|
||||
|
||||
win.on('unmaximize', () => {
|
||||
conf.set('maximize', false);
|
||||
});
|
||||
|
||||
win.on('resize', function () {
|
||||
const size = this.getSize();
|
||||
conf.set({
|
||||
width: size[0],
|
||||
height: size[1]
|
||||
});
|
||||
});
|
||||
|
||||
// On osx it's 'moved'
|
||||
win.on('move', function () {
|
||||
const pos = this.getPosition();
|
||||
conf.set({
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
});
|
||||
win.on('leave-full-screen', () => {
|
||||
win.webContents.send('leave-fullscreen');
|
||||
});
|
||||
|
||||
// To destroy tray icon when navigate to a new URL
|
||||
@@ -132,6 +116,11 @@ function createMainWindow() {
|
||||
}
|
||||
});
|
||||
|
||||
// Let us register listeners on the window, so we can update the state
|
||||
// automatically (the listeners will be removed when the window is closed)
|
||||
// and restore the maximized or full screen state
|
||||
mainWindowState.manage(win);
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
@@ -141,11 +130,6 @@ app.on('certificate-error', (event, webContents, url, error, certificate, callba
|
||||
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();
|
||||
@@ -153,67 +137,93 @@ app.on('activate', () => {
|
||||
});
|
||||
|
||||
app.on('ready', () => {
|
||||
electron.Menu.setApplicationMenu(appMenu);
|
||||
appMenu.setMenu({
|
||||
tabs: []
|
||||
});
|
||||
mainWindow = createMainWindow();
|
||||
|
||||
const page = mainWindow.webContents;
|
||||
|
||||
// TODO - use global shortcut instead
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
|
||||
// page.send('reload');
|
||||
mainWindow.reload();
|
||||
// page.send('destroytray');
|
||||
});
|
||||
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => {
|
||||
page.send('back');
|
||||
});
|
||||
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+]', () => {
|
||||
page.send('forward');
|
||||
});
|
||||
|
||||
page.on('dom-ready', () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
page.once('did-frame-finish-load', () => {
|
||||
const checkOS = isWindowsOrmacOS();
|
||||
if (checkOS && !isDev) {
|
||||
// Initate auto-updates on MacOS and Windows
|
||||
appUpdater();
|
||||
}
|
||||
});
|
||||
electron.powerMonitor.on('resume', () => {
|
||||
mainWindow.reload();
|
||||
mainWindow.webContents.send('destroytray');
|
||||
// Initate auto-updates on MacOS and Windows
|
||||
appUpdater();
|
||||
crashHandler();
|
||||
});
|
||||
|
||||
ipc.on('focus-app', () => {
|
||||
electron.powerMonitor.on('resume', () => {
|
||||
page.send('reload-viewer');
|
||||
});
|
||||
|
||||
ipcMain.on('focus-app', () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
ipc.on('quit-app', () => {
|
||||
ipcMain.on('quit-app', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
ipc.on('reload-main', () => {
|
||||
page.reload();
|
||||
// Reload full app not just webview, useful in debugging
|
||||
ipcMain.on('reload-full-app', () => {
|
||||
mainWindow.reload();
|
||||
page.send('destroytray');
|
||||
});
|
||||
|
||||
ipc.on('update-badge', (event, messageCount) => {
|
||||
if (process.platform === 'darwin') {
|
||||
app.setBadgeCount(messageCount);
|
||||
ipcMain.on('clear-app-settings', () => {
|
||||
global.mainWindowState.unmanage(mainWindow);
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
});
|
||||
|
||||
ipcMain.on('toggle-app', () => {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
// Unregister all the shortcuts so that they don't interfare with other apps
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
ipcMain.on('update-taskbar-icon', (event, data, text) => {
|
||||
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
|
||||
});
|
||||
|
||||
ipcMain.on('forward-message', (event, listener, ...params) => {
|
||||
page.send(listener, ...params);
|
||||
});
|
||||
|
||||
ipcMain.on('update-menu', (event, props) => {
|
||||
appMenu.setMenu(props);
|
||||
});
|
||||
|
||||
ipcMain.on('register-server-tab-shortcut', (event, index) => {
|
||||
// Array index == Shown index - 1
|
||||
page.send('switch-server-tab', index - 1);
|
||||
});
|
||||
|
||||
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => {
|
||||
setAutoLaunch(AutoLaunchValue);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true;
|
||||
});
|
||||
|
||||
// Send crash reports
|
||||
process.on('uncaughtException', err => {
|
||||
console.error(err);
|
||||
console.error(err.stack);
|
||||
});
|
||||
|
567
app/main/menu.js
567
app/main/menu.js
@@ -1,376 +1,397 @@
|
||||
'use strict';
|
||||
const os = require('os');
|
||||
const electron = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
const {dialog} = require('electron');
|
||||
const { app, shell, BrowserWindow, Menu } = require('electron');
|
||||
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');
|
||||
|
||||
const app = electron.app;
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
const shell = electron.shell;
|
||||
const appName = app.getName();
|
||||
|
||||
function sendAction(action) {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
win.restore();
|
||||
class AppMenu {
|
||||
getHistorySubmenu() {
|
||||
return [{
|
||||
label: 'Back',
|
||||
accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('back');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Forward',
|
||||
accelerator: process.platform === 'darwin' ? 'Command+Right' : 'Alt+Right',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('forward');
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
win.webContents.send(action);
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
const ses = win.webContents.session;
|
||||
ses.clearCache(() => {
|
||||
dialog.showMessageBox({type: 'info', buttons: [], message: 'Cache cleared!'});
|
||||
});
|
||||
}
|
||||
|
||||
const viewSubmenu = [
|
||||
{
|
||||
label: 'Reload',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('reload');
|
||||
getViewSubmenu() {
|
||||
return [{
|
||||
label: 'Reload',
|
||||
accelerator: 'CommandOrControl+R',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('reload-current-viewer');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'togglefullscreen'
|
||||
},
|
||||
{
|
||||
label: 'Zoom In',
|
||||
accelerator: 'CommandOrControl+=',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('zoomIn');
|
||||
}, {
|
||||
label: 'Hard Reload',
|
||||
accelerator: 'CommandOrControl+Shift+R',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('hard-reload');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Zoom Out',
|
||||
accelerator: 'CommandOrControl+-',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('zoomOut');
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
role: 'togglefullscreen'
|
||||
}, {
|
||||
label: 'Zoom In',
|
||||
accelerator: 'CommandOrControl+=',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('zoomIn');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Actual Size',
|
||||
accelerator: 'CommandOrControl+0',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('zoomActualSize');
|
||||
}, {
|
||||
label: 'Zoom Out',
|
||||
accelerator: 'CommandOrControl+-',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('zoomOut');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Toggle Tray Icon',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.send('toggletray');
|
||||
}, {
|
||||
label: 'Actual Size',
|
||||
accelerator: 'CommandOrControl+0',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('zoomActualSize');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Toggle DevTools for Zulip App',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.toggleDevTools();
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Toggle Tray Icon',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.send('toggletray');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Toggle DevTools for Active Tab',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('tab-devtools');
|
||||
}, {
|
||||
label: 'Toggle Sidebar',
|
||||
accelerator: 'CommandOrControl+S',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
const newValue = !ConfigUtil.getConfigItem('showSidebar');
|
||||
focusedWindow.webContents.send('toggle-sidebar', newValue);
|
||||
ConfigUtil.setConfigItem('showSidebar', newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Toggle DevTools for Zulip App',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.toggleDevTools();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Toggle DevTools for Active Tab',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('tab-devtools');
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
const helpSubmenu = [
|
||||
{
|
||||
label: `${appName} Website`,
|
||||
click() {
|
||||
shell.openExternal('https://zulip.org');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: `${appName + 'Desktop'} - ${app.getVersion()}`,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: 'Report an Issue...',
|
||||
click() {
|
||||
const body = `
|
||||
<!-- Please succinctly describe your issue and steps to reproduce it. -->
|
||||
-
|
||||
${app.getName()} ${app.getVersion()}
|
||||
Electron ${process.versions.electron}
|
||||
${process.platform} ${process.arch} ${os.release()}`;
|
||||
|
||||
shell.openExternal(`https://github.com/zulip/zulip-electron/issues/new?body=${encodeURIComponent(body)}`);
|
||||
}
|
||||
getHelpSubmenu() {
|
||||
return [{
|
||||
label: `${appName} Website`,
|
||||
click() {
|
||||
shell.openExternal('https://zulipchat.com/help/');
|
||||
}
|
||||
}, {
|
||||
label: `${appName + 'Desktop'} - ${app.getVersion()}`,
|
||||
enabled: false
|
||||
}, {
|
||||
label: 'Report an Issue...',
|
||||
click() {
|
||||
const body = `
|
||||
<!-- Please succinctly describe your issue and steps to reproduce it. -->
|
||||
-
|
||||
${app.getName()} ${app.getVersion()}
|
||||
Electron ${process.versions.electron}
|
||||
${process.platform} ${process.arch} ${os.release()}`;
|
||||
shell.openExternal(`https://github.com/zulip/zulip-electron/issues/new?body=${encodeURIComponent(body)}`);
|
||||
}
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
const darwinTpl = [
|
||||
getWindowSubmenu(tabs, activeTabIndex) {
|
||||
const initialSubmenu = [{
|
||||
role: 'minimize'
|
||||
}, {
|
||||
role: 'close'
|
||||
}];
|
||||
|
||||
{
|
||||
label: `${app.getName()}`,
|
||||
submenu: [
|
||||
{
|
||||
label: 'Zulip desktop',
|
||||
if (tabs.length > 0) {
|
||||
const ShortcutKey = process.platform === 'darwin' ? 'Cmd' : 'Ctrl';
|
||||
initialSubmenu.push({
|
||||
type: 'separator'
|
||||
});
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
initialSubmenu.push({
|
||||
label: tabs[i].webview.props.name,
|
||||
accelerator: tabs[i].props.role === 'function' ? '' : `${ShortcutKey} + ${tabs[i].props.index + 1}`,
|
||||
checked: tabs[i].props.index === activeTabIndex,
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('switch-server-tab', tabs[i].props.index);
|
||||
}
|
||||
},
|
||||
type: 'radio'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return initialSubmenu;
|
||||
}
|
||||
|
||||
getDarwinTpl(props) {
|
||||
const { tabs, activeTabIndex } = props;
|
||||
|
||||
return [{
|
||||
label: `${app.getName()}`,
|
||||
submenu: [{
|
||||
label: 'About Zulip',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('open-about');
|
||||
AppMenu.sendAction('open-about');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Manage Zulip Servers',
|
||||
}, {
|
||||
label: 'Settings',
|
||||
accelerator: 'Cmd+,',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('open-settings');
|
||||
AppMenu.sendAction('open-settings');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Keyboard shortcuts',
|
||||
}, {
|
||||
label: 'Keyboard Shortcuts',
|
||||
accelerator: 'Cmd+K',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('shortcut');
|
||||
AppMenu.sendAction('shortcut');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Clear Cache',
|
||||
}, {
|
||||
label: 'Reset App Settings',
|
||||
accelerator: 'Command+Shift+D',
|
||||
click() {
|
||||
clearCache();
|
||||
AppMenu.resetAppSettings();
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
label: 'Log Out',
|
||||
accelerator: 'Cmd+L',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('log-out');
|
||||
AppMenu.sendAction('log-out');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'services',
|
||||
submenu: []
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'hide'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'hideothers'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'unhide'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'quit'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{
|
||||
}]
|
||||
}, {
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
role: 'undo'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'redo'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'cut'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'copy'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'paste'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'pasteandmatchstyle'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'delete'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'selectall'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: viewSubmenu
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{
|
||||
role: 'minimize'
|
||||
},
|
||||
{
|
||||
role: 'close'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'front'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: helpSubmenu
|
||||
}]
|
||||
}, {
|
||||
label: 'View',
|
||||
submenu: this.getViewSubmenu()
|
||||
}, {
|
||||
label: 'History',
|
||||
submenu: this.getHistorySubmenu()
|
||||
}, {
|
||||
label: 'Window',
|
||||
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
|
||||
}, {
|
||||
role: 'help',
|
||||
submenu: this.getHelpSubmenu()
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
const otherTpl = [
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Zulip desktop',
|
||||
getOtherTpl(props) {
|
||||
const { tabs, activeTabIndex } = props;
|
||||
|
||||
return [{
|
||||
label: 'File',
|
||||
submenu: [{
|
||||
label: 'About Zulip',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('open-about');
|
||||
AppMenu.sendAction('open-about');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Manage Zulip Servers',
|
||||
}, {
|
||||
label: 'Settings',
|
||||
accelerator: 'Ctrl+,',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('open-settings');
|
||||
AppMenu.sendAction('open-settings');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Keyboard shortcuts',
|
||||
}, {
|
||||
label: 'Keyboard Shortcuts',
|
||||
accelerator: 'Ctrl+K',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('shortcut');
|
||||
AppMenu.sendAction('shortcut');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Clear Cache',
|
||||
}, {
|
||||
label: 'Reset App Settings',
|
||||
accelerator: 'Ctrl+Shift+D',
|
||||
click() {
|
||||
clearCache();
|
||||
AppMenu.resetAppSettings();
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
label: 'Log Out',
|
||||
accelerator: 'Ctrl+L',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('log-out');
|
||||
AppMenu.sendAction('log-out');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'quit'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{
|
||||
}, {
|
||||
role: 'quit',
|
||||
accelerator: 'Ctrl+Q'
|
||||
}]
|
||||
}, {
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
role: 'undo'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'redo'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'cut'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'copy'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'paste'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'pasteandmatchstyle'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'delete'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'selectall'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: viewSubmenu
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: helpSubmenu
|
||||
}]
|
||||
}, {
|
||||
label: 'View',
|
||||
submenu: this.getViewSubmenu()
|
||||
}, {
|
||||
label: 'History',
|
||||
submenu: this.getHistorySubmenu()
|
||||
}, {
|
||||
label: 'Window',
|
||||
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
|
||||
}, {
|
||||
role: 'help',
|
||||
submenu: this.getHelpSubmenu()
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
const tpl = process.platform === 'darwin' ? darwinTpl : otherTpl;
|
||||
static sendAction(action, ...params) {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
module.exports = electron.Menu.buildFromTemplate(tpl);
|
||||
if (process.platform === 'darwin') {
|
||||
win.restore();
|
||||
}
|
||||
|
||||
win.webContents.send(action, ...params);
|
||||
}
|
||||
|
||||
static resetAppSettings() {
|
||||
// We save App's settings/configurations in following files
|
||||
const settingFiles = ['window-state.json', 'domain.json', 'settings.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');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setMenu(props) {
|
||||
const tpl = process.platform === 'darwin' ? this.getDarwinTpl(props) : this.getOtherTpl(props);
|
||||
const menu = Menu.buildFromTemplate(tpl);
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AppMenu();
|
||||
|
34
app/main/startup.js
Normal file
34
app/main/startup.js
Normal 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
|
||||
};
|
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "zulip",
|
||||
"productName": "Zulip",
|
||||
"version": "1.0.0-beta",
|
||||
"version": "1.7.0",
|
||||
"description": "Zulip Desktop App",
|
||||
"license": "Apache-2.0",
|
||||
"email": "<svnitakash@gmail.com>",
|
||||
"copyright": "©2017 Kandra Labs, Inc.",
|
||||
"author": {
|
||||
"name": "Akash Nimare",
|
||||
"name": "Kandra Labs, Inc.",
|
||||
"email": "svnitakash@gmail.com"
|
||||
},
|
||||
"repository": {
|
||||
@@ -27,16 +27,14 @@
|
||||
"InstantMessaging"
|
||||
],
|
||||
"dependencies": {
|
||||
"electron-config": "0.2.1",
|
||||
"electron-debug": "1.1.0",
|
||||
"electron-is-dev": "0.1.2",
|
||||
"electron-localshortcut": "1.0.0",
|
||||
"electron-log": "1.3.0",
|
||||
"electron-spellchecker": "1.0.8",
|
||||
"electron-updater": "1.11.2",
|
||||
"https": "^1.0.0",
|
||||
"electron-is-dev": "0.3.0",
|
||||
"electron-log": "2.2.7",
|
||||
"electron-spellchecker": "1.1.2",
|
||||
"electron-updater": "2.16.2",
|
||||
"node-json-db": "0.7.3",
|
||||
"request": "2.79.0",
|
||||
"wurl": "2.1.0"
|
||||
"request": "2.81.0",
|
||||
"wurl": "2.5.0",
|
||||
"electron-window-state": "4.1.1",
|
||||
"auto-launch": "5.0.1"
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,13 @@
|
||||
/*******************
|
||||
* General rules *
|
||||
*******************/
|
||||
html, body {
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
user-select:none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#content {
|
||||
@@ -16,7 +18,7 @@ html, body {
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
.toggle-sidebar {
|
||||
background: #222c31;
|
||||
width: 54px;
|
||||
padding: 27px 0 20px 0;
|
||||
@@ -24,43 +26,59 @@ html, body {
|
||||
display: flex;
|
||||
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 {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'), local('MaterialIcons-Regular'), url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
|
||||
/*******************
|
||||
* Left Sidebar *
|
||||
*******************/
|
||||
#tabs-container {
|
||||
* Left Sidebar *
|
||||
*******************/
|
||||
|
||||
#tabs-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
/* Preferred icon size */
|
||||
font-size: 24px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
/* Preferred icon size */
|
||||
font-size: 24px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
@@ -70,6 +88,10 @@ html, body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-button i {
|
||||
color: #6c8592;
|
||||
font-size: 28px;
|
||||
@@ -79,54 +101,65 @@ html, body {
|
||||
color: #98a9b3;
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
margin: 5px 0;
|
||||
cursor: pointer;
|
||||
.tab:first-child {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tab.active::before {
|
||||
content: "";
|
||||
background: #fff;
|
||||
border-radius: 0 3px 3px 0;
|
||||
width: 4px;
|
||||
position: absolute;
|
||||
height: 35px;
|
||||
left: -10px;
|
||||
top: 5px;
|
||||
.tab {
|
||||
position: relative;
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab .server-icons {
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
padding: 3px;
|
||||
height: 30px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.tab .server-tab {
|
||||
background: #a4d3c4;
|
||||
background-size: 100%;
|
||||
border-radius: 4px;
|
||||
width: 35px;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
position: relative;
|
||||
margin: 5px 0;
|
||||
margin: 5px 0 2px 0;
|
||||
z-index: 11;
|
||||
line-height: 31px;
|
||||
color: #194a2b;
|
||||
color: #eee;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
opacity: 0.6;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.tab .server-tab:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tab .functional-tab {
|
||||
background: #eee;
|
||||
.tab.functional-tab {
|
||||
height: 46px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tab .functional-tab i {
|
||||
.tab.functional-tab.active .server-tab {
|
||||
padding: 2px 0;
|
||||
height: 40px;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.tab.functional-tab .server-tab i {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.tab.active .server-tab {
|
||||
opacity: 1;
|
||||
background-color: #648478;
|
||||
}
|
||||
|
||||
.tab .server-tab-badge.active {
|
||||
@@ -138,14 +171,15 @@ html, body {
|
||||
font-size: 10px;
|
||||
font-family: sans-serif;
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
right: 5px;
|
||||
z-index: 15;
|
||||
top: -2px;
|
||||
top: 6px;
|
||||
float: right;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 17px;
|
||||
display: block;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tab .server-tab-badge {
|
||||
@@ -154,7 +188,7 @@ html, body {
|
||||
|
||||
.tab .server-tab-badge.close-button {
|
||||
width: 16px;
|
||||
padding: 0 0 0 1px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tab .server-tab-badge.close-button i {
|
||||
@@ -162,23 +196,170 @@ html, body {
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
#add-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab .server-tab-shortcut {
|
||||
color: #648478;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-family: sans-serif;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
||||
/*******************
|
||||
* Webview Area *
|
||||
*******************/
|
||||
* Webview Area *
|
||||
*******************/
|
||||
|
||||
#webviews-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
webview {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s;
|
||||
transition: opacity 0.3s ease-in;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
webview.onload {
|
||||
transition: opacity 1s cubic-bezier(0.95, 0.05, 0.795, 0.035);
|
||||
}
|
||||
|
||||
webview.disabled {
|
||||
flex: 0 1;
|
||||
height: 0;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
webview:focus {
|
||||
outline: 0px solid transparent;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip styling */
|
||||
|
||||
#reload-tooltip,
|
||||
#setting-tooltip {
|
||||
font-family: sans-serif;
|
||||
background: #222c31;
|
||||
margin-left: 68px;
|
||||
padding: 6px 8px;
|
||||
position: absolute;
|
||||
margin-top: 0px;
|
||||
z-index: 1000;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: 55px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#reload-tooltip:after,
|
||||
#setting-tooltip:after {
|
||||
content: " ";
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-right: 8px solid #222c31;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
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;
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #222c31;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
box-shadow: #999 1px 1px;
|
||||
}
|
||||
|
||||
#collapse-button i {
|
||||
color: #efefef;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Full screen Popup container */
|
||||
|
||||
.popup .popuptext {
|
||||
visibility: hidden;
|
||||
background-color: #555;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 9px 0;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
font-family: arial;
|
||||
width: 240px;
|
||||
top: 15px;
|
||||
height: 20px;
|
||||
left: 43%;
|
||||
}
|
||||
|
||||
.popup .show {
|
||||
visibility: visible;
|
||||
animation: cssAnimation 0s ease-in 5s forwards;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes cssAnimation {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
42
app/renderer/css/network.css
Normal file
42
app/renderer/css/network.css
Normal file
@@ -0,0 +1,42 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
user-select:none;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: "Trebuchet MS", Helvetica, sans-serif;
|
||||
margin: 100px 200px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#description {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#reconnect {
|
||||
font-size: 16px;
|
||||
background: #009688;
|
||||
color: #fff;
|
||||
width: 84px;
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
line-height: 32px;
|
||||
margin: 20px auto 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#reconnect:hover {
|
||||
opacity: 0.8;
|
||||
}
|
@@ -1,16 +1,89 @@
|
||||
html, body {
|
||||
html,
|
||||
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: #fff;
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.3em 0.8em;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 15px;
|
||||
font-family: Courier New, Courier, monospace;
|
||||
background-color: #383430;
|
||||
color: #ededed;
|
||||
display: inline-block;
|
||||
margin: 0 0.1em;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
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; }
|
||||
|
||||
|
||||
td { padding: 5px; }
|
||||
|
||||
td:nth-child(odd) {
|
||||
text-align: right;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
src: url(../fonts/Montserrat-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
/* Preferred icon size */
|
||||
font-size: 24px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
font-family: 'Montserrat';
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
@@ -21,23 +94,23 @@ html, body {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#tabs-container {
|
||||
#nav-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
.nav {
|
||||
padding: 5px 0;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
.nav.active {
|
||||
color: #464e5a;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab.active::before {
|
||||
.nav.active::before {
|
||||
background: #464e5a;
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
@@ -48,7 +121,9 @@ html, body {
|
||||
|
||||
#settings-header {
|
||||
font-size: 22px;
|
||||
color: #5c6166;
|
||||
color: #222c31;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#settings-container {
|
||||
@@ -58,57 +133,75 @@ html, body {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.server-info-container {
|
||||
margin: 20px 0;
|
||||
#new-server-container {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 4px 0 6px 0;
|
||||
font-size: 18px;
|
||||
color: #000;
|
||||
font-weight: 500;
|
||||
color: #222c31;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
padding: 4px 0 6px 0;
|
||||
font-weight: bold;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
img.server-info-icon {
|
||||
background: #a4d3c4;
|
||||
background-size: 100%;
|
||||
border-radius: 4px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.server-info-left {
|
||||
margin-right: 20px;
|
||||
margin: 10px 20px 0 0;
|
||||
}
|
||||
|
||||
.server-info-right {
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.server-info-row {
|
||||
display: flex;
|
||||
line-height: 26px;
|
||||
height: 40px;
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.server-info-key {
|
||||
width: 40px;
|
||||
margin-right: 20px;
|
||||
text-align: right;
|
||||
.server-info-alias {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.server-info-value {
|
||||
.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;
|
||||
border: none;
|
||||
border-bottom: #ddd 1px solid;
|
||||
border-bottom: #ededed 1px solid;
|
||||
outline-width: 0;
|
||||
background: transparent;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.server-info-value:focus {
|
||||
border-bottom: #b0d8ce 2px solid;
|
||||
.setting-input-value:focus {
|
||||
border-bottom: #7cb980 1px solid;
|
||||
}
|
||||
|
||||
.setting-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
@@ -123,12 +216,14 @@ img.server-info-icon {
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
padding: 0 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.action i {
|
||||
margin-right: 5px;
|
||||
font-size: 18px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.settings-pane {
|
||||
@@ -147,16 +242,147 @@ img.server-info-icon {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.server-info.active {
|
||||
background: #ecf4ef;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
.settings-card {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
margin: 10px 0 10px 0;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 30px;
|
||||
margin: 10px 0 20px 0;
|
||||
background: #fff;
|
||||
width: 70%;
|
||||
border-left: 8px solid #bcbcbc;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
margin: 5px !important;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #ffffff;
|
||||
background: #ef5350;
|
||||
padding: 3px;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: #ffffff;
|
||||
background: #4EBFAC;
|
||||
padding: 3px;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.grey {
|
||||
color: #9E9E9E;
|
||||
background: #FAFAFA;
|
||||
border: 1px solid #9E9E9E;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: Courier New, Courier, monospace;
|
||||
}
|
||||
|
||||
i.open-tab-button {
|
||||
padding: 0 5px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reset-data-button {
|
||||
display: inline-block;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
width: 120px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background-color 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.reset-data-button:hover {
|
||||
background-color: #3c9f8d;
|
||||
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;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#open-create-org-link: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;
|
||||
}
|
9
app/renderer/css/preload.css
Normal file
9
app/renderer/css/preload.css
Normal file
@@ -0,0 +1,9 @@
|
||||
/* Override css rules */
|
||||
|
||||
.portico-wrap>.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.portico-container>.footer {
|
||||
display: none;
|
||||
}
|
BIN
app/renderer/fonts/Montserrat-Regular.ttf
Normal file
BIN
app/renderer/fonts/Montserrat-Regular.ttf
Normal file
Binary file not shown.
BIN
app/renderer/img/zulip_network.png
Normal file
BIN
app/renderer/img/zulip_network.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
@@ -4,11 +4,11 @@ const Tab = require(__dirname + '/../components/tab.js');
|
||||
|
||||
class FunctionalTab extends Tab {
|
||||
template() {
|
||||
return `<div class="tab">
|
||||
return `<div class="tab functional-tab">
|
||||
<div class="server-tab-badge close-button">
|
||||
<i class="material-icons">close</i>
|
||||
</div>
|
||||
<div class="server-tab functional-tab">
|
||||
<div class="server-tab">
|
||||
<i class="material-icons">${this.props.materialIcon}</i>
|
||||
</div>
|
||||
</div>`;
|
||||
|
@@ -1,12 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
const Tab = require(__dirname + '/../components/tab.js');
|
||||
const SystemUtil = require(__dirname + '/../utils/system-util.js');
|
||||
|
||||
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" style="background-image: url(${this.props.icon});"></div>
|
||||
<div class="server-tab">
|
||||
<img class="server-icons" src='${this.props.icon}'/>
|
||||
</div>
|
||||
<div class="server-tab-shortcut">${this.generateShortcutText()}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -26,6 +33,27 @@ class ServerTab extends Tab {
|
||||
this.$badge.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
generateShortcutText() {
|
||||
// Only provide shortcuts for server [0..10]
|
||||
if (this.props.index >= 10) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const shownIndex = this.props.index + 1;
|
||||
|
||||
let shortcutText = '';
|
||||
|
||||
if (SystemUtil.getOS() === 'Mac') {
|
||||
shortcutText = `⌘ ${shownIndex}`;
|
||||
} else {
|
||||
shortcutText = `Ctrl+${shownIndex}`;
|
||||
}
|
||||
|
||||
ipcRenderer.send('register-server-tab-shortcut', shownIndex);
|
||||
|
||||
return shortcutText;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServerTab;
|
||||
|
@@ -21,6 +21,8 @@ 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() {
|
||||
@@ -43,7 +45,4 @@ class Tab extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
Tab.SERVER_TAB = 0;
|
||||
Tab.SETTINGS_TAB = 1;
|
||||
|
||||
module.exports = Tab;
|
||||
|
@@ -1,10 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
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 {app, dialog, shell} = require('electron').remote;
|
||||
const {ipcRenderer} = require('electron');
|
||||
const { shell, app } = require('electron').remote;
|
||||
|
||||
const BaseComponent = require(__dirname + '/../components/base.js');
|
||||
|
||||
@@ -25,7 +27,8 @@ class WebView extends BaseComponent {
|
||||
src="${this.props.url}"
|
||||
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
|
||||
disablewebsecurity
|
||||
preload="js/preload.js"
|
||||
${this.props.preload ? 'preload="js/preload.js"' : ''}
|
||||
partition="persist:webviewsession"
|
||||
webpreferences="allowRunningInsecureContent, javascript=yes">
|
||||
</webview>`;
|
||||
}
|
||||
@@ -39,10 +42,10 @@ 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)) {
|
||||
if (LinkUtil.isInternal(domainPrefix, url) || url === (domainPrefix + '/')) {
|
||||
event.preventDefault();
|
||||
this.$el.loadURL(url);
|
||||
} else {
|
||||
@@ -52,19 +55,34 @@ 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('dom-ready', this.show.bind(this));
|
||||
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');
|
||||
}
|
||||
this.show();
|
||||
});
|
||||
|
||||
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);
|
||||
this.checkConnectivity();
|
||||
this.props.onNetworkError();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -90,13 +108,25 @@ class WebView extends BaseComponent {
|
||||
}
|
||||
|
||||
this.$el.classList.remove('disabled');
|
||||
setTimeout(() => {
|
||||
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'));
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.$el.focus();
|
||||
// focus Webview and it's contents when Window regain focus.
|
||||
const webContents = this.$el.getWebContents();
|
||||
if (webContents && !webContents.isFocused()) {
|
||||
this.$el.focus();
|
||||
webContents.focus();
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
@@ -111,25 +141,6 @@ class WebView extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
checkConnectivity() {
|
||||
return dialog.showMessageBox({
|
||||
title: 'Internet connection problem',
|
||||
message: 'No internet available! Try again?',
|
||||
type: 'warning',
|
||||
buttons: ['Try again', 'Close'],
|
||||
defaultId: 0
|
||||
}, index => {
|
||||
if (index === 0) {
|
||||
this.reload();
|
||||
ipcRenderer.send('reload');
|
||||
ipcRenderer.send('destroytray');
|
||||
}
|
||||
if (index === 1) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
zoomIn() {
|
||||
this.zoomFactor += 0.1;
|
||||
this.$el.setZoomFactor(this.zoomFactor);
|
||||
@@ -173,6 +184,10 @@ class WebView extends BaseComponent {
|
||||
this.hide();
|
||||
this.$el.reload();
|
||||
}
|
||||
|
||||
send(...param) {
|
||||
this.$el.send(...param);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebView;
|
||||
|
@@ -1,22 +1,36 @@
|
||||
'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');
|
||||
const ServerTab = require(__dirname + '/js/components/server-tab.js');
|
||||
const FunctionalTab = require(__dirname + '/js/components/functional-tab.js');
|
||||
const ConfigUtil = require(__dirname + '/js/utils/config-util.js');
|
||||
|
||||
class ServerManagerView {
|
||||
constructor() {
|
||||
this.$addServerButton = document.getElementById('add-tab');
|
||||
this.$tabsContainer = document.getElementById('tabs-container');
|
||||
|
||||
const $actionsContainer = document.getElementById('actions-container');
|
||||
this.$reloadButton = $actionsContainer.querySelector('#reload-action');
|
||||
this.$addServerButton = $actionsContainer.querySelector('#add-action');
|
||||
this.$settingsButton = $actionsContainer.querySelector('#settings-action');
|
||||
this.$content = document.getElementById('content');
|
||||
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');
|
||||
this.$fullscreenEscapeKey = process.platform === 'darwin' ? '^⌘F' : 'F11';
|
||||
this.$fullscreenPopup.innerHTML = `Press ${this.$fullscreenEscapeKey} to exit full screen`;
|
||||
|
||||
this.activeTabIndex = -1;
|
||||
this.tabs = [];
|
||||
@@ -24,9 +38,36 @@ class ServerManagerView {
|
||||
}
|
||||
|
||||
init() {
|
||||
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('showSidebar', true);
|
||||
this.toggleSidebar(showSidebar);
|
||||
}
|
||||
|
||||
initTabs() {
|
||||
@@ -34,28 +75,37 @@ 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);
|
||||
// Open last active tab
|
||||
this.activateTab(ConfigUtil.getConfigItem('lastActiveTab'));
|
||||
} else {
|
||||
this.openSettings();
|
||||
this.openSettings('Servers');
|
||||
}
|
||||
}
|
||||
|
||||
initServer(server, index) {
|
||||
this.tabs.push(new ServerTab({
|
||||
role: 'server',
|
||||
icon: server.icon,
|
||||
$root: this.$tabsContainer,
|
||||
onClick: this.activateTab.bind(this, index),
|
||||
onClick: this.activateLastTab.bind(this, index),
|
||||
index,
|
||||
onHover: this.onHover.bind(this, index, server.alias),
|
||||
onHoverOut: this.onHoverOut.bind(this, index),
|
||||
webview: new WebView({
|
||||
$root: this.$content,
|
||||
$root: this.$webviewsContainer,
|
||||
index,
|
||||
url: server.url,
|
||||
name: server.alias,
|
||||
isActive: () => {
|
||||
return index === this.activeTabIndex;
|
||||
},
|
||||
onNetworkError: this.openNetworkTroubleshooting.bind(this),
|
||||
onTitleChange: this.updateBadge.bind(this),
|
||||
nodeIntegration: false
|
||||
nodeIntegration: false,
|
||||
preload: true
|
||||
})
|
||||
}));
|
||||
}
|
||||
@@ -64,12 +114,38 @@ class ServerManagerView {
|
||||
this.$reloadButton.addEventListener('click', () => {
|
||||
this.tabs[this.activeTabIndex].webview.reload();
|
||||
});
|
||||
this.$addServerButton.addEventListener('click', this.openSettings.bind(this));
|
||||
this.$settingsButton.addEventListener('click', this.openSettings.bind(this));
|
||||
this.$addServerButton.addEventListener('click', () => {
|
||||
this.openSettings('Servers');
|
||||
});
|
||||
this.$settingsButton.addEventListener('click', () => {
|
||||
this.openSettings('General');
|
||||
});
|
||||
|
||||
this.sidebarHoverEvent(this.$addServerButton, this.$addServerTooltip);
|
||||
this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip);
|
||||
this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip);
|
||||
}
|
||||
|
||||
sidebarHoverEvent(SidebarButton, SidebarTooltip) {
|
||||
SidebarButton.addEventListener('mouseover', () => {
|
||||
SidebarTooltip.removeAttribute('style');
|
||||
});
|
||||
SidebarButton.addEventListener('mouseout', () => {
|
||||
SidebarTooltip.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
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]) {
|
||||
if (this.functionalTabs[tabProps.name] !== undefined) {
|
||||
this.activateTab(this.functionalTabs[tabProps.name]);
|
||||
return;
|
||||
}
|
||||
@@ -77,32 +153,37 @@ class ServerManagerView {
|
||||
this.functionalTabs[tabProps.name] = this.tabs.length;
|
||||
|
||||
this.tabs.push(new FunctionalTab({
|
||||
role: 'function',
|
||||
materialIcon: tabProps.materialIcon,
|
||||
$root: this.$tabsContainer,
|
||||
index: this.functionalTabs[tabProps.name],
|
||||
onClick: this.activateTab.bind(this, this.functionalTabs[tabProps.name]),
|
||||
onDestroy: this.destroyTab.bind(this, tabProps.name, this.functionalTabs[tabProps.name]),
|
||||
webview: new WebView({
|
||||
$root: this.$content,
|
||||
$root: this.$webviewsContainer,
|
||||
index: this.functionalTabs[tabProps.name],
|
||||
url: tabProps.url,
|
||||
name: tabProps.name,
|
||||
isActive: () => {
|
||||
return this.functionalTabs[tabProps.name] === this.activeTabIndex;
|
||||
},
|
||||
onNetworkError: this.openNetworkTroubleshooting.bind(this),
|
||||
onTitleChange: this.updateBadge.bind(this),
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
preload: false
|
||||
})
|
||||
}));
|
||||
|
||||
this.activateTab(this.functionalTabs[tabProps.name]);
|
||||
}
|
||||
|
||||
openSettings() {
|
||||
openSettings(nav = 'General') {
|
||||
this.openFunctionalTab({
|
||||
name: 'Settings',
|
||||
materialIcon: 'settings',
|
||||
url: `file://${__dirname}/preference.html`
|
||||
url: `file://${__dirname}/preference.html#${nav}`
|
||||
});
|
||||
this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav);
|
||||
}
|
||||
|
||||
openAbout() {
|
||||
@@ -113,8 +194,23 @@ class ServerManagerView {
|
||||
});
|
||||
}
|
||||
|
||||
openNetworkTroubleshooting() {
|
||||
this.openFunctionalTab({
|
||||
name: 'Network Troubleshooting',
|
||||
materialIcon: 'network_check',
|
||||
url: `file://${__dirname}/network.html`
|
||||
});
|
||||
}
|
||||
|
||||
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].loading) {
|
||||
if (!this.tabs[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -128,10 +224,15 @@ class ServerManagerView {
|
||||
|
||||
this.activeTabIndex = index;
|
||||
this.tabs[index].activate();
|
||||
|
||||
ipcRenderer.send('update-menu', {
|
||||
tabs: this.tabs,
|
||||
activeTabIndex: this.activeTabIndex
|
||||
});
|
||||
}
|
||||
|
||||
destroyTab(name, index) {
|
||||
if (this.tabs[index].loading) {
|
||||
if (this.tabs[index].webview.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -140,7 +241,37 @@ class ServerManagerView {
|
||||
delete this.tabs[index];
|
||||
delete this.functionalTabs[name];
|
||||
|
||||
this.activateTab(0, false);
|
||||
// Issue #188: If the functional tab was not focused, do not activate another tab.
|
||||
if (this.activeTabIndex === index) {
|
||||
this.activateTab(0, false);
|
||||
}
|
||||
}
|
||||
|
||||
destroyView() {
|
||||
// Clear global variables
|
||||
this.activeTabIndex = -1;
|
||||
this.tabs = [];
|
||||
this.functionalTabs = {};
|
||||
|
||||
// Clear DOM elements
|
||||
this.$tabsContainer.innerHTML = '';
|
||||
this.$webviewsContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -156,6 +287,14 @@ class ServerManagerView {
|
||||
ipcRenderer.send('update-badge', messageCountAll);
|
||||
}
|
||||
|
||||
toggleSidebar(show) {
|
||||
if (show) {
|
||||
this.$sidebar.classList.remove('sidebar-hide');
|
||||
} else {
|
||||
this.$sidebar.classList.add('sidebar-hide');
|
||||
}
|
||||
}
|
||||
|
||||
registerIpcs() {
|
||||
const webviewListeners = {
|
||||
'webview-reload': 'reload',
|
||||
@@ -179,12 +318,85 @@ class ServerManagerView {
|
||||
});
|
||||
}
|
||||
|
||||
ipcRenderer.on('open-settings', this.openSettings.bind(this));
|
||||
ipcRenderer.on('open-settings', (event, settingNav) => {
|
||||
this.openSettings(settingNav);
|
||||
});
|
||||
|
||||
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('hard-reload', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
ipcRenderer.on('enter-fullscreen', () => {
|
||||
this.$fullscreenPopup.classList.add('show');
|
||||
this.$fullscreenPopup.classList.remove('hidden');
|
||||
});
|
||||
|
||||
ipcRenderer.on('leave-fullscreen', () => {
|
||||
this.$fullscreenPopup.classList.remove('show');
|
||||
});
|
||||
|
||||
ipcRenderer.on('render-taskbar-icon', (event, messageCount) => {
|
||||
// Create a canvas from unread messagecounts
|
||||
function createOverlayIcon(messageCount) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = 128;
|
||||
canvas.width = 128;
|
||||
canvas.style.letterSpacing = '-5px';
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#f42020';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = 'white';
|
||||
if (messageCount > 99) {
|
||||
ctx.font = '65px Helvetica';
|
||||
ctx.fillText('99+', 64, 85);
|
||||
} else if (messageCount < 10) {
|
||||
ctx.font = '90px Helvetica';
|
||||
ctx.fillText(String(Math.min(99, messageCount)), 64, 96);
|
||||
} else {
|
||||
ctx.font = '85px Helvetica';
|
||||
ctx.fillText(String(Math.min(99, messageCount)), 64, 90);
|
||||
}
|
||||
return canvas;
|
||||
}
|
||||
ipcRenderer.send('update-taskbar-icon', createOverlayIcon(messageCount).toDataURL(), String(messageCount));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
const serverManagerView = new ServerManagerView();
|
||||
serverManagerView.init();
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
serverManagerView.reloadView();
|
||||
});
|
||||
};
|
||||
|
30
app/renderer/js/notification.js
Normal file
30
app/renderer/js/notification.js
Normal file
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const { remote } = require('electron');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/utils/config-util.js');
|
||||
|
||||
const app = remote.app;
|
||||
|
||||
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
|
||||
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
|
||||
app.setAppUserModelId('org.zulip.zulip-electron');
|
||||
|
||||
const NativeNotification = window.Notification;
|
||||
|
||||
class baseNotification extends NativeNotification {
|
||||
constructor(title, opts) {
|
||||
opts.silent = ConfigUtil.getConfigItem('silent') || false;
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
window.Notification = baseNotification;
|
||||
|
20
app/renderer/js/pages/network.js
Normal file
20
app/renderer/js/pages/network.js
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
class NetworkTroubleshootingView {
|
||||
constructor() {
|
||||
this.$reconnectButton = document.getElementById('reconnect');
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$reconnectButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
const networkTroubleshootingView = new NetworkTroubleshootingView();
|
||||
networkTroubleshootingView.init();
|
||||
};
|
@@ -1,144 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
const DomainUtil = require(__dirname + '/js/utils/domain-util.js');
|
||||
|
||||
class PreferenceView {
|
||||
constructor() {
|
||||
this.$newServerButton = document.getElementById('new-server-action');
|
||||
this.$saveServerButton = document.getElementById('save-server-action');
|
||||
this.$reloadServerButton = document.getElementById('reload-server-action');
|
||||
this.$serverInfoContainer = document.querySelector('.server-info-container');
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initServers();
|
||||
this.initActions();
|
||||
}
|
||||
|
||||
initServers() {
|
||||
const servers = DomainUtil.getDomains();
|
||||
this.$serverInfoContainer.innerHTML = servers.length ? '' : 'Add your first server to get started!';
|
||||
|
||||
this.initNewServerForm();
|
||||
|
||||
for (const i in servers) {
|
||||
this.initServer(servers[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
initServer(server, index) {
|
||||
const {
|
||||
alias,
|
||||
url,
|
||||
icon
|
||||
} = server;
|
||||
const serverInfoTemplate = `
|
||||
<div class="server-info">
|
||||
<div class="server-info-left">
|
||||
<img class="server-info-icon" src="${icon}"/>
|
||||
</div>
|
||||
<div class="server-info-right">
|
||||
<div class="server-info-row">
|
||||
<span class="server-info-key">Name</span>
|
||||
<input class="server-info-value" disabled value="${alias}"/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<span class="server-info-key">Url</span>
|
||||
<input class="server-info-value" disabled value="${url}"/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<span class="server-info-key">Icon</span>
|
||||
<input class="server-info-value" disabled value="${icon}"/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<span class="server-info-key">Actions</span>
|
||||
<div class="action server-info-value" id="delete-server-action-${index}">
|
||||
<i class="material-icons">indeterminate_check_box</i>
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
this.$serverInfoContainer.appendChild(this.insertNode(serverInfoTemplate));
|
||||
document.getElementById(`delete-server-action-${index}`).addEventListener('click', () => {
|
||||
DomainUtil.removeDomain(index);
|
||||
this.initServers();
|
||||
// alert('Success. Reload to apply changes.');
|
||||
ipcRenderer.send('reload-main');
|
||||
this.$reloadServerButton.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
initNewServerForm() {
|
||||
const newServerFormTemplate = `
|
||||
<div class="server-info active hidden">
|
||||
<div class="server-info-left">
|
||||
<img class="server-info-icon" src="https://chat.zulip.org/static/images/logo/zulip-icon-128x128.271d0f6a0ca2.png"/>
|
||||
</div>
|
||||
<div class="server-info-right">
|
||||
<div class="server-info-row">
|
||||
<span class="server-info-key">Name</span>
|
||||
<input id="server-info-name" class="server-info-value" placeholder="(Required)"/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<span class="server-info-key">Url</span>
|
||||
<input id="server-info-url" spellcheck="false" class="server-info-value" placeholder="(Required)"/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<span class="server-info-key">Icon</span>
|
||||
<input id="server-info-icon" class="server-info-value" placeholder="(Optional)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.$serverInfoContainer.appendChild(this.insertNode(newServerFormTemplate));
|
||||
|
||||
this.$newServerForm = document.querySelector('.server-info.active');
|
||||
this.$newServerAlias = this.$newServerForm.querySelectorAll('input.server-info-value')[0];
|
||||
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.server-info-value')[1];
|
||||
this.$newServerIcon = this.$newServerForm.querySelectorAll('input.server-info-value')[2];
|
||||
}
|
||||
|
||||
initActions() {
|
||||
this.$newServerButton.addEventListener('click', () => {
|
||||
this.$newServerForm.classList.remove('hidden');
|
||||
this.$saveServerButton.classList.remove('hidden');
|
||||
this.$newServerButton.classList.add('hidden');
|
||||
});
|
||||
this.$saveServerButton.addEventListener('click', () => {
|
||||
DomainUtil.checkDomain(this.$newServerUrl.value).then(domain => {
|
||||
const server = {
|
||||
alias: this.$newServerAlias.value,
|
||||
url: domain,
|
||||
icon: this.$newServerIcon.value
|
||||
};
|
||||
DomainUtil.addDomain(server);
|
||||
this.$saveServerButton.classList.add('hidden');
|
||||
this.$newServerButton.classList.remove('hidden');
|
||||
this.$newServerForm.classList.add('hidden');
|
||||
|
||||
this.initServers();
|
||||
// alert('Success. Reload to apply changes.');
|
||||
ipcRenderer.send('reload-main');
|
||||
this.$reloadServerButton.classList.remove('hidden');
|
||||
}, errorMessage => {
|
||||
alert(errorMessage);
|
||||
});
|
||||
});
|
||||
this.$reloadServerButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('reload-main');
|
||||
});
|
||||
}
|
||||
insertNode(html) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = html;
|
||||
return wrapper.firstElementChild;
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
const preferenceView = new PreferenceView();
|
||||
preferenceView.init();
|
||||
};
|
63
app/renderer/js/pages/preference/badge-settings.js
Normal file
63
app/renderer/js/pages/preference/badge-settings.js
Normal 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(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
|
||||
}
|
||||
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();
|
46
app/renderer/js/pages/preference/base-section.js
Normal file
46
app/renderer/js/pages/preference/base-section.js
Normal file
@@ -0,0 +1,46 @@
|
||||
'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">
|
||||
<div class="switch">
|
||||
<input class="toggle toggle-round" type="checkbox" checked>
|
||||
<label></label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="action">
|
||||
<div class="switch">
|
||||
<input class="toggle toggle-round" type="checkbox">
|
||||
<label></label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
reloadApp() {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseSection;
|
37
app/renderer/js/pages/preference/create-new-org.js
Normal file
37
app/renderer/js/pages/preference/create-new-org.js
Normal file
@@ -0,0 +1,37 @@
|
||||
'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;
|
239
app/renderer/js/pages/preference/general-section.js
Normal file
239
app/renderer/js/pages/preference/general-section.js
Normal file
@@ -0,0 +1,239 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const { app, dialog } = require('electron').remote;
|
||||
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const BaseSection = require(__dirname + '/base-section.js');
|
||||
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
|
||||
|
||||
class GeneralSection extends BaseSection {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<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">Cmd Or Ctrl+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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.updateTrayOption();
|
||||
this.updateBadgeOption();
|
||||
this.updateSilentOption();
|
||||
this.updateUpdateOption();
|
||||
this.updateSidebarOption();
|
||||
this.updateStartAtLoginOption();
|
||||
this.updateResetDataOption();
|
||||
this.showDesktopNotification();
|
||||
this.enableSpellchecker();
|
||||
|
||||
// Platform specific settings
|
||||
// Flashing taskbar on Windows
|
||||
if (process.platform === 'win32') {
|
||||
this.updateFlashTaskbar();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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'),
|
||||
value: ConfigUtil.getConfigItem('betaUpdate', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
|
||||
ConfigUtil.setConfigItem('betaUpdate', newValue);
|
||||
this.updateUpdateOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
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;
|
67
app/renderer/js/pages/preference/nav.js
Normal file
67
app/renderer/js/pages/preference/nav.js
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
const BaseComponent = require(__dirname + '/../../components/base.js');
|
||||
|
||||
class PreferenceNav extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
|
||||
this.props = props;
|
||||
|
||||
this.navItems = ['General', 'Network', 'Servers', 'Shortcuts'];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
template() {
|
||||
let navItemsTemplate = '';
|
||||
for (const navItem of this.navItems) {
|
||||
navItemsTemplate += `<div class="nav" id="nav-${navItem}">${navItem}</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div>
|
||||
<div id="settings-header">Settings</div>
|
||||
<div id="nav-container">${navItemsTemplate}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$el = this.generateNodeFromTemplate(this.template());
|
||||
this.props.$root.appendChild(this.$el);
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
for (const navItem of this.navItems) {
|
||||
const $item = document.getElementById(`nav-${navItem}`);
|
||||
$item.addEventListener('click', () => {
|
||||
this.props.onItemSelected(navItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
select(navItemToSelect) {
|
||||
for (const navItem of this.navItems) {
|
||||
if (navItem === navItemToSelect) {
|
||||
this.activate(navItem);
|
||||
} else {
|
||||
this.deactivate(navItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activate(navItem) {
|
||||
const $item = document.getElementById(`nav-${navItem}`);
|
||||
$item.classList.add('active');
|
||||
}
|
||||
|
||||
deactivate(navItem) {
|
||||
const $item = document.getElementById(`nav-${navItem}`);
|
||||
$item.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PreferenceNav;
|
102
app/renderer/js/pages/preference/network-section.js
Normal file
102
app/renderer/js/pages/preference/network-section.js
Normal 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 blue" 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;
|
68
app/renderer/js/pages/preference/new-server-form.js
Normal file
68
app/renderer/js/pages/preference/new-server-form.js
Normal file
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
const BaseComponent = require(__dirname + '/../../components/base.js');
|
||||
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
|
||||
|
||||
class NewServerForm extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-card">
|
||||
<div class="server-info-right">
|
||||
<div class="server-info-row">
|
||||
<input class="setting-input-value" autofocus placeholder="example.zulipchat.com"/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<div class="action blue server-save-action">
|
||||
<i class="material-icons">add_box</i>
|
||||
<span>Add</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initForm();
|
||||
this.initActions();
|
||||
}
|
||||
|
||||
initForm() {
|
||||
this.$newServerForm = this.generateNodeFromTemplate(this.template());
|
||||
this.$saveServerButton = this.$newServerForm.getElementsByClassName('server-save-action')[0];
|
||||
this.props.$root.innerHTML = '';
|
||||
this.props.$root.appendChild(this.$newServerForm);
|
||||
|
||||
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', () => {
|
||||
this.submitFormHandler();
|
||||
});
|
||||
this.$newServerUrl.addEventListener('keypress', event => {
|
||||
const EnterkeyCode = event.keyCode;
|
||||
// Submit form when Enter key is pressed
|
||||
if (EnterkeyCode === 13) {
|
||||
this.submitFormHandler();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NewServerForm;
|
109
app/renderer/js/pages/preference/preference.js
Normal file
109
app/renderer/js/pages/preference/preference.js
Normal file
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const BaseComponent = require(__dirname + '/js/components/base.js');
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/js/utils/config-util.js');
|
||||
|
||||
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() {
|
||||
super();
|
||||
|
||||
this.$sidebarContainer = document.getElementById('sidebar');
|
||||
this.$settingsContainer = document.getElementById('settings-container');
|
||||
}
|
||||
|
||||
init() {
|
||||
this.nav = new Nav({
|
||||
$root: this.$sidebarContainer,
|
||||
onItemSelected: this.handleNavigation.bind(this)
|
||||
});
|
||||
|
||||
this.setDefaultView();
|
||||
this.registerIpcs();
|
||||
this.setDefaultSettings();
|
||||
}
|
||||
|
||||
setDefaultView() {
|
||||
let nav = 'General';
|
||||
const hasTag = window.location.hash;
|
||||
if (hasTag) {
|
||||
nav = hasTag.substring(1);
|
||||
}
|
||||
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) {
|
||||
case 'Servers': {
|
||||
this.section = new ServersSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'General': {
|
||||
this.section = new GeneralSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
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();
|
||||
window.location.hash = `#${navItem}`;
|
||||
}
|
||||
|
||||
registerIpcs() {
|
||||
ipcRenderer.on('switch-settings-nav', (event, navItem) => {
|
||||
this.handleNavigation(navItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
const preferenceView = new PreferenceView();
|
||||
preferenceView.init();
|
||||
};
|
77
app/renderer/js/pages/preference/server-info-form.js
Normal file
77
app/renderer/js/pages/preference/server-info-form.js
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
const {dialog} = require('electron').remote;
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
const BaseComponent = require(__dirname + '/../../components/base.js');
|
||||
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
|
||||
|
||||
class ServerInfoForm extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-card">
|
||||
<div class="server-info-left">
|
||||
<img class="server-info-icon" src="${this.props.server.icon}"/>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<input class="setting-input-value" disabled value="${this.props.server.url}"/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<div class="action red server-delete-action">
|
||||
<i class="material-icons">indeterminate_check_box</i>
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initForm();
|
||||
this.initActions();
|
||||
}
|
||||
|
||||
initForm() {
|
||||
this.$serverInfoForm = this.generateNodeFromTemplate(this.template());
|
||||
this.$serverInfoAlias = this.$serverInfoForm.getElementsByClassName('server-info-alias')[0];
|
||||
this.$deleteServerButton = this.$serverInfoForm.getElementsByClassName('server-delete-action')[0];
|
||||
this.$openServerButton = this.$serverInfoForm.getElementsByClassName('open-tab-button')[0];
|
||||
this.props.$root.appendChild(this.$serverInfoForm);
|
||||
}
|
||||
|
||||
initActions() {
|
||||
this.$deleteServerButton.addEventListener('click', () => {
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['YES', 'NO'],
|
||||
defaultId: 0,
|
||||
message: 'Are you sure you want to delete this server?'
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
DomainUtil.removeDomain(this.props.index);
|
||||
this.props.onChange(this.props.index);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.$openServerButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
|
||||
});
|
||||
|
||||
this.$serverInfoAlias.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServerInfoForm;
|
73
app/renderer/js/pages/preference/servers-section.js
Normal file
73
app/renderer/js/pages/preference/servers-section.js
Normal file
@@ -0,0 +1,73 @@
|
||||
'use strict';
|
||||
|
||||
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) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-pane" id="server-settings-pane">
|
||||
<div class="title">Enter URL of your Zulip organization</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>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initServers();
|
||||
}
|
||||
|
||||
initServers() {
|
||||
this.props.$root.innerHTML = '';
|
||||
|
||||
const servers = DomainUtil.getDomains();
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.$serverInfoContainer = document.getElementById('server-info-container');
|
||||
this.$existingServers = document.getElementById('existing-servers');
|
||||
this.$newServerContainer = document.getElementById('new-server-container');
|
||||
this.$newServerButton = document.getElementById('new-server-action');
|
||||
|
||||
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.initNewServerForm();
|
||||
|
||||
this.$createOrganizationContainer = document.getElementById('create-organization-container');
|
||||
this.initCreateNewOrganization();
|
||||
|
||||
for (let i = 0; i < servers.length; i++) {
|
||||
new ServerInfoForm({
|
||||
$root: this.$serverInfoContainer,
|
||||
server: servers[i],
|
||||
index: i,
|
||||
onChange: this.reloadApp
|
||||
}).init();
|
||||
}
|
||||
}
|
||||
|
||||
initCreateNewOrganization() {
|
||||
new CreateOrganziation({
|
||||
$root: this.$createOrganizationContainer
|
||||
}).init();
|
||||
}
|
||||
|
||||
initNewServerForm() {
|
||||
new NewServerForm({
|
||||
$root: this.$newServerContainer,
|
||||
onChange: this.reloadApp
|
||||
}).init();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServersSection;
|
311
app/renderer/js/pages/preference/shortcuts-section.js
Normal file
311
app/renderer/js/pages/preference/shortcuts-section.js
Normal 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;
|
@@ -1,5 +1,12 @@
|
||||
'use strict';
|
||||
const {spellChecker} = require('./spellchecker');
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const SetupSpellChecker = require('./spellchecker');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/utils/config-util.js');
|
||||
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
require('./notification');
|
||||
|
||||
const logout = () => {
|
||||
// Create the menu for the below
|
||||
@@ -28,6 +35,28 @@ process.once('loaded', () => {
|
||||
|
||||
// To prevent failing this script on linux we need to load it after the document loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Init spellchecker
|
||||
spellChecker();
|
||||
// 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();
|
||||
}
|
||||
|
||||
// redirect users to network troubleshooting page
|
||||
const getRestartButton = document.querySelector('.restart_get_events_button');
|
||||
if (getRestartButton) {
|
||||
getRestartButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up spellchecker events after you navigate away from this page;
|
||||
// otherwise, you may experience errors
|
||||
window.addEventListener('beforeunload', () => {
|
||||
SetupSpellChecker.unsubscribeSpellChecker();
|
||||
});
|
||||
|
||||
|
@@ -1,27 +1,56 @@
|
||||
const {SpellCheckHandler, ContextMenuListener, ContextMenuBuilder} = require('electron-spellchecker');
|
||||
'use strict';
|
||||
|
||||
function spellChecker() {
|
||||
// Implement spellcheck using electron api
|
||||
window.spellCheckHandler = new SpellCheckHandler();
|
||||
window.spellCheckHandler.attachToInput();
|
||||
const { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } = require('electron-spellchecker');
|
||||
|
||||
// Start off as US English
|
||||
window.spellCheckHandler.switchLanguage('en-US');
|
||||
const ConfigUtil = require(__dirname + '/utils/config-util.js');
|
||||
|
||||
const contextMenuBuilder = new ContextMenuBuilder(window.spellCheckHandler);
|
||||
const contextMenuListener = new ContextMenuListener(info => {
|
||||
contextMenuBuilder.showPopupMenu(info);
|
||||
});
|
||||
class SetupSpellChecker {
|
||||
init() {
|
||||
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
|
||||
this.enableSpellChecker();
|
||||
}
|
||||
this.enableContextMenu();
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
enableSpellChecker() {
|
||||
try {
|
||||
this.SpellCheckHandler = new SpellCheckHandler();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
enableContextMenu() {
|
||||
if (this.SpellCheckHandler) {
|
||||
this.SpellCheckHandler.attachToInput();
|
||||
|
||||
const userLanguage = ConfigUtil.getConfigItem('spellcheckerLanguage');
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
process.platform === 'darwin' ?
|
||||
// On macOS, spellchecker fails to auto-detect the lanugage user is typing in
|
||||
// that's why we need to mention it explicitly
|
||||
this.SpellCheckHandler.switchLanguage(userLanguage) :
|
||||
// On Linux and Windows, spellchecker can automatically detects the language the user is typing in
|
||||
// and silently switches on the fly; thus we can start off as US English
|
||||
this.SpellCheckHandler.switchLanguage('en-US');
|
||||
}
|
||||
|
||||
const contextMenuBuilder = new ContextMenuBuilder(this.SpellCheckHandler);
|
||||
this.contextMenuListener = new ContextMenuListener(info => {
|
||||
contextMenuBuilder.showPopupMenu(info);
|
||||
});
|
||||
}
|
||||
|
||||
unsubscribeSpellChecker() {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (this.SpellCheckHandler) {
|
||||
this.SpellCheckHandler.unsubscribe();
|
||||
}
|
||||
if (this.contextMenuListener) {
|
||||
this.contextMenuListener.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
spellChecker
|
||||
};
|
||||
module.exports = new SetupSpellChecker();
|
||||
|
@@ -9,6 +9,8 @@ const {Tray, Menu, nativeImage, BrowserWindow} = remote;
|
||||
|
||||
const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/utils/config-util.js');
|
||||
|
||||
const iconPath = () => {
|
||||
if (process.platform === 'linux') {
|
||||
return APP_ICON + 'linux.png';
|
||||
@@ -45,7 +47,8 @@ const config = {
|
||||
|
||||
const renderCanvas = function (arg) {
|
||||
config.unreadCount = arg;
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
return new Promise(resolve => {
|
||||
const SIZE = config.size * config.pixelRatio;
|
||||
const PADDING = SIZE * 0.05;
|
||||
const CENTER = SIZE / 2;
|
||||
@@ -83,8 +86,6 @@ const renderCanvas = function (arg) {
|
||||
}
|
||||
|
||||
resolve(canvas);
|
||||
} else {
|
||||
reject(canvas);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -117,6 +118,8 @@ const createTray = function () {
|
||||
const contextMenu = Menu.buildFromTemplate([{
|
||||
label: 'About',
|
||||
click() {
|
||||
// We need to focus the main window first
|
||||
ipcRenderer.send('focus-app');
|
||||
sendAction('open-about');
|
||||
}
|
||||
},
|
||||
@@ -133,8 +136,9 @@ const createTray = function () {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Manage Zulip servers',
|
||||
label: 'Settings',
|
||||
click() {
|
||||
ipcRenderer.send('focus-app');
|
||||
sendAction('open-settings');
|
||||
}
|
||||
},
|
||||
@@ -149,9 +153,19 @@ const createTray = function () {
|
||||
}
|
||||
]);
|
||||
window.tray.setContextMenu(contextMenu);
|
||||
window.tray.on('click', () => {
|
||||
// Click event only works on Windows
|
||||
if (process.platform === 'win32') {
|
||||
ipcRenderer.send('toggle-app');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ipcRenderer.on('destroytray', event => {
|
||||
if (!window.tray) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.tray.destroy();
|
||||
if (window.tray.isDestroyed()) {
|
||||
window.tray = null;
|
||||
@@ -163,35 +177,46 @@ ipcRenderer.on('destroytray', event => {
|
||||
});
|
||||
|
||||
ipcRenderer.on('tray', (event, arg) => {
|
||||
if (arg === 0) {
|
||||
unread = arg;
|
||||
// Message Count // console.log("message count is zero.");
|
||||
window.tray.setImage(iconPath());
|
||||
window.tray.setToolTip('No unread messages');
|
||||
} else {
|
||||
unread = arg;
|
||||
renderNativeImage(arg).then(image => {
|
||||
window.tray.setImage(image);
|
||||
window.tray.setToolTip(arg + ' unread messages');
|
||||
});
|
||||
if (!window.tray) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggletray', event => {
|
||||
if (event) {
|
||||
if (window.tray) {
|
||||
window.tray.destroy();
|
||||
if (window.tray.isDestroyed()) {
|
||||
window.tray = null;
|
||||
}
|
||||
// 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;
|
||||
window.tray.setImage(iconPath());
|
||||
window.tray.setToolTip('No unread messages');
|
||||
} else {
|
||||
createTray();
|
||||
renderNativeImage(unread).then(image => {
|
||||
unread = arg;
|
||||
renderNativeImage(arg).then(image => {
|
||||
window.tray.setImage(image);
|
||||
window.tray.setToolTip(unread + ' unread messages');
|
||||
window.tray.setToolTip(arg + ' unread messages');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
createTray();
|
||||
function toggleTray() {
|
||||
if (window.tray) {
|
||||
window.tray.destroy();
|
||||
if (window.tray.isDestroyed()) {
|
||||
window.tray = null;
|
||||
}
|
||||
ConfigUtil.setConfigItem('trayIcon', false);
|
||||
} else {
|
||||
createTray();
|
||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||
renderNativeImage(unread).then(image => {
|
||||
window.tray.setImage(image);
|
||||
window.tray.setToolTip(unread + ' unread messages');
|
||||
});
|
||||
}
|
||||
ConfigUtil.setConfigItem('trayIcon', true);
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.on('toggletray', toggleTray);
|
||||
|
||||
if (ConfigUtil.getConfigItem('trayIcon', true)) {
|
||||
createTray();
|
||||
}
|
||||
|
54
app/renderer/js/utils/config-util.js
Normal file
54
app/renderer/js/utils/config-util.js
Normal file
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
const process = require('process');
|
||||
const JsonDB = require('node-json-db');
|
||||
|
||||
let instance = null;
|
||||
let app = null;
|
||||
|
||||
/* To make the util runnable in both main and renderer process */
|
||||
if (process.type === 'renderer') {
|
||||
app = require('electron').remote.app;
|
||||
} else {
|
||||
app = require('electron').app;
|
||||
}
|
||||
|
||||
class ConfigUtil {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
this.reloadDB();
|
||||
return instance;
|
||||
}
|
||||
|
||||
getConfigItem(key, defaultValue = null) {
|
||||
this.reloadDB();
|
||||
const value = this.db.getData('/')[key];
|
||||
if (value === undefined) {
|
||||
this.setConfigItem(key, defaultValue);
|
||||
return defaultValue;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
setConfigItem(key, value) {
|
||||
this.db.push(`/${key}`, value, true);
|
||||
this.reloadDB();
|
||||
}
|
||||
|
||||
removeConfigItem(key) {
|
||||
this.db.delete(`/${key}`);
|
||||
this.reloadDB();
|
||||
}
|
||||
|
||||
reloadDB() {
|
||||
this.db = new JsonDB(app.getPath('userData') + '/settings.json', true, true);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ConfigUtil();
|
@@ -1,12 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const {app} = require('electron').remote;
|
||||
const { app, dialog } = require('electron').remote;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const JsonDB = require('node-json-db');
|
||||
const request = require('request');
|
||||
|
||||
let instance = null;
|
||||
|
||||
const defaultIconUrl = __dirname + '../../../img/icon.png';
|
||||
const defaultIconUrl = '../renderer/img/icon.png';
|
||||
|
||||
class DomainUtil {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
@@ -15,7 +18,7 @@ class DomainUtil {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
this.db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
|
||||
this.reloadDB();
|
||||
// Migrate from old schema
|
||||
if (this.db.getData('/').domain) {
|
||||
this.addDomain({
|
||||
@@ -29,6 +32,7 @@ class DomainUtil {
|
||||
}
|
||||
|
||||
getDomains() {
|
||||
this.reloadDB();
|
||||
if (this.db.getData('/').domains === undefined) {
|
||||
return [];
|
||||
} else {
|
||||
@@ -37,46 +41,220 @@ class DomainUtil {
|
||||
}
|
||||
|
||||
getDomain(index) {
|
||||
this.reloadDB();
|
||||
return this.db.getData(`/domains[${index}]`);
|
||||
}
|
||||
|
||||
updateDomain(index, server) {
|
||||
this.reloadDB();
|
||||
this.db.push(`/domains[${index}]`, server, true);
|
||||
}
|
||||
|
||||
addDomain(server) {
|
||||
server.icon = server.icon || defaultIconUrl;
|
||||
this.db.push('/domains[]', server, true);
|
||||
return new Promise(resolve => {
|
||||
if (server.icon) {
|
||||
this.saveServerIcon(server.icon).then(localIconUrl => {
|
||||
server.icon = localIconUrl;
|
||||
this.db.push('/domains[]', server, true);
|
||||
this.reloadDB();
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
server.icon = defaultIconUrl;
|
||||
this.db.push('/domains[]', server, true);
|
||||
this.reloadDB();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeDomains() {
|
||||
this.db.delete('/domains');
|
||||
this.reloadDB();
|
||||
}
|
||||
|
||||
removeDomain(index) {
|
||||
this.db.delete(`/domains[${index}]`);
|
||||
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';
|
||||
|
||||
const serverConf = {
|
||||
icon: defaultIconUrl,
|
||||
url: domain,
|
||||
alias: domain
|
||||
};
|
||||
|
||||
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'
|
||||
];
|
||||
// If the domain contains following strings we just bypass the server
|
||||
const whitelistDomains = [
|
||||
'zulipdev.org'
|
||||
];
|
||||
if (!error && response.statusCode !== 404) {
|
||||
resolve(domain);
|
||||
} else if (error.toString().indexOf('Error: self signed certificate') >= 0) {
|
||||
if (window.confirm(`Do you trust certificate from ${domain}?`)) {
|
||||
resolve(domain);
|
||||
// Correct
|
||||
this.getServerSettings(domain).then(serverSettings => {
|
||||
resolve(serverSettings);
|
||||
}, () => {
|
||||
resolve(serverConf);
|
||||
});
|
||||
} else if (domain.indexOf(whitelistDomains) >= 0 || certsError.indexOf(error.toString()) >= 0) {
|
||||
if (silent) {
|
||||
this.getServerSettings(domain).then(serverSettings => {
|
||||
resolve(serverSettings);
|
||||
}, () => {
|
||||
resolve(serverConf);
|
||||
});
|
||||
} else {
|
||||
reject('Untrusted Certificate.');
|
||||
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');
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getServerSettings(domain) {
|
||||
const serverSettingsUrl = domain + '/api/v1/server_settings';
|
||||
return new Promise((resolve, reject) => {
|
||||
request(serverSettingsUrl, (error, response) => {
|
||||
if (!error && response.statusCode === 200) {
|
||||
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,
|
||||
url: data.realm_uri,
|
||||
alias: data.realm_name
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject('Zulip server version < 1.6.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
saveServerIcon(url) {
|
||||
// The save will always succeed. If url is invalid, downgrade to default icon.
|
||||
return new Promise(resolve => {
|
||||
const filePath = this.generateFilePath(url);
|
||||
const file = fs.createWriteStream(filePath);
|
||||
try {
|
||||
request(url).on('response', response => {
|
||||
response.on('error', err => {
|
||||
console.log(err);
|
||||
resolve(defaultIconUrl);
|
||||
});
|
||||
response.pipe(file).on('finish', () => {
|
||||
resolve(filePath);
|
||||
});
|
||||
}).on('error', err => {
|
||||
console.log(err);
|
||||
resolve(defaultIconUrl);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
resolve(defaultIconUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
|
@@ -19,10 +19,7 @@ class LinkUtil {
|
||||
const currentDomain = wurl('hostname', currentUrl);
|
||||
const newDomain = wurl('hostname', newUrl);
|
||||
|
||||
const skipImages = '.jpg|.gif|.png|.jpeg|.JPG|.PNG';
|
||||
|
||||
// We'll be needing this to open images in default browser
|
||||
return (currentDomain === newDomain) && !newUrl.match(skipImages);
|
||||
return (currentDomain === newDomain) && newUrl.includes('/#narrow');
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,28 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="responsive desktop">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip</title>
|
||||
<link rel="stylesheet" href="css/main.css" type="text/css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div id="sidebar">
|
||||
<div id="tabs-container"></div>
|
||||
<div id="actions-container">
|
||||
<div class="action-button" id="reload-action">
|
||||
<i class="material-icons md-48">refresh</i>
|
||||
</div>
|
||||
<div class="action-button" id="add-action">
|
||||
<i class="material-icons md-48">add_circle</i>
|
||||
</div>
|
||||
<div class="action-button" id="settings-action">
|
||||
<i class="material-icons md-48">settings</i>
|
||||
</div>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip</title>
|
||||
<link rel="stylesheet" href="css/main.css" type="text/css" media="screen">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="content">
|
||||
<div class="popup">
|
||||
<span class="popuptext hidden" id="fullscreen-popup"></span>
|
||||
</div>
|
||||
<div id="sidebar" class="toggle-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>
|
||||
</body>
|
||||
<script src="js/main.js"></script>
|
||||
</html>
|
||||
<div id="actions-container">
|
||||
<div class="action-button" id="reload-action">
|
||||
<i class="material-icons md-48">refresh</i>
|
||||
<span id="reload-tooltip" style="display:none">Reload</span>
|
||||
</div>
|
||||
<div class="action-button" id="settings-action">
|
||||
<i class="material-icons md-48">settings</i>
|
||||
<span id="setting-tooltip" style="display:none">Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-container">
|
||||
<div id="webviews-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
</html>
|
21
app/renderer/network.html
Normal file
21
app/renderer/network.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="responsive desktop">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip - Network Troubleshooting</title>
|
||||
<link rel="stylesheet" href="css/network.css" type="text/css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div id="picture"><img src="img/zulip_network.png"></div>
|
||||
<div id="title">Zulip can't connect</div>
|
||||
<div id="description">
|
||||
<div>Your computer seems to be offline.</div>
|
||||
<div>We will keep trying to reconnect, or you can try now.</div>
|
||||
</div>
|
||||
<div id="reconnect">Try now</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/pages/network.js"></script>
|
||||
</html>
|
@@ -5,41 +5,12 @@
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip - Settings</title>
|
||||
<link rel="stylesheet" href="css/preference.css" type="text/css" media="screen">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div id="sidebar">
|
||||
<div id="settings-header">Settings</div>
|
||||
<div id="tabs-container">
|
||||
<div class="tab" id="general-settings">General</div>
|
||||
<div class="tab active" id="server-settings">Servers</div>
|
||||
<div class="tab" id="about-settings">About</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settings-container">
|
||||
<div class="settings-pane" id="server-settings-pane">
|
||||
<div class="title">Manage Servers</div>
|
||||
<div class="actions-container">
|
||||
<div class="action" id="new-server-action">
|
||||
<i class="material-icons">add_box</i>
|
||||
<span>New Server</span>
|
||||
</div>
|
||||
<div class="action hidden" id="save-server-action">
|
||||
<i class="material-icons">check_box</i>
|
||||
<span>Save</span>
|
||||
</div>
|
||||
<div class="action hidden" id="reload-server-action">
|
||||
<i class="material-icons">refresh</i>
|
||||
<span>Reload to Apply Changes</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-info-container">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sidebar"></div>
|
||||
<div id="settings-container"></div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/pages/preference.js"></script>
|
||||
<script src="js/pages/preference/preference.js"></script>
|
||||
</html>
|
||||
|
BIN
app/resources/tray/trayosx@2x.png
Normal file
BIN
app/resources/tray/trayosx@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
app/resources/tray/trayosx@3x.png
Normal file
BIN
app/resources/tray/trayosx@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
app/resources/tray/trayosx@4x.png
Normal file
BIN
app/resources/tray/trayosx@4x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
@@ -17,7 +17,8 @@ install:
|
||||
- npm install
|
||||
- npm install -g gulp
|
||||
|
||||
build: off
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- npm run test
|
||||
- npm run test-e2e
|
||||
|
@@ -1,56 +1,84 @@
|
||||
# Development guide
|
||||
# Development setup
|
||||
|
||||
This is a guide to running the Zulip desktop app from a source tree,
|
||||
in order to contribute to developing it.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To build and run the app from source, you'll need the following:
|
||||
|
||||
* [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)
|
||||
* [NPM](https://www.npmjs.com/get-npm) and
|
||||
[node-gyp](https://github.com/nodejs/node-gyp#installation),
|
||||
if they don't come bundled with your Node.js installation
|
||||
* [Python](https://www.python.org/downloads/release/python-2713/)
|
||||
(v2.7.x recommended)
|
||||
* A C++ compiler compatible with C++11
|
||||
* Development headers for the libXext, libXtst, and libxkbfile libraries
|
||||
|
||||
### Debian/Ubuntu and friends
|
||||
|
||||
## System specific dependencies
|
||||
On a system running Debian, Ubuntu, or another Debian-based Linux
|
||||
distribution, you can install all dependencies through the package
|
||||
manager (see [here][nodesource-install] for more on the first command):
|
||||
|
||||
### Linux
|
||||
|
||||
Install following packages:
|
||||
```sh
|
||||
$ sudo apt-get install build-essential libxext-dev libxtst-dev libxkbfile-dev
|
||||
$ curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
|
||||
$ sudo apt install git nodejs python build-essential libxext-dev libxtst-dev libxkbfile-dev
|
||||
```
|
||||
|
||||
## Installation
|
||||
[nodesource-install]: https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions
|
||||
|
||||
### Other OSes
|
||||
|
||||
Other developers run the app on Windows, macOS, and possibly other OSes.
|
||||
PRs to add specific instructions to this doc are welcome!
|
||||
|
||||
On Windows, your C++ compiler should be Visual Studio 2015 or later.
|
||||
|
||||
## Download, build, and run
|
||||
|
||||
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:
|
||||
|
||||
Start the app:
|
||||
```sh
|
||||
$ npm start
|
||||
```
|
||||
|
||||
Start and watch changes
|
||||
|
||||
Start and watch changes:
|
||||
```sh
|
||||
$ npm run dev
|
||||
```
|
||||
### Making a release
|
||||
|
||||
To package app into an installer use command:
|
||||
## Troubleshooting
|
||||
|
||||
If you have any problems running the app, see the [most common
|
||||
issues](./troubleshooting.md).
|
||||
|
||||
## Making a release
|
||||
|
||||
To package the app into an installer:
|
||||
```
|
||||
npm run dist
|
||||
```
|
||||
It will start the packaging process for the operating system you are running this command on. The ready for distribution file (e.g. dmg, windows installer, deb package) will be outputted to the `dist` directory.
|
||||
|
||||
You can create a Windows installer only when running on Windows and similarly for Linux and OSX. So, to generate all three installers, you will need all three operating systems.
|
||||
This command will produce distributable packages or installers for the
|
||||
operating system you're running on:
|
||||
* on Windows, a Windows installer file
|
||||
* on macOS, a `.dmg` file
|
||||
* on Linux, a plain `.zip` file as well as a `.deb` file and an
|
||||
`AppImage` file.
|
||||
To generate all three types, you will need all three operating
|
||||
systems.
|
||||
|
||||
# Troubleshooting
|
||||
If you have any problems running the app please see the [most common issues](./troubleshooting.md).
|
||||
The output files appear in the `dist/` directory.
|
||||
|
2
docs/Home.md
Normal file
2
docs/Home.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Installation instructions
|
||||
* [[Windows]]
|
58
docs/Windows.md
Normal file
58
docs/Windows.md
Normal file
@@ -0,0 +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).
|
||||
|
||||
|
||||
|
3
docs/_Footer.md
Normal file
3
docs/_Footer.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Want to contribute to this Wiki?
|
||||
|
||||
[Edit `/docs` files and send a pull request.](https://github.com/zulip/zulip-electron/tree/master/docs)
|
26
docs/desktop-release.md
Normal file
26
docs/desktop-release.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# New release checklist -
|
||||
|
||||
## We need to cross check following things before pushing a new release + after updating electron version. This is just to make sure that nothing gets broken.
|
||||
## - Desktop notifications
|
||||
## - Spellchecker
|
||||
## - Auto updates
|
||||
**Check for the logs in -**
|
||||
- **on Linux:** `~/.config/Zulip/log.log`
|
||||
- **on OS X:** `~/Library/Logs/Zulip/log.log`
|
||||
- **on Windows:** `%USERPROFILE%\AppData\Roaming\Zulip\log.log`
|
||||
## - All the installer i.e.
|
||||
- Linux (.deb, AppImage)
|
||||
- Mac - (.dmg)
|
||||
- Windows - (web installer for 32/64bit)
|
||||
## - Check for errors in console (if any)
|
||||
## - Code signing verification on Mac and Windows
|
||||
## - Tray and menu options
|
||||
# We need to cross check all these things on -
|
||||
- Windows 7
|
||||
- Windows 8
|
||||
- Windows 10
|
||||
- Ubuntu 14.04/16.04
|
||||
- macOSX
|
||||
|
||||
|
||||
|
13
gulpfile.js
13
gulpfile.js
@@ -1,9 +1,10 @@
|
||||
'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
|
||||
@@ -28,9 +29,11 @@ gulp.task('reload:renderer', done => {
|
||||
done();
|
||||
});
|
||||
|
||||
// Test app using mocha+spectron
|
||||
gulp.task('test', () => {
|
||||
return gulp.src('tests/index.js').pipe(mocha());
|
||||
gulp.task('test-e2e', () => {
|
||||
return gulp.src('tests/*.js')
|
||||
.pipe(tape({
|
||||
reporter: tapColorize()
|
||||
}));
|
||||
});
|
||||
|
||||
gulp.task('default', ['dev', 'test']);
|
||||
gulp.task('default', ['dev', 'test-e2e']);
|
||||
|
105
help.md
Normal file
105
help.md
Normal 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
68
how-to-install.md
Normal 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
|
||||
```
|
70
package.json
70
package.json
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "zulip",
|
||||
"productName": "Zulip",
|
||||
"version": "1.0.0-beta",
|
||||
"version": "1.7.0",
|
||||
"main": "./app/main",
|
||||
"description": "Zulip Desktop App",
|
||||
"license": "Apache-2.0",
|
||||
"email": "<svnitakash@gmail.com>",
|
||||
"copyright": "©2017 Kandra Labs, Inc.",
|
||||
"author": {
|
||||
"name": "Akash Nimare",
|
||||
"name": "Kandra Labs, Inc.",
|
||||
"email": "svnitakash@gmail.com"
|
||||
},
|
||||
"repository": {
|
||||
@@ -20,20 +20,25 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "electron app --disable-http-cache",
|
||||
"postinstall": "install-app-deps",
|
||||
"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": "build --dir",
|
||||
"dist": "build",
|
||||
"mas": "build --mac mas",
|
||||
"build:win": "build --win nsis-web --ia32 --x64",
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "electron-builder",
|
||||
"mas": "electron-builder --mac mas",
|
||||
"travis": "cd ./scripts && ./travis-build-test.sh"
|
||||
},
|
||||
"pre-commit": [
|
||||
"test"
|
||||
],
|
||||
"build": {
|
||||
"appId": "org.zulip.zulip-electron",
|
||||
"asar": true,
|
||||
"files": [
|
||||
"**/*",
|
||||
"!docs${/*}",
|
||||
"!node_modules/@paulcbetts/cld/deps/cld${/*}"
|
||||
],
|
||||
"copyright": "©2017 Kandra Labs, Inc.",
|
||||
@@ -41,7 +46,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": [
|
||||
@@ -73,12 +78,22 @@
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "build/icon.ico"
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis-web",
|
||||
"arch": [
|
||||
"x64",
|
||||
"ia32"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "build/icon.ico",
|
||||
"publisherName": "Kandra Labs, Inc."
|
||||
},
|
||||
"nsis": {
|
||||
"perMachine": true,
|
||||
"oneClick": false
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
@@ -91,18 +106,28 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"assert": "1.4.1",
|
||||
"cp-file": "^5.0.0",
|
||||
"devtron": "1.4.0",
|
||||
"electron-builder": "17.10.0",
|
||||
"electron": "1.6.8",
|
||||
"electron-connect": "0.4.8",
|
||||
"electron-builder": "19.46.4",
|
||||
"electron": "1.6.15",
|
||||
"electron-connect": "0.6.2",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-mocha": "3.0.1",
|
||||
"chai-as-promised": "6.0.0",
|
||||
"chai": "^3.5.0",
|
||||
"spectron": "3.6.4",
|
||||
"xo": "0.18.1"
|
||||
"gulp-tape": "0.0.9",
|
||||
"is-ci": "^1.0.10",
|
||||
"pre-commit": "1.2.2",
|
||||
"spectron": "3.7.2",
|
||||
"tap-colorize": "^1.2.0",
|
||||
"tape": "^4.8.0",
|
||||
"xo": "0.18.2",
|
||||
"electron-debug": "1.4.0"
|
||||
},
|
||||
"xo": {
|
||||
"parserOptions": {
|
||||
"sourceType": "script",
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
},
|
||||
"esnext": true,
|
||||
"overrides": [
|
||||
{
|
||||
@@ -113,6 +138,7 @@
|
||||
500
|
||||
],
|
||||
"no-warning-comments": 0,
|
||||
"object-curly-spacing": 0,
|
||||
"capitalized-comments": 0,
|
||||
"no-else-return": 0,
|
||||
"no-path-concat": 0,
|
||||
@@ -120,7 +146,8 @@
|
||||
"guard-for-in": 0,
|
||||
"prefer-promise-reject-errors": 0,
|
||||
"import/no-unresolved": 0,
|
||||
"import/no-extraneous-dependencies": 0
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"no-prototype-builtins": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -132,5 +159,8 @@
|
||||
"browser",
|
||||
"mocha"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"resemblejs": "^2.2.6"
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,17 @@
|
||||
#!/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
|
||||
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
|
||||
fi
|
||||
|
||||
npm run test
|
||||
|
||||
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
npm run test-e2e
|
||||
fi
|
||||
|
5
scripts/travis-xvfb.sh
Executable file
5
scripts/travis-xvfb.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/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
|
7
tests/config.js
Normal file
7
tests/config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const path = require('path')
|
||||
|
||||
const TEST_APP_PRODUCT_NAME = 'ZulipTest'
|
||||
|
||||
module.exports = {
|
||||
TEST_APP_PRODUCT_NAME
|
||||
}
|
@@ -1,81 +1,13 @@
|
||||
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))
|
||||
})
|
||||
})
|
||||
const test = require('tape')
|
||||
const setup = require('./setup')
|
||||
|
||||
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'))
|
||||
})
|
99
tests/setup.js
Normal file
99
tests/setup.js
Normal file
@@ -0,0 +1,99 @@
|
||||
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'))
|
||||
}
|
19
tests/test-add-organization.js
Normal file
19
tests/test-add-organization.js
Normal file
@@ -0,0 +1,19 @@
|
||||
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'))
|
||||
})
|
||||
|
@@ -3,3 +3,9 @@
|
||||
* App icon will only show in the release version. The dev version will use the Electron icon
|
||||
* If you see issue, try deleting `node_modules` and `npm install`
|
||||
* Electron is more or less Chrome, you can get developer tools using `CMD+ALT+I`
|
||||
|
||||
### Error : ChecksumMismatchError
|
||||
- Try deleteing `node_modules` && `app/node_modules` directories. Re-install dependencies using `npm install`.
|
||||
|
||||
### Error : Module version mismatch. Expected 50, got 51
|
||||
- Make sure you have installed [node-gyp](https://github.com/nodejs/node-gyp#installation) dependencies properly.
|
||||
|
@@ -103,7 +103,20 @@ cleanUp()
|
||||
|
||||
# }}}
|
||||
|
||||
# this function is called when user hits Ctrl-C
|
||||
catchControl_c () {
|
||||
echo -en "\n## Ctrl-C caught; Quitting \n"
|
||||
# exit shell script
|
||||
exit $?;
|
||||
}
|
||||
|
||||
|
||||
|
||||
envSetup $*
|
||||
gitCheckout
|
||||
npmInstallStart
|
||||
cleanUp
|
||||
|
||||
# initialise trap to call catchControl_c function and trap keyboard interrupt (control-c)
|
||||
trap catchControl_c SIGINT
|
||||
sleep 1000
|
Reference in New Issue
Block a user