Compare commits

...

98 Commits

Author SHA1 Message Date
Akash Nimare
c52bfd6a45 release: 🎉 v2.3.3. 2018-07-13 18:18:25 +05:30
Akash Nimare
428087110a tools: Add scripts to push/reset to pull request. 2018-07-13 17:05:46 +05:30
Abhigyan Khaund
781883a898 crash-reporter: Remove crash-reporter file and its use, as it is replaced by sentry. 2018-07-13 12:57:48 +05:30
Abhigyan Khaund
7299cd51e6 sentry: Initialize sentry for main and renderer process using logger-util.
captureExceptions are used to report expected exceptions from domain-util.js and
config-util.js.
All files that include logger-util automatically includes sentry for those files
and report unexpected exceptions to sentry.
2018-07-13 12:57:48 +05:30
Akash Nimare
a45254cff0 builder: Update electron-builder to v20.20.4. 2018-07-13 10:38:01 +05:30
Abhigyan Khaund
4095c121ba certificates: Make default option "No" on enter key press to reject certificate. 2018-07-12 20:32:33 +05:30
Abhigyan Khaund
6787b8d557 file-attachments: Download file-attachments to Downloads folder.
This PR adds a feature to download file-attachments to "Downloads" folder and shows a
notification when the download is complete. On clicking the notification user can
open the downloaded file with system default app. A user can set/change the download location
from the settings > general.
2018-07-12 18:39:43 +05:30
Akash Nimare
306e0f3b5e macOS: Add dock bouncing effect on macOS.
This PR adds a new setting option of controlling the
dock bouncing feature on macOS.

Fixes: #510.
2018-07-10 10:01:36 +05:30
Abhigyan Khaund
f030d5d56b package.json: Move adm dependecy to app package.json. 2018-07-06 17:00:57 +05:30
Abhigyan Khaund
acf7c5a238 error-logs: Create a zip file for app logs.
This PR adds the function that when a user clicks of "Show App Logs" from the menu, open the Downloads folder with a zip file containing all the Logs and User Configs. This will help the user to easily share logs with us.
2018-07-05 00:20:51 +05:30
Akash Nimare
da22f75300 builder: Update electron builder + updater. 2018-07-04 17:55:04 +05:30
Abhigyan Khaund
df35ffc3b0 config-files: Move all the configuration files into config folder. (#508)
This commit moves all the configuration(.json) files into a config folder
if the config folder doesn't already exist. It also updates the places
where these files are being accessed with the new address.
This will help us zip the folder easily in the future.
2018-07-04 14:46:13 +05:30
Abhigyan Khaund
eeb0b4a666 logger: Replace all instances of console.log with our logger-util. 2018-06-27 15:31:01 +05:30
Abhigyan Khaund
0a893c97c7 settings: Add an option to validate and add custom/self-signed certificates.
This PR helps to validate custom/self-signed certificates for servers
by saving the certificate file in certificates folder in user's appData folder.
We now use this certificate with the request while validating the server
when adding the organization. This validation of certificate is done by the request module itself.

Fixes: #126.
2018-06-22 12:50:20 +05:30
Akash Nimare
99a1711bb0 Open image-attachements in browser if inline preview is unavailable.
Fixes: #503.
2018-06-22 12:42:38 +05:30
Abhigyan Khaund
a27cf9e0e7 proxy-setting: Feature to use system proxy settings.
This PR uses resolveProxy to read system proxy settings and store
them in proper proxy format string using ConfigUtil. It removes the previous
use proxy option and replaces it with use system proxy and manual proxy
options.

Fixes: #296.
2018-06-15 23:40:41 +05:30
Abhigyan Khaund
22d6c6a2f3 config-util: Change return to a single return statement in isConfigItemExists.
Also add comments to describe the isConfigItemExists function.
2018-06-14 21:47:44 +05:30
Akash Nimare
c742591471 Use escape-html module for escaping realm name.
Since this module gives better results in compare
to window's default escape function.
2018-06-13 23:42:09 +05:30
Abhigyan Khaund
339a415d60 config-util: Add isConfigItemExists to check if a key exists. 2018-06-09 01:04:20 +05:30
Akash Nimare
f7696cc04d sidebar: Escape HTML for already added realm.
For the new servers we are already pushing the realm
details after escaping data but for already saved servers
we should escape the same to avoid any security issue.
2018-06-05 02:23:18 +05:30
Akash Nimare
1eabf5c5a2 Update settings error message. 2018-06-03 03:15:28 +05:30
Akash Nimare
4d020c639b Document error message properly. 2018-06-03 03:08:05 +05:30
Akash Nimare
85a6cf6890 Add changelog file. 2018-05-31 03:33:35 +05:30
Akash Nimare
ef1ad820aa release: new release v2.3.2. 2018-05-29 02:59:05 +05:30
Abhigyan Khaund
10f6d80e5f documentation: Change #electron stream links to #desktop stream link. 2018-05-25 21:44:57 +05:30
Akash Nimare
50ef02c324 Update package.json. 2018-05-24 04:01:45 +05:30
Akash Nimare
deed18d75f electron: Fix app not rendering colors properly.
There is a bug in v2 of electron which causes
pale colors. Resetting the color profile to srgb
fixes this problem. This is a temporary solution
until electron officially fixes this.

More info - https://github.com/electron/electron/issues/10732.

Fixes: #497.
2018-05-24 03:56:14 +05:30
Akash Nimare
0e3c6bceeb node: Update node to v8 on appveyor. 2018-05-22 01:03:54 +05:30
Akash Nimare
c2e138c16a tests: Temporarily disable e2e tests on appveyor. 2018-05-21 23:23:41 +05:30
Akash Nimare
8d1b027b0e tests: Temporarily disable e2e tests. 2018-05-21 21:52:23 +05:30
Akash Nimare
7bb8d78973 node: Update node to v8 on travis. 2018-05-21 04:08:58 +05:30
Akash Nimare
a7c7791bf8 new release v2.3.1. 2018-05-21 03:40:22 +05:30
Akash Nimare
5d120b4416 shortcuts-menu: Add a tip-section to link to webapp's built-in hotkeys. 2018-05-21 03:29:56 +05:30
Abhigyan Khaund
ceaf13dee2 shortcuts-menu: Add a tip-section to link to webapp's built-in hotkeys.
Fixes: #459
2018-05-21 03:27:30 +05:30
Akash Nimare
f81381dfec electron: Update electron to v2.0.1. 2018-05-21 03:18:18 +05:30
Akash Nimare
dbe89cdd09 security: Use proper method to escape html.
Now using the `escape-html` module so that we can decode
the realm description properly and escape the html at the
same time. The encodeURIComponent function doesn't
provide this kind of flexibility. We need to decode the
real description properly since we show the same in the
tooltip and setting page.
2018-05-21 03:11:03 +05:30
Priyank Patel
14c59bdae1 Update node modules.
* dependencies: Upgrade rc to the depth of 5.

This updates rc dependecies, to fix a vulnerability

deep-extend - Prototype Pollution
Refs:  https://nodesecurity.io/advisories/612

* dependencies: Upgrade stringstream to the depth of 6.

This solves, 5 vulnerabilities which were using the vulnerable
version of stringstream.

stringstream - Out-of-bounds Read
Refs: https://nodesecurity.io/advisories/664

* gulp: Upgrade gulp to v4.0.0.

This solves, 5 vulnerabilities.

minimatch - Regular Expression Denial of Service
https://nodesecurity.io/advisories/118

lodash - Prototype Pollution
https://nodesecurity.io/advisories/577

* dependencies: Update request to the depth of 6.

This solves 5 vulnerabilities.

stringstream - Out-of-bounds Read
https://nodesecurity.io/advisories/664
2018-05-21 02:17:18 +05:30
Akash Nimare
0ac3e3f6d3 New release v2.3.0. 2018-05-17 00:59:39 +05:30
Akash Nimare
d69c1339e6 security: Do proper HTML escaping for server data.
This commit fixes a security bug which was caused by
using innerHTML and not doing proper HTML escaping.
Ideally, we should be doing proper HTML escaping for
any data we get from the server to avoid XSS attack.
We already handle realm icon and url very well, the
realm description was not handled previously but this commit
now fixes this in a right way.
2018-05-16 18:25:03 +05:30
Akash Nimare
fe56a20334 setting: Add an option to disable the auto-updates. (#493)
Fixes: #491.
2018-05-16 16:00:21 +05:30
Akash Nimare
cfc97c9b73 Update electron-builder. 2018-05-15 19:01:33 +05:30
Abhigyan Khaund
2e70b515da menu: Add option to check for updates.
Fixes: #479.
2018-05-15 17:20:40 +05:30
Akash Nimare
51e414a508 windows: Update deprecated electron api (toPng -> toPNG). 2018-05-08 19:24:42 +05:30
Akash Nimare
8e7a9bf230 release: new beta release v2.2.0. 2018-05-08 03:23:52 +05:30
Akash Nimare
6493ddb8ec builder: Update electron-updater. 2018-05-08 03:21:53 +05:30
Akash Nimare
31edbe0d67 mac: Update dmg installer image.
Fixes: #474.
2018-05-08 03:17:31 +05:30
Abhigyan Khaund
9980fee785 loading-indicator: Fix loading indicator when server is loaded.
This PR changes the current implementation of the loading indicator by attaching
the indicator to the right dom element, so that it doesn't show up once a server
is loaded.

Fixes: #482.
2018-05-08 00:41:20 +05:30
Akash Nimare
ff9986ec6b crash-reporter: Remove deprecated autoSubmit api. 2018-05-03 04:15:20 +05:30
Akash Nimare
f3423d394c release: New beta release v2.1.0-beta. 2018-05-03 03:39:15 +05:30
Akash Nimare
a1da199627 Update app dependencies. 2018-05-03 03:09:53 +05:30
Abhigyan Khaund
537fbe8f9e feature: Add DND button in left sidebar.
This adds the do not disturb button to the left sidebar
which disables sound and notifications. It also disables
flash taskbar on windows.

Fixes: #298.
2018-05-03 02:35:13 +05:30
Akash Nimare
3fccb33fca electron. Update electron to v2.0.0. 2018-05-02 02:44:11 +05:30
Abhigyan Khaund
5638590c8b internal-links: Download file attachment links using downloadURL.
This commit download file attachments using downloadURL method of
webContent, the same way loadURL opens internal links. This
removes the use of hidden webview added in
f70432f4e3.

Improves: #469.
2018-05-01 20:37:33 +05:30
Priyank Patel
29ed00981d hidden-webview: Move hidden webview so it does't get deleted.
It turns out if you add/remove an org the hidden webview get deleted
since its in #webview-container where other sidebar webviews like which
get removed and readded through that process.

Improves: #469.
2018-05-01 06:09:36 +05:30
Akash Nimare
d7638c0b95 artifacts: Update artifacts name. 2018-05-01 04:14:40 +05:30
Akash Nimare
7fadbe877b electron: Update app dependencies.
electron: v1.8.6
electron-builder: v20.11.1
electron-updater: v2.21.8
2018-05-01 00:59:59 +05:30
Akash Nimare
32a21889fb snap: Update snap summary. 2018-04-20 18:55:45 +05:30
Akash Nimare
c4a961f9da snap: Update snap config. 2018-04-19 23:48:58 +05:30
Akash Nimare
ceaa898570 snap: Add source branch for snap. 2018-04-19 23:19:01 +05:30
Akash Nimare
73fe17041d snap: Update Snap configuration. 2018-04-19 22:11:33 +05:30
Akash Nimare
9f756cad3e snap: Update snap icon path. 2018-04-19 20:58:48 +05:30
Akash Nimare
6db6b7c482 reconnect-util: Do not throw error message on server page while reconnecting. 2018-04-18 22:00:30 +05:30
Akash Nimare
09c45e75e8 Update package-lock files. 2018-04-18 17:51:22 +05:30
Akash Nimare
120b80cf65 release: 🎉 v2.0.0. 2018-04-17 15:51:32 +05:30
Abhigyan Khaund
22f705960d sidebar: Add scrollbar for list of organizations on overflow. 2018-04-16 19:38:26 +05:30
Priyank Patel
ca8ce1deaa report-issue: Add report issue UX using send-feedback electron element.
This uses @electron-elements/send-feedback package to easily implement
UX for reporting issues.
2018-04-16 11:34:11 +05:30
Priyank Patel
f70432f4e3 internal-links: open non-image links in hidden webview.
* This will make sure that the current server webview will not 
reload due to URL change.
* Add an option to allow users to download the file attachments.

Improves: #469.
2018-04-11 23:21:01 +05:30
Abhigyan Khaund
60d693700e internal-links: Open image link in webapp lightbox.
This will open the image in the webapp lightbox. It shows the same behaviour
that happens when clicking on the image preview.

Improves: #469.
2018-04-09 18:16:57 +05:30
Akash Nimare
6e7333eab6 shortcut: Add F5 shortcut for reloading the app. 2018-04-07 19:52:46 +05:30
Akash Nimare
0d8dd1cd90 internal-links: Open file attachments within the app.
Changing the behaviour of opening the attachments in the
default browser to the app. This commit enables users to
download the attachments. If the attachment type is image
then we open the same in the app though users need to
click on the go-back button.

To-do - Open the image link in a lightbox.

Improves: #469.
2018-04-04 18:02:40 +05:30
Akash Nimare
0ee3757774 shortcuts: Update string for selecting the keyboard shortcuts node. 2018-03-30 14:56:17 +05:30
Akash Nimare
811df9f381 shortcuts: Remove unused shortcuts. 2018-03-30 01:56:48 +05:30
Akash Nimare
8bd02cc7e4 shortcuts: Update toggle sidebar shortcut to CMD/CTRL+SHIFT+S. 2018-03-30 01:53:07 +05:30
Akash Nimare
9d5d221371 test: Add a test for new org link. 2018-03-28 02:36:50 +05:30
Akash Nimare
6006f1a3f8 builder: Update electron-builder to v20.8.1.
Fixes: #442.
2018-03-27 21:27:32 +05:30
Robert
4f96df4a34 linux: Add Snap support for Linux.
This adds a new build configs in package.json for electron-builder
to be able to generate a snap package for zulip. 

Fixes #443.
2018-03-26 22:27:04 +05:30
Abhigyan Khaund
a13558fa16 org-settings: Responsive UI for connected orgs in smaller window sizes.
Fixes: #456.
2018-03-26 21:09:52 +05:30
Abhigyan Khaund
a1d19a385c menu: Warning dialog box for Reset App Settings. 2018-03-26 21:05:26 +05:30
Robert
c98667236e tools: Fix reinstall script for working across all platforms.
Make reinstall script compatible with Windows. 

Fixes #440.
2018-03-24 00:04:26 +05:30
Akash Nimare
b80c3d007b server-page: Responsive tweaks for divider. 2018-03-23 12:18:44 +05:30
Akash Nimare
70fb198a0b release: 🎉 new-release v1.9.0. 2018-03-23 03:59:56 +05:30
Abhigyan Khaund
7276bfeaa7 org-settings-page: UI fixes and tweaks for the org-settings page.
Fixes:
* Move "Disconnect" button to be at the far right.
* Ellipsis text-overflow for server-url.
* Show server url as title tooltip on hover.
* Remove settings-card's right transition on hover.

Improves: #456.
2018-03-23 03:45:50 +05:30
Akash Nimare
abd71330ba setting-page: Minor UI fixes for add new server page.
Minor fixes - 
* Update link to create a new organization.
* Update placeholder text for demo zulip server.
* Remove border from connect button.
* Add overflow for smaller window.
* Fix a typo in background color.
* Use buttons for server actions.
* Make new server page responsive.
* Focus buttons on tab.
* Update styling of button.
* Make add new server page responsive.

Fixes: #457.
2018-03-23 01:36:43 +05:30
Akash Nimare
e776222d6b browser-window: Prevent drag and drop events.
This stops a remote code execution via drag and drop event in
the main/renderer process.

Fixes #453.
2018-03-22 23:23:18 +05:30
Akash Nimare
30b05571e7 electron: Upgrade to v1.8.4.
This upgrade includes some webview security fixes.
https://www.electronjs.org/blog/webview-fix
2018-03-22 11:14:32 +05:30
Akash Nimare
0a155c63e7 security: Do not allow running insecure contents in app. 2018-03-22 10:57:35 +05:30
Akash Nimare
13c750ac6c tests: Update Spectron to latest. 2018-03-20 20:54:32 +05:30
Akash Nimare
48799f75d1 tests: Fix failing renderer tests. 2018-03-20 20:53:10 +05:30
Akash Nimare
29f4e702ad Merge branch 'master' of github.com:zulip/zulip-electron 2018-03-19 20:45:54 +05:30
Akash Nimare
3eb4cf4f64 Ubuntu: Add Zulip link to the binrary.
Zulip can't be run from the terminal because we were not
linking the binary to the bin. The electron-builder used to support
this by default but we as we added the afterInstall
script it got removed.

More info -
https://github.com/electron-userland/electron-builder/issues/2682,
https://github.com/electron-userland/electron-builder/issues/2689

Fixes #444.
2018-03-19 20:38:38 +05:30
Abhigyan Khaund
1a97d8a5b0 design: Improve connected organization design.
This PR updates the connected organization page.
Changes - 
* All the server details are now inline. 
* Change "Delete" to "Disconnect". 
* Club org logo and name together on the right, and URL and "Disconnect" to the left.
* Link to organization logo on the server tab.
 
Improves #340.
2018-03-19 17:10:42 +05:30
Akash Nimare
2f96ec6199 settings: Fix a typo in silent setting option. 2018-03-17 03:49:43 +05:30
Akash Nimare
52de465457 settings: Fix app settings overriding issue. (#449)
This fixes an issue which was caused by saving the last active tab
before a functional tab activates. The bug was introduced in f409bb0449.
It was unnoticed from v1.5.0 to the latest v1.8.2.

The bug causes a serious issue where the app can't change the app settings and all settings were
getting overridden when a user switches back to setting page.

Fixes #448.
2018-03-17 03:06:41 +05:30
Ganesha Danu Enastika
e3039cf5a9 tools: Add missing Ubuntu dependancy. 2018-03-14 23:43:07 +05:30
Akash Nimare
6c120269eb design: Redesign Add new server page. (#446)
Changes - 
* New setting nav item for connected organizations.
* All new page for adding a new server.
* Minor UI changes in setting page.
* Update default height, width of the main window.
* Fix webview showing outline.
* Hide Server nav item in setting page.

Fixes #340.
2018-03-14 23:41:12 +05:30
Akash Nimare
b31fc6b66d setting-page: Better looking nav items. 2018-03-10 23:14:42 +05:30
Akash Nimare
435e5f086e webview: Fix webview flickering. (#439)
Rewrote styling for the webview so that it takes the whole window
when the app loads up.

Fixes #249.
2018-03-09 14:35:13 +05:30
Akash Nimare
603ad7dfcd setting-page: Make setting navs better. 2018-03-07 21:00:30 +05:30
64 changed files with 7614 additions and 5056 deletions

7
.gitignore vendored
View File

@@ -7,6 +7,13 @@ node_modules/
# Compiled binary build directory
dist/
#snap generated files
snap/parts
snap/prime
snap/snap
snap/stage
snap/*.snap
# Logs
logs
*.log

View File

@@ -15,7 +15,7 @@ addons:
language: node_js
node_js:
- '6'
- '8'
before_install:
- ./scripts/travis-xvfb.sh

View File

@@ -12,7 +12,7 @@ Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If yo
* The whole Zulip documentation, such as setting up a development environment, setting up with the Zulip webapp project, and testing, can be read [here](https://zulip.readthedocs.io).
* If you have any questions regarding zulip-electron, open an [issue](https://github.com/zulip/zulip-electron/issues/new/) or ask it on [chat.zulip.org](https://chat.zulip.org/#narrow/stream/electron).
* If you have any questions regarding zulip-electron, open an [issue](https://github.com/zulip/zulip-electron/issues/new/) or ask it on [chat.zulip.org](https://chat.zulip.org/#narrow/stream/16-desktop).
## Issue
Ensure the bug was not already reported by searching on GitHub under [issues](https://github.com/zulip/zulip-electron/issues). If you're unable to find an open issue addressing the bug, open a [new issue](https://github.com/zulip/zulip-electron/issues/new).

View File

@@ -1,11 +1,11 @@
'use strict';
const { app, dialog } = require('electron');
const { app, dialog, shell } = require('electron');
const { autoUpdater } = require('electron-updater');
const isDev = require('electron-is-dev');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
function appUpdater() {
function appUpdater(updateFromMenu = false) {
// Don't initiate auto-updates in development
if (isDev) {
return;
@@ -17,6 +17,8 @@ function appUpdater() {
return;
}
let updateAvailable = false;
// Create Logs directory
const LogsDir = `${app.getPath('userData')}/Logs`;
@@ -28,7 +30,58 @@ function appUpdater() {
autoUpdater.logger = log;
// Handle auto updates for beta/pre releases
autoUpdater.allowPrerelease = ConfigUtil.getConfigItem('betaUpdate') || false;
const isBetaUpdate = ConfigUtil.getConfigItem('betaUpdate');
autoUpdater.allowPrerelease = isBetaUpdate || false;
const eventsListenerRemove = ['update-available', 'update-not-available'];
autoUpdater.on('update-available', info => {
if (updateFromMenu) {
dialog.showMessageBox({
message: `A new version ${info.version}, of Zulip Desktop is available`,
detail: 'The update will be downloaded in the background. You will be notified when it is ready to be installed.'
});
updateAvailable = true;
// This is to prevent removal of 'update-downloaded' and 'error' event listener.
eventsListenerRemove.forEach(event => {
autoUpdater.removeAllListeners(event);
});
}
});
autoUpdater.on('update-not-available', () => {
if (updateFromMenu) {
dialog.showMessageBox({
message: 'No updates available',
detail: `You are running the latest version of Zulip Desktop.\nVersion: ${app.getVersion()}`
});
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
}
});
autoUpdater.on('error', error => {
if (updateFromMenu) {
const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates');
dialog.showMessageBox({
type: 'error',
buttons: ['Manual Download', 'Cancel'],
message: messageText,
detail: (error).toString() + `\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulipchat.com/apps/.\n
Current Version: ${app.getVersion()}`
}, response => {
if (response === 0) {
shell.openExternal('https://zulipchat.com/apps/');
}
});
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
}
});
// Ask the user if update is available
// eslint-disable-next-line no-unused-vars

View File

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

View File

@@ -5,7 +5,6 @@ const windowStateKeeper = require('electron-window-state');
const isDev = require('electron-is-dev');
const appMenu = require('./menu');
const { appUpdater } = require('./autoupdater');
const { crashHandler } = require('./crash-reporter');
const { setAutoLaunch } = require('./startup');
@@ -13,6 +12,8 @@ const { app, ipcMain } = electron;
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
const ProxyUtil = require('./../renderer/js/utils/proxy-util.js');
const { sentryInit } = require('./../renderer/js/utils/sentry-util.js');
// Adds debug features like hotkeys for triggering dev tools and reload
// in development mode
@@ -52,8 +53,9 @@ const iconPath = () => {
function createMainWindow() {
// Load the previous state with fallback to defaults
const mainWindowState = windowStateKeeper({
defaultWidth: 1000,
defaultHeight: 600
defaultWidth: 1100,
defaultHeight: 720,
path: `${app.getPath('userData')}/config`
});
// Let's keep the window position global so that we can access it in other process
@@ -71,7 +73,6 @@ function createMainWindow() {
minHeight: 400,
webPreferences: {
plugins: true,
allowDisplayingInsecureContent: true,
nodeIntegration: true
},
show: false
@@ -132,6 +133,10 @@ function createMainWindow() {
// Decrease load on GPU (experimental)
app.disableHardwareAcceleration();
// Temporary fix for Electron render colors differently
// More info here - https://github.com/electron/electron/issues/10732
app.commandLine.appendSwitch('force-color-profile', 'srgb');
// eslint-disable-next-line max-params
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
event.preventDefault();
@@ -150,6 +155,15 @@ app.on('ready', () => {
});
mainWindow = createMainWindow();
// Initialize sentry for main process
sentryInit();
const isSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
if (isSystemProxy) {
ProxyUtil.resolveSystemProxy(mainWindow);
}
const page = mainWindow.webContents;
page.on('dom-ready', () => {
@@ -162,8 +176,9 @@ app.on('ready', () => {
page.once('did-frame-finish-load', () => {
// Initate auto-updates on MacOS and Windows
appUpdater();
crashHandler();
if (ConfigUtil.getConfigItem('autoUpdate')) {
appUpdater();
}
});
// Temporarily remove this event
@@ -225,6 +240,17 @@ app.on('ready', () => {
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => {
setAutoLaunch(AutoLaunchValue);
});
ipcMain.on('downloadFile', (event, url, downloadPath) => {
page.downloadURL(url);
page.session.once('will-download', (event, item) => {
const filePath = path.join(downloadPath, item.getFilename());
item.setSavePath(filePath);
item.once('done', () => {
page.send('downloadFileCompleted', filePath, item.getFilename());
});
});
});
});
app.on('before-quit', () => {

View File

@@ -5,6 +5,12 @@ const request = require('request');
const semver = require('semver');
const ConfigUtil = require('../renderer/js/utils/config-util');
const LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util');
const Logger = require('../renderer/js/utils/logger-util');
const logger = new Logger({
file: 'linux-update-util.log',
timestamp: true
});
function linuxUpdateNotification() {
let url = 'https://api.github.com/repos/zulip/zulip-electron/releases';
@@ -17,7 +23,8 @@ function linuxUpdateNotification() {
request(options, (error, response, body) => {
if (error) {
console.log('Error:', error);
logger.error('Linux update error.');
logger.error(error);
return;
}
if (response.statusCode < 400) {
@@ -32,7 +39,7 @@ function linuxUpdateNotification() {
}
}
} else {
console.log('Status:', response.statusCode);
logger.log('Linux update response status: ', response.statusCode);
}
});
}

View File

@@ -1,15 +1,23 @@
'use strict';
const os = require('os');
const path = require('path');
const { app, shell, BrowserWindow, Menu } = require('electron');
const { app, shell, BrowserWindow, Menu, dialog } = require('electron');
const fs = require('fs-extra');
const AdmZip = require('adm-zip');
const { appUpdater } = require('./autoupdater');
const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');
const DNDUtil = require(__dirname + '/../renderer/js/utils/dnd-util.js');
const Logger = require(__dirname + '/../renderer/js/utils/logger-util.js');
const appName = app.getName();
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
class AppMenu {
getHistorySubmenu() {
return [{
@@ -87,7 +95,7 @@ class AppMenu {
}
}, {
label: 'Toggle Sidebar',
accelerator: 'CommandOrControl+S',
accelerator: 'CommandOrControl+Shift+S',
click(item, focusedWindow) {
if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem('showSidebar');
@@ -134,18 +142,29 @@ class AppMenu {
}, {
label: 'Show App Logs',
click() {
shell.openItem(app.getPath('userData'));
const zip = new AdmZip();
let date = new Date();
date = date.toLocaleDateString().replace(/\//g, '-');
// Create a zip file of all the logs and config data
zip.addLocalFolder(`${app.getPath('appData')}/${appName}/Logs`);
zip.addLocalFolder(`${app.getPath('appData')}/${appName}/config`);
// Put the log file in downloads folder
const logFilePath = `${app.getPath('downloads')}/Zulip-logs-${date}.zip`;
zip.writeZip(logFilePath);
// Open and select the log file
shell.showItemInFolder(logFilePath);
}
}, {
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)}`);
// the goal is to notify the main.html BrowserWindow
// which may not be the focused window.
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('open-feedback-modal');
});
}
}];
}
@@ -197,6 +216,11 @@ class AppMenu {
AppMenu.sendAction('open-about');
}
}
}, {
label: `Check for Update`,
click() {
AppMenu.checkForUpdate();
}
}, {
type: 'separator'
}, {
@@ -217,6 +241,13 @@ class AppMenu {
}
}, {
type: 'separator'
}, {
label: 'Toggle Do Not Disturb',
accelerator: 'Command+Shift+M',
click() {
const dndUtil = DNDUtil.toggle();
AppMenu.sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
}
}, {
label: 'Reset App Settings',
accelerator: 'Command+Shift+D',
@@ -297,6 +328,11 @@ class AppMenu {
AppMenu.sendAction('open-about');
}
}
}, {
label: `Check for Update`,
click() {
AppMenu.checkForUpdate();
}
}, {
type: 'separator'
}, {
@@ -319,6 +355,13 @@ class AppMenu {
}
}, {
type: 'separator'
}, {
label: 'Toggle Do Not Disturb',
accelerator: 'Ctrl+Shift+M',
click() {
const dndUtil = DNDUtil.toggle();
AppMenu.sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
}
}, {
label: 'Reset App Settings',
accelerator: 'Ctrl+Shift+D',
@@ -387,21 +430,37 @@ class AppMenu {
win.webContents.send(action, ...params);
}
static checkForUpdate() {
appUpdater(true);
}
static resetAppSettings() {
// We save App's settings/configurations in following files
const settingFiles = ['window-state.json', 'domain.json', 'settings.json'];
const resetAppSettingsMessage = 'By proceeding you will be removing all connected organizations and preferences from Zulip.';
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');
// We save App's settings/configurations in following files
const settingFiles = ['config/window-state.json', 'config/domain.json', 'config/settings.json', 'config/certificates.json'];
dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
message: 'Are you sure?',
detail: resetAppSettingsMessage
}, response => {
if (response === 0) {
settingFiles.forEach(settingFileName => {
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
fs.access(getSettingFilesPath, error => {
if (error) {
logger.error('Error while resetting app settings.');
logger.error(error);
} else {
fs.unlink(getSettingFilesPath, () => {
AppMenu.sendAction('clear-app-data');
});
}
});
}
});
});
}
});
}

1168
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "1.8.2",
"version": "2.3.3",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"copyright": "Kandra Labs, Inc.",
@@ -26,19 +26,23 @@
"InstantMessaging"
],
"dependencies": {
"auto-launch": "5.0.1",
"@electron-elements/send-feedback": "1.0.7",
"@sentry/electron": "0.5.5",
"adm-zip": "0.4.11",
"auto-launch": "5.0.5",
"electron-is-dev": "0.3.0",
"electron-log": "2.2.7",
"electron-log": "2.2.14",
"electron-spellchecker": "1.1.2",
"electron-updater": "2.18.2",
"electron-updater": "2.23.3",
"electron-window-state": "4.1.1",
"escape-html": "1.0.3",
"is-online": "7.0.0",
"node-json-db": "0.7.3",
"request": "2.81.0",
"request": "2.85.0",
"semver": "5.4.1",
"wurl": "2.5.0"
},
"optionalDependencies": {
"node-mac-notifier": "0.0.13"
"node-mac-notifier": "0.1.0"
}
}

View File

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

View File

@@ -13,9 +13,6 @@ body {
#content {
display: flex;
height: 100%;
background: #fff url(../img/ic_loading.gif) no-repeat;
background-size: 60px 60px;
background-position: center;
}
.toggle-sidebar {
@@ -28,7 +25,7 @@ body {
-webkit-app-region: drag;
overflow: hidden;
transition: all 0.5s ease;
z-index: 1;
z-index: 2;
}
.toggle-sidebar div {
@@ -45,6 +42,28 @@ body {
transition: all 0.6s ease-out;
}
#view-controls-container {
height: calc(100% - 208px);
overflow-y: hidden;
}
#view-controls-container:hover {
overflow-y: overlay;
}
#view-controls-container::-webkit-scrollbar {
width: 4px;
}
#view-controls-container::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
}
#view-controls-container::-webkit-scrollbar-thumb {
background-color: darkgrey;
outline: 1px solid slategrey;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
@@ -121,11 +140,14 @@ body {
}
.action-button.active {
background-color: rgba(255, 255, 255, 0.25);
/* background-color: rgba(255, 255, 255, 0.25); */
background-color: #efefef;
opacity: 0.9;
padding-right: 14px;
}
.action-button.active i {
color: #eee;
color: #1c262b;
}
.tab:first-child {
@@ -248,37 +270,63 @@ body {
width: 100%;
}
/*Pseudo element for loading indicator*/
#webviews-container::before {
content: "";
position: absolute;
z-index: 1;
background: #fff url(../img/ic_loading.gif) no-repeat;
background-size: 60px 60px;
background-position: center;
width: 100%;
height: 100%;
}
/*When the active webview is loaded*/
#webviews-container.loaded::before {
opacity: 0;
z-index: -1;
visibility: hidden;
}
webview {
opacity: 1;
/* transition: opacity 0.3s ease-in; */
flex-grow: 1;
position: absolute;
width: 100%;
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
}
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 ease-out;
webview.active {
opacity: 1;
z-index: 1;
visibility: visible;
}
webview:focus {
webview.disabled {
opacity: 0;
}
webview.focus {
outline: 0px solid transparent;
}
/* Tooltip styling */
#dnd-tooltip,
#back-tooltip,
#reload-tooltip,
#setting-tooltip {
font-family: sans-serif;
background: #222c31;
margin-left: 45px;
margin-left: 48px;
padding: 6px 8px;
position: absolute;
margin-top: 0px;
@@ -290,6 +338,7 @@ webview:focus {
font-size: 14px;
}
#dnd-tooltip:after,
#back-tooltip:after,
#reload-tooltip:after,
#setting-tooltip:after {
@@ -350,6 +399,8 @@ webview:focus {
height: 100%;
width: 100%;
position: relative;
flex-grow: 1;
flex-basis: 0px;
}
.hidden {
@@ -392,3 +443,26 @@ webview:focus {
opacity: 1;
}
}
send-feedback {
width: 60%;
height: 85%;
}
#feedback-modal {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(68, 67, 67, 0.81);
align-items: center;
justify-content: center;
z-index: 2;
transition: all 1s ease-out;
}
#feedback-modal.show {
display: flex;
}

View File

@@ -95,11 +95,13 @@ td:nth-child(odd) {
}
#sidebar {
width: 80px;
padding: 30px;
width: 150px;
min-width: 100px;
padding: 30px 30px 30px 35px;
display: flex;
flex-direction: column;
font-size: 16px;
background: #f2f2f2;
}
#nav-container {
@@ -113,7 +115,7 @@ td:nth-child(odd) {
}
.nav.active {
color: #464e5a;
color: #4ebfac;
cursor: default;
position: relative;
}
@@ -121,12 +123,19 @@ td:nth-child(odd) {
.nav.active::before {
background: #464e5a;
width: 3px;
height: 16px;
height: 18px;
position: absolute;
left: -8px;
content: '';
}
/* We don't want to show this in nav item since we have the + button for adding an Organization */
#nav-AddServer {
display: none;
}
#settings-header {
font-size: 22px;
color: #222c31;
@@ -142,12 +151,12 @@ td:nth-child(odd) {
}
#new-server-container {
opacity: 1;
transition: opacity 0.3s;
padding-left: 42px;
padding-top: 25px;
margin-right: 16px;
}
.title {
padding: 4px 0 6px 0;
font-weight: 500;
color: #222c31;
}
@@ -159,6 +168,16 @@ td:nth-child(odd) {
padding: 4px 0 6px 0;
}
.add-server-info-row {
display: flex;
margin: 8px 0 0 0;
}
.add-server-info-right {
flex-grow: 1;
margin-top: 10px;
}
.sub-title {
padding: 4px 0 6px 0;
font-weight: bold;
@@ -169,20 +188,40 @@ img.server-info-icon {
width: 36px;
height: 36px;
padding: 4px;
cursor: pointer;
vertical-align: middle;
}
.server-info-left {
margin: 10px 20px 0 0;
margin: 4px 20px 0 0;
min-width: 40%;
}
.server-info-right {
margin-top: 4px;
width: 55%;
display: flex;
align-items: baseline;
justify-content: space-between;
flex-grow: 1;
margin-right: 10px;
}
.server-info-row {
display: flex;
margin: 8px 0 0 0;
display: inline-block;
margin: 5px 0 0 0;
}
.server-info-left .server-info-row {
display: inline-flex;
align-items: inherit;
vertical-align: -2px;
position: relative;
}
.server-url {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.server-info-alias {
@@ -190,6 +229,10 @@ img.server-info-icon {
cursor: pointer;
}
.server-url-info {
margin-right: 10px;
}
.setting-input-key {
font-size: 14px;
height: 27px;
@@ -203,22 +246,20 @@ img.server-info-icon {
.setting-input-value {
flex-grow: 1;
font-size: 14px;
height: 22px;
border-radius: 3px;
padding: 7px;
padding: 13px;
border: #ededed 2px solid;
outline-width: 0;
background: transparent;
max-width: 500px;
max-width: 450px;
}
.setting-input-value:focus {
border: #7cb980 2px solid;
border-radius: 3px;
border: #4EBFAC 2px solid;
}
.setting-block {
width: 100%;
.manual-proxy-block {
width: 96%;
}
.actions-container {
@@ -270,7 +311,6 @@ img.server-info-icon {
}
.settings-card:hover {
border-left: 8px solid #bcbcbc;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 0px 0px rgba(0, 0, 0, 0.12);
}
@@ -280,11 +320,15 @@ img.server-info-icon {
}
.red {
color: #ffffff;
background: #ef5350;
padding: 3px;
padding-right: 10px;
padding-left: 10px;
color: #ef5350;
padding: 8px;
border: rgba(239, 83, 80, 0.5) solid 1px;
}
.red:hover {
color: #e63431;
border: rgba(239, 83, 80, 0.7) solid 1px;
;
}
.blue {
@@ -314,13 +358,14 @@ img.server-info-icon {
}
i.open-tab-button {
padding: 0 5px;
font-size: 18px;
padding-left: 2px;
font-size: 19px;
cursor: pointer;
}
.reset-data-button,
.custom-css-button {
.custom-css-button,
.download-folder-button {
display: inline-block;
border: none;
padding: 10px;
@@ -340,7 +385,8 @@ i.open-tab-button {
color: #fff;
}
.selected-css-path {
.selected-css-path,
.download-folder-path {
background: #eeeeee;
padding: 10px;
margin-top: 10px;
@@ -368,17 +414,10 @@ i.open-tab-button {
}
#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;
@@ -433,10 +472,140 @@ input.toggle-round:checked+label:after {
}
/* Add new server modal */
.add-server-modal {
display: block;
position: fixed;
z-index: 1;
padding-top: 15vh;
left: 0;
top: 0;
margin: auto;
width: 100%;
height: 100%;
/* background: rgba(61, 64, 67, 15); */
background: linear-gradient(35deg, #003b52, #45b59b);
overflow: auto;
}
/* Modal Content */
.modal-container {
background-color: #f4f7f8;
margin: auto;
padding: 57px;
border: #dae1e3 1px solid;
width: 550px;
height: 370px;
border-radius: 4px;
}
.add-server-modal .page-title {
text-align: center;
font-size: 1.6rem;
}
.divider {
margin-bottom: 30px;
margin-top: 30px;
color: #7d878a;
}
.divider hr {
margin-left: 8px;
margin-right: 8px;
width: 44%;
}
.left {
float: left;
}
.right {
float: right;
}
.server-center {
width: 100%;
text-align: center;
align-items: center;
padding-top: 13px;
margin-left: -5px;
}
.server-center button {
font-weight: bold;
font-size: 1.1rem;
margin: auto;
align-items: center;
text-align: center;
color: #fff;
background: #4EBFAC;
border-color: none;
border: none;
width: 98%;
height: 46px;
border-radius: 3px;
cursor: pointer;
}
.server-center button:hover {
background: #329588;
}
.server-center button:focus {
background: #329588;
}
.certificates-card {
width:70%
}
.certificate-input {
width:100%;
margin-top: 10px;
display:inline-flex;
}
.certificate-input div {
align-self:center;
}
.certificate-input .setting-input-value {
margin-left:10px;
max-width: 100%;
}
#add-certificate-button {
width:20%;
margin-right:0px;
height: 35px;
}
.tip {
background-color: hsl(46,63%,95%);
border: 1px solid hsl(46,63%,84%);
border-radius: 4px;
}
.md-14 {
font-size: 14px;
vertical-align: middle;
padding-right: 6px;
}
#open-hotkeys-link {
text-decoration: underline;
cursor: pointer;
}
/* responsive grid */
@media (max-width: 650px) {
.selected-css-path {
.selected-css-path,
.download-folder-path {
margin-right: 15px;
}
#css-delete-action {
@@ -445,4 +614,54 @@ input.toggle-round:checked+label:after {
#css-delete-action span {
display: none;
}
}
}
@media (max-width: 720px) {
.modal-container {
width: 60vw;
padding: 40px;
min-width: 300px;
}
.server-center button {
margin-right: -12px;
width: 100%;
}
.divider {
margin-right: -8px;
}
.divider hr {
margin-left: 6px;
margin-right: 6px;
width: 43%;
}
#new-server-container {
padding-left: 0px;
}
}
@media (max-width: 600px) {
.divider {
margin-left: 4%;
}
.divider hr {
margin-left: 2px;
margin-right: 2px;
width: 40%;
}
}
@media (max-width: 900px) {
.settings-card {
flex-direction: column;
align-items: center;
}
.server-info-right {
flex-direction: column;
align-items: center;
}
.action {
margin-top: 10px;
}
}

View File

@@ -0,0 +1,54 @@
const { ipcRenderer } = require('electron');
const { shell, app } = require('electron').remote;
const LinkUtil = require('../utils/link-util');
const DomainUtil = require('../utils/domain-util');
const ConfigUtil = require('../utils/config-util');
const dingSound = new Audio('../resources/sounds/ding.ogg');
function handleExternalLink(event) {
const { url } = event;
const domainPrefix = DomainUtil.getDomain(this.props.index).url;
const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`);
// Whitelist URLs which are allowed to be opened in the app
const {
isInternalUrl: isWhiteListURL,
isUploadsUrl: isUploadsURL
} = LinkUtil.isInternal(domainPrefix, url);
if (isWhiteListURL) {
event.preventDefault();
// download txt, pdf, mp3, mp4 etc.. by using downloadURL in the
// main process which allows the user to save the files to their desktop
// and not trigger webview reload while image in webview will
// do nothing and will not save it
if (!LinkUtil.isImage(url) && isUploadsURL) {
ipcRenderer.send('downloadFile', url, downloadPath);
ipcRenderer.once('downloadFileCompleted', (event, filePath, fileName) => {
const downloadNotification = new Notification('Download Complete', {
body: `Click to open ${fileName}`,
silent: true // We'll play our own sound - ding.ogg
});
// Play sound to indicate download complete
if (!ConfigUtil.getConfigItem('silent')) {
dingSound.play();
}
downloadNotification.onclick = () => {
shell.openItem(filePath);
};
});
return;
}
// open internal urls inside the current webview.
this.$el.loadURL(url);
} else {
event.preventDefault();
shell.openExternal(url);
}
}
module.exports = handleExternalLink;

View File

@@ -3,13 +3,12 @@
const path = require('path');
const fs = require('fs');
const DomainUtil = require(__dirname + '/../utils/domain-util.js');
const ConfigUtil = require(__dirname + '/../utils/config-util.js');
const SystemUtil = require(__dirname + '/../utils/system-util.js');
const LinkUtil = require(__dirname + '/../utils/link-util.js');
const { shell, app, dialog } = require('electron').remote;
const { app, dialog } = require('electron').remote;
const BaseComponent = require(__dirname + '/../components/base.js');
const handleExternalLink = require(__dirname + '/../components/handle-external-link.js');
const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
class WebView extends BaseComponent {
@@ -19,9 +18,10 @@ class WebView extends BaseComponent {
this.props = props;
this.zoomFactor = 1.0;
this.loading = false;
this.loading = true;
this.badgeCount = 0;
this.customCSS = ConfigUtil.getConfigItem('customCSS');
this.$webviewsContainer = document.querySelector('#webviews-container').classList;
}
template() {
@@ -46,16 +46,7 @@ class WebView extends BaseComponent {
registerListeners() {
this.$el.addEventListener('new-window', event => {
const { url } = event;
const domainPrefix = DomainUtil.getDomain(this.props.index).url;
if (LinkUtil.isInternal(domainPrefix, url) || url === (domainPrefix + '/')) {
event.preventDefault();
this.$el.loadURL(url);
} else {
event.preventDefault();
shell.openExternal(url);
}
handleExternalLink.call(this, event);
});
if (shouldSilentWebview) {
@@ -84,11 +75,16 @@ class WebView extends BaseComponent {
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('●');
// bounce the dock
if (ConfigUtil.getConfigItem('dockBouncing')) {
app.dock.bounce();
}
}
});
@@ -96,6 +92,7 @@ class WebView extends BaseComponent {
if (this.props.role === 'server') {
this.$el.classList.add('onload');
}
this.loading = false;
this.show();
});
@@ -129,14 +126,21 @@ class WebView extends BaseComponent {
return;
}
// To show or hide the loading indicator in the the active tab
if (this.loading) {
this.$webviewsContainer.remove('loaded');
} else {
this.$webviewsContainer.add('loaded');
}
this.$el.classList.remove('disabled');
this.$el.classList.add('active');
setTimeout(() => {
if (this.props.role === 'server') {
this.$el.classList.remove('onload');
}
}, 1000);
this.focus();
this.loading = false;
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'));
@@ -168,6 +172,7 @@ class WebView extends BaseComponent {
hide() {
this.$el.classList.add('disabled');
this.$el.classList.remove('active');
}
load() {
@@ -228,6 +233,9 @@ class WebView extends BaseComponent {
reload() {
this.hide();
// Shows the loading indicator till the webview is reloaded
this.$webviewsContainer.remove('loaded');
this.loading = true;
this.$el.reload();
}

View File

@@ -0,0 +1,62 @@
const { app } = require('electron').remote;
const path = require('path');
const fs = require('fs');
const SendFeedback = require('@electron-elements/send-feedback');
// make the button color match zulip app's theme
SendFeedback.customStyles = `
button:hover, button:focus {
border-color: #4EBFAC;
color: #4EBFAC;
}
button:active {
background-color: #f1f1f1;
color: #4EBFAC;
}
button {
background-color: #4EBFAC;
border-color: #4EBFAC;
}
`;
customElements.define('send-feedback', SendFeedback);
const sendFeedback = document.querySelector('send-feedback');
const feedbackHolder = sendFeedback.parentElement;
// customize the fields of custom elements
sendFeedback.title = 'Report Issue';
sendFeedback.titleLabel = 'Issue title:';
sendFeedback.titlePlaceholder = 'Enter issue title';
sendFeedback.textareaLabel = 'Describe the issue:';
sendFeedback.textareaPlaceholder = 'Succinctly describe your issue and steps to reproduce it...';
sendFeedback.buttonLabel = 'Report Issue';
sendFeedback.loaderSuccessText = '';
sendFeedback.useReporter('emailReporter', {
email: 'akash@zulipchat.com'
});
feedbackHolder.addEventListener('click', e => {
// only remove the class if the grey out faded
// part is clicked and not the feedback element itself
if (e.target === e.currentTarget) {
feedbackHolder.classList.remove('show');
}
});
sendFeedback.addEventListener('feedback-submitted', () => {
setTimeout(() => {
feedbackHolder.classList.remove('show');
}, 1000);
});
const dataDir = app.getPath('userData');
const logsDir = path.join(dataDir, '/Logs');
sendFeedback.logs.push(...fs.readdirSync(logsDir).map(file => path.join(logsDir, file)));
module.exports = {
feedbackHolder,
sendFeedback
};

View File

@@ -3,7 +3,7 @@
const { ipcRenderer, remote } = require('electron');
const isDev = require('electron-is-dev');
const { session } = remote;
const { session, app } = remote;
require(__dirname + '/js/tray.js');
const DomainUtil = require(__dirname + '/js/utils/domain-util.js');
@@ -11,7 +11,17 @@ 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');
const DNDUtil = require(__dirname + '/js/utils/dnd-util.js');
const ReconnectUtil = require(__dirname + '/js/utils/reconnect-util.js');
const Logger = require(__dirname + '/js/utils/logger-util.js');
const { feedbackHolder } = require(__dirname + '/js/feedback.js');
const escape = require('escape-html');
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
class ServerManagerView {
constructor() {
@@ -23,12 +33,14 @@ class ServerManagerView {
this.$settingsButton = $actionsContainer.querySelector('#settings-action');
this.$webviewsContainer = document.getElementById('webviews-container');
this.$backButton = $actionsContainer.querySelector('#back-action');
this.$dndButton = $actionsContainer.querySelector('#dnd-action');
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.$backTooltip = $actionsContainer.querySelector('#back-tooltip');
this.$dndTooltip = $actionsContainer.querySelector('#dnd-tooltip');
this.$sidebar = document.getElementById('sidebar');
@@ -54,7 +66,17 @@ class ServerManagerView {
loadProxy() {
return new Promise(resolve => {
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
// To change proxyEnable to useManualProxy in older versions
const proxyEnabledOld = ConfigUtil.isConfigItemExists('useProxy');
if (proxyEnabledOld) {
const proxyEnableOldState = ConfigUtil.getConfigItem('useProxy');
if (proxyEnableOldState) {
ConfigUtil.setConfigItem('useManualProxy', true);
}
ConfigUtil.removeConfigItem('useProxy');
}
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
if (proxyEnabled) {
session.fromPartition('persist:webviewsession').setProxy({
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
@@ -78,16 +100,24 @@ class ServerManagerView {
// Default settings which should be respected
const settingOptions = {
trayIcon: true,
useProxy: false,
useManualProxy: false,
useSystemProxy: false,
showSidebar: true,
badgeOption: true,
startAtLogin: false,
startMinimized: false,
enableSpellchecker: true,
showNotification: true,
autoUpdate: true,
betaUpdate: false,
silent: false,
lastActiveTab: 0
lastActiveTab: 0,
dnd: false,
dndPreviousSettings: {
showNotification: true,
silent: false
},
downloadsPath: `${app.getPath('downloads')}`
};
// Platform specific settings
@@ -95,6 +125,12 @@ class ServerManagerView {
if (process.platform === 'win32') {
// Only available on Windows
settingOptions.flashTaskbarOnMessage = true;
settingOptions.dndPreviousSettings.flashTaskbarOnMessage = true;
}
if (process.platform === 'darwin') {
// Only available on macOS
settingOptions.dockBouncing = true;
}
for (const i in settingOptions) {
@@ -122,7 +158,7 @@ class ServerManagerView {
// Remove focus from the settings icon at sidebar bottom
this.$settingsButton.classList.remove('active');
} else {
this.openSettings('Servers');
this.openSettings('AddServer');
}
}
@@ -155,11 +191,16 @@ class ServerManagerView {
}
initActions() {
this.initDNDButton();
this.$dndButton.addEventListener('click', () => {
const dndUtil = DNDUtil.toggle();
ipcRenderer.send('forward-message', 'toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
});
this.$reloadButton.addEventListener('click', () => {
this.tabs[this.activeTabIndex].webview.reload();
});
this.$addServerButton.addEventListener('click', () => {
this.openSettings('Servers');
this.openSettings('AddServer');
});
this.$settingsButton.addEventListener('click', () => {
this.openSettings('General');
@@ -175,10 +216,16 @@ class ServerManagerView {
});
});
this.sidebarHoverEvent(this.$addServerButton, this.$addServerTooltip);
this.sidebarHoverEvent(this.$addServerButton, this.$addServerTooltip, true);
this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip);
this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip);
this.sidebarHoverEvent(this.$backButton, this.$backTooltip);
this.sidebarHoverEvent(this.$dndButton, this.$dndTooltip);
}
initDNDButton() {
const dnd = ConfigUtil.getConfigItem('dnd', false);
this.toggleDNDButton(dnd);
}
getTabIndex() {
@@ -187,9 +234,17 @@ class ServerManagerView {
return currentIndex;
}
sidebarHoverEvent(SidebarButton, SidebarTooltip) {
sidebarHoverEvent(SidebarButton, SidebarTooltip, addServer = false) {
SidebarButton.addEventListener('mouseover', () => {
SidebarTooltip.removeAttribute('style');
// To handle position of add server tooltip due to scrolling of list of organizations
// This could not be handled using CSS, hence the top of the tooltip is made same
// as that of its parent element.
// This needs to handled only for the add server tooltip and not others.
if (addServer) {
const { top } = SidebarButton.getBoundingClientRect();
SidebarTooltip.style.top = top + 'px';
}
});
SidebarButton.addEventListener('mouseout', () => {
SidebarTooltip.style.display = 'none';
@@ -197,8 +252,13 @@ class ServerManagerView {
}
onHover(index, serverName) {
this.$serverIconTooltip[index].innerHTML = serverName;
this.$serverIconTooltip[index].innerHTML = escape(serverName);
this.$serverIconTooltip[index].removeAttribute('style');
// To handle position of servers' tooltip due to scrolling of list of organizations
// This could not be handled using CSS, hence the top of the tooltip is made same
// as that of its parent element.
const { top } = this.$serverIconTooltip[index].parentElement.getBoundingClientRect();
this.$serverIconTooltip[index].style.top = top + 'px';
}
onHoverOut(index) {
@@ -238,6 +298,9 @@ class ServerManagerView {
preload: false
})
}));
// To show loading indicator the first time a functional tab is opened, indicator is
// closed when the functional tab DOM is ready, handled in webview.js
this.$webviewsContainer.classList.remove('loaded');
this.activateTab(this.functionalTabs[tabProps.name]);
}
@@ -268,10 +331,10 @@ class ServerManagerView {
}
activateLastTab(index) {
// Open last active tab
ConfigUtil.setConfigItem('lastActiveTab', index);
// Open all the tabs in background
// Open all the tabs in background, also activate the tab based on the index
this.activateTab(index);
// Save last active tab
ConfigUtil.setConfigItem('lastActiveTab', index);
}
activateTab(index, hideOldTab = true) {
@@ -311,7 +374,7 @@ class ServerManagerView {
webContents.send('toggle-sidebar', state);
});
ipcRenderer.on('toogle-silent', (event, state) => {
ipcRenderer.on('toggle-silent', (event, state) => {
const webviews = document.querySelectorAll('webview');
webviews.forEach(webview => {
try {
@@ -324,6 +387,15 @@ class ServerManagerView {
}
});
});
ipcRenderer.on('toggle-dnd', (event, state, newSettings) => {
this.toggleDNDButton(state);
ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent);
const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector);
const webContents = webview.getWebContents();
webContents.send('toggle-dnd', state, newSettings);
});
}
destroyTab(name, index) {
@@ -343,6 +415,9 @@ class ServerManagerView {
}
destroyView() {
// Show loading indicator
this.$webviewsContainer.classList.remove('loaded');
// Clear global variables
this.activeTabIndex = -1;
this.tabs = [];
@@ -390,6 +465,12 @@ class ServerManagerView {
}
}
// Toggles the dnd button icon.
toggleDNDButton(alert) {
this.$dndTooltip.textContent = (alert ? 'Turn Off' : 'Enable') + ' Do Not Disturb';
this.$dndButton.querySelector('i').textContent = alert ? 'notifications_off' : 'notifications';
}
registerIpcs() {
const webviewListeners = {
'webview-reload': 'reload',
@@ -439,6 +520,7 @@ class ServerManagerView {
this.loadProxy().then(() => {
if (showAlert) {
alert('Proxy settings saved!');
ipcRenderer.send('reload-full-app');
}
});
});
@@ -496,6 +578,10 @@ class ServerManagerView {
}
ipcRenderer.send('update-taskbar-icon', createOverlayIcon(messageCount).toDataURL(), String(messageCount));
});
ipcRenderer.on('open-feedback-modal', () => {
feedbackHolder.classList.add('show');
});
}
}
@@ -509,7 +595,7 @@ window.onload = () => {
window.addEventListener('offline', () => {
reconnectUtil.clearState();
console.log('No internet connection, you are offline.');
logger.log('No internet connection, you are offline.');
});
// only start electron-connect (auto reload on change) when its ran

View File

@@ -1,4 +1,10 @@
const { remote } = require('electron');
const Logger = require('../utils/logger-util.js');
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
// Do not change this
const appId = 'org.zulip.zulip-electron';
@@ -28,8 +34,8 @@ function loadBots(sync = false) {
botsListLoaded = true;
})
.fail(error => {
console.log('Request failed: ', error.responseText);
console.log('Request status: ', error.statusText);
logger.log('Load bots request failed: ', error.responseText);
logger.log('Load bots request status: ', error.statusText);
});
if (sync) {
$.ajaxSetup({async: true});

View File

@@ -0,0 +1,91 @@
'use-strict';
const { dialog } = require('electron').remote;
const BaseComponent = require(__dirname + '/../../components/base.js');
const CertificateUtil = require(__dirname + '/../../utils/certificate-util.js');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
class AddCertificate extends BaseComponent {
constructor(props) {
super();
this.props = props;
this._certFile = '';
}
template() {
return `
<div class="settings-card server-center certificates-card">
<div class="certificate-input">
<div>Organization URL :</div>
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
</div>
<div class="certificate-input">
<div>Custom CA's certificate file :</div>
<button id="add-certificate-button">Add</button>
</div>
</div>
`;
}
init() {
this.$addCertificate = this.generateNodeFromTemplate(this.template());
this.props.$root.appendChild(this.$addCertificate);
this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button');
this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0];
this.initListeners();
}
validateAndAdd() {
const certificate = this._certFile;
const serverUrl = this.serverUrl.value;
if (certificate !== '' && serverUrl !== '') {
const server = encodeURIComponent(DomainUtil.formatUrl(serverUrl));
const fileName = certificate.substring(certificate.lastIndexOf('/') + 1);
const copy = CertificateUtil.copyCertificate(server, certificate, fileName);
if (!copy) {
return;
}
CertificateUtil.setCertificate(server, fileName);
dialog.showMessageBox({
title: 'Success',
message: `Certificate saved!`
});
this.serverUrl.value = '';
} else {
dialog.showErrorBox('Error', `Please, ${serverUrl === '' ?
'Enter an Organization URL' : 'Choose certificate file'}`);
}
}
addHandler() {
const showDialogOptions = {
title: 'Select file',
defaultId: 1,
properties: ['openFile'],
filters: [{ name: 'crt, pem', extensions: ['crt', 'pem'] }]
};
dialog.showOpenDialog(showDialogOptions, selectedFile => {
if (selectedFile) {
this._certFile = selectedFile[0] || '';
this.validateAndAdd();
}
});
}
initListeners() {
this.addCertificateButton.addEventListener('click', () => {
this.addHandler();
});
this.serverUrl.addEventListener('keypress', event => {
const EnterkeyCode = event.keyCode;
if (EnterkeyCode === 13) {
this.addHandler();
}
});
}
}
module.exports = AddCertificate;

View File

@@ -0,0 +1,64 @@
'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 AddCertificate = require(__dirname + '/add-certificate.js');
class ConnectedOrgSection extends BaseSection {
constructor(props) {
super();
this.props = props;
}
template() {
return `
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">Connected organizations</div>
<div class="title" id="existing-servers">All the connected orgnizations will appear here.</div>
<div id="server-info-container"></div>
<div class="page-title">Add Custom Certificates</div>
<div id="add-certificate-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');
const noServerText = 'All the connected orgnizations will appear here';
// Show noServerText if no servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? noServerText : '';
for (let i = 0; i < servers.length; i++) {
new ServerInfoForm({
$root: this.$serverInfoContainer,
server: servers[i],
index: i,
onChange: this.reloadApp
}).init();
}
this.$addCertificateContainer = document.getElementById('add-certificate-container');
this.initAddCertificate();
}
initAddCertificate() {
new AddCertificate({
$root: this.$addCertificateContainer
}).init();
}
}
module.exports = ConnectedOrgSection;

View File

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

View File

@@ -31,6 +31,10 @@ class GeneralSection extends BaseSection {
<div class="setting-description">Show app unread badge</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="dock-bounce-option" style= "display:${process.platform === 'darwin' ? '' : 'none'}">
<div class="setting-description">Bounce dock on new private message</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>
@@ -48,7 +52,11 @@ class GeneralSection extends BaseSection {
</div>
</div>
<div class="title">App Updates</div>
<div class="settings-card">
<div class="settings-card">
<div class="setting-row" id="autoupdate-option">
<div class="setting-description">Enable auto updates</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="betaupdate-option">
<div class="setting-description">Get beta updates</div>
<div class="setting-control"></div>
@@ -87,6 +95,20 @@ class GeneralSection extends BaseSection {
</div>
</div>
</div>
<div class="title">Advanced</div>
<div class="settings-card">
<div class="setting-row" id="download-folder">
<div class="setting-description">
Default download location
</div>
<button class="download-folder-button blue">Choose</button>
</div>
<div class="setting-row">
<div class="setting-description">
<div class="download-folder-path">${ConfigUtil.getConfigItem('downloadsPath')}</div>
</div>
</div>
</div>
<div class="title">Reset Application Data</div>
<div class="settings-card">
<div class="setting-row" id="resetdata-option">
@@ -104,7 +126,8 @@ class GeneralSection extends BaseSection {
this.updateTrayOption();
this.updateBadgeOption();
this.updateSilentOption();
this.updateUpdateOption();
this.autoUpdateOption();
this.betaUpdateOption();
this.updateSidebarOption();
this.updateStartAtLoginOption();
this.updateResetDataOption();
@@ -114,12 +137,18 @@ class GeneralSection extends BaseSection {
this.addCustomCSS();
this.showCustomCSSPath();
this.removeCustomCSS();
this.downloadFolder();
// Platform specific settings
// Flashing taskbar on Windows
if (process.platform === 'win32') {
this.updateFlashTaskbar();
}
// Dock bounce on macOS
if (process.platform === 'darwin') {
this.updateDockBouncing();
}
}
updateTrayOption() {
@@ -148,6 +177,18 @@ class GeneralSection extends BaseSection {
});
}
updateDockBouncing() {
this.generateSettingOption({
$element: document.querySelector('#dock-bounce-option .setting-control'),
value: ConfigUtil.getConfigItem('dockBouncing', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('dockBouncing');
ConfigUtil.setConfigItem('dockBouncing', newValue);
this.updateDockBouncing();
}
});
}
updateFlashTaskbar() {
this.generateSettingOption({
$element: document.querySelector('#flash-taskbar-option .setting-control'),
@@ -160,14 +201,26 @@ class GeneralSection extends BaseSection {
});
}
updateUpdateOption() {
autoUpdateOption() {
this.generateSettingOption({
$element: document.querySelector('#autoupdate-option .setting-control'),
value: ConfigUtil.getConfigItem('autoUpdate', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('autoUpdate');
ConfigUtil.setConfigItem('autoUpdate', newValue);
this.autoUpdateOption();
}
});
}
betaUpdateOption() {
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();
this.betaUpdateOption();
}
});
}
@@ -180,7 +233,7 @@ class GeneralSection extends BaseSection {
const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue);
this.updateSilentOption();
currentBrowserWindow.send('toogle-silent', newValue);
currentBrowserWindow.send('toggle-silent', newValue);
}
});
}
@@ -310,6 +363,28 @@ class GeneralSection extends BaseSection {
});
}
downloadFolderDialog() {
const showDialogOptions = {
title: 'Select Download Location',
defaultId: 1,
properties: ['openDirectory']
};
dialog.showOpenDialog(showDialogOptions, selectedFolder => {
if (selectedFolder) {
ConfigUtil.setConfigItem('downloadsPath', selectedFolder[0]);
const downloadFolderPath = document.querySelector('.download-folder-path');
downloadFolderPath.innerText = selectedFolder[0];
}
});
}
downloadFolder() {
const downloadFolder = document.querySelector('#download-folder .download-folder-button');
downloadFolder.addEventListener('click', () => {
this.downloadFolderDialog();
});
}
}
module.exports = GeneralSection;

View File

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

View File

@@ -16,11 +16,15 @@ class NetworkSection extends BaseSection {
<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-row" id="use-system-settings">
<div class="setting-description">Use system proxy settings (requires restart)</div>
<div class="setting-control"></div>
</div>
<div class="setting-block">
<div class="setting-row" id="use-manual-settings">
<div class="setting-description">Manual proxy configuration</div>
<div class="setting-control"></div>
</div>
<div class="manual-proxy-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"/>
@@ -51,7 +55,7 @@ class NetworkSection extends BaseSection {
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.$manualProxyBlock = this.props.$root.querySelector('.manual-proxy-block');
this.initProxyOption();
this.$proxyPAC.value = ConfigUtil.getConfigItem('proxyPAC', '');
@@ -68,31 +72,54 @@ class NetworkSection extends BaseSection {
}
initProxyOption() {
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
this.toggleProxySettings(proxyEnabled);
const manualProxyEnabled = ConfigUtil.getConfigItem('useManualProxy', false);
this.toggleManualProxySettings(manualProxyEnabled);
this.updateProxyOption();
}
toggleProxySettings(option) {
toggleManualProxySettings(option) {
if (option) {
this.$settingBlock.classList.remove('hidden');
this.$manualProxyBlock.classList.remove('hidden');
} else {
this.$settingBlock.classList.add('hidden');
this.$manualProxyBlock.classList.add('hidden');
}
}
updateProxyOption() {
this.generateSettingOption({
$element: document.querySelector('#use-proxy-option .setting-control'),
value: ConfigUtil.getConfigItem('useProxy', false),
$element: document.querySelector('#use-system-settings .setting-control'),
value: ConfigUtil.getConfigItem('useSystemProxy', 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);
const newValue = !ConfigUtil.getConfigItem('useSystemProxy');
const manualProxyValue = ConfigUtil.getConfigItem('useManualProxy');
if (manualProxyValue && newValue) {
ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue);
this.toggleManualProxySettings(!manualProxyValue);
}
if (newValue === false) {
// Remove proxy system proxy settings
ConfigUtil.setConfigItem('proxyRules', '');
ipcRenderer.send('forward-message', 'reload-proxy', true);
}
ConfigUtil.setConfigItem('useSystemProxy', newValue);
this.updateProxyOption();
}
});
this.generateSettingOption({
$element: document.querySelector('#use-manual-settings .setting-control'),
value: ConfigUtil.getConfigItem('useManualProxy', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('useManualProxy');
const systemProxyValue = ConfigUtil.getConfigItem('useSystemProxy');
this.toggleManualProxySettings(newValue);
if (systemProxyValue && newValue) {
ConfigUtil.setConfigItem('useSystemProxy', !systemProxyValue);
}
ConfigUtil.setConfigItem('proxyRules', '');
ConfigUtil.setConfigItem('useManualProxy', newValue);
// Reload app only when turning manual proxy off, hence !newValue
ipcRenderer.send('forward-message', 'reload-proxy', !newValue);
this.updateProxyOption();
}
});

View File

@@ -2,6 +2,7 @@
const BaseComponent = require(__dirname + '/../../components/base.js');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
const shell = require('electron').shell;
class NewServerForm extends BaseComponent {
constructor(props) {
@@ -11,19 +12,26 @@ class NewServerForm extends BaseComponent {
template() {
return `
<div class="settings-card">
<div class="server-info-right">
<div class="title">URL of Zulip organization</div>
<div class="server-info-row">
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or chat.your-organization.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 class="server-input-container">
<div class="title">Organization URL</div>
<div class="add-server-info-row">
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
</div>
<div class="server-center">
<div class="server-save-action">
<button id="connect">Connect</button>
</div>
</div>
<div class="server-center">
<div class="divider">
<hr class="left"/>OR<hr class="right" />
</div>
</div>
<div class="server-center">
<div class="server-save-action">
<button id="open-create-org-link">Create a new organization</button>
</div>
</div>
</div>
`;
}
@@ -43,17 +51,25 @@ class NewServerForm extends BaseComponent {
}
submitFormHandler() {
this.$saveServerButton.children[1].innerHTML = 'Adding...';
this.$saveServerButton.children[0].innerHTML = 'Connecting...';
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
DomainUtil.addDomain(serverConf).then(() => {
this.props.onChange(this.props.index);
});
}, errorMessage => {
this.$saveServerButton.children[1].innerHTML = 'Add';
this.$saveServerButton.children[0].innerHTML = 'Connect';
alert(errorMessage);
});
}
openCreateNewOrgExternalLink() {
const link = 'https://zulipchat.com/new/';
const externalCreateNewOrgEl = document.getElementById('open-create-org-link');
externalCreateNewOrgEl.addEventListener('click', () => {
shell.openExternal(link);
});
}
initActions() {
this.$saveServerButton.addEventListener('click', () => {
this.submitFormHandler();
@@ -65,6 +81,8 @@ class NewServerForm extends BaseComponent {
this.submitFormHandler();
}
});
// open create new org link in default browser
this.openCreateNewOrgExternalLink();
}
}

View File

@@ -7,6 +7,7 @@ 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 ConnectedOrgSection = require(__dirname + '/js/pages/preference/connected-org-section.js');
const ShortcutsSection = require(__dirname + '/js/pages/preference/shortcuts-section.js');
class PreferenceView extends BaseComponent {
@@ -39,7 +40,7 @@ class PreferenceView extends BaseComponent {
handleNavigation(navItem) {
this.nav.select(navItem);
switch (navItem) {
case 'Servers': {
case 'AddServer': {
this.section = new ServersSection({
$root: this.$settingsContainer
});
@@ -51,6 +52,12 @@ class PreferenceView extends BaseComponent {
});
break;
}
case 'Organizations': {
this.section = new ConnectedOrgSection({
$root: this.$settingsContainer
});
break;
}
case 'Network': {
this.section = new NetworkSection({
$root: this.$settingsContainer
@@ -90,6 +97,15 @@ class PreferenceView extends BaseComponent {
ipcRenderer.on('toggletray', (event, state) => {
this.handleToggle('tray-option', state);
});
ipcRenderer.on('toggle-dnd', (event, state, newSettings) => {
this.handleToggle('show-notification-option', newSettings.showNotification);
this.handleToggle('silent-option', newSettings.silent);
if (process.platform === 'win32') {
this.handleToggle('flash-taskbar-option', newSettings.flashTaskbarOnMessage);
}
});
}
}

View File

@@ -16,19 +16,18 @@ class ServerInfoForm extends BaseComponent {
<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-right">
<div class="server-info-row server-url">
<span class="server-url-info" title="${this.props.server.url}">${this.props.server.url}</span>
</div>
<div class="server-info-row">
<div class="action red server-delete-action">
<i class="material-icons">indeterminate_check_box</i>
<span>Delete</span>
<span>Disconnect</span>
</div>
</div>
</div>
@@ -44,6 +43,7 @@ class ServerInfoForm extends BaseComponent {
initForm() {
this.$serverInfoForm = this.generateNodeFromTemplate(this.template());
this.$serverInfoAlias = this.$serverInfoForm.getElementsByClassName('server-info-alias')[0];
this.$serverIcon = this.$serverInfoForm.getElementsByClassName('server-info-icon')[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);
@@ -71,7 +71,12 @@ class ServerInfoForm extends BaseComponent {
this.$serverInfoAlias.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
});
this.$serverIcon.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
});
}
}
module.exports = ServerInfoForm;

View File

@@ -1,10 +1,7 @@
'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) {
@@ -14,13 +11,14 @@ class ServersSection extends BaseSection {
template() {
return `
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">Register or login to a Zulip organization to get started</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 class="add-server-modal">
<div class="modal-container">
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">Add a Zulip organization</div>
<div id="new-server-container"></div>
</div>
</div>
</div>
`;
}
@@ -31,35 +29,10 @@ class ServersSection extends BaseSection {
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 ? '' : '';
// Show Existing servers if servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Connected organizations';
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() {

View File

@@ -1,6 +1,7 @@
'use strict';
const BaseSection = require(__dirname + '/base-section.js');
const shell = require('electron').shell;
class ShortcutsSection extends BaseSection {
constructor(props) {
@@ -23,6 +24,10 @@ class ShortcutsSection extends BaseSection {
<tr>
<td><kbd>${userOSKey}</kbd><kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
<td>Toggle Do Not Disturb</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>D</kbd></td>
@@ -78,14 +83,6 @@ class ShortcutsSection extends BaseSection {
<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>
@@ -121,7 +118,7 @@ class ShortcutsSection extends BaseSection {
<td>Actual Size</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>S</kbd></td>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
<td>Toggle Sidebar</td>
</tr>
<tr>
@@ -163,6 +160,7 @@ class ShortcutsSection extends BaseSection {
</table>
<div class="setting-control"></div>
</div>
<div class="settings-card tip"><b><i class="material-icons md-14">settings</i>Tip: </b>These desktop app shortcuts extend the Zulip webapp's <span id="open-hotkeys-link">keyboard shortcuts</span>.</div>
</div>
`;
}
@@ -182,6 +180,10 @@ class ShortcutsSection extends BaseSection {
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
<td>Toggle Do Not Disturb</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>L</kbd></td>
@@ -256,7 +258,7 @@ class ShortcutsSection extends BaseSection {
<td>Actual Size</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>S</kbd></td>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
<td>Toggle Sidebar</td>
</tr>
<tr>
@@ -298,13 +300,22 @@ class ShortcutsSection extends BaseSection {
</table>
<div class="setting-control"></div>
</div>
<div class="tip"><b><i class="material-icons md-14">lightbulb_outline</i>Tip: </b>These desktop app shortcuts extend the Zulip webapp's <span id="open-hotkeys-link">keyboard shortcuts</span>.</div>
</div>
`;
}
openHotkeysExternalLink() {
const link = 'https://zulipchat.com/help/keyboard-shortcuts';
const externalCreateNewOrgEl = document.getElementById('open-hotkeys-link');
externalCreateNewOrgEl.addEventListener('click', () => {
shell.openExternal(link);
});
}
init() {
this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin();
this.openHotkeysExternalLink();
}
}

View File

@@ -1,13 +1,17 @@
'use strict';
const { ipcRenderer } = require('electron');
const { ipcRenderer, shell } = require('electron');
const SetupSpellChecker = require('./spellchecker');
const ConfigUtil = require(__dirname + '/utils/config-util.js');
const LinkUtil = require(__dirname + '/utils/link-util.js');
// eslint-disable-next-line import/no-unassigned-import
require('./notification');
// Prevent drag and drop event in main process which prevents remote code executaion
require(__dirname + '/shared/preventdrag.js');
const logout = () => {
// Create the menu for the below
document.querySelector('.dropdown-toggle').click();
@@ -20,7 +24,7 @@ const shortcut = () => {
// Create the menu for the below
const node = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
// Additional check
if (node.text.trim().toLowerCase() === 'keyboard shortcuts') {
if (node.text.trim().toLowerCase() === 'keyboard shortcuts (?)') {
node.click();
} else {
// Atleast click the dropdown
@@ -52,6 +56,29 @@ document.addEventListener('DOMContentLoaded', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
}
// Open image attachment link in the lightbox instead of opening in the default browser
const { $, lightbox } = window;
$('#main_div').on('click', '.message_content p a', function (e) {
const url = $(this).attr('href');
if (LinkUtil.isImage(url)) {
const $img = $(this).parent().siblings('.message_inline_image').find('img');
// prevent the image link from opening in a new page.
e.preventDefault();
// prevent the message compose dialog from happening.
e.stopPropagation();
// Open image in the default browser if image preview is unavailable
if (!$img[0]) {
shell.openExternal(window.location.origin + url);
}
// Open image in lightbox
lightbox.open($img);
}
});
});
// Clean up spellchecker events after you navigate away from this page;
@@ -60,3 +87,10 @@ window.addEventListener('beforeunload', () => {
SetupSpellChecker.unsubscribeSpellChecker();
});
// electron's globalShortcut can cause unexpected results
// so adding the reload shortcut in the old-school way
document.addEventListener('keydown', event => {
if (event.code === 'F5') {
ipcRenderer.send('forward-message', 'hard-reload');
}
});

View File

@@ -0,0 +1,17 @@
'use strict';
// This is a security fix. Following function prevents drag and drop event in the app
// so that attackers can't execute any remote code within the app
// It doesn't affect the compose box so that users can still
// use drag and drop event to share files etc
const preventDragAndDrop = () => {
const preventEvents = ['dragover', 'drop'];
preventEvents.forEach(dragEvents => {
document.addEventListener(dragEvents, event => {
event.preventDefault();
});
});
};
preventDragAndDrop();

View File

@@ -3,6 +3,12 @@
const { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } = require('electron-spellchecker');
const ConfigUtil = require(__dirname + '/utils/config-util.js');
const Logger = require(__dirname + '/utils/logger-util.js');
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
class SetupSpellChecker {
init() {
@@ -16,7 +22,7 @@ class SetupSpellChecker {
try {
this.SpellCheckHandler = new SpellCheckHandler();
} catch (err) {
console.log(err);
logger.error(err);
}
}

View File

@@ -98,7 +98,7 @@ const renderNativeImage = function (arg) {
return Promise.resolve()
.then(() => renderCanvas(arg))
.then(canvas => {
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPng();
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
return Promise.resolve(nativeImage.createFromBuffer(pngData, config.pixelRatio));
});
};

View File

@@ -0,0 +1,86 @@
'use strict';
const { app, dialog } = require('electron').remote;
const fs = require('fs');
const path = require('path');
const JsonDB = require('node-json-db');
const Logger = require('./logger-util');
const { initSetUp } = require('./default-util');
initSetUp();
const logger = new Logger({
file: `certificate-util.log`,
timestamp: true
});
let instance = null;
const certificatesDir = `${app.getPath('userData')}/certificates`;
class CertificateUtil {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
this.reloadDB();
return instance;
}
getCertificate(server, defaultValue = null) {
this.reloadDB();
const value = this.db.getData('/')[server];
if (value === undefined) {
return defaultValue;
} else {
return value;
}
}
// Function to copy the certificate to userData folder
copyCertificate(server, location, fileName) {
let copied = false;
const filePath = `${certificatesDir}/${fileName}`;
try {
fs.copyFileSync(location, filePath);
copied = true;
} catch (err) {
dialog.showErrorBox(
'Error saving certificate',
'We encountered error while saving the certificate.'
);
logger.error('Error while copying the certificate to certificates folder.');
logger.error(err);
}
return copied;
}
setCertificate(server, fileName) {
const filePath = `${certificatesDir}/${fileName}`;
this.db.push(`/${server}`, filePath, true);
this.reloadDB();
}
removeCertificate(server) {
this.db.delete(`/${server}`);
this.reloadDB();
}
reloadDB() {
const settingsJsonPath = path.join(app.getPath('userData'), '/config/certificates.json');
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
'Error saving settings',
'We encountered error while saving the certificate.'
);
logger.error('Error while JSON parsing certificates.json: ');
logger.error(err);
}
}
this.db = new JsonDB(settingsJsonPath, true, true);
}
}
module.exports = new CertificateUtil();

View File

@@ -48,6 +48,12 @@ class ConfigUtil {
return value;
}
}
// This function returns whether a key exists in the configuration file (settings.json)
isConfigItemExists(key) {
this.reloadDB();
const value = this.db.getData('/')[key];
return (value !== undefined);
}
setConfigItem(key, value) {
this.db.push(`/${key}`, value, true);
@@ -60,7 +66,7 @@ class ConfigUtil {
}
reloadDB() {
const settingsJsonPath = path.join(app.getPath('userData'), '/settings.json');
const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.json');
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
@@ -69,10 +75,11 @@ class ConfigUtil {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
'Error saving settings',
'We encountered error while saving current settings.'
'We encountered an error while saving the settings.'
);
logger.error('Error while JSON parsing settings.json: ');
logger.error(err);
logger.reportSentry(err);
}
}
this.db = new JsonDB(settingsJsonPath, true, true);

View File

@@ -10,6 +10,8 @@ if (process.type === 'renderer') {
const zulipDir = app.getPath('userData');
const logDir = `${zulipDir}/Logs/`;
const certificatesDir = `${zulipDir}/certificates/`;
const configDir = `${zulipDir}/config/`;
const initSetUp = () => {
// if it is the first time the app is running
// create zulip dir in userData folder to
@@ -22,6 +24,51 @@ const initSetUp = () => {
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
if (!fs.existsSync(certificatesDir)) {
fs.mkdirSync(certificatesDir);
}
// Migrate config files from app data folder to config folder inside app
// data folder. This will be done once when a user updates to the new version.
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir);
const domainJson = `${zulipDir}/domain.json`;
const certificatesJson = `${zulipDir}/certificates.json`;
const settingsJson = `${zulipDir}/settings.json`;
const updatesJson = `${zulipDir}/updates.json`;
const windowStateJson = `${zulipDir}/window-state.json`;
const configData = [
{
path: domainJson,
fileName: `domain.json`
},
{
path: certificatesJson,
fileName: `certificates.json`
},
{
path: settingsJson,
fileName: `settings.json`
},
{
path: updatesJson,
fileName: `updates.json`
}
];
configData.forEach(data => {
if (fs.existsSync(data.path)) {
fs.copyFileSync(data.path, configDir + data.fileName);
fs.unlinkSync(data.path);
}
});
// window-state.json is only deleted not moved, as the electron-window-state
// package will recreate the file in the config folder.
if (fs.existsSync(windowStateJson)) {
fs.unlinkSync(windowStateJson);
}
}
setupCompleted = true;
}
};

View File

@@ -0,0 +1,41 @@
'use strict';
const ConfigUtil = require(__dirname + '/config-util.js');
function toggle() {
const dnd = !ConfigUtil.getConfigItem('dnd', false);
const dndSettingList = ['showNotification', 'silent'];
if (process.platform === 'win32') {
dndSettingList.push('flashTaskbarOnMessage');
}
let newSettings;
if (dnd) {
const oldSettings = {};
newSettings = {};
// Iterate through the dndSettingList.
for (const settingName of dndSettingList) {
// Store the current value of setting.
oldSettings[settingName] = ConfigUtil.getConfigItem(settingName);
// New value of setting.
newSettings[settingName] = (settingName === 'silent');
}
// Store old value in oldSettings.
ConfigUtil.setConfigItem('dndPreviousSettings', oldSettings);
} else {
newSettings = ConfigUtil.getConfigItem('dndPreviousSettings');
}
for (const settingName of dndSettingList) {
ConfigUtil.setConfigItem(settingName, newSettings[settingName]);
}
ConfigUtil.setConfigItem('dnd', dnd);
return {dnd, newSettings};
}
module.exports = {
toggle
};

View File

@@ -5,8 +5,12 @@ const fs = require('fs');
const path = require('path');
const JsonDB = require('node-json-db');
const request = require('request');
const escape = require('escape-html');
const Logger = require('./logger-util');
const CertificateUtil = require(__dirname + '/certificate-util.js');
const logger = new Logger({
file: `domain-util.log`,
timestamp: true
@@ -104,7 +108,19 @@ class DomainUtil {
domain = this.formatUrl(domain);
const checkDomain = domain + '/static/audio/zulip.ogg';
const certificate = CertificateUtil.getCertificate(encodeURIComponent(domain));
let certificateLocation = '';
if (certificate) {
// To handle case where certificate has been moved from the location in certificates.json
try {
certificateLocation = fs.readFileSync(certificate);
} catch (err) {
logger.warn('Error while trying to get certificate: ' + err);
}
}
// If certificate for the domain exists add it as a ca key in the request's parameter else consider only domain as the parameter for request
const checkDomain = (certificateLocation) ? ({url: domain + '/static/audio/zulip.ogg', ca: certificateLocation}) : domain + '/static/audio/zulip.ogg';
const serverConf = {
icon: defaultIconUrl,
@@ -114,21 +130,16 @@ class DomainUtil {
return new Promise((resolve, reject) => {
request(checkDomain, (error, response) => {
const certsError =
[
'Error: self signed certificate',
'Error: unable to verify the first certificate',
'Error: unable to get local issuer certificate'
];
// If the domain contains following strings we just bypass the server
const whitelistDomains = [
'zulipdev.org'
];
// make sure that error is a error or string not undefined
// make sure that error is an error or string not undefined
// so validation does not throw error.
error = error || '';
const certsError = error.toString().includes('certificate');
if (!error && response.statusCode < 400) {
// Correct
this.getServerSettings(domain).then(serverSettings => {
@@ -136,7 +147,7 @@ class DomainUtil {
}, () => {
resolve(serverConf);
});
} else if (domain.indexOf(whitelistDomains) >= 0 || certsError.indexOf(error.toString()) >= 0) {
} else if (domain.indexOf(whitelistDomains) >= 0 || certsError) {
if (silent) {
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
@@ -144,15 +155,19 @@ class DomainUtil {
resolve(serverConf);
});
} else {
// Report error to sentry to get idea of possible certificate errors
// users get when adding the servers
logger.reportSentry(new Error(error));
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.`;
const certErrorDetail = `The organization 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.
\nIf you have a valid certificate please add it from Settings>Organizations and try to add the organization again.
\nUnless you have a good reason to believe otherwise, you should not proceed.
\nYou can click here if you'd like to proceed with the connection.`;
dialog.showMessageBox({
type: 'warning',
buttons: ['Yes', 'No'],
defaultId: 0,
defaultId: 1,
message: certErrorMessage,
detail: certErrorDetail
}, response => {
@@ -169,7 +184,8 @@ class DomainUtil {
}
} else {
const invalidZulipServerError = `${domain} does not appear to be a valid Zulip server. Make sure that \
\n(1) you can connect to that URL in a web browser and \n (2) if you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings \n (3) its a zulip server`;
\n (1) you can connect to that URL in a web browser and \n (2) if you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings \n (3) its a zulip server \
\n (4) the server has a valid certificate, you can add custom certificates in Settings>Organizations`;
reject(invalidZulipServerError);
}
});
@@ -188,7 +204,7 @@ class DomainUtil {
// 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
alias: escape(data.realm_name)
});
}
} else {
@@ -206,18 +222,24 @@ class DomainUtil {
try {
request(url).on('response', response => {
response.on('error', err => {
console.log(err);
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
});
response.pipe(file).on('finish', () => {
resolve(filePath);
});
}).on('error', err => {
console.log(err);
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
});
} catch (err) {
console.log(err);
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
}
});
@@ -235,7 +257,7 @@ class DomainUtil {
}
reloadDB() {
const domainJsonPath = path.join(app.getPath('userData'), '/domain.json');
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
try {
const file = fs.readFileSync(domainJsonPath, 'utf8');
JSON.parse(file);
@@ -249,6 +271,7 @@ class DomainUtil {
);
logger.error('Error while JSON parsing domain.json: ');
logger.error(err);
logger.reportSentry(err);
}
}
this.db = new JsonDB(domainJsonPath, true, true);

View File

@@ -19,7 +19,20 @@ class LinkUtil {
const currentDomain = wurl('hostname', currentUrl);
const newDomain = wurl('hostname', newUrl);
return (currentDomain === newDomain) && newUrl.includes('/#narrow');
const sameDomainUrl = (currentDomain === newDomain || newUrl === currentUrl + '/');
const isUploadsUrl = newUrl.includes(currentUrl + '/user_uploads/');
const isInternalUrl = newUrl.includes('/#narrow') || isUploadsUrl;
return {
isInternalUrl: sameDomainUrl && isInternalUrl,
isUploadsUrl
};
}
isImage(url) {
// test for images extension as well as urls like .png?s=100
const isImageUrl = /\.(bmp|gif|jpg|jpeg|png|webp)\?*.*$/i;
return isImageUrl.test(url);
}
}

View File

@@ -52,7 +52,7 @@ class LinuxUpdateUtil {
}
reloadDB() {
const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/updates.json');
const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/config/updates.json');
try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
JSON.parse(file);
@@ -61,7 +61,7 @@ class LinuxUpdateUtil {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
'Error saving update notifications.',
'We encountered error while saving update notifications.'
'We encountered an error while saving the update notifications.'
);
logger.error('Error while JSON parsing updates.json: ');
logger.error(err);

View File

@@ -2,8 +2,10 @@ const NodeConsole = require('console').Console;
const fs = require('fs');
const isDev = require('electron-is-dev');
const { initSetUp } = require('./default-util');
const { sentryInit, captureException } = require('./sentry-util');
initSetUp();
sentryInit();
let app = null;
if (process.type === 'renderer') {
app = require('electron').remote.app;
@@ -82,6 +84,10 @@ class Logger {
`${date.getMinutes()}:${date.getSeconds()}`;
return timestamp;
}
reportSentry(err) {
captureException(err);
}
}
module.exports = Logger;

View File

@@ -0,0 +1,96 @@
'use strict';
const ConfigUtil = require('./config-util.js');
let instance = null;
class ProxyUtil {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
return instance;
}
resolveSystemProxy(mainWindow) {
const page = mainWindow.webContents;
const ses = page.session;
const resolveProxyUrl = 'www.google.com';
// Check HTTP Proxy
const httpProxy = new Promise(resolve => {
ses.resolveProxy('http://' + resolveProxyUrl, proxy => {
let httpString = '';
if (proxy !== 'DIRECT') {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
httpString = 'http=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(httpString);
});
});
// Check HTTPS Proxy
const httpsProxy = new Promise(resolve => {
ses.resolveProxy('https://' + resolveProxyUrl, proxy => {
let httpsString = '';
if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY' || proxy.includes('HTTPS'))) {
httpsString += 'https=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(httpsString);
});
});
// Check FTP Proxy
const ftpProxy = new Promise(resolve => {
ses.resolveProxy('ftp://' + resolveProxyUrl, proxy => {
let ftpString = '';
if (proxy !== 'DIRECT') {
if (proxy.includes('PROXY')) {
ftpString += 'ftp=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(ftpString);
});
});
// Check SOCKS Proxy
const socksProxy = new Promise(resolve => {
ses.resolveProxy('socks4://' + resolveProxyUrl, proxy => {
let socksString = '';
if (proxy !== 'DIRECT') {
if (proxy.includes('SOCKS5')) {
socksString += 'socks=' + proxy.split('SOCKS5')[1] + ';';
} else if (proxy.includes('SOCKS4')) {
socksString += 'socks=' + proxy.split('SOCKS4')[1] + ';';
} else if (proxy.includes('PROXY')) {
socksString += 'socks=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(socksString);
});
});
Promise.all([httpProxy, httpsProxy, ftpProxy, socksProxy]).then(values => {
let proxyString = '';
values.forEach(proxy => {
proxyString += proxy;
});
ConfigUtil.setConfigItem('systemProxyRules', proxyString);
const useSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
if (useSystemProxy) {
ConfigUtil.setConfigItem('proxyRules', proxyString);
}
});
}
}
module.exports = new ProxyUtil();

View File

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

View File

@@ -0,0 +1,16 @@
const { init, captureException } = require('@sentry/electron');
const isDev = require('electron-is-dev');
const sentryInit = () => {
if (!isDev) {
init({
dsn: 'SENTRY_DSN',
sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second
});
}
};
module.exports = {
sentryInit,
captureException
};

View File

@@ -24,6 +24,10 @@
</div>
</div>
<div id="actions-container">
<div class="action-button" id="dnd-action">
<i class="material-icons md-48">notifications</i>
<span id="dnd-tooltip" style="display:none">Do Not Disturb</span>
</div>
<div class="action-button" id="reload-action">
<i class="material-icons md-48">refresh</i>
<span id="reload-tooltip" style="display:none">Reload</span>
@@ -42,6 +46,11 @@
<div id="webviews-container"></div>
</div>
</div>
<div id="feedback-modal">
<send-feedback></send-feedback>
</div>
</body>
<script src="js/main.js"></script>
</html>
<script>require('./js/shared/preventdrag.js')</script>
</html>

View File

@@ -17,5 +17,6 @@
<div id="reconnect">Try now</div>
</div>
</body>
<script src="js/pages/network.js"></script>
<script src="js/pages/network.js"></script>
<script>require('./js/shared/preventdrag.js')</script>
</html>

View File

@@ -13,4 +13,5 @@
</div>
</body>
<script src="js/pages/preference/preference.js"></script>
<script>require('./js/shared/preventdrag.js')</script>
</html>

Binary file not shown.

View File

@@ -6,9 +6,9 @@ os: Previous Visual Studio 2015
cache:
- node_modules
install:
- ps: Install-Product node 6 x64
- ps: Install-Product node 8 x64
- git reset --hard HEAD
- npm install npm -g
- node --version
@@ -21,4 +21,4 @@ build: off
test_script:
- npm run test
- npm run test-e2e
# - npm run test-e2e

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

665
changelog.md Normal file
View File

@@ -0,0 +1,665 @@
# Version History
All notable changes to the Zulip desktop app are documented in this file.
### v2.3.2 --2018-05-28
This is a quick release since we recently updated the certificate for code signing the app and we want to make sure the previous versions of the app get auto-updates without any conflicts.
<hr>
### v2.3.1 --2018-05-23
**Enhancements**:
* Add a new setting to disable auto-updates
* Add a menu item to check for updates manually
* Other minor improvements
**Fixes**:
* Fix app not rendering colors properly
* **Security fix** - Do proper HTML escaping for server data to avoid XSS attacks
* Fix loading indicator when server is loaded
* Other minor fixes
**Updated dependencies**:
electron - `v2.0.1`
electron-builder - `v20.13.4`
electron-updater - `v2.21.10`
<hr>
### v2.2.0-beta --2018-05-08
**Enhancements**:
* Added do not disturb feature
* New DMG installer image
* Minor improvements
**Fixes**:
* Fix download functionality of file attachment links
* Fix tray icon not showing unread counts
**Updated dependencies**:
electron - `v2.0.0`
electron-builder - `v2.21.10`
electron-updater - `v2.21.8`
<hr>
### v2.0.0 -- 2018-04-20
**Enhancements**:
* [Snap](https://snapcraft.io/zulip) support for Linux
* Add an option to download the file attachments instead of opening it in the browser
* Open image link in webapp lightbox
* Add scrollbar for list of organizations on overflow
* Better report issue UX
* Add F5 shortcut for reloading the app
* Responsive UI for connected orgs in smaller window sizes
* Minor improvements
**Fixes**:
* Remove unused shortcuts
* Update toggle sidebar shortcut to CMD/CTRL+SHIFT+S
* Warning dialog box for Reset App Settings
* Fix reinstall script for working across all platforms
* Other minor fixes
**Updated dependencies**:
* Update electron-builder to `v20.8.1`
<hr>
### v1.9.0 --2018-03-23
**Enhancements**:
* Major UI changes in the add new server and setting page
* Some users wanted to change the look of the Zulip. Now you have the power. Feel free to add your own CSS using the all-new setting option **Add Custom CSS**
* Added i18n locale helper script. Internalization is coming in the next release
* Added **What's new** in `help` submenu so that you can see all the latest changes in the app
* Other small improvements
**Fixes**:
* Add Zulip binary link to the bin. It was a regression we missed somehow. You can now start the app using `zulip` from a terminal [Linux]
* Fix app settings overriding issue. This bug was unnoticed from `v1.4.0`. The bug was overriding the app settings which was pretty bad. Don't worry this works as expected now
* Prevent drag and drop events to avoid few security risks
* Do not allow running insecure contents in the app
* The ugly white flickering in the webview is now fixed
* Fix position of left-sidebar on toggle
* Fix actions tooltip not visible on hover
* Other small fixes
**Updated dependencies**:
* electron - `v1.8.4`
* Spectron - `v3.8.0`
<hr>
### v1.8.2 --2018-02-27
**Enhancements**:
* Add bots mention support to reply option
* Show a notification when a new update available [Linux]
* Add back button in left-sidebar
* Add a Loading indication for new server button
* Improve help menu
* Add menu-item to reveal app logs in file manager
* Setting page and left-sidebar UI improvements
* Other minor improvments
**Fixes**:
* Fix broken last active tab
* Fix Zoom In shortcut
* Check whether the internet is working before reloading
* New organization link overlapping existing servers
* Disable electron-connect when it's run from npm start
* Do not auto-reload app when the system comes back from sleep
* Only toggle the setting state if the element is present
* Other minor fixes
**Updated dependencies**:
* electron - `v1.8.2`
<hr>
### v1.8.1 --2018-01-19
**Enhancements**:
* Reply from notifications [macOS]
* Add a setting option to start the app in the background
* 32-bit Debian installer
* 32-bit AppImage installer
* Automatically add apt-repo and GPG public keys on installing Debian file [Linux]
* Remove app data, config files etc on uninstalling Debian installer [Linux]
* Add logger utility for debugging that makes easier to sharing the apps
**Fixes**:
* Handle corrupted config files
* Reload full app on system hibernation
* Load default icon if organization icon is not available
* Focus app when a notification is triggered
* Reflect changes in the preference page (#362)
* Set the default value of flash taskbar setting [Windows]
* Rename Zoom In keyboard shortcut
* Fix checkDomain, so it checks all error codes
* Disable hardware acceleration to decrease the load on GPU
* Minor bug fixes
**Updated dependencies**:
* electron - `v1.7.10`
* electron-builder - `v19.53.6`
* electron-updater - `v2.18.2`
<hr>
### v1.7.0 --2017-11-24
**Enhancements**:
* Updated to Electron `v1.6.15`, electron-builder `v19.46.4`
* Improved setting page for new users
* On the developer side, we have added few tests to make sure app doesn't fail
* Minor improvements
<hr>
### 1.6.0-beta --2017-11-16
**Enhancements**:
* Added setting option to enable/disable spellcheck
* Added setting option to control Windows taskbar flashing [Windows]
* Auto-updates for Linux [AppImage]
* Better tray icons for retina display
* Reset app settings from Menu item
* Show detailed error message on invalid Zulip server
**Fixes**:
* Fixed a bug which was caused by app's shortcuts. From now on our shortcuts won't hijack other apps shortcuts
* Removed [electron-localshortcut](https://github.com/parro-it/electron-localshortcut) completely. Now we only depends on menu accelerators for keyboard shortcuts
* Handle certificate issue properly
* Other minor fixes
<hr>
### v1.5.0 --2017-10-11
**Enhancements**:
* Added an option to clear app data
* Added an option to show/hide desktop notifications
* Redesigned setting page
* Red dot over dock icon for PM [macOS]
* Show server-info on hovering the server icon
**Fixes**:
* Fetch correct organization icon from server_settings API
* Minor improvements
<hr>
### v1.4.0 --2017-09-04
**Enhancements**:
* Added proxy support
* Added setting option for badges
* Start app at login setting option
* Added app category for Linux
* Minor improvements in settings page
* apt-repo on bintray for linux - [here](https://bintray.com/zulip/debian/zulip-elec)
**Fixes**
* Fixed auto-updates
* Better warning message on certificate error
* Don't allow duplicate servers
<hr>
### 1.3.0-beta -- 2017-08-09
**Enhancements**:
* Added back/forward option under `History` submenu
* Added taskbar overlay icon [Windows]
* Added `Window` submenu by which users can switch to other servers
* Added flashing taskbar icon on incoming message [Windows]
* Option to hide the left sidebar
* Staged rollouts
* Minor style improvements
**Fixes**
* Handle focus event properly
* Open external links in default browser
* Allow user to change installation directory
* Fixed server-icon path error [Windows]
* Fixed server validation error
* Fetch server details from Zulip api
* Added tooltip in left sidebar
* Added publisher name in windows installer
**Updated dependencies**:
* electron - `v1.6.11`
* spellchecker - `v1.2.0`
* electron-builder - `v19.9.1`
<hr>
### v1.2.0-beta -- 2017-07-12
**Enhancements**:
* Added new setting page
* Setting for beta-updates. You can opt in for beta updates via selecting it from setting page
* Added Keyboard shortcuts to switch between multiple servers
* Desktop notifications support on Windows
**Fixes**
* Unregister keyboard shortcuts on windows close
* Show warning dialog while deleting server
* Allow server which is signed by root cert
* Other minor bug fixes
<hr>
### v1.1.0-beta --2017-06-23
**Enhancements**:
* New network error page. App will auto-reload once the network comes back
* Quit shortcut - CTRL+Q [Windows]
* Improved preferences
**Fixes**:
* Fixed power-monitor module error
* Fixed code signing error [macOS]
<hr>
### v1.0.0-beta --2017-06-21
**Enhancements**:
* Sign in to multiple teams
* Windows 7 desktop notification support
* Show badge count for each Zulip server
* Toggle window on clicking tray icon [Windows]
* Windows installer is properly code signed using [DigiCert](https://www.digicert.com/) certificate. You may get a warning regarding the certificate, please make it false positive
**Fixes**:
* Close/hide app properly
* Keep app running in background on clicking X
<hr>
### v0.5.10 --2017-05-12
**Enhancements**:
* Added self-signed server support
* Unread message counts in tray icon
<hr>
### v0.5.9 --2017-04-12
**Enhancements**:
* Properly signed app for macOS
* Toggle tray icon
* Better error handling when no internet connenction
**Fixes**:
* Fixed permission issue on windows
* Fixed wrong keyboard shortcuts
<hr>
### v0.5.8 --2017-02-13
**Enhacements**:
Smaller Windows installer size
<hr>
### v0.5.7 --2017-02-08
Minor improvements
<hr>
### v0.5.6 --2017-02-07
**Enhacements**:
- Using NSIS instead of [Squirrel.Windows](https://github.com/Squirrel/Squirrel.Windows) on Windows
- Autoupdates: now using [electron-updater](https://www.npmjs.com/package/electron-updater)
- Removed nuts dependency
**Fixes**:
- Windows installer error
- Compile and build error in native node modules + spellchecker [Windows]
- Missing icon on installing [Windows]
<hr>
### v0.5.4 --2017-01-09
**Fixes**:
- Fixed :
- Auto-updates
- Spellchecker
- Zooming functionality
- Removed unused node modules
- Using stable version for node modules
- Added icon for AppImage
<hr>
### v0.5.3 --2016-12-24
**Enhancements**:
- 🎉 Added automatic update support on Windows
**Fixes**:
- Fixed javascript error when electron has been left running and try to right-click
- Fixed error - Unregister Spellchecker while window is closed
- Fixed - Keyboard shortcuts not getting unregistered
- Minor UI Changes
<hr>
### v0.5.2 --2016-12-13
**Enhancements**:
- ⚡️ Added automatic app updates for macOS
- Added windows installer for 32 bit machine
- Better tray icons
**Fixes**:
- Blurry Unity icon fixed
<hr>
### v0.5.1 --2016-11-23
**Enhacements**:
- Added Spellchecker support with correct spell suggestions
- Added Code-Signing on MAC
- Added Win/linux/Mac installers
- Added Continuous Integration on travis and appveyor
- Open internal links in app only and external in default browser
- Better icons
- Electron version updated to `v1.4.7`
- Using two package.json structure
- Node integration disabled in main window due to jquery error
- Now using electron-builder for packaging instead of electron-packager
- Removed electron-context-menu and simple-spellchecker dependency
**Fixes**:
- Better error handling when user enters wrong zulip server
- Fixed close Zulip window JavaScript error
- Fixed opening image in default browser
- Fixed native notifications issue on linux
<hr>
### v0.0.1-alpha -- 2016-08-31
**Enhacements**:
* Added DMG installer for macOS

View File

@@ -15,6 +15,7 @@ To build and run the app from source, you'll need the following:
* [Python](https://www.python.org/downloads/release/python-2713/)
(v2.7.x recommended)
* A C++ compiler compatible with C++11
* Linux users also need [Snapcraft](https://snapcraft.io/)
* Development headers for the libXext, libXtst, and libxkbfile libraries
### Debian/Ubuntu and friends
@@ -25,7 +26,7 @@ manager (see [here][nodesource-install] for more on the first command):
```sh
$ 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
$ sudo apt install git nodejs python build-essential snapcraft libxext-dev libxtst-dev libxkbfile-dev libgconf-2-4
```
[nodesource-install]: https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions
@@ -76,7 +77,7 @@ 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
* on Linux, a plain `.zip` file as well as a `.deb` file, `.snap` file and an
`AppImage` file.
To generate all three types, you will need all three operating
systems.

8608
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "1.8.2",
"version": "2.3.3",
"main": "./app/main",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
@@ -22,7 +22,7 @@
},
"scripts": {
"start": "electron app --disable-http-cache --no-electron-connect",
"reinstall": "./tools/reinstall-node-modules",
"reinstall": "node ./tools/reinstall-node-modules.js",
"postinstall": "electron-builder install-app-deps",
"test": "xo",
"test-e2e": "gulp test-e2e",
@@ -46,7 +46,8 @@
],
"copyright": "©2017 Kandra Labs, Inc.",
"mac": {
"category": "public.app-category.productivity"
"category": "public.app-category.productivity",
"artifactName": "${productName}-${version}-${arch}.${ext}"
},
"linux": {
"category": "Chat;GNOME;GTK;Network;InstantMessaging",
@@ -55,32 +56,41 @@
"target": [
"deb",
"zip",
"AppImage"
"AppImage",
"snap"
],
"maintainer": "Akash Nimare <svnitakash@gmail.com>"
"maintainer": "Akash Nimare <svnitakash@gmail.com>",
"artifactName": "${productName}-${version}-${arch}.${ext}"
},
"deb": {
"synopsis": "Zulip Desktop App",
"afterInstall": "./scripts/debian-add-repo.sh",
"afterRemove": "./scripts/debian-uninstaller.sh"
},
"snap": {
"synopsis": "Zulip Desktop App"
},
"dmg": {
"background": "build/appdmg.png",
"icon": "build/icon.icns",
"iconSize": 128,
"iconSize": 100,
"contents": [
{
"x": 380,
"y": 240,
"y": 280,
"type": "link",
"path": "/Applications"
},
{
"x": 122,
"y": 240,
"x": 110,
"y": 280,
"type": "file"
}
]
],
"window": {
"width": 500,
"height": 500
}
},
"win": {
"target": [
@@ -113,17 +123,17 @@
"assert": "1.4.1",
"cp-file": "^5.0.0",
"devtron": "1.4.0",
"electron": "1.8.2",
"electron-builder": "19.53.6",
"electron": "2.0.1",
"electron-builder": "20.20.4",
"electron-connect": "0.6.2",
"electron-debug": "1.4.0",
"google-translate-api": "2.3.0",
"gulp": "3.9.1",
"gulp": "^4.0.0",
"gulp-tape": "0.0.9",
"is-ci": "^1.0.10",
"nodemon": "^1.14.11",
"pre-commit": "1.2.2",
"spectron": "3.7.2",
"spectron": "3.8.0",
"tap-colorize": "^1.2.0",
"tape": "^4.8.0",
"xo": "0.18.2"

View File

@@ -2,6 +2,10 @@
# This script runs when user install the debian package
# Link to the binary
ln -sf '/opt/${productFilename}/${executable}' '/usr/local/bin/${executable}';
echo 'Successfully added /opt/${productFilename}/${executable} to /usr/local/bin/${executable}'
# Install apt repository source list if it does not exist
if ! grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep zulip.list; then
sudo apt-key adv --keyserver pool.sks-keyservers.net --recv 69AD12704E71A4803DCA3A682424BE5AE9BD10D9

View File

@@ -28,4 +28,8 @@ appDirectory=/home/$getSudoUser/.config/Zulip/;
if [ -d $appDirectory ]; then
sudo rm -rf $appDirectory;
fi
fi
# Delete the link to the binary
echo 'Removing binary link'
sudo rm -f '/usr/local/bin/${executable}';

View File

@@ -15,6 +15,6 @@ fi
npm run test
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
npm run test-e2e
fi
# if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
# npm run test-e2e
# fi

BIN
snap/gui/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

37
snap/snapcraft.yaml Normal file
View File

@@ -0,0 +1,37 @@
name: zulip
version: 2.0.0
summary: Zulip Desktop Client for Linux
description: Zulip combines the immediacy of Slack with an email threading model. With Zulip, you can catch up on important conversations while ignoring irrelevant ones.
confinement: strict
grade: stable
icon: snap/gui/icon.png
apps:
zulip:
command: env TMPDIR=$XDG_RUNTIME_DIR desktop-launch $SNAP/zulip
plugs:
- desktop
- desktop-legacy
- home
- x11
- unity7
- browser-support
- network
- gsettings
- pulseaudio
- opengl
parts:
app:
plugin: dump
stage-packages:
- libasound2
- libgconf2-4
- libnotify4
- libnspr4
- libnss3
- libpcre3
- libpulse0
- libxss1
- libxtst6
source: dist/linux-unpacked
after:
- desktop-gtk2

View File

@@ -7,7 +7,7 @@ test('app runs', function (t) {
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[2]/input'))
.then(() => app.client.waitForExist('//*[@id="connect"]')) // id of the connect button
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})

View File

@@ -0,0 +1,17 @@
const test = require('tape')
const setup = require('./setup')
// Create new org link should open in the default browser [WIP]
test('new-org-link', 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.click('#open-create-org-link')) // Click on new org link button
.then(() => setup.wait(5000))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})

81
tools/push-to-pull-request Executable file
View File

@@ -0,0 +1,81 @@
#!/bin/bash
set -e
usage () {
cat >&2 <<EOF
usage: $0 PULL_REQUEST_ID [REMOTE]
Force-push our HEAD to the given GitHub pull request branch.
Useful for a maintainer to run just before pushing to master,
after tweaking the branch and/or rebasing to latest. This causes
GitHub to see the subsequent push to master as representing a
merge of the PR, rather than requiring the PR to be manually
(and to the casual observer misleadingly) closed instead.
REMOTE defaults to the value of the Git config variable
\`zulip.zulipRemote\` if set, else to \`upstream\`.
See also \`reset-to-pull-request\`.
EOF
exit 1
}
remote_default="$(git config zulip.zulipRemote || echo upstream)"
pr_id="$1"
remote="${2:-"$remote_default"}"
if [ -z "$pr_id" ]; then
usage
fi
remote_url="$(git config remote."$remote".url)"
repo_fq="$(echo "$remote_url" | perl -lne 'print $1 if (
m, ^ git\@github\.com:
([^/]+ / [^/]+?)
(?:\.git)?
$ ,x )')"
if [ -z "$repo_fq" ]; then
# We're pretty specific about what we expect the URL to look like;
# there are probably more cases we could legitimately cover, which
# we can add if/when they come up for someone.
echo "error: couldn't parse remote URL as GitHub repo" >&2
exit 1
fi
# See https://developer.github.com/v3/pulls/#get-a-single-pull-request .
# This is the old REST API; the new GraphQL API does look neat, but it
# seems to require authentication even for simple lookups of public data,
# and that'd be a pain for a simple script like this.
pr_url=https://api.github.com/repos/"${repo_fq}"/pulls/"${pr_id}"
pr_details="$(curl -s "$pr_url")"
pr_jq () {
echo "$pr_details" | jq "$@"
}
if [ "$(pr_jq -r .message)" = "Not Found" ]; then
echo "Invalid PR URL: $pr_url"
exit 1
fi
if [ "$(pr_jq .maintainer_can_modify)" != "true" ]; then
# This happens when the PR has already been merged or closed, or
# if the contributor has turned off the (default) setting to allow
# maintainers of the target repo to push to their PR branch.
#
# The latter seems to be rare (in Greg's experience doing the
# manual equivalent of this script for many different
# contributors, none have ever chosen this setting), but give a
# decent error message if it does happen.
echo "error: PR already closed, or contributor has disallowed pushing to branch" >&2
exit 1
fi
pr_head_repo_fq="$(pr_jq -r .head.repo.full_name)"
pr_head_refname="$(pr_jq -r .head.ref)"
set -x
exec git push git@github.com:"$pr_head_repo_fq" +@:"$pr_head_refname"

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env node
const {exec} = require('child_process');
const path = require('path');
const isWindows = process.platform === 'win32';
const command = path.join(__dirname, `reinstall-node-modules${isWindows ? '.cmd' : ''}`);
const proc = exec(command, error => {
if (error) {
console.error(error);
}
});
proc.stdout.on('data', data => console.log(data.toString()));
proc.stderr.on('data', data => console.error(data.toString()));
proc.on('exit', code => {
process.exit(code);
});

19
tools/reset-to-pull-request Executable file
View File

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