Compare commits

..

86 Commits

Author SHA1 Message Date
Anders Kaseorg
814ce071a3 release: New release v5.0.0.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:38:33 -07:00
Anders Kaseorg
92fb176f67 Revert "auth: Move social login process to browser."
This reverts commit 49b29bfed6 (#863).

The design of this feature is still under discussion; we expect it to
return after the security release.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
a03f569af9 CVE-2020-10857: Whitelist safe URL protocols for shell.openExternal.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
af59bb7c99 handleExternalLink: Do not navigate the current window.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
4390966a62 Always show downloaded files in file manager.
shell.openItem is unsafe.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
a6d942fe6c CVE-2020-10858: Lock down session permission requests.
This fixes a vulnerability reported by Matt Austin.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
9d4093b3d8 CVE-2020-10856: Enable context isolation.
This fixes a vulnerability reported by Matt Austin.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
20a6c5d128 preload: Use IPC for logout, shortcut, showNotificationSettings.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
c843e179fc tray: Remove tray variable from window.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
438d4fffa7 notification: Convert loadBots from jQuery to fetch.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:32:23 -07:00
Tim Abbott
5c164bfa7d webview: Disable insecure content.
Zulip servers in production are designed to only serve content over
HTTPS.  And a development environment's root page will be served over
HTTP.

So there is no purpose in enabling allowInsecureContent, even
conditionally for use against Zulip development environments; we should
just remove the setting.
2020-03-30 19:32:23 -07:00
Anders Kaseorg
cbc89a72a2 tray: Work around Electron segfault on certain platforms.
Set the tray icon’s context menu immediately after creating the Tray
object.  This seems to prevent an Electron segfault at startup on
certain platforms, such as Ubuntu 16.04 i386.  See
https://github.com/electron/electron/issues/22652 and its linked
issues.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:26:29 -07:00
Anders Kaseorg
fb1e163130 typescript: Fix errors hidden by skipLibCheck.
This requires temporarily downgrading to @types/node@^12 (see
https://github.com/electron/electron/issues/21612).

Leave skipLibCheck on for now as it still saves a few seconds when
running tsc.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-26 16:35:13 -07:00
Anders Kaseorg
2ebeeedba8 dependencies: Move fs-extra from devDependencies to dependencies.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-24 21:07:25 -07:00
Anders Kaseorg
82a7f97ca6 dependencies: Upgrade dependencies.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-23 17:22:48 -07:00
Anders Kaseorg
55eb768064 xo: Upgrade xo to 0.28.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-23 16:53:14 -07:00
Anders Kaseorg
611932c66d xo: Unabbreviate variable names.
To satisfy unicorn/prevent-abbreviations in xo 0.28.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-23 16:53:12 -07:00
Anders Kaseorg
5deffa5022 changelog: Add missing changelog entry for v4.0.3.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-23 15:08:21 -07:00
Anders Kaseorg
6f01f1362a js: Declare 'use strict' on tests too.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-09 22:14:23 -07:00
Anders Kaseorg
9d2739f050 js: Declare 'use strict' on all scripts and no modules.
And enable the import/unambiguous ESLint rule as a check on our
partition between scripts and modules.  After this commit, if you add
a new file and get this error:

  ✖  1:1  This module could be parsed as a valid script.  import/unambiguous

* For a module, add an `import` or `export` declaration to make the
  file unambiguously a module (the empty `export {};` declaration
  suffices).
* For a script, add the file to the xo overrides section of
  package.json that marks it "sourceType": "script", and add a 'use
  strict' declaration.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-09 20:04:43 -07:00
Akash Nimare
01f6e77237 macOS: Fix undo redo not working on macOS.
The default API provided by Electron doesn't work
as expected. More info here -
https://github.com/electron/electron/issues/15728

Fixes: #866.
2020-03-10 00:32:05 +05:30
Manav Mehta
7ac35cc087 macOS: Replace deprecated isDarkMode() with shouldUseDarkColors.
Fixes: #891.
2020-03-08 14:55:43 +05:30
Anders Kaseorg
32e6b3054f xo: Use more xo defaults.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 21:46:18 -08:00
Anders Kaseorg
40bf2a1f20 xo: Lint *.js too.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 21:22:04 -08:00
Anders Kaseorg
dee2f05ac0 locale-helper: Move supported-locales.js to supported-locales.json.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 20:45:57 -08:00
Anders Kaseorg
ca5de73155 xo: Reenable several easy rules.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 20:15:01 -08:00
Anders Kaseorg
d0f8c040c7 package.json: Add tsc to test script.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 18:26:06 -08:00
Anders Kaseorg
7cf40f1e08 typescript: One more switch to ES export syntax.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 18:15:42 -08:00
Anders Kaseorg
598c0df60b package.json: Reformat to match npm generated output.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 17:52:10 -08:00
Anders Kaseorg
d3bcd7306a typescript: Switch to ES import/export syntax.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 17:21:03 -08:00
Anders Kaseorg
b3261bcdff js: Explode more singleton classes to modules.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 16:27:44 -08:00
Manav Mehta
20c6f487c4 typescript: Implement some TODOs. 2020-03-04 14:21:25 -08:00
Anders Kaseorg
340797ca10 typescript: Refine some type annotations to avoid any.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 12:12:31 -08:00
Anders Kaseorg
220aac2d54 js: Explode singleton classes to modules.
Singleton classes may have a purpose.  This was not that purpose.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 11:54:45 -08:00
Akash Nimare
d9f6cf4cc9 docs: Update recommended node version for development. 2020-03-04 16:59:21 +05:30
Anders Kaseorg
dc3e5d4930 package.json: Sort xo overrides.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 21:50:02 -08:00
Anders Kaseorg
fc2b80c36a main: Fix realm icon updating.
Commit c937317ecf (#605) should have
updated this, but didn’t.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 21:50:02 -08:00
Anders Kaseorg
ab667d8053 checkCertError: Fix showMessageBox usage.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 21:50:02 -08:00
Manav Mehta
8b9a10a23d Update report issue placeholder.
Fixes: #873.
2020-03-04 11:16:31 +05:30
Anders Kaseorg
15af3e732f sentry-util: Hard-code the Sentry DSN.
Commit 088ddf9c62 (#755) does not work,
because neither the .env file nor the environment variables it
provides are available to normal users at runtime.  This silently
broke Sentry data collection.  When we upgraded @sentry/electron in
commit 107e522914, the silent failure
became an error that prevented the app from starting.

The Sentry DSN is not a secret, so we should just commit it to the
repository.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 11:07:00 +05:30
am2505
534f4c1463 Convert Promise to async-await.
Fixes #878.
2020-03-03 20:40:10 -08:00
Anders Kaseorg
063324550e run-dev: Fix for one package.json structure.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 20:00:36 -08:00
Anders Kaseorg
268471df6b typings: Remove redundant ZulipWebWindow.$.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 18:17:13 -08:00
Anders Kaseorg
ca6b2312be typings: Get Window.Notification from upstream.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 18:16:01 -08:00
Anders Kaseorg
ff026e5763 typings: Get NotificationOptions.silent from upstream.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 18:14:13 -08:00
Anders Kaseorg
8f810481e3 typings: Get requestIdleCallback from DefinitelyTyped.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 18:13:26 -08:00
Anders Kaseorg
f91e95647a typings: Use type declarations from DefinitelyTyped.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 18:01:24 -08:00
Anders Kaseorg
e9536f247b dependencies: Use one package.json structure.
The two package.json structure is no longer needed.
https://www.electron.build/tutorials/two-package-structure

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 11:02:59 -08:00
Anders Kaseorg
067cbf32a1 build: Fix cld sources exclusion.
As of commit 107e522914, @paulcbetts/cld
was renamed to cld.

docs is not being included anyway since it’s outside of app.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 10:59:41 -08:00
Anders Kaseorg
6824978114 dependencies: Remove @types/dotenv compatibility stub.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 20:31:20 -08:00
Anders Kaseorg
fa86f1ca25 dependencies: Upgrade everything to latest.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 20:31:19 -08:00
Anders Kaseorg
a63b3873ae dependencies: Remove electron-debug.
We already implement all of its functionality.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 20:15:43 -08:00
Anders Kaseorg
5064ea4b47 dependencies: Upgrade node-json-db from 0.9.2 to 1.0.3.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 19:52:39 -08:00
Anders Kaseorg
6b0d8520c5 dependencies: Remove unused dependencies assert, cp-file, is-ci.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 19:19:37 -08:00
Anders Kaseorg
4b16164155 cleanup: Remove unused tests/e2e directory.
It is not used even by test-e2e.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 19:18:25 -08:00
Anders Kaseorg
6036a44fb2 new-server-form: Remove useless .server-save-action wrappers.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 19:08:45 -08:00
Anders Kaseorg
598b96b6e8 webview: Wait for dom-ready before sending messages.
Fixes tests/test-add-organization.js.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 18:52:37 -08:00
Anders Kaseorg
675bc2f06c appveyor.yml, .travis.yml: Test current Node.js releases.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 16:30:12 -08:00
Tim Abbott
eb2988a5e4 dependencies: Update typescript and typescript-eslint.
The changes are mostly done via `xo --fix`; the other changes are
either trivial or disabling new linter rules that we plan to address
in future commits.
2020-02-29 23:39:55 -08:00
Tim Abbott
39ea18228c dependencies: Update gulp testing packages. 2020-02-29 22:54:50 -08:00
Tim Abbott
909e0f07e3 dependencies: Upgrade linters and fix linter errors.
The changes here are mostly straightforward; the one exception is
removing a zulipdev.org hack.

We disable some lint rules we'll want to address later (E.g. we want
to switch to using async/await rather than .then()).  But those are
out of scope for this commit.
2020-02-29 22:47:42 -08:00
Tim Abbott
31af6596bf dependencies: Upgrade Electron to version 8.
This is the latest Electron release, which means we're now getting
nearly modern Chrome (hopefully with fewer rendering bugs and better
performance).
2020-02-29 21:49:19 -08:00
Tim Abbott
3c9914542f badge: Clear badge counts on Linux as well.
My Linux desktop environment doesn't display unread badges, it seems,
but this is clearly how this code should read.
2020-02-29 21:39:56 -08:00
Tim Abbott
f4b9605742 electron: Update some setter/getters to user newer properties.
This removes a few deprecation warnings on app startup.
2020-02-29 21:39:56 -08:00
Tim Abbott
e2fc9241fa dependencies: Upgrade to Electron 7.
This works without any other changes, thanks to Electron's deprecation
process being done over multiple releases.
2020-02-29 21:39:56 -08:00
Tim Abbott
3b18357c74 download: Use removeListener for removing the updated listener.
This is slightly cleaner code, and also fixes a typescript error (that
might be a bug) we'll get when we upgrade to Electron 7.
2020-02-29 21:39:56 -08:00
Tim Abbott
c4beedf740 proxy: Migration to use async/await.
This is required for the upgrade to Electron 7, which removes the old
callback-based form of these APIs.
2020-02-29 21:39:56 -08:00
Anders Kaseorg
b34bf7236f .travis.yml: Fix npm ci invocation for app directory.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-29 18:49:01 -08:00
Tim Abbott
e32968b2f3 preferences: Convert one more dialog to use async/await.
This should have been in the main version update commit.
2020-02-29 18:32:09 -08:00
Anders Kaseorg
747fbb5ab0 .travis.yml: Run npm ci, not npm install.
This enforces that package-lock.json is up to date in Git.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-29 18:30:05 -08:00
vsvipul
107e522914 deps: Update Electron and related packages to Electron v6.
This updates most of our direct dependencies to much newer versions
(Electron v6, with compatible versions of related packages like
Spectron).

Further, it updates all of our recursive dependencies with `npm update
--depth=999`.

Modified by tabbott to migrate to async/await for dialogs rather than
the old synchronous API.
2020-02-29 18:28:42 -08:00
Tim Abbott
c83bc08359 i18n: Add additional automatically output strings.
I'm skeptical of the setup that these are generated dynamically while
working in the development environment; that seems not robust.
2020-02-29 17:38:23 -08:00
Anders Kaseorg
ef0b056437 package.json: Fix clean-ts-files script to spare app/node_modules.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-29 17:03:53 -08:00
Tim Abbott
9370f783ba i18n: Update generated translation data.
I feel like this should probably not be in Git.
2020-02-29 16:14:00 -08:00
Tim Abbott
227e59fee2 gitattributes: Disable binary handling of lock files.
This makes it possible to see what packages change after npm
operations.
2020-02-29 13:09:29 -08:00
Akash Nimare
b7147d0b29 webview: Update web security preference.
Electron docs suggests that we should not use
`disablewebsecurity` thus removing the same.
2020-02-27 16:55:47 +05:30
Akash Nimare
f93053eb20 release: New release 4.1.0-beta. 2020-02-27 10:37:55 +05:30
ViPuL
49b29bfed6 auth: Move social login process to browser.
Moves the social login to browser since there
was no way to verify the authencity of the
auth process for a custom server and to
prevent phishing attacks.

Fixes #849.

Co-authored-by: Kanishk Kakar <kanishk.kakar@gmail.com>
2020-02-25 20:05:27 +05:30
Akash Nimare
0fb610f858 macOS: Add colorless tray icon for macOS.
Fixes: #825.
2020-02-05 21:41:10 +05:30
ViPuL
1d9a923245 startup: Use inbuilt electron API for autostartup. (#859)
Switch from using the external auto-launch module to
inbuilt setLoginItemSettings for windows and macOS,
as some users reported issues on windows.

Fixes: #851.
2020-01-28 12:26:41 +05:30
Ross Brunton
9582d32de8 Added option to select download locations.
Added an option that, when enabled, will mean any file downloads that
would normally go to ~/Downloads (or wherever), in fact prompt.
2020-01-21 16:41:56 +05:30
ViPuL
a2a21631f2 Decode server name in Window menu. 2020-01-08 11:58:44 +05:30
Akash Nimare
9490265a03 dock: Toggle app on clicking the dock icon. 2019-12-29 00:49:46 +05:30
Brandon Liu
70bd619fa8 Fix size of macOS dock icon.
Fixes: #845.
2019-11-30 17:07:31 +05:30
Akash Nimare
c5797e4edb release: New release v4.0.3. 2019-11-21 12:47:29 +05:30
Kanishk Kakar
32321daef2 docs: Add release notes for v4.0.2-beta. (#841) 2019-11-17 04:27:55 +05:30
96 changed files with 9753 additions and 10979 deletions

2
.gitattributes vendored
View File

@@ -1,7 +1,5 @@
* text=auto eol=lf * text=auto eol=lf
package-lock.json binary
app/package-lock.json binary
*.gif binary *.gif binary
*.jpg binary *.jpg binary
*.jpeg binary *.jpeg binary

4
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# Dependency directories # Dependency directory
node_modules/ /node_modules/
# npm cache directory # npm cache directory
.npm .npm

View File

@@ -15,17 +15,17 @@ addons:
language: node_js language: node_js
node_js: node_js:
- '8' - '10'
- '12'
before_install: before_install:
- ./scripts/travis-xvfb.sh - ./scripts/travis-xvfb.sh
- npm install -g gulp - npm install -g gulp
- npm install - npm ci
cache: cache:
directories: directories:
- node_modules - node_modules
- app/node_modules
script: script:
- npm run travis - npm run travis

View File

@@ -1,11 +1,11 @@
'use strict'; import { app, dialog } from 'electron';
import { app, dialog, shell } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import { linuxUpdateNotification } from './linuxupdater'; // Required only in case of linux import { linuxUpdateNotification } from './linuxupdater'; // Required only in case of linux
import log = require('electron-log'); import log from 'electron-log';
import isDev = require('electron-is-dev'); import isDev from 'electron-is-dev';
import ConfigUtil = require('../renderer/js/utils/config-util'); import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as LinkUtil from '../renderer/js/utils/link-util';
export function appUpdater(updateFromMenu = false): void { export function appUpdater(updateFromMenu = false): void {
// Don't initiate auto-updates in development // Don't initiate auto-updates in development
@@ -62,20 +62,19 @@ export function appUpdater(updateFromMenu = false): void {
} }
}); });
autoUpdater.on('error', error => { autoUpdater.on('error', async error => {
if (updateFromMenu) { if (updateFromMenu) {
const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates'); const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates');
dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'error', type: 'error',
buttons: ['Manual Download', 'Cancel'], buttons: ['Manual Download', 'Cancel'],
message: messageText, message: messageText,
detail: (error).toString() + `\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulipchat.com/apps/.\n detail: (error).toString() + `\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulipchat.com/apps/.\n
Current Version: ${app.getVersion()}` Current Version: ${app.getVersion()}`
}, response => {
if (response === 0) {
shell.openExternal('https://zulipchat.com/apps/');
}
}); });
if (response === 0) {
LinkUtil.openBrowser(new URL('https://zulipchat.com/apps/'));
}
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these // Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times. // listeners don't trigger multiple times.
autoUpdater.removeAllListeners(); autoUpdater.removeAllListeners();
@@ -83,23 +82,22 @@ Current Version: ${app.getVersion()}`
}); });
// Ask the user if update is available // Ask the user if update is available
autoUpdater.on('update-downloaded', event => { autoUpdater.on('update-downloaded', async event => {
// Ask user to update the app // Ask user to update the app
dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'question', type: 'question',
buttons: ['Install and Relaunch', 'Install Later'], buttons: ['Install and Relaunch', 'Install Later'],
defaultId: 0, defaultId: 0,
message: `A new update ${event.version} has been downloaded`, message: `A new update ${event.version} has been downloaded`,
detail: 'It will be installed the next time you restart the application' detail: 'It will be installed the next time you restart the application'
}, response => {
if (response === 0) {
setTimeout(() => {
autoUpdater.quitAndInstall();
// force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit();
}, 1000);
}
}); });
if (response === 0) {
setTimeout(() => {
autoUpdater.quitAndInstall();
// force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit();
}, 1000);
}
}); });
// Init for updates // Init for updates
autoUpdater.checkForUpdates(); autoUpdater.checkForUpdates();

View File

@@ -1,19 +1,16 @@
'use strict';
import { sentryInit } from '../renderer/js/utils/sentry-util'; import { sentryInit } from '../renderer/js/utils/sentry-util';
import { appUpdater } from './autoupdater'; import { appUpdater } from './autoupdater';
import { setAutoLaunch } from './startup'; import { setAutoLaunch } from './startup';
import windowStateKeeper = require('electron-window-state'); import windowStateKeeper from 'electron-window-state';
import path = require('path'); import path from 'path';
import fs = require('fs'); import fs from 'fs';
import isDev = require('electron-is-dev'); import electron, { app, ipcMain, session, dialog } from 'electron';
import electron = require('electron');
const { app, ipcMain, session } = electron;
import AppMenu = require('./menu'); import * as AppMenu from './menu';
import BadgeSettings = require('../renderer/js/pages/preference/badge-settings'); import * as BadgeSettings from '../renderer/js/pages/preference/badge-settings';
import ConfigUtil = require('../renderer/js/utils/config-util'); import * as ConfigUtil from '../renderer/js/utils/config-util';
import ProxyUtil = require('../renderer/js/utils/proxy-util'); import * as ProxyUtil from '../renderer/js/utils/proxy-util';
interface PatchedGlobal extends NodeJS.Global { interface PatchedGlobal extends NodeJS.Global {
mainWindowState: windowStateKeeper.State; mainWindowState: windowStateKeeper.State;
@@ -21,12 +18,6 @@ interface PatchedGlobal extends NodeJS.Global {
const globalPatched = global as PatchedGlobal; const globalPatched = global as PatchedGlobal;
// Adds debug features like hotkeys for triggering dev tools and reload
// in development mode
if (isDev) {
require('electron-debug')();
}
// Prevent window being garbage collected // Prevent window being garbage collected
let mainWindow: Electron.BrowserWindow; let mainWindow: Electron.BrowserWindow;
let badgeCount: number; let badgeCount: number;
@@ -57,6 +48,15 @@ const iconPath = (): string => {
return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png'); return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
}; };
// toggle the app window
const toggleApp = (): void => {
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
mainWindow.show();
} else {
mainWindow.hide();
}
};
function createMainWindow(): Electron.BrowserWindow { function createMainWindow(): Electron.BrowserWindow {
// Load the previous state with fallback to defaults // Load the previous state with fallback to defaults
const mainWindowState: windowStateKeeper.State = windowStateKeeper({ const mainWindowState: windowStateKeeper.State = windowStateKeeper({
@@ -81,7 +81,8 @@ function createMainWindow(): Electron.BrowserWindow {
webPreferences: { webPreferences: {
plugins: true, plugins: true,
nodeIntegration: true, nodeIntegration: true,
partition: 'persist:webviewsession' partition: 'persist:webviewsession',
webviewTag: true
}, },
show: false show: false
}); });
@@ -93,12 +94,12 @@ function createMainWindow(): Electron.BrowserWindow {
win.loadURL(mainURL); win.loadURL(mainURL);
// Keep the app running in background on close event // Keep the app running in background on close event
win.on('close', e => { win.on('close', event => {
if (ConfigUtil.getConfigItem("quitOnClose")) { if (ConfigUtil.getConfigItem('quitOnClose')) {
app.quit(); app.quit();
} }
if (!isQuitting) { if (!isQuitting) {
e.preventDefault(); event.preventDefault();
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
app.hide(); app.hide();
@@ -119,8 +120,8 @@ function createMainWindow(): Electron.BrowserWindow {
}); });
// To destroy tray icon when navigate to a new URL // To destroy tray icon when navigate to a new URL
win.webContents.on('will-navigate', e => { win.webContents.on('will-navigate', event => {
if (e) { if (event) {
win.webContents.send('destroytray'); win.webContents.send('destroytray');
} }
}); });
@@ -147,7 +148,10 @@ app.on('certificate-error', (event: Event, _webContents: Electron.WebContents, _
}); });
app.on('activate', () => { app.on('activate', () => {
if (!mainWindow) { if (mainWindow) {
// if there is already a window toggle the app
toggleApp();
} else {
mainWindow = createMainWindow(); mainWindow = createMainWindow();
} }
}); });
@@ -161,7 +165,7 @@ app.on('ready', () => {
// Auto-hide menu bar on Windows + Linux // Auto-hide menu bar on Windows + Linux
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
const shouldHideMenu = ConfigUtil.getConfigItem('autoHideMenubar') || false; const shouldHideMenu = ConfigUtil.getConfigItem('autoHideMenubar') || false;
mainWindow.setAutoHideMenuBar(shouldHideMenu); mainWindow.autoHideMenuBar = shouldHideMenu;
mainWindow.setMenuBarVisibility(!shouldHideMenu); mainWindow.setMenuBarVisibility(!shouldHideMenu);
} }
@@ -198,6 +202,27 @@ app.on('ready', () => {
} }
}); });
const permissionCallbacks = new Map();
let nextPermissionId = 0;
page.session.setPermissionRequestHandler((webContents, permission, callback, details) => {
const {origin} = new URL(details.requestingUrl);
permissionCallbacks.set(nextPermissionId, callback);
page.send('permission-request', nextPermissionId, {
webContentsId: webContents.id === mainWindow.webContents.id ?
null :
webContents.id,
origin,
permission
});
nextPermissionId++;
});
ipcMain.on('permission-response', (event: Event, permissionId: number, grant: boolean) => {
permissionCallbacks.get(permissionId)(grant);
permissionCallbacks.delete(permissionId);
});
// Temporarily remove this event // Temporarily remove this event
// electron.powerMonitor.on('resume', () => { // electron.powerMonitor.on('resume', () => {
// mainWindow.reload(); // mainWindow.reload();
@@ -251,38 +276,34 @@ app.on('ready', () => {
}); });
ipcMain.on('toggle-app', () => { ipcMain.on('toggle-app', () => {
if (!mainWindow.isVisible() || mainWindow.isMinimized()) { toggleApp();
mainWindow.show();
} else {
mainWindow.hide();
}
}); });
ipcMain.on('toggle-badge-option', () => { ipcMain.on('toggle-badge-option', () => {
BadgeSettings.updateBadge(badgeCount, mainWindow); BadgeSettings.updateBadge(badgeCount, mainWindow);
}); });
ipcMain.on('toggle-menubar', (_event: Electron.IpcMessageEvent, showMenubar: boolean) => { ipcMain.on('toggle-menubar', (_event: Electron.IpcMainEvent, showMenubar: boolean) => {
mainWindow.setAutoHideMenuBar(showMenubar); mainWindow.autoHideMenuBar = showMenubar;
mainWindow.setMenuBarVisibility(!showMenubar); mainWindow.setMenuBarVisibility(!showMenubar);
page.send('toggle-autohide-menubar', showMenubar, true); page.send('toggle-autohide-menubar', showMenubar, true);
}); });
ipcMain.on('update-badge', (_event: Electron.IpcMessageEvent, messageCount: number) => { ipcMain.on('update-badge', (_event: Electron.IpcMainEvent, messageCount: number) => {
badgeCount = messageCount; badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow); BadgeSettings.updateBadge(badgeCount, mainWindow);
page.send('tray', messageCount); page.send('tray', messageCount);
}); });
ipcMain.on('update-taskbar-icon', (_event: Electron.IpcMessageEvent, data: any, text: string) => { ipcMain.on('update-taskbar-icon', (_event: Electron.IpcMainEvent, data: string, text: string) => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow); BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
}); });
ipcMain.on('forward-message', (_event: Electron.IpcMessageEvent, listener: any, ...params: any[]) => { ipcMain.on('forward-message', (_event: Electron.IpcMainEvent, listener: string, ...parameters: any[]) => {
page.send(listener, ...params); page.send(listener, ...parameters);
}); });
ipcMain.on('update-menu', (_event: Electron.IpcMessageEvent, props: any) => { ipcMain.on('update-menu', (_event: Electron.IpcMainEvent, props: any) => {
AppMenu.setMenu(props); AppMenu.setMenu(props);
const activeTab = props.tabs[props.activeTabIndex]; const activeTab = props.tabs[props.activeTabIndex];
if (activeTab) { if (activeTab) {
@@ -290,35 +311,50 @@ app.on('ready', () => {
} }
}); });
ipcMain.on('toggleAutoLauncher', (_event: Electron.IpcMessageEvent, AutoLaunchValue: boolean) => { ipcMain.on('toggleAutoLauncher', (_event: Electron.IpcMainEvent, AutoLaunchValue: boolean) => {
setAutoLaunch(AutoLaunchValue); setAutoLaunch(AutoLaunchValue);
}); });
ipcMain.on('downloadFile', (_event: Electron.IpcMessageEvent, url: string, downloadPath: string) => { ipcMain.on('downloadFile', (_event: Electron.IpcMainEvent, url: string, downloadPath: string) => {
page.downloadURL(url); page.downloadURL(url);
page.session.once('will-download', (_event: Event, item) => { page.session.once('will-download', async (_event: Event, item) => {
const filePath = path.join(downloadPath, item.getFilename()); let setFilePath: string;
let shortFileName: string;
if (ConfigUtil.getConfigItem('promptDownload', false)) {
const showDialogOptions: object = {
defaultPath: path.join(downloadPath, item.getFilename())
};
const getTimeStamp = (): any => { const result = await dialog.showSaveDialog(mainWindow, showDialogOptions);
const date = new Date(); if (result.canceled) {
return date.getTime(); item.cancel();
}; return;
}
setFilePath = result.filePath;
shortFileName = path.basename(setFilePath);
} else {
const getTimeStamp = (): number => {
const date = new Date();
return date.getTime();
};
const formatFile = (filePath: string): string => { const formatFile = (filePath: string): string => {
const fileExtension = path.extname(filePath); const fileExtension = path.extname(filePath);
const baseName = path.basename(filePath, fileExtension); const baseName = path.basename(filePath, fileExtension);
return `${baseName}-${getTimeStamp()}${fileExtension}`; return `${baseName}-${getTimeStamp()}${fileExtension}`;
}; };
// Update the name and path of the file if it already exists const filePath = path.join(downloadPath, item.getFilename());
const updatedFilePath = path.join(downloadPath, formatFile(filePath)); // Update the name and path of the file if it already exists
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
const setFilePath = fs.existsSync(filePath) ? updatedFilePath : filePath; setFilePath = fs.existsSync(filePath) ? updatedFilePath : filePath;
shortFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename();
}
item.setSavePath(setFilePath); item.setSavePath(setFilePath);
item.on('updated', (_event: Event, state) => { const updatedListener = (_event: Event, state: string): void => {
switch (state) { switch (state) {
case 'interrupted': { case 'interrupted': {
// Can interrupted to due to network error, cancel download then // Can interrupted to due to network error, cancel download then
@@ -337,36 +373,36 @@ app.on('ready', () => {
console.info('Unknown updated state of download item'); console.info('Unknown updated state of download item');
} }
} }
}); };
item.on('updated', updatedListener);
item.once('done', (_event: Event, state) => { item.once('done', (_event: Event, state) => {
const getFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename();
if (state === 'completed') { if (state === 'completed') {
page.send('downloadFileCompleted', item.getSavePath(), getFileName); page.send('downloadFileCompleted', item.getSavePath(), shortFileName);
} else { } else {
console.log('Download failed state: ', state); console.log('Download failed state:', state);
page.send('downloadFileFailed'); page.send('downloadFileFailed');
} }
// To stop item for listening to updated events of this file // To stop item for listening to updated events of this file
item.removeAllListeners('updated'); item.removeListener('updated', updatedListener);
}); });
}); });
}); });
ipcMain.on('realm-name-changed', (_event: Electron.IpcMessageEvent, serverURL: string, realmName: string) => { ipcMain.on('realm-name-changed', (_event: Electron.IpcMainEvent, serverURL: string, realmName: string) => {
page.send('update-realm-name', serverURL, realmName); page.send('update-realm-name', serverURL, realmName);
}); });
ipcMain.on('realm-icon-changed', (_event: Electron.IpcMessageEvent, serverURL: string, iconURL: string) => { ipcMain.on('realm-icon-changed', (_event: Electron.IpcMainEvent, serverURL: string, iconURL: string) => {
page.send('update-realm-icon', serverURL, iconURL); page.send('update-realm-icon', serverURL, iconURL);
}); });
// Using event.sender.send instead of page.send here to // Using event.sender.send instead of page.send here to
// make sure the value of errorReporting is sent only once on load. // make sure the value of errorReporting is sent only once on load.
ipcMain.on('error-reporting', (event: Electron.IpcMessageEvent) => { ipcMain.on('error-reporting', (event: Electron.IpcMainEvent) => {
event.sender.send('error-reporting-val', errorReporting); event.sender.send('error-reporting-val', errorReporting);
}); });
ipcMain.on('save-last-tab', (_event: Electron.IpcMessageEvent, index: number) => { ipcMain.on('save-last-tab', (_event: Electron.IpcMainEvent, index: number) => {
ConfigUtil.setConfigItem('lastActiveTab', index); ConfigUtil.setConfigItem('lastActiveTab', index);
}); });
@@ -375,19 +411,12 @@ app.on('ready', () => {
setInterval(() => { setInterval(() => {
// Set user idle if no activity in 1 second (idleThresholdSeconds) // Set user idle if no activity in 1 second (idleThresholdSeconds)
const idleThresholdSeconds = 1; // 1 second const idleThresholdSeconds = 1; // 1 second
const idleState = electron.powerMonitor.getSystemIdleState(idleThresholdSeconds);
// TODO: Remove typecast to any when types get added if (idleState === 'active') {
// TODO: use powerMonitor.getSystemIdleState when upgrading electron page.send('set-active');
// powerMonitor.querySystemIdleState is deprecated in current electron } else {
// version at the time of writing. page.send('set-idle');
const powerMonitor = electron.powerMonitor as any; }
powerMonitor.querySystemIdleState(idleThresholdSeconds, (idleState: string) => {
if (idleState === 'active') {
page.send('set-active');
} else {
page.send('set-idle');
}
});
}, idleCheckInterval); }, idleCheckInterval);
}); });

View File

@@ -1,11 +1,11 @@
import { app, Notification } from 'electron'; import { app, Notification } from 'electron';
import request = require('request'); import request from 'request';
import semver = require('semver'); import semver from 'semver';
import ConfigUtil = require('../renderer/js/utils/config-util'); import * as ConfigUtil from '../renderer/js/utils/config-util';
import ProxyUtil = require('../renderer/js/utils/proxy-util'); import * as ProxyUtil from '../renderer/js/utils/proxy-util';
import LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util'); import * as LinuxUpdateUtil from '../renderer/js/utils/linux-update-util';
import Logger = require('../renderer/js/utils/logger-util'); import Logger from '../renderer/js/utils/logger-util';
const logger = new Logger({ const logger = new Logger({
file: 'linux-update-util.log', file: 'linux-update-util.log',

File diff suppressed because it is too large Load Diff

View File

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

1940
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +0,0 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "4.0.2-beta",
"desktopName": "zulip.desktop",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"copyright": "Kandra Labs, Inc.",
"author": {
"name": "Kandra Labs, Inc.",
"email": "support@zulipchat.com"
},
"repository": {
"type": "git",
"url": "https://github.com/zulip/zulip-desktop.git"
},
"bugs": {
"url": "https://github.com/zulip/zulip-desktop/issues"
},
"main": "main/index.js",
"keywords": [
"Zulip",
"Group Chat app",
"electron-app",
"electron",
"Desktop app",
"InstantMessaging"
],
"dependencies": {
"@electron-elements/send-feedback": "1.0.8",
"@sentry/electron": "0.14.0",
"adm-zip": "0.4.11",
"auto-launch": "5.0.5",
"backoff": "2.5.0",
"dotenv": "8.0.0",
"electron-is-dev": "0.3.0",
"electron-log": "2.2.14",
"electron-spellchecker": "1.1.2",
"electron-updater": "4.0.6",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"i18n": "0.8.3",
"node-json-db": "0.9.2",
"request": "2.85.0",
"semver": "5.4.1",
"wurl": "2.5.0"
},
"optionalDependencies": {
"node-mac-notifier": "1.1.0"
}
}

View File

@@ -14,33 +14,19 @@
<div class="maintenance-info"> <div class="maintenance-info">
<p class="detail maintainer"> <p class="detail maintainer">
Maintained by Maintained by
<a onclick="linkInBrowser('website')">Zulip</a> <a href="https://zulipchat.com" target="_blank" rel="noopener noreferrer">Zulip</a>
</p> </p>
<p class="detail license"> <p class="detail license">
Available under the Available under the
<a onclick="linkInBrowser('license')">Apache 2.0 License</a> <a href="https://github.com/zulip/zulip-desktop/blob/master/LICENSE" target="_blank" rel="noopener noreferrer">Apache 2.0 License</a>
</p> </p>
</div> </div>
</div> </div>
<script> <script>
const { app } = require('electron').remote; const { app } = require('electron').remote;
const { shell } = require('electron');
const version_tag = document.querySelector('#version'); const version_tag = document.querySelector('#version');
version_tag.innerHTML = 'v' + app.getVersion(); 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-desktop/blob/master/LICENSE";
break;
}
shell.openExternal(url);
}
</script> </script>
<script>require('./js/shared/preventdrag.js')</script> <script>require('./js/shared/preventdrag.js')</script>
</body> </body>

View File

@@ -1,11 +1,7 @@
'use strict'; export default class BaseComponent {
class BaseComponent {
generateNodeFromTemplate(template: string): Element | null { generateNodeFromTemplate(template: string): Element | null {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.innerHTML = template; wrapper.innerHTML = template;
return wrapper.firstElementChild; return wrapper.firstElementChild;
} }
} }
export = BaseComponent;

View File

@@ -1,8 +1,6 @@
'use strict'; import Tab from './tab';
import Tab = require('./tab'); export default class FunctionalTab extends Tab {
class FunctionalTab extends Tab {
$closeButton: Element; $closeButton: Element;
template(): string { template(): string {
return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}"> return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
@@ -41,11 +39,9 @@ class FunctionalTab extends Tab {
this.$closeButton.classList.remove('active'); this.$closeButton.classList.remove('active');
}); });
this.$closeButton.addEventListener('click', (e: Event) => { this.$closeButton.addEventListener('click', (event: Event) => {
this.props.onDestroy(); this.props.onDestroy();
e.stopPropagation(); event.stopPropagation();
}); });
} }
} }
export = FunctionalTab;

View File

@@ -1,83 +1,56 @@
import { ipcRenderer, remote } from 'electron'; import { ipcRenderer, remote } from 'electron';
import LinkUtil = require('../utils/link-util'); import * as LinkUtil from '../utils/link-util';
import DomainUtil = require('../utils/domain-util'); import * as ConfigUtil from '../utils/config-util';
import ConfigUtil = require('../utils/config-util'); import type WebView from './webview';
const { shell, app } = remote; const { shell, app } = remote;
const dingSound = new Audio('../resources/sounds/ding.ogg'); const dingSound = new Audio('../resources/sounds/ding.ogg');
// TODO: TypeScript - Figure out a way to pass correct type here. export default function handleExternalLink(this: WebView, event: Electron.NewWindowEvent): void {
function handleExternalLink(this: any, event: any): void { event.preventDefault();
const { url } = event;
const domainPrefix = DomainUtil.getDomain(this.props.index).url; const url = new URL(event.url);
const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`); const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`);
const shouldShowInFolder = ConfigUtil.getConfigItem('showDownloadFolder', false);
// Whitelist URLs which are allowed to be opened in the app if (LinkUtil.isUploadsUrl(this.props.url, url)) {
const { ipcRenderer.send('downloadFile', url.href, downloadPath);
isInternalUrl: isWhiteListURL, ipcRenderer.once('downloadFileCompleted', (_event: Event, filePath: string, fileName: string) => {
isUploadsUrl: isUploadsURL const downloadNotification = new Notification('Download Complete', {
} = LinkUtil.isInternal(domainPrefix, url); body: `Click to show ${fileName} in folder`,
silent: true // We'll play our own sound - ding.ogg
if (isWhiteListURL) {
event.preventDefault();
// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
// Show pdf attachments in a new window
// if (LinkUtil.isPDF(url) && isUploadsURL) {
// ipcRenderer.send('pdf-view', url);
// return;
// }
// download txt, 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
// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
// if (!LinkUtil.isImage(url) && !LinkUtil.isPDF(url) && isUploadsURL) {
if (!LinkUtil.isImage(url) && isUploadsURL) {
ipcRenderer.send('downloadFile', url, downloadPath);
ipcRenderer.once('downloadFileCompleted', (_event: Event, filePath: string, fileName: string) => {
const downloadNotification = new Notification('Download Complete', {
body: shouldShowInFolder ? `Click to show ${fileName} in folder` : `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.addEventListener('click', () => {
if (shouldShowInFolder) {
// Reveal file in download folder
shell.showItemInFolder(filePath);
} else {
// Open file in the default native app
shell.openItem(filePath);
}
});
ipcRenderer.removeAllListeners('downloadFileFailed');
}); });
ipcRenderer.once('downloadFileFailed', () => { // Play sound to indicate download complete
// Automatic download failed, so show save dialog prompt and download if (!ConfigUtil.getConfigItem('silent')) {
// through webview dingSound.play();
this.$el.downloadURL(url); }
ipcRenderer.removeAllListeners('downloadFileCompleted');
});
return;
}
// open internal urls inside the current webview. downloadNotification.addEventListener('click', () => {
this.$el.loadURL(url); // Reveal file in download folder
shell.showItemInFolder(filePath);
});
ipcRenderer.removeAllListeners('downloadFileFailed');
});
ipcRenderer.once('downloadFileFailed', () => {
// Automatic download failed, so show save dialog prompt and download
// through webview
// Only do this if it is the automatic download, otherwise show an error (so we aren't showing two save
// prompts right after each other)
if (ConfigUtil.getConfigItem('promptDownload', false)) {
// We need to create a "new Notification" to display it, but just `Notification(...)` on its own
// doesn't work
new Notification('Download Complete', { // eslint-disable-line no-new
body: 'Download failed'
});
} else {
this.$el.downloadURL(url.href);
}
ipcRenderer.removeAllListeners('downloadFileCompleted');
});
} else { } else {
event.preventDefault(); LinkUtil.openBrowser(url);
shell.openExternal(url);
} }
} }
export = handleExternalLink;

View File

@@ -1,11 +1,9 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import Tab = require('./tab'); import Tab from './tab';
import SystemUtil = require('../utils/system-util'); import * as SystemUtil from '../utils/system-util';
class ServerTab extends Tab { export default class ServerTab extends Tab {
$badge: Element; $badge: Element;
template(): string { template(): string {
@@ -64,5 +62,3 @@ class ServerTab extends Tab {
return shortcutText; return shortcutText;
} }
} }
export = ServerTab;

View File

@@ -1,14 +1,12 @@
'use strict'; import WebView from './webview';
import BaseComponent from './base';
import WebView = require('./webview');
import BaseComponent = require('./base');
// TODO: TypeScript - Type annotate props // TODO: TypeScript - Type annotate props
interface TabProps { interface TabProps {
[key: string]: any; [key: string]: any;
} }
class Tab extends BaseComponent { export default class Tab extends BaseComponent {
props: TabProps; props: TabProps;
webview: WebView; webview: WebView;
$el: Element; $el: Element;
@@ -44,5 +42,3 @@ class Tab extends BaseComponent {
this.webview.$el.parentNode.removeChild(this.webview.$el); this.webview.$el.parentNode.removeChild(this.webview.$el);
} }
} }
export = Tab;

View File

@@ -1,12 +1,11 @@
'use strict'; import { ipcRenderer, remote } from 'electron';
import { remote } from 'electron';
import path = require('path'); import path from 'path';
import fs = require('fs'); import fs from 'fs';
import ConfigUtil = require('../utils/config-util'); import * as ConfigUtil from '../utils/config-util';
import SystemUtil = require('../utils/system-util'); import * as SystemUtil from '../utils/system-util';
import BaseComponent = require('../components/base'); import BaseComponent from './base';
import handleExternalLink = require('../components/handle-external-link'); import handleExternalLink from './handle-external-link';
const { app, dialog } = remote; const { app, dialog } = remote;
@@ -17,7 +16,7 @@ interface WebViewProps {
[key: string]: any; [key: string]: any;
} }
class WebView extends BaseComponent { export default class WebView extends BaseComponent {
props: any; props: any;
zoomFactor: number; zoomFactor: number;
badgeCount: number; badgeCount: number;
@@ -25,6 +24,7 @@ class WebView extends BaseComponent {
customCSS: string; customCSS: string;
$webviewsContainer: DOMTokenList; $webviewsContainer: DOMTokenList;
$el: Electron.WebviewTag; $el: Electron.WebviewTag;
domReady?: Promise<void>;
// This is required because in main.js we access WebView.method as // This is required because in main.js we access WebView.method as
// webview[method]. // webview[method].
@@ -34,7 +34,7 @@ class WebView extends BaseComponent {
super(); super();
this.props = props; this.props = props;
this.zoomFactor = 1.0; this.zoomFactor = 1;
this.loading = true; this.loading = true;
this.badgeCount = 0; this.badgeCount = 0;
this.customCSS = ConfigUtil.getConfigItem('customCSS'); this.customCSS = ConfigUtil.getConfigItem('customCSS');
@@ -47,16 +47,18 @@ class WebView extends BaseComponent {
data-tab-id="${this.props.tabIndex}" data-tab-id="${this.props.tabIndex}"
src="${this.props.url}" src="${this.props.url}"
${this.props.nodeIntegration ? 'nodeIntegration' : ''} ${this.props.nodeIntegration ? 'nodeIntegration' : ''}
disablewebsecurity
${this.props.preload ? 'preload="js/preload.js"' : ''} ${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession" partition="persist:webviewsession"
name="${this.props.name}" name="${this.props.name}"
webpreferences="allowRunningInsecureContent, javascript=yes"> webpreferences="${this.props.nodeIntegration ? '' : 'contextIsolation, '}javascript=yes">
</webview>`; </webview>`;
} }
init(): void { init(): void {
this.$el = this.generateNodeFromTemplate(this.template()) as Electron.WebviewTag; this.$el = this.generateNodeFromTemplate(this.template()) as Electron.WebviewTag;
this.domReady = new Promise(resolve => {
this.$el.addEventListener('dom-ready', () => resolve(), true);
});
this.props.$root.append(this.$el); this.props.$root.append(this.$el);
this.registerListeners(); this.registerListeners();
@@ -155,7 +157,7 @@ class WebView extends BaseComponent {
} }
showNotificationSettings(): void { showNotificationSettings(): void {
this.$el.executeJavaScript('showNotificationSettings()'); ipcRenderer.sendTo(this.$el.getWebContentsId(), 'show-notification-settings');
} }
show(): void { show(): void {
@@ -190,8 +192,8 @@ class WebView extends BaseComponent {
this.customCSS = null; this.customCSS = null;
ConfigUtil.setConfigItem('customCSS', null); ConfigUtil.setConfigItem('customCSS', null);
const errMsg = 'The custom css previously set is deleted!'; const errorMessage = 'The custom css previously set is deleted!';
dialog.showErrorBox('custom css file deleted!', errMsg); dialog.showErrorBox('custom css file deleted!', errorMessage);
return; return;
} }
@@ -238,16 +240,16 @@ class WebView extends BaseComponent {
} }
zoomActualSize(): void { zoomActualSize(): void {
this.zoomFactor = 1.0; this.zoomFactor = 1;
this.$el.setZoomFactor(this.zoomFactor); this.$el.setZoomFactor(this.zoomFactor);
} }
logOut(): void { logOut(): void {
this.$el.executeJavaScript('logout()'); ipcRenderer.sendTo(this.$el.getWebContentsId(), 'logout');
} }
showShortcut(): void { showShortcut(): void {
this.$el.executeJavaScript('shortcut()'); ipcRenderer.sendTo(this.$el.getWebContentsId(), 'shortcut');
} }
openDevTools(): void { openDevTools(): void {
@@ -289,9 +291,8 @@ class WebView extends BaseComponent {
this.init(); this.init();
} }
send(channel: string, ...param: any[]): void { async send(channel: string, ...parameters: any[]): Promise<void> {
this.$el.send(channel, ...param); await this.domReady;
this.$el.send(channel, ...parameters);
} }
} }
export = WebView;

View File

@@ -1,14 +1,12 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import events = require('events'); import { EventEmitter } from 'events';
import { NotificationData, newNotification } from './notification';
type ListenerType = ((...args: any[]) => void); type ListenerType = ((...args: any[]) => void);
// we have and will have some non camelcase stuff class ElectronBridge extends EventEmitter {
// while working with zulip so just turning the rule off
// for the whole file.
/* eslint-disable @typescript-eslint/camelcase */
class ElectronBridge extends events {
send_notification_reply_message_supported: boolean; send_notification_reply_message_supported: boolean;
idle_on_system: boolean; idle_on_system: boolean;
last_active_on_system: number; last_active_on_system: number;
@@ -23,13 +21,31 @@ class ElectronBridge extends events {
this.last_active_on_system = Date.now(); this.last_active_on_system = Date.now();
} }
send_event(eventName: string | symbol, ...args: any[]): void { send_event = (eventName: string | symbol, ...args: any[]): void => {
this.emit(eventName, ...args); this.emit(eventName, ...args);
} };
on_event(eventName: string, listener: ListenerType): void { on_event = (eventName: string, listener: ListenerType): void => {
this.on(eventName, listener); this.on(eventName, listener);
} };
new_notification = (
title: string,
options: NotificationOptions | undefined,
dispatch: (type: string, eventInit: EventInit) => boolean
): NotificationData =>
newNotification(title, options, dispatch);
get_idle_on_system = (): boolean => this.idle_on_system;
get_last_active_on_system = (): number => this.last_active_on_system;
get_send_notification_reply_message_supported = (): boolean =>
this.send_notification_reply_message_supported;
set_send_notification_reply_message_supported = (value: boolean): void => {
this.send_notification_reply_message_supported = value;
};
} }
const electron_bridge = new ElectronBridge(); const electron_bridge = new ElectronBridge();
@@ -54,6 +70,4 @@ electron_bridge.on('realm_icon_url', iconURL => {
// functions zulip side will emit event using ElectronBrigde.send_event // functions zulip side will emit event using ElectronBrigde.send_event
// which is alias of .emit and on this side we can handle the data by adding // which is alias of .emit and on this side we can handle the data by adding
// a listener for the event. // a listener for the event.
export = electron_bridge; export default electron_bridge;
/* eslint-enable @typescript-eslint/camelcase */

View File

@@ -1,8 +1,8 @@
import { remote } from 'electron'; import { remote } from 'electron';
import SendFeedback from '@electron-elements/send-feedback'; import SendFeedback from '@electron-elements/send-feedback';
import path = require('path'); import path from 'path';
import fs = require('fs'); import fs from 'fs';
const { app } = remote; const { app } = remote;
@@ -34,12 +34,26 @@ customElements.define('send-feedback', SendFeedback);
export const sendFeedback: SendFeedbackType = document.querySelector('send-feedback'); export const sendFeedback: SendFeedbackType = document.querySelector('send-feedback');
export const feedbackHolder = sendFeedback.parentElement; export const feedbackHolder = sendFeedback.parentElement;
/* eslint-disable no-multi-str */
// customize the fields of custom elements // customize the fields of custom elements
sendFeedback.title = 'Report Issue'; sendFeedback.title = 'Report Issue';
sendFeedback.titleLabel = 'Issue title:'; sendFeedback.titleLabel = 'Issue title:';
sendFeedback.titlePlaceholder = 'Enter issue title'; sendFeedback.titlePlaceholder = 'Enter issue title';
sendFeedback.textareaLabel = 'Describe the issue:'; sendFeedback.textareaLabel = 'Describe the issue:';
sendFeedback.textareaPlaceholder = 'Succinctly describe your issue and steps to reproduce it...'; sendFeedback.textareaPlaceholder = 'Succinctly describe your issue and steps to reproduce it...\n\n\
---\n\
<!-- Please Include: -->\n\
- **Operating System**:\n\
- [ ] Windows\n\
- [ ] Linux/Ubuntu\n\
- [ ] macOS\n\
- **Clear steps to reproduce the issue**:\n\
- **Relevant error messages and/or screenshots**:\n\
';
/* eslint-enable no-multi-str */
sendFeedback.buttonLabel = 'Report Issue'; sendFeedback.buttonLabel = 'Report Issue';
sendFeedback.loaderSuccessText = ''; sendFeedback.loaderSuccessText = '';
@@ -47,10 +61,10 @@ sendFeedback.useReporter('emailReporter', {
email: 'akash@zulipchat.com' email: 'akash@zulipchat.com'
}); });
feedbackHolder.addEventListener('click', (e: Event) => { feedbackHolder.addEventListener('click', (event: Event) => {
// only remove the class if the grey out faded // only remove the class if the grey out faded
// part is clicked and not the feedback element itself // part is clicked and not the feedback element itself
if (e.target === e.currentTarget) { if (event.target === event.currentTarget) {
feedbackHolder.classList.remove('show'); feedbackHolder.classList.remove('show');
} }
}); });

122
app/renderer/js/injected.ts Normal file
View File

@@ -0,0 +1,122 @@
'use strict';
(() => {
const zulipWindow = window as typeof window & {
electron_bridge: any;
narrow: any;
page_params: any;
raw_electron_bridge: any;
};
const electron_bridge = {
...zulipWindow.raw_electron_bridge,
get idle_on_system() {
return this.get_idle_on_system();
},
get last_active_on_system() {
return this.get_last_active_on_system();
},
get send_notification_reply_message_supported() {
return this.get_send_notification_reply_message_supported();
},
set send_notification_reply_message_supported(value: boolean) {
this.set_send_notification_reply_message_supported(value);
}
};
zulipWindow.electron_bridge = electron_bridge;
(async () => {
if (document.readyState === 'loading') {
await new Promise(resolve => {
document.addEventListener('DOMContentLoaded', () => {
resolve();
});
});
}
const { page_params } = zulipWindow;
if (page_params) {
electron_bridge.send_event('zulip-loaded', {
serverLanguage: page_params.default_language
});
}
})();
electron_bridge.on_event('narrow-by-topic', (id: string) => {
const { narrow } = zulipWindow;
const narrowByTopic = narrow.by_topic || narrow.by_subject;
narrowByTopic(id, { trigger: 'notification' });
});
function attributeListener<T extends EventTarget>(type: string): PropertyDescriptor {
const symbol = Symbol('on' + type);
function listener(this: T, ev: Event): void {
if ((this as any)[symbol].call(this, ev) === false) {
ev.preventDefault();
}
}
return {
configurable: true,
enumerable: true,
get(this: T) {
return (this as any)[symbol];
},
set(this: T, value: unknown) {
if (typeof value === 'function') {
if (!(symbol in this)) {
this.addEventListener(type, listener);
}
(this as any)[symbol] = value;
} else if (symbol in this) {
this.removeEventListener(type, listener);
delete (this as any)[symbol];
}
}
};
}
const NativeNotification = Notification;
class InjectedNotification extends EventTarget {
constructor(title: string, options?: NotificationOptions) {
super();
Object.assign(
this,
electron_bridge.new_notification(title, options, (type: string, eventInit: EventInit) =>
this.dispatchEvent(new Event(type, eventInit))
)
);
}
static get maxActions(): number {
return NativeNotification.maxActions;
}
static get permission(): NotificationPermission {
return NativeNotification.permission;
}
static async requestPermission(callback?: NotificationPermissionCallback): Promise<NotificationPermission> {
if (callback) {
callback(await Promise.resolve(NativeNotification.permission));
}
return NativeNotification.permission;
}
}
Object.defineProperties(InjectedNotification.prototype, {
onclick: attributeListener('click'),
onclose: attributeListener('close'),
onerror: attributeListener('error'),
onshow: attributeListener('show')
});
window.Notification = InjectedNotification as any;
})();

View File

@@ -1,27 +1,26 @@
'use strict'; import { ipcRenderer, remote, clipboard } from 'electron';
import { ipcRenderer, remote, clipboard, shell } from 'electron';
import { feedbackHolder } from './feedback'; import { feedbackHolder } from './feedback';
import path = require('path'); import path from 'path';
import escape = require('escape-html'); import escape from 'escape-html';
import isDev = require('electron-is-dev'); import isDev from 'electron-is-dev';
const { session, app, Menu, dialog } = remote; const { session, app, Menu, dialog } = remote;
// eslint-disable-next-line import/no-unassigned-import // eslint-disable-next-line import/no-unassigned-import
require('./tray'); import './tray';
import DomainUtil = require('./utils/domain-util'); import * as DomainUtil from './utils/domain-util';
import WebView = require('./components/webview'); import WebView from './components/webview';
import ServerTab = require('./components/server-tab'); import ServerTab from './components/server-tab';
import FunctionalTab = require('./components/functional-tab'); import FunctionalTab from './components/functional-tab';
import ConfigUtil = require('./utils/config-util'); import * as ConfigUtil from './utils/config-util';
import DNDUtil = require('./utils/dnd-util'); import * as DNDUtil from './utils/dnd-util';
import ReconnectUtil = require('./utils/reconnect-util'); import ReconnectUtil from './utils/reconnect-util';
import Logger = require('./utils/logger-util'); import Logger from './utils/logger-util';
import CommonUtil = require('./utils/common-util'); import * as CommonUtil from './utils/common-util';
import EnterpriseUtil = require('./utils/enterprise-util'); import * as EnterpriseUtil from './utils/enterprise-util';
import Messages = require('./../../resources/messages'); import * as LinkUtil from './utils/link-util';
import * as Messages from '../../resources/messages';
interface FunctionalTabProps { interface FunctionalTabProps {
name: string; name: string;
@@ -57,8 +56,8 @@ interface SettingsOptions {
flashTaskbarOnMessage?: boolean; flashTaskbarOnMessage?: boolean;
}; };
downloadsPath: string; downloadsPath: string;
showDownloadFolder: boolean;
quitOnClose: boolean; quitOnClose: boolean;
promptDownload: boolean;
flashTaskbarOnMessage?: boolean; flashTaskbarOnMessage?: boolean;
dockBouncing?: boolean; dockBouncing?: boolean;
loading?: AnyObject; loading?: AnyObject;
@@ -137,46 +136,43 @@ class ServerManagerView {
this.tabIndex = 0; this.tabIndex = 0;
} }
init(): void { async init(): Promise<void> {
this.loadProxy().then(() => { await this.loadProxy();
this.initDefaultSettings(); this.initDefaultSettings();
this.initSidebar(); this.initSidebar();
if (EnterpriseUtil.configFile) { if (EnterpriseUtil.configFile) {
this.initPresetOrgs(); this.initPresetOrgs();
} }
this.initTabs(); this.initTabs();
this.initActions(); this.initActions();
this.registerIpcs(); this.registerIpcs();
});
} }
loadProxy(): Promise<boolean> { async loadProxy(): Promise<void> {
return new Promise(resolve => { // To change proxyEnable to useManualProxy in older versions
// To change proxyEnable to useManualProxy in older versions const proxyEnabledOld = ConfigUtil.isConfigItemExists('useProxy');
const proxyEnabledOld = ConfigUtil.isConfigItemExists('useProxy'); if (proxyEnabledOld) {
if (proxyEnabledOld) { const proxyEnableOldState = ConfigUtil.getConfigItem('useProxy');
const proxyEnableOldState = ConfigUtil.getConfigItem('useProxy'); if (proxyEnableOldState) {
if (proxyEnableOldState) { ConfigUtil.setConfigItem('useManualProxy', true);
ConfigUtil.setConfigItem('useManualProxy', true);
}
ConfigUtil.removeConfigItem('useProxy');
} }
ConfigUtil.removeConfigItem('useProxy');
}
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy'); const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
if (proxyEnabled) { if (proxyEnabled) {
session.fromPartition('persist:webviewsession').setProxy({ await session.fromPartition('persist:webviewsession').setProxy({
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''), pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
proxyRules: ConfigUtil.getConfigItem('proxyRules', ''), proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '') proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
}, resolve); });
} else { } else {
session.fromPartition('persist:webviewsession').setProxy({ await session.fromPartition('persist:webviewsession').setProxy({
pacScript: '', pacScript: '',
proxyRules: '', proxyRules: '',
proxyBypassRules: '' proxyBypassRules: ''
}, resolve); });
} }
});
} }
// Settings are initialized only when user clicks on General/Server/Network section settings // Settings are initialized only when user clicks on General/Server/Network section settings
@@ -207,8 +203,8 @@ class ServerManagerView {
silent: false silent: false
}, },
downloadsPath: `${app.getPath('downloads')}`, downloadsPath: `${app.getPath('downloads')}`,
showDownloadFolder: false, quitOnClose: false,
quitOnClose: false promptDownload: false
}; };
// Platform specific settings // Platform specific settings
@@ -228,13 +224,12 @@ class ServerManagerView {
settingOptions.autoHideMenubar = false; settingOptions.autoHideMenubar = false;
} }
for (const i in settingOptions) { for (const [setting, value] of Object.entries(settingOptions)) {
const setting = i as keyof SettingsOptions;
// give preference to defaults defined in global_config.json // give preference to defaults defined in global_config.json
if (EnterpriseUtil.configItemExists(setting)) { if (EnterpriseUtil.configItemExists(setting)) {
ConfigUtil.setConfigItem(setting, EnterpriseUtil.getConfigItem(setting), true); ConfigUtil.setConfigItem(setting, EnterpriseUtil.getConfigItem(setting), true);
} else if (ConfigUtil.getConfigItem(setting) === null) { } else if (ConfigUtil.getConfigItem(setting) === null) {
ConfigUtil.setConfigItem(setting, settingOptions[setting]); ConfigUtil.setConfigItem(setting, value);
} }
} }
} }
@@ -278,16 +273,15 @@ class ServerManagerView {
if (preAddedDomains.length > 0) { if (preAddedDomains.length > 0) {
// user already has servers added // user already has servers added
// ask them before reloading the app // ask them before reloading the app
dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'question', type: 'question',
buttons: ['Yes', 'Later'], buttons: ['Yes', 'Later'],
defaultId: 0, defaultId: 0,
message: 'New server' + (domainsAdded.length > 1 ? 's' : '') + ' added. Reload app now?' message: 'New server' + (domainsAdded.length > 1 ? 's' : '') + ' added. Reload app now?'
}, response => {
if (response === 0) {
ipcRenderer.send('reload-full-app');
}
}); });
if (response === 0) {
ipcRenderer.send('reload-full-app');
}
} else { } else {
ipcRenderer.send('reload-full-app'); ipcRenderer.send('reload-full-app');
} }
@@ -304,7 +298,7 @@ class ServerManagerView {
dialog.showErrorBox(title, content); dialog.showErrorBox(title, content);
if (DomainUtil.getDomains().length === 0) { if (DomainUtil.getDomains().length === 0) {
// no orgs present, stop showing loading gif // no orgs present, stop showing loading gif
this.openSettings('AddServer'); await this.openSettings('AddServer');
} }
} }
} }
@@ -312,8 +306,8 @@ class ServerManagerView {
initTabs(): void { initTabs(): void {
const servers = DomainUtil.getDomains(); const servers = DomainUtil.getDomains();
if (servers.length > 0) { if (servers.length > 0) {
for (let i = 0; i < servers.length; i++) { for (const [i, server] of servers.entries()) {
this.initServer(servers[i], i); this.initServer(server, i);
} }
// Open last active tab // Open last active tab
let lastActiveTab = ConfigUtil.getConfigItem('lastActiveTab'); let lastActiveTab = ConfigUtil.getConfigItem('lastActiveTab');
@@ -323,13 +317,13 @@ class ServerManagerView {
// checkDomain() and webview.load() for lastActiveTab before the others // checkDomain() and webview.load() for lastActiveTab before the others
DomainUtil.updateSavedServer(servers[lastActiveTab].url, lastActiveTab); DomainUtil.updateSavedServer(servers[lastActiveTab].url, lastActiveTab);
this.activateTab(lastActiveTab); this.activateTab(lastActiveTab);
for (let i = 0; i < servers.length; i++) { for (const [i, server] of servers.entries()) {
// after the lastActiveTab is activated, we load the others in the background // after the lastActiveTab is activated, we load the others in the background
// without activating them, to prevent flashing of server icons // without activating them, to prevent flashing of server icons
if (i === lastActiveTab) { if (i === lastActiveTab) {
continue; continue;
} }
DomainUtil.updateSavedServer(servers[i].url, i); DomainUtil.updateSavedServer(server.url, i);
this.tabs[i].webview.load(); this.tabs[i].webview.load();
} }
// Remove focus from the settings icon at sidebar bottom // Remove focus from the settings icon at sidebar bottom
@@ -347,7 +341,7 @@ class ServerManagerView {
this.tabs.push(new ServerTab({ this.tabs.push(new ServerTab({
role: 'server', role: 'server',
icon: server.icon, icon: server.icon,
name: server.alias, name: CommonUtil.decodeString(server.alias),
$root: this.$tabsContainer, $root: this.$tabsContainer,
onClick: this.activateLastTab.bind(this, index), onClick: this.activateLastTab.bind(this, index),
index, index,
@@ -361,6 +355,8 @@ class ServerManagerView {
url: server.url, url: server.url,
role: 'server', role: 'server',
name: CommonUtil.decodeString(server.alias), name: CommonUtil.decodeString(server.alias),
hasPermission: (origin: string, permission: string) =>
origin === server.url && permission === 'notifications',
isActive: () => { isActive: () => {
return index === this.activeTabIndex; return index === this.activeTabIndex;
}, },
@@ -463,7 +459,7 @@ class ServerManagerView {
$altIcon.classList.add('server-icon'); $altIcon.classList.add('server-icon');
$altIcon.classList.add('alt-icon'); $altIcon.classList.add('alt-icon');
$parent.removeChild($img); $img.remove();
$parent.append($altIcon); $parent.append($altIcon);
this.addContextMenu($altIcon as HTMLImageElement, index); this.addContextMenu($altIcon as HTMLImageElement, index);
@@ -552,14 +548,14 @@ class ServerManagerView {
this.activateTab(this.functionalTabs[tabProps.name]); this.activateTab(this.functionalTabs[tabProps.name]);
} }
openSettings(nav = 'General'): void { async openSettings(nav = 'General'): Promise<void> {
this.openFunctionalTab({ this.openFunctionalTab({
name: 'Settings', name: 'Settings',
materialIcon: 'settings', materialIcon: 'settings',
url: `file://${rendererDirectory}/preference.html#${nav}` url: `file://${rendererDirectory}/preference.html#${nav}`
}); });
this.$settingsButton.classList.add('active'); this.$settingsButton.classList.add('active');
this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav); await this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav);
} }
openAbout(): void { openAbout(): void {
@@ -711,10 +707,8 @@ class ServerManagerView {
} }
updateGeneralSettings(setting: string, value: any): void { updateGeneralSettings(setting: string, value: any): void {
const selector = 'webview:not([class*=disabled])'; if (this.getActiveWebview()) {
const webview: Electron.WebviewTag = document.querySelector(selector); const webContents = this.getActiveWebview().getWebContents();
if (webview) {
const webContents = webview.getWebContents();
webContents.send(setting, value); webContents.send(setting, value);
} }
} }
@@ -738,28 +732,33 @@ class ServerManagerView {
return !(url.endsWith('/login/') || this.tabs[tabIndex].webview.loading); return !(url.endsWith('/login/') || this.tabs[tabIndex].webview.loading);
} }
getActiveWebview(): Electron.WebviewTag {
const selector = 'webview:not(.disabled)';
const webview: Electron.WebviewTag = document.querySelector(selector);
return webview;
}
addContextMenu($serverImg: HTMLImageElement, index: number): void { addContextMenu($serverImg: HTMLImageElement, index: number): void {
$serverImg.addEventListener('contextmenu', e => { $serverImg.addEventListener('contextmenu', event => {
e.preventDefault(); event.preventDefault();
const template = [ const template = [
{ {
label: 'Disconnect organization', label: 'Disconnect organization',
click: () => { click: async () => {
dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'warning', type: 'warning',
buttons: ['YES', 'NO'], buttons: ['YES', 'NO'],
defaultId: 0, defaultId: 0,
message: 'Are you sure you want to disconnect this organization?' message: 'Are you sure you want to disconnect this organization?'
}, response => {
if (response === 0) {
if (DomainUtil.removeDomain(index)) {
ipcRenderer.send('reload-full-app');
} else {
const { title, content } = Messages.orgRemovalError(DomainUtil.getDomain(index).url);
dialog.showErrorBox(title, content);
}
}
}); });
if (response === 0) {
if (DomainUtil.removeDomain(index)) {
ipcRenderer.send('reload-full-app');
} else {
const { title, content } = Messages.orgRemovalError(DomainUtil.getDomain(index).url);
dialog.showErrorBox(title, content);
}
}
} }
}, },
{ {
@@ -797,15 +796,38 @@ class ServerManagerView {
'tab-devtools': 'openDevTools' 'tab-devtools': 'openDevTools'
}; };
for (const key in webviewListeners) { for (const [key, method] of Object.entries(webviewListeners)) {
ipcRenderer.on(key, () => { ipcRenderer.on(key, () => {
const activeWebview = this.tabs[this.activeTabIndex].webview; const activeWebview = this.tabs[this.activeTabIndex].webview;
if (activeWebview) { if (activeWebview) {
activeWebview[webviewListeners[key] as string](); activeWebview[method]();
} }
}); });
} }
ipcRenderer.on('permission-request', (
event: Event,
permissionId: number,
{webContentsId, origin, permission}: {
webContentsId: number | null;
origin: string;
permission: string;
}
) => {
const grant = webContentsId === null ?
origin === 'null' && permission === 'notifications' :
this.tabs.some(
({webview}) =>
webview.$el.getWebContentsId() === webContentsId &&
webview.props.hasPermission?.(origin, permission)
);
console.log(
grant ? 'Granted' : 'Denied', 'permissions request for',
permission, 'from', origin
);
ipcRenderer.send('permission-response', permissionId, grant);
});
ipcRenderer.on('show-network-error', (event: Event, index: number) => { ipcRenderer.on('show-network-error', (event: Event, index: number) => {
this.openNetworkTroubleshooting(index); this.openNetworkTroubleshooting(index);
}); });
@@ -818,8 +840,7 @@ class ServerManagerView {
ipcRenderer.on('open-help', () => { ipcRenderer.on('open-help', () => {
// Open help page of current active server // Open help page of current active server
const helpPage = this.getCurrentActiveServer() + '/help'; LinkUtil.openBrowser(new URL('/help', this.getCurrentActiveServer()));
shell.openExternal(helpPage);
}); });
ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index)); ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index));
@@ -842,13 +863,12 @@ class ServerManagerView {
this.openSettings('AddServer'); this.openSettings('AddServer');
}); });
ipcRenderer.on('reload-proxy', (event: Event, showAlert: boolean) => { ipcRenderer.on('reload-proxy', async (event: Event, showAlert: boolean) => {
this.loadProxy().then(() => { await this.loadProxy();
if (showAlert) { if (showAlert) {
alert('Proxy settings saved!'); alert('Proxy settings saved!');
ipcRenderer.send('reload-full-app'); ipcRenderer.send('reload-full-app');
} }
});
}); });
ipcRenderer.on('toggle-sidebar', (event: Event, show: boolean) => { ipcRenderer.on('toggle-sidebar', (event: Event, show: boolean) => {
@@ -887,9 +907,7 @@ class ServerManagerView {
ipcRenderer.on('toggle-dnd', (event: Event, state: boolean, newSettings: SettingsOptions) => { ipcRenderer.on('toggle-dnd', (event: Event, state: boolean, newSettings: SettingsOptions) => {
this.toggleDNDButton(state); this.toggleDNDButton(state);
ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent); ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent);
const selector = 'webview:not([class*=disabled])'; const webContents = this.getActiveWebview().getWebContents();
const webview: Electron.WebviewTag = document.querySelector(selector);
const webContents = webview.getWebContents();
webContents.send('toggle-dnd', state, newSettings); webContents.send('toggle-dnd', state, newSettings);
}); });
@@ -897,7 +915,7 @@ class ServerManagerView {
// TODO: TypeScript - Type annotate getDomains() or this domain paramter. // TODO: TypeScript - Type annotate getDomains() or this domain paramter.
DomainUtil.getDomains().forEach((domain: any, index: number) => { DomainUtil.getDomains().forEach((domain: any, index: number) => {
if (domain.url.includes(serverURL)) { if (domain.url.includes(serverURL)) {
const serverTooltipSelector = `.tab .server-tooltip`; const serverTooltipSelector = '.tab .server-tooltip';
const serverTooltips = document.querySelectorAll(serverTooltipSelector); const serverTooltips = document.querySelectorAll(serverTooltipSelector);
serverTooltips[index].innerHTML = escape(realmName); serverTooltips[index].innerHTML = escape(realmName);
this.tabs[index].props.name = escape(realmName); this.tabs[index].props.name = escape(realmName);
@@ -916,18 +934,18 @@ class ServerManagerView {
}); });
ipcRenderer.on('update-realm-icon', (event: Event, serverURL: string, iconURL: string) => { ipcRenderer.on('update-realm-icon', (event: Event, serverURL: string, iconURL: string) => {
// TODO: TypeScript - Type annotate getDomains() or this domain paramter. DomainUtil.getDomains().forEach(async (domain, index) => {
DomainUtil.getDomains().forEach((domain: any, index: number) => {
if (domain.url.includes(serverURL)) { if (domain.url.includes(serverURL)) {
DomainUtil.saveServerIcon(iconURL).then((localIconUrl: string) => { const localIconUrl: string = await DomainUtil.saveServerIcon({
const serverImgsSelector = `.tab .server-icons`; url: serverURL,
const serverImgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(serverImgsSelector); icon: iconURL
serverImgs[index].src = localIconUrl;
domain.icon = localIconUrl;
DomainUtil.db.push(`/domains[${index}]`, domain, true);
DomainUtil.reloadDB();
}); });
const serverImgsSelector = '.tab .server-icons';
const serverImgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(serverImgsSelector);
serverImgs[index].src = localIconUrl;
domain.icon = localIconUrl;
DomainUtil.db.push(`/domains[${index}]`, domain, true);
DomainUtil.reloadDB();
} }
}); });
}); });
@@ -994,6 +1012,15 @@ class ServerManagerView {
this.openSettings('AddServer'); this.openSettings('AddServer');
}); });
// Redo and undo functionality since the default API doesn't work on macOS
ipcRenderer.on('undo', () => {
return this.getActiveWebview().undo();
});
ipcRenderer.on('redo', () => {
return this.getActiveWebview().redo();
});
ipcRenderer.on('set-active', () => { ipcRenderer.on('set-active', () => {
const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview'); const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => { webviews.forEach(webview => {
@@ -1027,4 +1054,4 @@ window.addEventListener('load', () => {
} }
}); });
export = new ServerManagerView(); export {};

View File

@@ -1,19 +1,17 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { import {
appId, customReply, focusCurrentServer, parseReply, setupReply appId, customReply, focusCurrentServer, parseReply
} from './helpers'; } from './helpers';
import url = require('url'); import MacNotifier from 'node-mac-notifier';
import MacNotifier = require('node-mac-notifier'); import * as ConfigUtil from '../utils/config-util';
import ConfigUtil = require('../utils/config-util'); import electron_bridge from '../electron-bridge';
type ReplyHandler = (response: string) => void; type ReplyHandler = (response: string) => void;
type ClickHandler = () => void; type ClickHandler = () => void;
let replyHandler: ReplyHandler; let replyHandler: ReplyHandler;
let clickHandler: ClickHandler; let clickHandler: ClickHandler;
declare const window: ZulipWebWindow;
interface NotificationHandlerArgs { interface NotificationHandlerArgs {
response: string; response: string;
} }
@@ -21,14 +19,13 @@ interface NotificationHandlerArgs {
class DarwinNotification { class DarwinNotification {
tag: string; tag: string;
constructor(title: string, opts: NotificationOptions) { constructor(title: string, options: NotificationOptions) {
const silent: boolean = ConfigUtil.getConfigItem('silent') || false; const silent: boolean = ConfigUtil.getConfigItem('silent') || false;
const { host, protocol } = location; const { icon } = options;
const { icon } = opts; const profilePic = new URL(icon, location.href).href;
const profilePic = url.resolve(`${protocol}//${host}`, icon);
this.tag = opts.tag; this.tag = options.tag;
const notification = new MacNotifier(title, Object.assign(opts, { const notification = new MacNotifier(title, Object.assign(options, {
bundleId: appId, bundleId: appId,
canReply: true, canReply: true,
silent, silent,
@@ -58,22 +55,22 @@ class DarwinNotification {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied'; return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
} }
set onreply(handler: ReplyHandler) {
replyHandler = handler;
}
get onreply(): ReplyHandler { get onreply(): ReplyHandler {
return replyHandler; return replyHandler;
} }
set onclick(handler: ClickHandler) { set onreply(handler: ReplyHandler) {
clickHandler = handler; replyHandler = handler;
} }
get onclick(): ClickHandler { get onclick(): ClickHandler {
return clickHandler; return clickHandler;
} }
set onclick(handler: ClickHandler) {
clickHandler = handler;
}
// not something that is common or // not something that is common or
// used by zulip server but added to be // used by zulip server but added to be
// future proff. // future proff.
@@ -87,15 +84,15 @@ class DarwinNotification {
} }
} }
notificationHandler({ response }: NotificationHandlerArgs): void { async notificationHandler({ response }: NotificationHandlerArgs): Promise<void> {
response = parseReply(response); response = await parseReply(response);
focusCurrentServer(); focusCurrentServer();
if (window.electron_bridge.send_notification_reply_message_supported) { if (electron_bridge.send_notification_reply_message_supported) {
window.electron_bridge.send_event('send_notification_reply_message', this.tag, response); electron_bridge.send_event('send_notification_reply_message', this.tag, response);
return; return;
} }
setupReply(this.tag); electron_bridge.emit('narrow-by-topic', this.tag);
if (replyHandler) { if (replyHandler) {
replyHandler(response); replyHandler(response);
return; return;

View File

@@ -1,15 +1,13 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { focusCurrentServer } from './helpers'; import { focusCurrentServer } from './helpers';
import ConfigUtil = require('../utils/config-util'); import * as ConfigUtil from '../utils/config-util';
const NativeNotification = window.Notification; const NativeNotification = window.Notification;
class BaseNotification extends NativeNotification { export default class BaseNotification extends NativeNotification {
constructor(title: string, opts: NotificationOptions) { constructor(title: string, options: NotificationOptions) {
opts.silent = true; options.silent = true;
super(title, opts); super(title, options);
this.addEventListener('click', () => { this.addEventListener('click', () => {
// focus to the server who sent the // focus to the server who sent the
@@ -19,8 +17,8 @@ class BaseNotification extends NativeNotification {
}); });
} }
static requestPermission(): void { static async requestPermission(): Promise<NotificationPermission> {
return; // eslint-disable-line no-useless-return return this.permission;
} }
// Override default Notification permission // Override default Notification permission
@@ -28,5 +26,3 @@ class BaseNotification extends NativeNotification {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied'; return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
} }
} }
export = BaseNotification;

View File

@@ -1,6 +1,6 @@
import { remote } from 'electron'; import { remote } from 'electron';
import Logger = require('../utils/logger-util'); import Logger from '../utils/logger-util';
const logger = new Logger({ const logger = new Logger({
file: 'errors.log', file: 'errors.log',
@@ -15,32 +15,23 @@ const botsList: BotListItem[] = [];
let botsListLoaded = false; let botsListLoaded = false;
// this function load list of bots from the server // this function load list of bots from the server
// sync=True for a synchronous getJSON request
// in case botsList isn't already completely loaded when required in parseRely // in case botsList isn't already completely loaded when required in parseRely
export function loadBots(sync = false): void { export async function loadBots(): Promise<void> {
const { $ } = window;
botsList.length = 0; botsList.length = 0;
if (sync) { const response = await fetch('/json/users');
$.ajaxSetup({async: false}); if (response.ok) {
} const { members } = await response.json();
$.getJSON('/json/users') members.forEach((membersRow: any) => {
.done((data: any) => { if (membersRow.is_bot) {
const { members } = data; const bot = `@${membersRow.full_name}`;
members.forEach((membersRow: any) => { const mention = `@**${bot.replace(/^@/, '')}**`;
if (membersRow.is_bot) { botsList.push([bot, mention]);
const bot = `@${membersRow.full_name}`; }
const mention = `@**${bot.replace(/^@/, '')}**`;
botsList.push([bot, mention]);
}
});
botsListLoaded = true;
})
.fail((error: any) => {
logger.log('Load bots request failed: ', error.responseText);
logger.log('Load bots request status: ', error.statusText);
}); });
if (sync) { botsListLoaded = true;
$.ajaxSetup({async: true}); } else {
logger.log('Load bots request failed: ', await response.text());
logger.log('Load bots request status: ', response.status);
} }
} }
@@ -80,16 +71,14 @@ const webContentsId = webContents.id;
// this function will focus the server that sent // this function will focus the server that sent
// the notification. Main function implemented in main.js // the notification. Main function implemented in main.js
export function focusCurrentServer(): void { export function focusCurrentServer(): void {
// TODO: TypeScript: currentWindow of type BrowserWindow doesn't currentWindow.webContents.send('focus-webview-with-id', webContentsId);
// have a .send() property per typescript.
(currentWindow as any).send('focus-webview-with-id', webContentsId);
} }
// this function parses the reply from to notification // this function parses the reply from to notification
// making it easier to reply from notification eg // making it easier to reply from notification eg
// @username in reply will be converted to @**username** // @username in reply will be converted to @**username**
// #stream in reply will be converted to #**stream** // #stream in reply will be converted to #**stream**
// bot mentions are not yet supported // bot mentions are not yet supported
export function parseReply(reply: string): string { export async function parseReply(reply: string): Promise<string> {
const usersDiv = document.querySelectorAll('#user_presences li'); const usersDiv = document.querySelectorAll('#user_presences li');
const streamHolder = document.querySelectorAll('#stream_filters li'); const streamHolder = document.querySelectorAll('#stream_filters li');
@@ -128,9 +117,9 @@ export function parseReply(reply: string): string {
reply = reply.replace(regex, streamMention); reply = reply.replace(regex, streamMention);
}); });
// If botsList isn't completely loaded yet, make a synchronous getJSON request for list // If botsList isn't completely loaded yet, make a request for list
if (botsListLoaded === false) { if (!botsListLoaded) {
loadBots(true); await loadBots();
} }
// Iterate for every bot name and replace in reply // Iterate for every bot name and replace in reply
@@ -145,9 +134,3 @@ export function parseReply(reply: string): string {
reply = reply.replace(/\\n/, '\n'); reply = reply.replace(/\\n/, '\n');
return reply; return reply;
} }
export function setupReply(id: string): void {
const { narrow } = window;
const narrowByTopic = narrow.by_topic || narrow.by_subject;
narrowByTopic(id, { trigger: 'notification' });
}

View File

@@ -1,25 +1,72 @@
'use strict';
import { remote } from 'electron'; import { remote } from 'electron';
import * as params from '../utils/params-util'; import electron_bridge from '../electron-bridge';
import { appId, loadBots } from './helpers'; import { appId, loadBots } from './helpers';
import DefaultNotification = require('./default-notification'); import DefaultNotification from './default-notification';
const { app } = remote; const { app } = remote;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid // From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work. // On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
app.setAppUserModelId(appId); app.setAppUserModelId(appId);
window.Notification = DefaultNotification; let Notification = DefaultNotification;
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
window.Notification = require('./darwin-notifications'); Notification = require('./darwin-notifications');
} }
window.addEventListener('load', () => { export interface NotificationData {
// eslint-disable-next-line no-undef, @typescript-eslint/camelcase close(): void;
if (params.isPageParams() && page_params.realm_uri) { title: string;
loadBots(); dir: NotificationDirection;
lang: string;
body: string;
tag: string;
image: string;
icon: string;
badge: string;
vibrate: readonly number[];
timestamp: number;
renotify: boolean;
silent: boolean;
requireInteraction: boolean;
data: unknown;
actions: readonly NotificationAction[];
}
export function newNotification(
title: string,
options: NotificationOptions | undefined,
dispatch: (type: string, eventInit: EventInit) => boolean
): NotificationData {
const notification = new Notification(title, options);
for (const type of ['click', 'close', 'error', 'show']) {
notification.addEventListener(type, (ev: Event) => {
if (!dispatch(type, ev)) {
ev.preventDefault();
}
});
} }
return {
close: () => notification.close(),
title: notification.title,
dir: notification.dir,
lang: notification.lang,
body: notification.body,
tag: notification.tag,
image: notification.image,
icon: notification.icon,
badge: notification.badge,
vibrate: notification.vibrate,
timestamp: notification.timestamp,
renotify: notification.renotify,
silent: notification.silent,
requireInteraction: notification.requireInteraction,
data: notification.data,
actions: notification.actions
};
}
electron_bridge.once('zulip-loaded', () => {
loadBots();
}); });

View File

@@ -1,16 +1,10 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
class NetworkTroubleshootingView { export function init($reconnectButton: Element, $settingsButton: Element): void {
init($reconnectButton: Element, $settingsButton: Element): void { $reconnectButton.addEventListener('click', () => {
$reconnectButton.addEventListener('click', () => { ipcRenderer.send('forward-message', 'reload-viewer');
ipcRenderer.send('forward-message', 'reload-viewer'); });
}); $settingsButton.addEventListener('click', () => {
$settingsButton.addEventListener('click', () => { ipcRenderer.send('forward-message', 'open-settings');
ipcRenderer.send('forward-message', 'open-settings'); });
});
}
} }
export = new NetworkTroubleshootingView();

View File

@@ -2,15 +2,15 @@
import { remote, OpenDialogOptions } from 'electron'; import { remote, OpenDialogOptions } from 'electron';
import path = require('path'); import path from 'path';
import BaseComponent = require('../../components/base'); import BaseComponent from '../../components/base';
import CertificateUtil = require('../../utils/certificate-util'); import * as CertificateUtil from '../../utils/certificate-util';
import DomainUtil = require('../../utils/domain-util'); import * as DomainUtil from '../../utils/domain-util';
import t = require('../../utils/translation-util'); import * as t from '../../utils/translation-util';
const { dialog } = remote; const { dialog } = remote;
class AddCertificate extends BaseComponent { export default class AddCertificate extends BaseComponent {
// TODO: TypeScript - Here props should be object type // TODO: TypeScript - Here props should be object type
props: any; props: any;
_certFile: string; _certFile: string;
@@ -59,7 +59,7 @@ class AddCertificate extends BaseComponent {
CertificateUtil.setCertificate(server, fileName); CertificateUtil.setCertificate(server, fileName);
dialog.showMessageBox({ dialog.showMessageBox({
title: 'Success', title: 'Success',
message: `Certificate saved!` message: 'Certificate saved!'
}); });
this.serverUrl.value = ''; this.serverUrl.value = '';
} else { } else {
@@ -68,18 +68,17 @@ class AddCertificate extends BaseComponent {
} }
} }
addHandler(): void { async addHandler(): Promise<void> {
const showDialogOptions: OpenDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: 'Select file', title: 'Select file',
properties: ['openFile'], properties: ['openFile'],
filters: [{ name: 'crt, pem', extensions: ['crt', 'pem'] }] filters: [{ name: 'crt, pem', extensions: ['crt', 'pem'] }]
}; };
dialog.showOpenDialog(showDialogOptions, selectedFile => { const { filePaths, canceled } = await dialog.showOpenDialog(showDialogOptions);
if (selectedFile) { if (!canceled) {
this._certFile = selectedFile[0] || ''; this._certFile = filePaths[0] || '';
this.validateAndAdd(); this.validateAndAdd();
} }
});
} }
initListeners(): void { initListeners(): void {
@@ -88,13 +87,9 @@ class AddCertificate extends BaseComponent {
}); });
this.serverUrl.addEventListener('keypress', event => { this.serverUrl.addEventListener('keypress', event => {
const EnterkeyCode = event.keyCode; if (event.key === 'Enter') {
if (EnterkeyCode === 13) {
this.addHandler(); this.addHandler();
} }
}); });
} }
} }
export = AddCertificate;

View File

@@ -1,63 +1,43 @@
'use strict'; import electron, { app } from 'electron';
import { app } from 'electron';
import electron = require('electron'); import * as ConfigUtil from '../../utils/config-util';
import ConfigUtil = require('../../utils/config-util');
let instance: BadgeSettings | any = null; function showBadgeCount(messageCount: number, mainWindow: electron.BrowserWindow): void {
if (process.platform === 'win32') {
class BadgeSettings { updateOverlayIcon(messageCount, mainWindow);
constructor() { } else {
if (instance) { app.badgeCount = messageCount;
return instance;
} else {
instance = this;
}
return instance;
}
showBadgeCount(messageCount: number, mainWindow: electron.BrowserWindow): void {
if (process.platform === 'win32') {
this.updateOverlayIcon(messageCount, mainWindow);
} else {
// This should work on both macOS and Linux
app.setBadgeCount(messageCount);
}
}
hideBadgeCount(mainWindow: electron.BrowserWindow): void {
if (process.platform === 'darwin') {
app.setBadgeCount(0);
}
if (process.platform === 'win32') {
mainWindow.setOverlayIcon(null, '');
}
}
updateBadge(badgeCount: number, mainWindow: electron.BrowserWindow): void {
if (ConfigUtil.getConfigItem('badgeOption', true)) {
this.showBadgeCount(badgeCount, mainWindow);
} else {
this.hideBadgeCount(mainWindow);
}
}
updateOverlayIcon(messageCount: number, mainWindow: electron.BrowserWindow): void {
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
}
if (messageCount === 0) {
mainWindow.setOverlayIcon(null, '');
} else {
mainWindow.webContents.send('render-taskbar-icon', messageCount);
}
}
updateTaskbarIcon(data: string, text: string, mainWindow: electron.BrowserWindow): void {
const img = electron.nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
} }
} }
export = new BadgeSettings(); function hideBadgeCount(mainWindow: electron.BrowserWindow): void {
if (process.platform === 'win32') {
mainWindow.setOverlayIcon(null, '');
} else {
app.badgeCount = 0;
}
}
export function updateBadge(badgeCount: number, mainWindow: electron.BrowserWindow): void {
if (ConfigUtil.getConfigItem('badgeOption', true)) {
showBadgeCount(badgeCount, mainWindow);
} else {
hideBadgeCount(mainWindow);
}
}
function updateOverlayIcon(messageCount: number, mainWindow: electron.BrowserWindow): void {
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
}
if (messageCount === 0) {
mainWindow.setOverlayIcon(null, '');
} else {
mainWindow.webContents.send('render-taskbar-icon', messageCount);
}
}
export function updateTaskbarIcon(data: string, text: string, mainWindow: electron.BrowserWindow): void {
const img = electron.nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
}

View File

@@ -1,10 +1,8 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import BaseComponent = require('../../components/base'); import BaseComponent from '../../components/base';
class BaseSection extends BaseComponent { export default class BaseSection extends BaseComponent {
// TODO: TypeScript - Here props should be object type // TODO: TypeScript - Here props should be object type
generateSettingOption(props: any): void { generateSettingOption(props: any): void {
const {$element, disabled, value, clickHandler} = props; const {$element, disabled, value, clickHandler} = props;
@@ -20,7 +18,7 @@ class BaseSection extends BaseComponent {
} }
generateOptionTemplate(settingOption: boolean, disabled: boolean): string { generateOptionTemplate(settingOption: boolean, disabled: boolean): string {
const label = disabled ? `<label class="disallowed" title="Setting locked by system administrator."/>` : `<label/>`; const label = disabled ? '<label class="disallowed" title="Setting locked by system administrator."/>' : '<label/>';
if (settingOption) { if (settingOption) {
return ` return `
<div class="action"> <div class="action">
@@ -46,5 +44,3 @@ class BaseSection extends BaseComponent {
ipcRenderer.send('forward-message', 'reload-viewer'); ipcRenderer.send('forward-message', 'reload-viewer');
} }
} }
export = BaseSection;

View File

@@ -1,15 +1,13 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import BaseSection = require('./base-section'); import BaseSection from './base-section';
import DomainUtil = require('../../utils/domain-util'); import * as DomainUtil from '../../utils/domain-util';
import ServerInfoForm = require('./server-info-form'); import ServerInfoForm from './server-info-form';
import AddCertificate = require('./add-certificate'); import AddCertificate from './add-certificate';
import FindAccounts = require('./find-accounts'); import FindAccounts from './find-accounts';
import t = require('../../utils/translation-util'); import * as t from '../../utils/translation-util';
class ConnectedOrgSection extends BaseSection { export default class ConnectedOrgSection extends BaseSection {
// TODO: TypeScript - Here props should be object type // TODO: TypeScript - Here props should be object type
props: any; props: any;
$serverInfoContainer: Element | null; $serverInfoContainer: Element | null;
@@ -57,10 +55,10 @@ class ConnectedOrgSection extends BaseSection {
// Show noServerText if no servers are there otherwise hide it // Show noServerText if no servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? noServerText : ''; this.$existingServers.innerHTML = servers.length === 0 ? noServerText : '';
for (let i = 0; i < servers.length; i++) { for (const [i, server] of servers.entries()) {
new ServerInfoForm({ new ServerInfoForm({
$root: this.$serverInfoContainer, $root: this.$serverInfoContainer,
server: servers[i], server,
index: i, index: i,
onChange: this.reloadApp onChange: this.reloadApp
}).init(); }).init();
@@ -86,5 +84,3 @@ class ConnectedOrgSection extends BaseSection {
}).init(); }).init();
} }
} }
export = ConnectedOrgSection;

View File

@@ -1,11 +1,10 @@
'use-strict'; 'use-strict';
import { shell } from 'electron'; import BaseComponent from '../../components/base';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
import BaseComponent = require('../../components/base'); export default class FindAccounts extends BaseComponent {
import t = require('../../utils/translation-util');
class FindAccounts extends BaseComponent {
// TODO: TypeScript - Here props should be object type // TODO: TypeScript - Here props should be object type
props: any; props: any;
$findAccounts: Element | null; $findAccounts: Element | null;
@@ -45,7 +44,7 @@ class FindAccounts extends BaseComponent {
if (!url.startsWith('http')) { if (!url.startsWith('http')) {
url = 'https://' + url; url = 'https://' + url;
} }
shell.openExternal(url + '/accounts/find'); LinkUtil.openBrowser(new URL('/accounts/find', url));
} }
initListeners(): void { initListeners(): void {
@@ -60,7 +59,7 @@ class FindAccounts extends BaseComponent {
}); });
this.$serverUrlField.addEventListener('keypress', event => { this.$serverUrlField.addEventListener('keypress', event => {
if (event.keyCode === 13) { if (event.key === 'Enter') {
this.findAccounts(this.$serverUrlField.value); this.findAccounts(this.$serverUrlField.value);
} }
}); });
@@ -74,5 +73,3 @@ class FindAccounts extends BaseComponent {
}); });
} }
} }
export = FindAccounts;

View File

@@ -1,18 +1,17 @@
'use strict';
import { ipcRenderer, remote, OpenDialogOptions } from 'electron'; import { ipcRenderer, remote, OpenDialogOptions } from 'electron';
import path = require('path'); import path from 'path';
import fs = require('fs-extra'); import fs from 'fs-extra';
const { app, dialog } = remote; const { app, dialog } = remote;
const currentBrowserWindow = remote.getCurrentWindow(); const currentBrowserWindow = remote.getCurrentWindow();
import BaseSection = require('./base-section'); import BaseSection from './base-section';
import ConfigUtil = require('../../utils/config-util'); import * as ConfigUtil from '../../utils/config-util';
import EnterpriseUtil = require('./../../utils/enterprise-util'); import * as EnterpriseUtil from '../../utils/enterprise-util';
import t = require('../../utils/translation-util'); import * as t from '../../utils/translation-util';
class GeneralSection extends BaseSection { export default class GeneralSection extends BaseSection {
// TODO: TypeScript - Here props should be object type // TODO: TypeScript - Here props should be object type
props: any; props: any;
constructor(props: any) { constructor(props: any) {
@@ -93,29 +92,25 @@ class GeneralSection extends BaseSection {
</div> </div>
<div class="title">${t.__('Advanced')}</div> <div class="title">${t.__('Advanced')}</div>
<div class="settings-card"> <div class="settings-card">
<div class="setting-row" id="enable-error-reporting"> <div class="setting-row" id="enable-error-reporting">
<div class="setting-description">${t.__('Enable error reporting (requires restart)')}</div> <div class="setting-description">${t.__('Enable error reporting (requires restart)')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="show-download-folder"> <div class="setting-row" id="add-custom-css">
<div class="setting-description">${t.__('Show downloaded files in file manager')}</div> <div class="setting-description">
<div class="setting-control"></div> ${t.__('Add custom CSS')}
</div> </div>
<div class="setting-row" id="add-custom-css"> <button class="custom-css-button green">${t.__('Upload')}</button>
<div class="setting-description"> </div>
${t.__('Add custom CSS')} <div class="setting-row" id="remove-custom-css">
</div> <div class="setting-description">
<button class="custom-css-button green">${t.__('Upload')}</button> <div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div>
</div> </div>
<div class="setting-row" id="remove-custom-css"> <div class="action red" id="css-delete-action">
<div class="setting-description"> <i class="material-icons">indeterminate_check_box</i>
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div> <span>${t.__('Delete')}</span>
</div> </div>
<div class="action red" id="css-delete-action"> </div>
<i class="material-icons">indeterminate_check_box</i>
<span>${t.__('Delete')}</span>
</div>
</div>
<div class="setting-row" id="download-folder"> <div class="setting-row" id="download-folder">
<div class="setting-description"> <div class="setting-description">
${t.__('Default download location')} ${t.__('Default download location')}
@@ -127,7 +122,10 @@ class GeneralSection extends BaseSection {
<div class="download-folder-path">${ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`)}</div> <div class="download-folder-path">${ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`)}</div>
</div> </div>
</div> </div>
<div class="setting-row" id="prompt-download">
<div class="setting-description">${t.__('Ask where to save files before downloading')}</div>
<div class="setting-control"></div>
</div>
</div> </div>
<div class="title">${t.__('Reset Application Data')}</div> <div class="title">${t.__('Reset Application Data')}</div>
<div class="settings-card"> <div class="settings-card">
@@ -158,8 +156,8 @@ class GeneralSection extends BaseSection {
this.showCustomCSSPath(); this.showCustomCSSPath();
this.removeCustomCSS(); this.removeCustomCSS();
this.downloadFolder(); this.downloadFolder();
this.showDownloadFolder();
this.updateQuitOnCloseOption(); this.updateQuitOnCloseOption();
this.updatePromptDownloadOption();
this.enableErrorReporting(); this.enableErrorReporting();
// Platform specific settings // Platform specific settings
@@ -281,9 +279,7 @@ class GeneralSection extends BaseSection {
const newValue = !ConfigUtil.getConfigItem('silent', true); const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue); ConfigUtil.setConfigItem('silent', newValue);
this.updateSilentOption(); this.updateSilentOption();
// TODO: TypeScript: currentWindow of type BrowserWindow doesn't currentBrowserWindow.webContents.send('toggle-silent', newValue);
// have a .send() property per typescript.
(currentBrowserWindow as any).send('toggle-silent', newValue);
} }
}); });
} }
@@ -362,37 +358,35 @@ class GeneralSection extends BaseSection {
}); });
} }
clearAppDataDialog(): void { async clearAppDataDialog(): Promise<void> {
const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.'; const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.';
const getAppPath = path.join(app.getPath('appData'), app.getName()); const getAppPath = path.join(app.getPath('appData'), app.name);
dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'warning', type: 'warning',
buttons: ['YES', 'NO'], buttons: ['YES', 'NO'],
defaultId: 0, defaultId: 0,
message: 'Are you sure', message: 'Are you sure',
detail: clearAppDataMessage detail: clearAppDataMessage
}, response => {
if (response === 0) {
fs.remove(getAppPath);
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000);
}
}); });
if (response === 0) {
fs.remove(getAppPath);
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000);
}
} }
customCssDialog(): void { async customCssDialog(): Promise<void> {
const showDialogOptions: OpenDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: 'Select file', title: 'Select file',
properties: ['openFile'], properties: ['openFile'],
filters: [{ name: 'CSS file', extensions: ['css'] }] filters: [{ name: 'CSS file', extensions: ['css'] }]
}; };
dialog.showOpenDialog(showDialogOptions, selectedFile => { const { filePaths, canceled } = await dialog.showOpenDialog(showDialogOptions);
if (selectedFile) { if (!canceled) {
ConfigUtil.setConfigItem('customCSS', selectedFile[0]); ConfigUtil.setConfigItem('customCSS', filePaths[0]);
ipcRenderer.send('forward-message', 'hard-reload'); ipcRenderer.send('forward-message', 'hard-reload');
} }
});
} }
updateResetDataOption(): void { updateResetDataOption(): void {
@@ -431,25 +425,25 @@ class GeneralSection extends BaseSection {
removeCustomCSS(): void { removeCustomCSS(): void {
const removeCSSButton = document.querySelector('#css-delete-action'); const removeCSSButton = document.querySelector('#css-delete-action');
removeCSSButton.addEventListener('click', () => { removeCSSButton.addEventListener('click', () => {
ConfigUtil.setConfigItem('customCSS', ""); ConfigUtil.setConfigItem('customCSS', '');
ipcRenderer.send('forward-message', 'hard-reload'); ipcRenderer.send('forward-message', 'hard-reload');
}); });
} }
downloadFolderDialog(): void { async downloadFolderDialog(): Promise<void> {
const showDialogOptions: OpenDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: 'Select Download Location', title: 'Select Download Location',
properties: ['openDirectory'] properties: ['openDirectory']
}; };
dialog.showOpenDialog(showDialogOptions, selectedFolder => { const { filePaths, canceled } = await dialog.showOpenDialog(showDialogOptions);
if (selectedFolder) { if (!canceled) {
ConfigUtil.setConfigItem('downloadsPath', selectedFolder[0]); ConfigUtil.setConfigItem('downloadsPath', filePaths[0]);
const downloadFolderPath: HTMLElement = document.querySelector('.download-folder-path'); const downloadFolderPath: HTMLElement = document.querySelector('.download-folder-path');
downloadFolderPath.innerText = selectedFolder[0]; downloadFolderPath.textContent = filePaths[0];
} }
});
} }
downloadFolder(): void { downloadFolder(): void {
const downloadFolder = document.querySelector('#download-folder .download-folder-button'); const downloadFolder = document.querySelector('#download-folder .download-folder-button');
downloadFolder.addEventListener('click', () => { downloadFolder.addEventListener('click', () => {
@@ -457,17 +451,15 @@ class GeneralSection extends BaseSection {
}); });
} }
showDownloadFolder(): void { updatePromptDownloadOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#show-download-folder .setting-control'), $element: document.querySelector('#prompt-download .setting-control'),
value: ConfigUtil.getConfigItem('showDownloadFolder', false), value: ConfigUtil.getConfigItem('promptDownload', false),
clickHandler: () => { clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('showDownloadFolder'); const newValue = !ConfigUtil.getConfigItem('promptDownload');
ConfigUtil.setConfigItem('showDownloadFolder', newValue); ConfigUtil.setConfigItem('promptDownload', newValue);
this.showDownloadFolder(); this.updatePromptDownloadOption();
} }
}); });
} }
} }
export = GeneralSection;

View File

@@ -1,9 +1,7 @@
'use strict'; import BaseComponent from '../../components/base';
import * as t from '../../utils/translation-util';
import BaseComponent = require('../../components/base'); export default class PreferenceNav extends BaseComponent {
import t = require('../../utils/translation-util');
class PreferenceNav extends BaseComponent {
// TODO: TypeScript - Here props should be object type // TODO: TypeScript - Here props should be object type
props: any; props: any;
navItems: string[]; navItems: string[];
@@ -64,5 +62,3 @@ class PreferenceNav extends BaseComponent {
$item.classList.remove('active'); $item.classList.remove('active');
} }
} }
export = PreferenceNav;

View File

@@ -1,12 +1,10 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import BaseSection = require('./base-section'); import BaseSection from './base-section';
import ConfigUtil = require('../../utils/config-util'); import * as ConfigUtil from '../../utils/config-util';
import t = require('../../utils/translation-util'); import * as t from '../../utils/translation-util';
class NetworkSection extends BaseSection { export default class NetworkSection extends BaseSection {
// TODO: TypeScript - Here props should be object type // TODO: TypeScript - Here props should be object type
props: any; props: any;
$proxyPAC: HTMLInputElement; $proxyPAC: HTMLInputElement;
@@ -104,7 +102,7 @@ class NetworkSection extends BaseSection {
ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue); ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue);
this.toggleManualProxySettings(!manualProxyValue); this.toggleManualProxySettings(!manualProxyValue);
} }
if (newValue === false) { if (!newValue) {
// Remove proxy system proxy settings // Remove proxy system proxy settings
ConfigUtil.setConfigItem('proxyRules', ''); ConfigUtil.setConfigItem('proxyRules', '');
ipcRenderer.send('forward-message', 'reload-proxy', false); ipcRenderer.send('forward-message', 'reload-proxy', false);
@@ -132,5 +130,3 @@ class NetworkSection extends BaseSection {
}); });
} }
} }
export = NetworkSection;

View File

@@ -1,12 +1,11 @@
'use strict'; import { ipcRenderer } from 'electron';
import { shell, ipcRenderer } from 'electron'; import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
import BaseComponent = require('../../components/base'); export default class NewServerForm extends BaseComponent {
import DomainUtil = require('../../utils/domain-util');
import t = require('../../utils/translation-util');
class NewServerForm extends BaseComponent {
// TODO: TypeScript - Here props should be object type // TODO: TypeScript - Here props should be object type
props: any; props: any;
$newServerForm: Element; $newServerForm: Element;
@@ -25,9 +24,7 @@ class NewServerForm extends BaseComponent {
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/> <input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
</div> </div>
<div class="server-center"> <div class="server-center">
<div class="server-save-action"> <button id="connect">${t.__('Connect')}</button>
<button id="connect">${t.__('Connect')}</button>
</div>
</div> </div>
<div class="server-center"> <div class="server-center">
<div class="divider"> <div class="divider">
@@ -35,9 +32,7 @@ class NewServerForm extends BaseComponent {
</div> </div>
</div> </div>
<div class="server-center"> <div class="server-center">
<div class="server-save-action"> <button id="open-create-org-link">${t.__('Create a new organization')}</button>
<button id="open-create-org-link">${t.__('Create a new organization')}</button>
</div>
</div> </div>
<div class="server-center"> <div class="server-center">
<div class="server-network-option"> <div class="server-network-option">
@@ -56,29 +51,31 @@ class NewServerForm extends BaseComponent {
initForm(): void { initForm(): void {
this.$newServerForm = this.generateNodeFromTemplate(this.template()); this.$newServerForm = this.generateNodeFromTemplate(this.template());
this.$saveServerButton = this.$newServerForm.querySelectorAll('.server-save-action')[0] as HTMLButtonElement; this.$saveServerButton = this.$newServerForm.querySelector('#connect');
this.props.$root.innerHTML = ''; this.props.$root.innerHTML = '';
this.props.$root.append(this.$newServerForm); this.props.$root.append(this.$newServerForm);
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement; this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
} }
submitFormHandler(): void { async submitFormHandler(): Promise<void> {
this.$saveServerButton.children[0].innerHTML = 'Connecting...'; this.$saveServerButton.innerHTML = 'Connecting...';
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => { let serverConf;
DomainUtil.addDomain(serverConf).then(() => { try {
this.props.onChange(this.props.index); serverConf = await DomainUtil.checkDomain(this.$newServerUrl.value);
}); } catch (errorMessage) {
}, errorMessage => { this.$saveServerButton.innerHTML = 'Connect';
this.$saveServerButton.children[0].innerHTML = 'Connect';
alert(errorMessage); alert(errorMessage);
}); return;
}
await DomainUtil.addDomain(serverConf);
this.props.onChange(this.props.index);
} }
openCreateNewOrgExternalLink(): void { openCreateNewOrgExternalLink(): void {
const link = 'https://zulipchat.com/new/'; const link = 'https://zulipchat.com/new/';
const externalCreateNewOrgEl = document.querySelector('#open-create-org-link'); const externalCreateNewOrgElement = document.querySelector('#open-create-org-link');
externalCreateNewOrgEl.addEventListener('click', () => { externalCreateNewOrgElement.addEventListener('click', () => {
shell.openExternal(link); LinkUtil.openBrowser(new URL(link));
}); });
} }
@@ -92,9 +89,7 @@ class NewServerForm extends BaseComponent {
this.submitFormHandler(); this.submitFormHandler();
}); });
this.$newServerUrl.addEventListener('keypress', event => { this.$newServerUrl.addEventListener('keypress', event => {
const EnterkeyCode = event.keyCode; if (event.key === 'Enter') {
// Submit form when Enter key is pressed
if (EnterkeyCode === 13) {
this.submitFormHandler(); this.submitFormHandler();
} }
}); });
@@ -103,5 +98,3 @@ class NewServerForm extends BaseComponent {
this.networkSettingsLink(); this.networkSettingsLink();
} }
} }
export = NewServerForm;

View File

@@ -1,18 +1,16 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import BaseComponent = require('../../components/base'); import BaseComponent from '../../components/base';
import Nav = require('./nav'); import Nav from './nav';
import ServersSection = require('./servers-section'); import ServersSection from './servers-section';
import GeneralSection = require('./general-section'); import GeneralSection from './general-section';
import NetworkSection = require('./network-section'); import NetworkSection from './network-section';
import ConnectedOrgSection = require('./connected-org-section'); import ConnectedOrgSection from './connected-org-section';
import ShortcutsSection = require('./shortcuts-section'); import ShortcutsSection from './shortcuts-section';
type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection; type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection;
class PreferenceView extends BaseComponent { export default class PreferenceView extends BaseComponent {
$sidebarContainer: Element; $sidebarContainer: Element;
$settingsContainer: Element; $settingsContainer: Element;
nav: Nav; nav: Nav;
@@ -37,7 +35,7 @@ class PreferenceView extends BaseComponent {
let nav = 'General'; let nav = 'General';
const hasTag = window.location.hash; const hasTag = window.location.hash;
if (hasTag) { if (hasTag) {
nav = hasTag.substring(1); nav = hasTag.slice(1);
} }
this.handleNavigation(nav); this.handleNavigation(nav);
} }
@@ -122,5 +120,3 @@ window.addEventListener('load', () => {
const preferenceView = new PreferenceView(); const preferenceView = new PreferenceView();
preferenceView.init(); preferenceView.init();
}); });
export = PreferenceView;

View File

@@ -1,15 +1,13 @@
'use strict';
import { remote, ipcRenderer } from 'electron'; import { remote, ipcRenderer } from 'electron';
import BaseComponent = require('../../components/base'); import BaseComponent from '../../components/base';
import DomainUtil = require('../../utils/domain-util'); import * as DomainUtil from '../../utils/domain-util';
import Messages = require('./../../../../resources/messages'); import * as Messages from '../../../../resources/messages';
import t = require('../../utils/translation-util'); import * as t from '../../utils/translation-util';
const { dialog } = remote; const { dialog } = remote;
class ServerInfoForm extends BaseComponent { export default class ServerInfoForm extends BaseComponent {
// TODO: TypeScript - Here props should be object type // TODO: TypeScript - Here props should be object type
props: any; props: any;
$serverInfoForm: Element; $serverInfoForm: Element;
@@ -61,22 +59,21 @@ class ServerInfoForm extends BaseComponent {
} }
initActions(): void { initActions(): void {
this.$deleteServerButton.addEventListener('click', () => { this.$deleteServerButton.addEventListener('click', async () => {
dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'warning', type: 'warning',
buttons: [t.__('YES'), t.__('NO')], buttons: [t.__('YES'), t.__('NO')],
defaultId: 0, defaultId: 0,
message: t.__('Are you sure you want to disconnect this organization?') message: t.__('Are you sure you want to disconnect this organization?')
}, response => {
if (response === 0) {
if (DomainUtil.removeDomain(this.props.index)) {
ipcRenderer.send('reload-full-app');
} else {
const { title, content } = Messages.orgRemovalError(DomainUtil.getDomain(this.props.index).url);
dialog.showErrorBox(title, content);
}
}
}); });
if (response === 0) {
if (DomainUtil.removeDomain(this.props.index)) {
ipcRenderer.send('reload-full-app');
} else {
const { title, content } = Messages.orgRemovalError(DomainUtil.getDomain(this.props.index).url);
dialog.showErrorBox(title, content);
}
}
}); });
this.$openServerButton.addEventListener('click', () => { this.$openServerButton.addEventListener('click', () => {
@@ -92,5 +89,3 @@ class ServerInfoForm extends BaseComponent {
}); });
} }
} }
export = ServerInfoForm;

View File

@@ -1,10 +1,8 @@
'use strict'; import BaseSection from './base-section';
import NewServerForm from './new-server-form';
import * as t from '../../utils/translation-util';
import BaseSection = require('./base-section'); export default class ServersSection extends BaseSection {
import NewServerForm = require('./new-server-form');
import t = require('../../utils/translation-util');
class ServersSection extends BaseSection {
// TODO: TypeScript - Here props should be object type // TODO: TypeScript - Here props should be object type
props: any; props: any;
$newServerContainer: Element; $newServerContainer: Element;
@@ -46,5 +44,3 @@ class ServersSection extends BaseSection {
}).init(); }).init();
} }
} }
export = ServersSection;

View File

@@ -1,17 +1,15 @@
'use strict'; import BaseSection from './base-section';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
import { shell } from 'electron'; export default class ShortcutsSection extends BaseSection {
import BaseSection = require('./base-section');
import t = require('../../utils/translation-util');
class ShortcutsSection extends BaseSection {
// TODO: TypeScript - Here props should be object type // TODO: TypeScript - Here props should be object type
props: any; props: any;
constructor(props: any) { constructor(props: any) {
super(); super();
this.props = props; this.props = props;
} }
// TODO - Deduplicate templateMac and templateWinLin functions. In theory // TODO - Deduplicate templateMac and templateWinLin functions. In theory
// they both should be the same the only thing different should be the userOSKey // they both should be the same the only thing different should be the userOSKey
// variable but there seems to be inconsistences between both function, one has more // variable but there seems to be inconsistences between both function, one has more
@@ -330,16 +328,15 @@ class ShortcutsSection extends BaseSection {
openHotkeysExternalLink(): void { openHotkeysExternalLink(): void {
const link = 'https://zulipchat.com/help/keyboard-shortcuts'; const link = 'https://zulipchat.com/help/keyboard-shortcuts';
const externalCreateNewOrgEl = document.querySelector('#open-hotkeys-link'); const externalCreateNewOrgElement = document.querySelector('#open-hotkeys-link');
externalCreateNewOrgEl.addEventListener('click', () => { externalCreateNewOrgElement.addEventListener('click', () => {
shell.openExternal(link); LinkUtil.openBrowser(new URL(link));
}); });
} }
init(): void { init(): void {
this.props.$root.innerHTML = (process.platform === 'darwin') ? this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin(); this.templateMac() : this.templateWinLin();
this.openHotkeysExternalLink(); this.openHotkeysExternalLink();
} }
} }
export = ShortcutsSection;

View File

@@ -1,47 +1,31 @@
// we have and will have some non camelcase stuff import { contextBridge, ipcRenderer, webFrame } from 'electron';
// while working with zulip and electron bridge import fs from 'fs';
// so turning the rule off for the whole file. import * as SetupSpellChecker from './spellchecker';
/* eslint-disable @typescript-eslint/camelcase */
'use strict'; import isDev from 'electron-is-dev';
import { ipcRenderer, shell } from 'electron'; import * as NetworkError from './pages/network';
import SetupSpellChecker from './spellchecker';
import isDev = require('electron-is-dev');
import LinkUtil = require('./utils/link-util');
import params = require('./utils/params-util');
import NetworkError = require('./pages/network');
interface PatchedGlobal extends NodeJS.Global {
logout: () => void;
shortcut: () => void;
showNotificationSettings: () => void;
}
const globalPatched = global as PatchedGlobal;
// eslint-disable-next-line import/no-unassigned-import // eslint-disable-next-line import/no-unassigned-import
require('./notification'); import './notification';
// Prevent drag and drop event in main process which prevents remote code executaion // Prevent drag and drop event in main process which prevents remote code executaion
require(__dirname + '/shared/preventdrag.js'); // eslint-disable-next-line import/no-unassigned-import
import './shared/preventdrag';
declare let window: ZulipWebWindow; import electron_bridge from './electron-bridge';
contextBridge.exposeInMainWorld('raw_electron_bridge', electron_bridge);
window.electron_bridge = require('./electron-bridge'); ipcRenderer.on('logout', () => {
const logout = (): void => {
// Create the menu for the below // Create the menu for the below
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle'); const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click(); dropdown.click();
const nodes: NodeListOf<HTMLElement> = document.querySelectorAll('.dropdown-menu li:last-child a'); const nodes: NodeListOf<HTMLElement> = document.querySelectorAll('.dropdown-menu li:last-child a');
nodes[nodes.length - 1].click(); nodes[nodes.length - 1].click();
}; });
const shortcut = (): void => { ipcRenderer.on('shortcut', () => {
// Create the menu for the below // Create the menu for the below
const node: HTMLElement = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]'); const node: HTMLElement = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
// Additional check // Additional check
@@ -52,9 +36,9 @@ const shortcut = (): void => {
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle'); const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click(); dropdown.click();
} }
}; });
const showNotificationSettings = (): void => { ipcRenderer.on('show-notification-settings', () => {
// Create the menu for the below // Create the menu for the below
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle'); const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click(); dropdown.click();
@@ -68,50 +52,19 @@ const showNotificationSettings = (): void => {
setTimeout(() => { setTimeout(() => {
notificationItem[2].click(); notificationItem[2].click();
}, 100); }, 100);
};
process.once('loaded', (): void => {
globalPatched.logout = logout;
globalPatched.shortcut = shortcut;
globalPatched.showNotificationSettings = showNotificationSettings;
}); });
// To prevent failing this script on linux we need to load it after the document loaded electron_bridge.once('zulip-loaded', ({ serverLanguage }) => {
document.addEventListener('DOMContentLoaded', (): void => {
if (params.isPageParams()) {
// Get the default language of the server // Get the default language of the server
const serverLanguage = page_params.default_language; // eslint-disable-line no-undef if (serverLanguage) {
if (serverLanguage) { // Init spellchecker
// Init spellchecker SetupSpellChecker.init(serverLanguage);
SetupSpellChecker.init(serverLanguage); }
} // redirect users to network troubleshooting page
// redirect users to network troubleshooting page const getRestartButton = document.querySelector('.restart_get_events_button');
const getRestartButton = document.querySelector('.restart_get_events_button'); if (getRestartButton) {
if (getRestartButton) { getRestartButton.addEventListener('click', () => {
getRestartButton.addEventListener('click', () => { ipcRenderer.send('forward-message', 'reload-viewer');
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 (this: HTMLElement, e: Event) {
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);
}
}); });
} }
}); });
@@ -152,8 +105,8 @@ ipcRenderer.on('set-active', () => {
if (isDev) { if (isDev) {
console.log('active'); console.log('active');
} }
window.electron_bridge.idle_on_system = false; electron_bridge.idle_on_system = false;
window.electron_bridge.last_active_on_system = Date.now(); electron_bridge.last_active_on_system = Date.now();
}); });
// Set user as idle and time of last activity is left unchanged // Set user as idle and time of last activity is left unchanged
@@ -161,5 +114,9 @@ ipcRenderer.on('set-idle', () => {
if (isDev) { if (isDev) {
console.log('idle'); console.log('idle');
} }
window.electron_bridge.idle_on_system = true; electron_bridge.idle_on_system = true;
}); });
webFrame.executeJavaScript(
fs.readFileSync(require.resolve('./injected'), 'utf8')
);

View File

@@ -1,5 +1,3 @@
'use strict';
// This is a security fix. Following function prevents drag and drop event in the app // 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 // so that attackers can't execute any remote code within the app
// It doesn't affect the compose box so that users can still // It doesn't affect the compose box so that users can still
@@ -15,3 +13,5 @@ const preventDragAndDrop = (): void => {
}; };
preventDragAndDrop(); preventDragAndDrop();
export {};

View File

@@ -1,56 +1,59 @@
'use strict'; import type { Subject } from 'rxjs';
import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker'; import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker';
import ConfigUtil = require('./utils/config-util'); import * as ConfigUtil from './utils/config-util';
import Logger = require('./utils/logger-util'); import Logger from './utils/logger-util';
declare module 'electron-spellchecker' {
interface SpellCheckHandler {
currentSpellcheckerChanged: Subject<true>;
currentSpellcheckerLanguage: string;
}
}
const logger = new Logger({ const logger = new Logger({
file: 'errors.log', file: 'errors.log',
timestamp: true timestamp: true
}); });
class SetupSpellChecker { let spellCheckHandler: SpellCheckHandler;
SpellCheckHandler: typeof SpellCheckHandler; let contextMenuListener: ContextMenuListener;
contextMenuListener: typeof ContextMenuListener;
init(serverLanguage: string): void { export function init(serverLanguage: string): void {
if (ConfigUtil.getConfigItem('enableSpellchecker')) { if (ConfigUtil.getConfigItem('enableSpellchecker')) {
this.enableSpellChecker(); enableSpellChecker();
}
this.enableContextMenu(serverLanguage);
} }
enableContextMenu(serverLanguage);
}
enableSpellChecker(): void { function enableSpellChecker(): void {
try { try {
this.SpellCheckHandler = new SpellCheckHandler(); spellCheckHandler = new SpellCheckHandler();
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
}
}
enableContextMenu(serverLanguage: string): void {
if (this.SpellCheckHandler) {
this.SpellCheckHandler.attachToInput();
this.SpellCheckHandler.switchLanguage(serverLanguage);
this.SpellCheckHandler.currentSpellcheckerChanged.subscribe(() => {
this.SpellCheckHandler.switchLanguage(this.SpellCheckHandler.currentSpellcheckerLanguage);
});
}
const contextMenuBuilder = new ContextMenuBuilder(this.SpellCheckHandler);
this.contextMenuListener = new ContextMenuListener((info: object) => {
contextMenuBuilder.showPopupMenu(info);
});
}
unsubscribeSpellChecker(): void {
if (this.SpellCheckHandler) {
this.SpellCheckHandler.unsubscribe();
}
if (this.contextMenuListener) {
this.contextMenuListener.unsubscribe();
}
} }
} }
export = new SetupSpellChecker(); function enableContextMenu(serverLanguage: string): void {
if (spellCheckHandler) {
spellCheckHandler.attachToInput();
spellCheckHandler.switchLanguage(serverLanguage);
spellCheckHandler.currentSpellcheckerChanged.subscribe(() => {
spellCheckHandler.switchLanguage(spellCheckHandler.currentSpellcheckerLanguage);
});
}
const contextMenuBuilder = new ContextMenuBuilder(spellCheckHandler);
contextMenuListener = new ContextMenuListener(info => {
contextMenuBuilder.showPopupMenu(info);
});
}
export function unsubscribeSpellChecker(): void {
if (spellCheckHandler) {
spellCheckHandler.unsubscribe();
}
if (contextMenuListener) {
contextMenuListener.unsubscribe();
}
}

View File

@@ -1,13 +1,20 @@
'use strict';
import { ipcRenderer, remote, WebviewTag, NativeImage } from 'electron'; import { ipcRenderer, remote, WebviewTag, NativeImage } from 'electron';
import path = require('path'); import path from 'path';
import ConfigUtil = require('./utils/config-util.js'); import * as ConfigUtil from './utils/config-util';
const { Tray, Menu, nativeImage, BrowserWindow } = remote;
const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray'); const { Tray, Menu, nativeImage, BrowserWindow, nativeTheme } = remote;
declare let window: ZulipWebWindow; let tray: Electron.Tray;
// get the theme on macOS
const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
const ICON_DIR = process.platform === 'darwin' ? `../../resources/tray/${theme}` : '../../resources/tray';
const TRAY_SUFFIX = 'tray';
const APP_ICON = path.join(__dirname, ICON_DIR, TRAY_SUFFIX);
const iconPath = (): string => { const iconPath = (): string => {
if (process.platform === 'linux') { if (process.platform === 'linux') {
@@ -43,69 +50,59 @@ const config = {
thick: process.platform === 'win32' thick: process.platform === 'win32'
}; };
const renderCanvas = function (arg: number): Promise<HTMLCanvasElement> { const renderCanvas = function (arg: number): HTMLCanvasElement {
config.unreadCount = arg; config.unreadCount = arg;
return new Promise(resolve => { const SIZE = config.size * config.pixelRatio;
const SIZE = config.size * config.pixelRatio; const PADDING = SIZE * 0.05;
const PADDING = SIZE * 0.05; const CENTER = SIZE / 2;
const CENTER = SIZE / 2; const HAS_COUNT = config.showUnreadCount && config.unreadCount;
const HAS_COUNT = config.showUnreadCount && config.unreadCount; const color = config.unreadCount ? config.unreadColor : config.readColor;
const color = config.unreadCount ? config.unreadColor : config.readColor; const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor;
const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = SIZE; canvas.width = SIZE;
canvas.height = SIZE; canvas.height = SIZE;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
// Circle // Circle
// If (!config.thick || config.thick && HAS_COUNT) { // If (!config.thick || config.thick && HAS_COUNT) {
ctx.beginPath(); ctx.beginPath();
ctx.arc(CENTER, CENTER, (SIZE / 2) - PADDING, 0, 2 * Math.PI, false); ctx.arc(CENTER, CENTER, (SIZE / 2) - PADDING, 0, 2 * Math.PI, false);
ctx.fillStyle = backgroundColor; ctx.fillStyle = backgroundColor;
ctx.fill(); ctx.fill();
ctx.lineWidth = SIZE / (config.thick ? 10 : 20); ctx.lineWidth = SIZE / (config.thick ? 10 : 20);
ctx.strokeStyle = backgroundColor; ctx.strokeStyle = backgroundColor;
ctx.stroke(); ctx.stroke();
// Count or Icon // Count or Icon
if (HAS_COUNT) { if (HAS_COUNT) {
ctx.fillStyle = color; ctx.fillStyle = color;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
if (config.unreadCount > 99) { if (config.unreadCount > 99) {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`; ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`;
ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15)); ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15));
} else if (config.unreadCount < 10) { } else if (config.unreadCount < 10) {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.20)); ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.2));
} else { } else {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.15)); ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.15));
}
resolve(canvas);
} }
}); }
return canvas;
}; };
/** /**
* Renders the tray icon as a native image * Renders the tray icon as a native image
* @param arg: Unread count * @param arg: Unread count
* @return the native image * @return the native image
*/ */
const renderNativeImage = function (arg: number): Promise<NativeImage> { const renderNativeImage = function (arg: number): NativeImage {
return Promise.resolve() const canvas = renderCanvas(arg);
.then(() => renderCanvas(arg)) const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
.then(canvas => { return nativeImage.createFromBuffer(pngData, {
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG(); scaleFactor: config.pixelRatio
});
// TODO: Fix the function to correctly use Promise correctly.
// the Promise.resolve().then(...) above is useless we should
// start with renderCanvas(arg).then
// eslint-disable-next-line promise/no-return-wrap
return Promise.resolve(nativeImage.createFromBuffer(pngData, {
scaleFactor: config.pixelRatio
}));
});
}; };
function sendAction(action: string): void { function sendAction(action: string): void {
@@ -119,7 +116,6 @@ function sendAction(action: string): void {
} }
const createTray = function (): void { const createTray = function (): void {
window.tray = new Tray(iconPath());
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
label: 'Zulip', label: 'Zulip',
@@ -144,22 +140,23 @@ const createTray = function (): void {
} }
} }
]); ]);
window.tray.setContextMenu(contextMenu); tray = new Tray(iconPath());
tray.setContextMenu(contextMenu);
if (process.platform === 'linux' || process.platform === 'win32') { if (process.platform === 'linux' || process.platform === 'win32') {
window.tray.on('click', () => { tray.on('click', () => {
ipcRenderer.send('toggle-app'); ipcRenderer.send('toggle-app');
}); });
} }
}; };
ipcRenderer.on('destroytray', (event: Event): Event => { ipcRenderer.on('destroytray', (event: Event): Event => {
if (!window.tray) { if (!tray) {
return undefined; return undefined;
} }
window.tray.destroy(); tray.destroy();
if (window.tray.isDestroyed()) { if (tray.isDestroyed()) {
window.tray = null; tray = null;
} else { } else {
throw new Error('Tray icon not properly destroyed.'); throw new Error('Tray icon not properly destroyed.');
} }
@@ -168,42 +165,40 @@ ipcRenderer.on('destroytray', (event: Event): Event => {
}); });
ipcRenderer.on('tray', (_event: Event, arg: number): void => { ipcRenderer.on('tray', (_event: Event, arg: number): void => {
if (!window.tray) { if (!tray) {
return; return;
} }
// We don't want to create tray from unread messages on macOS since it already has dock badges. // We don't want to create tray from unread messages on macOS since it already has dock badges.
if (process.platform === 'linux' || process.platform === 'win32') { if (process.platform === 'linux' || process.platform === 'win32') {
if (arg === 0) { if (arg === 0) {
unread = arg; unread = arg;
window.tray.setImage(iconPath()); tray.setImage(iconPath());
window.tray.setToolTip('No unread messages'); tray.setToolTip('No unread messages');
} else { } else {
unread = arg; unread = arg;
renderNativeImage(arg).then(image => { const image = renderNativeImage(arg);
window.tray.setImage(image); tray.setImage(image);
window.tray.setToolTip(arg + ' unread messages'); tray.setToolTip(arg + ' unread messages');
});
} }
} }
}); });
function toggleTray(): void { function toggleTray(): void {
let state; let state;
if (window.tray) { if (tray) {
state = false; state = false;
window.tray.destroy(); tray.destroy();
if (window.tray.isDestroyed()) { if (tray.isDestroyed()) {
window.tray = null; tray = null;
} }
ConfigUtil.setConfigItem('trayIcon', false); ConfigUtil.setConfigItem('trayIcon', false);
} else { } else {
state = true; state = true;
createTray(); createTray();
if (process.platform === 'linux' || process.platform === 'win32') { if (process.platform === 'linux' || process.platform === 'win32') {
renderNativeImage(unread).then(image => { const image = renderNativeImage(unread);
window.tray.setImage(image); tray.setImage(image);
window.tray.setToolTip(unread + ' unread messages'); tray.setToolTip(unread + ' unread messages');
});
} }
ConfigUtil.setConfigItem('trayIcon', true); ConfigUtil.setConfigItem('trayIcon', true);
} }
@@ -218,3 +213,5 @@ ipcRenderer.on('toggletray', toggleTray);
if (ConfigUtil.getConfigItem('trayIcon', true)) { if (ConfigUtil.getConfigItem('trayIcon', true)) {
createTray(); createTray();
} }
export {};

View File

@@ -1,96 +1,80 @@
'use strict';
import { remote } from 'electron'; import { remote } from 'electron';
import JsonDB from 'node-json-db'; import { JsonDB } from 'node-json-db';
import { initSetUp } from './default-util'; import { initSetUp } from './default-util';
import fs = require('fs'); import fs from 'fs';
import path = require('path'); import path from 'path';
import Logger = require('./logger-util'); import Logger from './logger-util';
const { app, dialog } = remote; const { app, dialog } = remote;
initSetUp(); initSetUp();
const logger = new Logger({ const logger = new Logger({
file: `certificate-util.log`, file: 'certificate-util.log',
timestamp: true timestamp: true
}); });
let instance: null | CertificateUtil = null;
const certificatesDir = `${app.getPath('userData')}/certificates`; const certificatesDir = `${app.getPath('userData')}/certificates`;
class CertificateUtil { let db: JsonDB;
db: JsonDB;
constructor() { reloadDB();
if (instance) {
return instance;
} else {
instance = this;
}
this.reloadDB(); export function getCertificate(server: string, defaultValue: any = null): any {
return instance; reloadDB();
} const value = db.getData('/')[server];
if (value === undefined) {
getCertificate(server: string, defaultValue: any = null): any { return defaultValue;
this.reloadDB(); } else {
const value = this.db.getData('/')[server]; return value;
if (value === undefined) {
return defaultValue;
} else {
return value;
}
}
// Function to copy the certificate to userData folder
copyCertificate(_server: string, location: string, fileName: string): boolean {
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: string, fileName: string): void {
const filePath = `${fileName}`;
this.db.push(`/${server}`, filePath, true);
this.reloadDB();
}
removeCertificate(server: string): void {
this.db.delete(`/${server}`);
this.reloadDB();
}
reloadDB(): void {
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);
} }
} }
export = new CertificateUtil(); // Function to copy the certificate to userData folder
export function copyCertificate(_server: string, location: string, fileName: string): boolean {
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;
}
export function setCertificate(server: string, fileName: string): void {
const filePath = `${fileName}`;
db.push(`/${server}`, filePath, true);
reloadDB();
}
export function removeCertificate(server: string): void {
db.delete(`/${server}`);
reloadDB();
}
function reloadDB(): void {
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);
}
}
db = new JsonDB(settingsJsonPath, true, true);
}

View File

@@ -1,25 +1,8 @@
'use strict'; // unescape already encoded/escaped strings
export function decodeString(stringInput: string): string {
let instance: null | CommonUtil = null; const parser = new DOMParser();
const dom = parser.parseFromString(
class CommonUtil { '<!doctype html><body>' + stringInput,
constructor() { 'text/html');
if (instance) { return dom.body.textContent;
return instance;
} else {
instance = this;
}
return instance;
}
// unescape already encoded/escaped strings
decodeString(stringInput: string): string {
const parser = new DOMParser();
const dom = parser.parseFromString(
'<!doctype html><body>' + stringInput,
'text/html');
return dom.body.textContent;
}
} }
export = new CommonUtil();

View File

@@ -1,18 +1,16 @@
'use strict'; import { JsonDB } from 'node-json-db';
import JsonDB from 'node-json-db';
import fs = require('fs'); import fs from 'fs';
import path = require('path'); import path from 'path';
import electron = require('electron'); import electron from 'electron';
import Logger = require('./logger-util'); import Logger from './logger-util';
import EnterpriseUtil = require('./enterprise-util'); import * as EnterpriseUtil from './enterprise-util';
const logger = new Logger({ const logger = new Logger({
file: 'config-util.log', file: 'config-util.log',
timestamp: true timestamp: true
}); });
let instance: null | ConfigUtil = null;
let dialog: Electron.Dialog = null; let dialog: Electron.Dialog = null;
let app: Electron.App = null; let app: Electron.App = null;
@@ -26,81 +24,68 @@ if (process.type === 'renderer') {
app = electron.app; app = electron.app;
} }
class ConfigUtil { let db: JsonDB;
db: JsonDB;
constructor() { reloadDB();
if (instance) {
return instance;
} else {
instance = this;
}
this.reloadDB(); export function getConfigItem(key: string, defaultValue: any = null): any {
return instance; try {
db.reload();
} catch (err) {
logger.error('Error while reloading settings.json: ');
logger.error(err);
} }
const value = db.getData('/')[key];
getConfigItem(key: string, defaultValue: any = null): any { if (value === undefined) {
try { setConfigItem(key, defaultValue);
this.db.reload(); return defaultValue;
} catch (err) { } else {
logger.error('Error while reloading settings.json: '); return value;
logger.error(err);
}
const value = this.db.getData('/')[key];
if (value === undefined) {
this.setConfigItem(key, defaultValue);
return defaultValue;
} else {
return value;
}
}
// This function returns whether a key exists in the configuration file (settings.json)
isConfigItemExists(key: string): boolean {
try {
this.db.reload();
} catch (err) {
logger.error('Error while reloading settings.json: ');
logger.error(err);
}
const value = this.db.getData('/')[key];
return (value !== undefined);
}
setConfigItem(key: string, value: any, override? : boolean): void {
if (EnterpriseUtil.configItemExists(key) && !override) {
// if item is in global config and we're not trying to override
return;
}
this.db.push(`/${key}`, value, true);
this.db.save();
}
removeConfigItem(key: string): void {
this.db.delete(`/${key}`);
this.db.save();
}
reloadDB(): void {
const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.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 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);
} }
} }
export = new ConfigUtil(); // This function returns whether a key exists in the configuration file (settings.json)
export function isConfigItemExists(key: string): boolean {
try {
db.reload();
} catch (err) {
logger.error('Error while reloading settings.json: ');
logger.error(err);
}
const value = db.getData('/')[key];
return (value !== undefined);
}
export function setConfigItem(key: string, value: any, override? : boolean): void {
if (EnterpriseUtil.configItemExists(key) && !override) {
// if item is in global config and we're not trying to override
return;
}
db.push(`/${key}`, value, true);
db.save();
}
export function removeConfigItem(key: string): void {
db.delete(`/${key}`);
db.save();
}
function reloadDB(): void {
const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.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 an error while saving the settings.'
);
logger.error('Error while JSON parsing settings.json: ');
logger.error(err);
logger.reportSentry(err);
}
}
db = new JsonDB(settingsJsonPath, true, true);
}

View File

@@ -1,11 +1,12 @@
import fs = require('fs'); import electron from 'electron';
import fs from 'fs';
let app: Electron.App = null; let app: Electron.App = null;
let setupCompleted = false; let setupCompleted = false;
if (process.type === 'renderer') { if (process.type === 'renderer') {
app = require('electron').remote.app; app = electron.remote.app;
} else { } else {
app = require('electron').app; app = electron.app;
} }
const zulipDir = app.getPath('userData'); const zulipDir = app.getPath('userData');
@@ -41,19 +42,19 @@ export const initSetUp = (): void => {
const configData = [ const configData = [
{ {
path: domainJson, path: domainJson,
fileName: `domain.json` fileName: 'domain.json'
}, },
{ {
path: certificatesJson, path: certificatesJson,
fileName: `certificates.json` fileName: 'certificates.json'
}, },
{ {
path: settingsJson, path: settingsJson,
fileName: `settings.json` fileName: 'settings.json'
}, },
{ {
path: updatesJson, path: updatesJson,
fileName: `updates.json` fileName: 'updates.json'
} }
]; ];
configData.forEach(data => { configData.forEach(data => {

View File

@@ -1,6 +1,4 @@
'use strict'; import * as ConfigUtil from './config-util';
import ConfigUtil = require('./config-util');
// TODO: TypeScript - add to Setting interface // TODO: TypeScript - add to Setting interface
// the list of settings since we have fixed amount of them // the list of settings since we have fixed amount of them

View File

@@ -1,331 +1,307 @@
'use strict'; import { JsonDB } from 'node-json-db';
import JsonDB from 'node-json-db';
import escape = require('escape-html'); import escape from 'escape-html';
import request = require('request'); import request from 'request';
import fs = require('fs'); import fs from 'fs';
import path = require('path'); import path from 'path';
import Logger = require('./logger-util'); import Logger from './logger-util';
import electron = require('electron'); import { ipcRenderer, remote } from 'electron';
import RequestUtil = require('./request-util'); import * as RequestUtil from './request-util';
import EnterpriseUtil = require('./enterprise-util'); import * as EnterpriseUtil from './enterprise-util';
import Messages = require('../../../resources/messages'); import * as Messages from '../../../resources/messages';
const { ipcRenderer } = electron; const { app, dialog } = remote;
const { app, dialog } = electron.remote;
interface ServerConf {
url: string;
alias?: string;
icon?: string;
ignoreCerts?: boolean;
}
const logger = new Logger({ const logger = new Logger({
file: `domain-util.log`, file: 'domain-util.log',
timestamp: true timestamp: true
}); });
let instance: null | DomainUtil = null;
const defaultIconUrl = '../renderer/img/icon.png'; const defaultIconUrl = '../renderer/img/icon.png';
class DomainUtil { export let db: JsonDB;
db: JsonDB;
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
this.reloadDB(); reloadDB();
// Migrate from old schema // Migrate from old schema
if (this.db.getData('/').domain) { if (db.getData('/').domain) {
this.addDomain({ addDomain({
alias: 'Zulip', alias: 'Zulip',
url: this.db.getData('/domain') url: db.getData('/domain')
}); });
this.db.delete('/domain'); db.delete('/domain');
} }
return instance; export function getDomains(): ServerConf[] {
reloadDB();
if (db.getData('/').domains === undefined) {
return [];
} else {
return db.getData('/domains');
} }
}
getDomains(): any { export function getDomain(index: number): ServerConf {
this.reloadDB(); reloadDB();
if (this.db.getData('/').domains === undefined) { return db.getData(`/domains[${index}]`);
return []; }
} else {
return this.db.getData('/domains'); export function shouldIgnoreCerts(url: string): boolean {
const domains = getDomains();
for (const domain of domains) {
if (domain.url === url) {
return domain.ignoreCerts;
} }
} }
return null;
}
getDomain(index: number): any { function updateDomain(index: number, server: ServerConf): void {
this.reloadDB(); reloadDB();
return this.db.getData(`/domains[${index}]`); db.push(`/domains[${index}]`, server, true);
}
export async function addDomain(server: ServerConf): Promise<void> {
const { ignoreCerts } = server;
if (server.icon) {
const localIconUrl = await saveServerIcon(server, ignoreCerts);
server.icon = localIconUrl;
db.push('/domains[]', server, true);
reloadDB();
} else {
server.icon = defaultIconUrl;
db.push('/domains[]', server, true);
reloadDB();
} }
}
shouldIgnoreCerts(url: string): boolean { export function removeDomains(): void {
const domains = this.getDomains(); db.delete('/domains');
for (const domain of domains) { reloadDB();
if (domain.url === url) { }
return domain.ignoreCerts;
}
}
return null;
}
updateDomain(index: number, server: object): void { export function removeDomain(index: number): boolean {
this.reloadDB(); if (EnterpriseUtil.isPresetOrg(getDomain(index).url)) {
this.db.push(`/domains[${index}]`, server, true);
}
addDomain(server: any): Promise<void> {
const { ignoreCerts } = server;
return new Promise(resolve => {
if (server.icon) {
this.saveServerIcon(server, ignoreCerts).then(localIconUrl => {
server.icon = localIconUrl;
this.db.push('/domains[]', server, true);
this.reloadDB();
resolve();
});
} else {
server.icon = defaultIconUrl;
this.db.push('/domains[]', server, true);
this.reloadDB();
resolve();
}
});
}
removeDomains(): void {
this.db.delete('/domains');
this.reloadDB();
}
removeDomain(index: number): boolean {
if (EnterpriseUtil.isPresetOrg(this.getDomain(index).url)) {
return false;
}
this.db.delete(`/domains[${index}]`);
this.reloadDB();
return true;
}
// Check if domain is already added
duplicateDomain(domain: any): boolean {
domain = this.formatUrl(domain);
const servers = this.getDomains();
for (const i in servers) {
if (servers[i].url === domain) {
return true;
}
}
return false; return false;
} }
db.delete(`/domains[${index}]`);
reloadDB();
return true;
}
async checkCertError(domain: any, serverConf: any, error: string, silent: boolean): Promise<string | object> { // Check if domain is already added
if (silent) { export function duplicateDomain(domain: string): boolean {
// since getting server settings has already failed domain = formatUrl(domain);
return serverConf; return getDomains().some(server => server.url === domain);
} else { }
// Report error to sentry to get idea of possible certificate errors
// users get when adding the servers
logger.reportSentry(new Error(error).toString());
const certErrorMessage = Messages.certErrorMessage(domain, error);
const certErrorDetail = Messages.certErrorDetail();
const response = await (dialog.showMessageBox({ async function checkCertError(domain: string, serverConf: ServerConf, error: string, silent: boolean): Promise<ServerConf> {
type: 'warning', if (silent) {
buttons: ['Yes', 'No'], // since getting server settings has already failed
defaultId: 1, return serverConf;
message: certErrorMessage, } else {
detail: certErrorDetail // Report error to sentry to get idea of possible certificate errors
}) as any); // TODO: TypeScript - Figure this out // users get when adding the servers
if (response === 0) { logger.reportSentry(new Error(error).toString());
// set ignoreCerts parameter to true in case user responds with yes const certErrorMessage = Messages.certErrorMessage(domain, error);
serverConf.ignoreCerts = true; const certErrorDetail = Messages.certErrorDetail();
try {
return await this.getServerSettings(domain, serverConf.ignoreCerts);
} catch (_) {
if (error === Messages.noOrgsError(domain)) {
throw new Error(error);
}
return serverConf;
}
} else {
throw new Error('Untrusted certificate.');
}
}
}
// ignoreCerts parameter helps in fetching server icon and const { response } = await dialog.showMessageBox({
// other server details when user chooses to ignore certificate warnings type: 'warning',
async checkDomain(domain: any, ignoreCerts = false, silent = false): Promise<any> { buttons: ['Yes', 'No'],
if (!silent && this.duplicateDomain(domain)) { defaultId: 1,
// Do not check duplicate in silent mode message: certErrorMessage,
throw new Error('This server has been added.'); detail: certErrorDetail
}
domain = this.formatUrl(domain);
const serverConf = {
icon: defaultIconUrl,
url: domain,
alias: domain,
ignoreCerts
};
try {
return await this.getServerSettings(domain, serverConf.ignoreCerts);
} catch (err) {
// If the domain contains following strings we just bypass the server
const whitelistDomains = [
'zulipdev.org'
];
// make sure that error is an error or string not undefined
// so validation does not throw error.
const error = err || '';
const certsError = error.toString().includes('certificate');
if (domain.indexOf(whitelistDomains) >= 0 || certsError) {
return this.checkCertError(domain, serverConf, error, silent);
} else {
throw Messages.invalidZulipServerError(domain);
}
}
}
getServerSettings(domain: any, ignoreCerts = false): Promise<object | string> {
const serverSettingsOptions = {
url: domain + '/api/v1/server_settings',
...RequestUtil.requestOptions(domain, ignoreCerts)
};
return new Promise((resolve, reject) => {
request(serverSettingsOptions, (error: string, response: any) => {
if (!error && response.statusCode === 200) {
const data = JSON.parse(response.body);
if (data.hasOwnProperty('realm_icon') && data.realm_icon) {
resolve({
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
url: data.realm_uri,
alias: escape(data.realm_name),
ignoreCerts
});
} else {
reject(Messages.noOrgsError(domain));
}
} else {
reject(response);
}
});
}); });
if (response === 0) {
// set ignoreCerts parameter to true in case user responds with yes
serverConf.ignoreCerts = true;
try {
return await getServerSettings(domain, serverConf.ignoreCerts);
} catch (_) {
if (error === Messages.noOrgsError(domain)) {
throw new Error(error);
}
return serverConf;
}
} else {
throw new Error('Untrusted certificate.');
}
}
}
// ignoreCerts parameter helps in fetching server icon and
// other server details when user chooses to ignore certificate warnings
export async function checkDomain(domain: string, ignoreCerts = false, silent = false): Promise<ServerConf> {
if (!silent && duplicateDomain(domain)) {
// Do not check duplicate in silent mode
throw new Error('This server has been added.');
} }
saveServerIcon(server: any, ignoreCerts = false): Promise<string> { domain = formatUrl(domain);
const url = server.icon;
const domain = server.url;
const serverIconOptions = { const serverConf = {
url, icon: defaultIconUrl,
...RequestUtil.requestOptions(domain, ignoreCerts) url: domain,
}; alias: domain,
ignoreCerts
};
// The save will always succeed. If url is invalid, downgrade to default icon. try {
return new Promise(resolve => { return await getServerSettings(domain, serverConf.ignoreCerts);
const filePath = this.generateFilePath(url); } catch (err) {
const file = fs.createWriteStream(filePath); // Make sure that error is an error or string not undefined
try { // so validation does not throw error.
request(serverIconOptions).on('response', (response: any) => { const error = err || '';
response.on('error', (err: string) => {
logger.log('Could not get server icon.'); const certsError = error.toString().includes('certificate');
logger.log(err); if (certsError) {
logger.reportSentry(err); const result = await checkCertError(domain, serverConf, error, silent);
resolve(defaultIconUrl); return result;
} else {
throw new Error(Messages.invalidZulipServerError(domain));
}
}
}
async function getServerSettings(domain: string, ignoreCerts = false): Promise<ServerConf> {
const serverSettingsOptions = {
url: domain + '/api/v1/server_settings',
...RequestUtil.requestOptions(domain, ignoreCerts)
};
return new Promise((resolve, reject) => {
request(serverSettingsOptions, (error: string, response: any) => {
if (!error && response.statusCode === 200) {
const data = JSON.parse(response.body);
if (Object.prototype.hasOwnProperty.call(data, 'realm_icon') && data.realm_icon) {
resolve({
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
url: data.realm_uri,
alias: escape(data.realm_name),
ignoreCerts
}); });
response.pipe(file).on('finish', () => { } else {
resolve(filePath); reject(Messages.noOrgsError(domain));
}); }
}).on('error', (err: string) => { } else {
reject(response);
}
});
});
}
export async function saveServerIcon(server: ServerConf, ignoreCerts = false): Promise<string> {
const url = server.icon;
const domain = server.url;
const serverIconOptions = {
url,
...RequestUtil.requestOptions(domain, ignoreCerts)
};
// The save will always succeed. If url is invalid, downgrade to default icon.
return new Promise(resolve => {
const filePath = generateFilePath(url);
const file = fs.createWriteStream(filePath);
try {
request(serverIconOptions).on('response', (response: any) => {
response.on('error', (err: string) => {
logger.log('Could not get server icon.'); logger.log('Could not get server icon.');
logger.log(err); logger.log(err);
logger.reportSentry(err); logger.reportSentry(err);
resolve(defaultIconUrl); resolve(defaultIconUrl);
}); });
} catch (err) { response.pipe(file).on('finish', () => {
resolve(filePath);
});
}).on('error', (err: string) => {
logger.log('Could not get server icon.'); logger.log('Could not get server icon.');
logger.log(err); logger.log(err);
logger.reportSentry(err); logger.reportSentry(err);
resolve(defaultIconUrl); resolve(defaultIconUrl);
} });
});
}
async updateSavedServer(url: string, index: number): Promise<void> {
// Does not promise successful update
const oldIcon = this.getDomain(index).icon;
const { ignoreCerts } = this.getDomain(index);
try {
const newServerConf = await this.checkDomain(url, ignoreCerts, true);
const localIconUrl = await this.saveServerIcon(newServerConf, ignoreCerts);
if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') {
newServerConf.icon = localIconUrl;
this.updateDomain(index, newServerConf);
this.reloadDB();
}
} catch (err) { } catch (err) {
ipcRenderer.send('forward-message', 'show-network-error', index); logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
} }
} });
}
reloadDB(): void { export async function updateSavedServer(url: string, index: number): Promise<void> {
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json'); // Does not promise successful update
try { const oldIcon = getDomain(index).icon;
const file = fs.readFileSync(domainJsonPath, 'utf8'); const { ignoreCerts } = getDomain(index);
JSON.parse(file); try {
} catch (err) { const newServerConf = await checkDomain(url, ignoreCerts, true);
if (fs.existsSync(domainJsonPath)) { const localIconUrl = await saveServerIcon(newServerConf, ignoreCerts);
fs.unlinkSync(domainJsonPath); if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') {
dialog.showErrorBox( newServerConf.icon = localIconUrl;
'Error saving new organization', updateDomain(index, newServerConf);
'There seems to be error while saving new organization, ' + reloadDB();
'you may have to re-add your previous organizations back.'
);
logger.error('Error while JSON parsing domain.json: ');
logger.error(err);
logger.reportSentry(err);
}
}
this.db = new JsonDB(domainJsonPath, true, true);
}
generateFilePath(url: string): string {
const dir = `${app.getPath('userData')}/server-icons`;
const extension = path.extname(url).split('?')[0];
let hash = 5381;
let len = url.length;
while (len) {
hash = (hash * 33) ^ url.charCodeAt(--len);
}
// Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return `${dir}/${hash >>> 0}${extension}`;
}
formatUrl(domain: any): string {
const hasPrefix = (domain.indexOf('http') === 0);
if (hasPrefix) {
return domain;
} else {
return (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`;
} }
} catch (err) {
ipcRenderer.send('forward-message', 'show-network-error', index);
} }
} }
export = new DomainUtil(); export function reloadDB(): void {
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
try {
const file = fs.readFileSync(domainJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(domainJsonPath)) {
fs.unlinkSync(domainJsonPath);
dialog.showErrorBox(
'Error saving new organization',
'There seems to be error while saving new organization, ' +
'you may have to re-add your previous organizations back.'
);
logger.error('Error while JSON parsing domain.json: ');
logger.error(err);
logger.reportSentry(err);
}
}
db = new JsonDB(domainJsonPath, true, true);
}
function generateFilePath(url: string): string {
const dir = `${app.getPath('userData')}/server-icons`;
const extension = path.extname(url).split('?')[0];
let hash = 5381;
let { length } = url;
while (length) {
hash = (hash * 33) ^ url.charCodeAt(--length);
}
// Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return `${dir}/${hash >>> 0}${extension}`;
}
export function formatUrl(domain: string): string {
if (domain.startsWith('http://') || domain.startsWith('https://')) {
return domain;
}
if (domain.startsWith('localhost:')) {
return `http://${domain}`;
}
return `https://${domain}`;
}

View File

@@ -1,80 +1,68 @@
import fs = require('fs'); import fs from 'fs';
import path = require('path'); import path from 'path';
import Logger = require('./logger-util'); import Logger from './logger-util';
const logger = new Logger({ const logger = new Logger({
file: 'enterprise-util.log', file: 'enterprise-util.log',
timestamp: true timestamp: true
}); });
let instance: null | EnterpriseUtil = null; // todo: replace enterpriseSettings type with an interface once settings are final
export let enterpriseSettings: any;
export let configFile: boolean;
class EnterpriseUtil { reloadDB();
// todo: replace enterpriseSettings type with an interface once settings are final
enterpriseSettings: any;
configFile: boolean;
constructor() {
if (instance) {
return instance;
}
instance = this;
this.reloadDB(); function reloadDB(): void {
let enterpriseFile = '/etc/zulip-desktop-config/global_config.json';
if (process.platform === 'win32') {
enterpriseFile = 'C:\\Program Files\\Zulip-Desktop-Config\\global_config.json';
} }
reloadDB(): void { enterpriseFile = path.resolve(enterpriseFile);
let enterpriseFile = '/etc/zulip-desktop-config/global_config.json'; if (fs.existsSync(enterpriseFile)) {
if (process.platform === 'win32') { configFile = true;
enterpriseFile = 'C:\\Program Files\\Zulip-Desktop-Config\\global_config.json'; try {
const file = fs.readFileSync(enterpriseFile, 'utf8');
enterpriseSettings = JSON.parse(file);
} catch (err) {
logger.log('Error while JSON parsing global_config.json: ');
logger.log(err);
} }
} else {
enterpriseFile = path.resolve(enterpriseFile); configFile = false;
if (fs.existsSync(enterpriseFile)) {
this.configFile = true;
try {
const file = fs.readFileSync(enterpriseFile, 'utf8');
this.enterpriseSettings = JSON.parse(file);
} catch (err) {
logger.log('Error while JSON parsing global_config.json: ');
logger.log(err);
}
} else {
this.configFile = false;
}
}
getConfigItem(key: string, defaultValue?: any): any {
this.reloadDB();
if (!this.configFile) {
return defaultValue;
}
if (defaultValue === undefined) {
defaultValue = null;
}
return this.configItemExists(key) ? this.enterpriseSettings[key] : defaultValue;
}
configItemExists(key: string): boolean {
this.reloadDB();
if (!this.configFile) {
return false;
}
return (this.enterpriseSettings[key] !== undefined);
}
isPresetOrg(url: string): boolean {
if (!this.configFile || !this.configItemExists('presetOrganizations')) {
return false;
}
const presetOrgs = this.enterpriseSettings.presetOrganizations;
for (const org of presetOrgs) {
if (url.includes(org)) {
return true;
}
}
return false;
} }
} }
export = new EnterpriseUtil(); export function getConfigItem(key: string, defaultValue?: any): any {
reloadDB();
if (!configFile) {
return defaultValue;
}
if (defaultValue === undefined) {
defaultValue = null;
}
return configItemExists(key) ? enterpriseSettings[key] : defaultValue;
}
export function configItemExists(key: string): boolean {
reloadDB();
if (!configFile) {
return false;
}
return (enterpriseSettings[key] !== undefined);
}
export function isPresetOrg(url: string): boolean {
if (!configFile || !configItemExists('presetOrganizations')) {
return false;
}
const presetOrgs = enterpriseSettings.presetOrganizations;
for (const org of presetOrgs) {
if (url.includes(org)) {
return true;
}
}
return false;
}

View File

@@ -1,51 +1,45 @@
'use strict'; import { shell } from 'electron';
import escape from 'escape-html';
import fs from 'fs';
import os from 'os';
import path from 'path';
// TODO: TypeScript - Add @types/ export function isUploadsUrl(server: string, url: URL): boolean {
import wurl = require('wurl'); return url.origin === server && url.pathname.startsWith('/user_uploads/');
let instance: null | LinkUtil = null;
interface IsInternalResponse {
isInternalUrl: boolean;
isUploadsUrl: boolean;
} }
class LinkUtil { export function openBrowser(url: URL): void {
constructor() { if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
if (instance) { shell.openExternal(url.href);
return instance; } else {
} else { // For security, indirect links to non-whitelisted protocols
instance = this; // through a real web browser via a local HTML file.
} const dir = fs.mkdtempSync(
path.join(os.tmpdir(), 'zulip-redirect-')
return instance; );
} const file = path.join(dir, 'redirect.html');
fs.writeFileSync(file, `\
isInternal(currentUrl: string, newUrl: string): IsInternalResponse { <!DOCTYPE html>
const currentDomain = wurl('hostname', currentUrl); <html>
const newDomain = wurl('hostname', newUrl); <head>
<meta charset="UTF-8" />
const sameDomainUrl = (currentDomain === newDomain || newUrl === currentUrl + '/'); <meta http-equiv="Refresh" content="0; url=${escape(url.href)}" />
const isUploadsUrl = newUrl.includes(currentUrl + '/user_uploads/'); <title>Redirecting</title>
const isInternalUrl = newUrl.includes('/#narrow') || isUploadsUrl; <style>
html {
return { font-family: menu, "Helvetica Neue", sans-serif;
isInternalUrl: sameDomainUrl && isInternalUrl, }
isUploadsUrl </style>
}; </head>
} <body>
<p>Opening <a href="${escape(url.href)}">${escape(url.href)}</a>…</p>
isImage(url: string): boolean { </body>
// test for images extension as well as urls like .png?s=100 </html>
const isImageUrl = /\.(bmp|gif|jpg|jpeg|png|webp)\?*.*$/i; `);
return isImageUrl.test(url); shell.openItem(file);
} setTimeout(() => {
fs.unlinkSync(file);
isPDF(url: string): boolean { fs.rmdirSync(dir);
// test for pdf extension }, 15000);
const isPDFUrl = /\.(pdf)\?*.*$/i;
return isPDFUrl.test(url);
} }
} }
export = new LinkUtil();

View File

@@ -1,10 +1,9 @@
'use strict'; import { JsonDB } from 'node-json-db';
import JsonDB from 'node-json-db';
import fs = require('fs'); import fs from 'fs';
import path = require('path'); import path from 'path';
import electron = require('electron'); import electron from 'electron';
import Logger = require('./logger-util'); import Logger from './logger-util';
const remote = const remote =
process.type === 'renderer' ? electron.remote : electron; process.type === 'renderer' ? electron.remote : electron;
@@ -16,61 +15,47 @@ const logger = new Logger({
/* To make the util runnable in both main and renderer process */ /* To make the util runnable in both main and renderer process */
const { dialog, app } = remote; const { dialog, app } = remote;
let instance: null | LinuxUpdateUtil = null;
class LinuxUpdateUtil { let db: JsonDB;
db: JsonDB;
constructor() { reloadDB();
if (instance) {
return instance;
} else {
instance = this;
}
this.reloadDB(); export function getUpdateItem(key: string, defaultValue: any = null): any {
return instance; reloadDB();
} const value = db.getData('/')[key];
if (value === undefined) {
getUpdateItem(key: string, defaultValue: any = null): any { setUpdateItem(key, defaultValue);
this.reloadDB(); return defaultValue;
const value = this.db.getData('/')[key]; } else {
if (value === undefined) { return value;
this.setUpdateItem(key, defaultValue);
return defaultValue;
} else {
return value;
}
}
setUpdateItem(key: string, value: any): void {
this.db.push(`/${key}`, value, true);
this.reloadDB();
}
removeUpdateItem(key: string): void {
this.db.delete(`/${key}`);
this.reloadDB();
}
reloadDB(): void {
const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/config/updates.json');
try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
'Error saving update notifications.',
'We encountered an error while saving the update notifications.'
);
logger.error('Error while JSON parsing updates.json: ');
logger.error(err);
}
}
this.db = new JsonDB(linuxUpdateJsonPath, true, true);
} }
} }
export = new LinuxUpdateUtil(); export function setUpdateItem(key: string, value: any): void {
db.push(`/${key}`, value, true);
reloadDB();
}
export function removeUpdateItem(key: string): void {
db.delete(`/${key}`);
reloadDB();
}
function reloadDB(): void {
const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/config/updates.json');
try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
'Error saving update notifications.',
'We encountered an error while saving the update notifications.'
);
logger.error('Error while JSON parsing updates.json: ');
logger.error(err);
}
}
db = new JsonDB(linuxUpdateJsonPath, true, true);
}

View File

@@ -2,10 +2,10 @@ import { Console as NodeConsole } from 'console'; // eslint-disable-line node/pr
import { initSetUp } from './default-util'; import { initSetUp } from './default-util';
import { sentryInit, captureException } from './sentry-util'; import { sentryInit, captureException } from './sentry-util';
import fs = require('fs'); import fs from 'fs';
import os = require('os'); import os from 'os';
import isDev = require('electron-is-dev'); import isDev from 'electron-is-dev';
import electron = require('electron'); import electron from 'electron';
// this interface adds [key: string]: any so // this interface adds [key: string]: any so
// we can do console[type] later on in the code // we can do console[type] later on in the code
interface PatchedConsole extends Console { interface PatchedConsole extends Console {
@@ -13,7 +13,7 @@ interface PatchedConsole extends Console {
} }
interface LoggerOptions { interface LoggerOptions {
timestamp?: any; timestamp?: true | (() => string);
file?: string; file?: string;
level?: boolean; level?: boolean;
logInDevMode?: boolean; logInDevMode?: boolean;
@@ -44,20 +44,20 @@ if (process.type === 'renderer') {
const browserConsole: PatchedConsole = console; const browserConsole: PatchedConsole = console;
const logDir = `${app.getPath('userData')}/Logs`; const logDir = `${app.getPath('userData')}/Logs`;
class Logger { export default class Logger {
nodeConsole: PatchedConsole; nodeConsole: PatchedConsole;
timestamp: any; // TODO: TypeScript - Figure out how to make this work with string | Function. timestamp?: () => string;
level: boolean; level: boolean;
logInDevMode: boolean; logInDevMode: boolean;
[key: string]: any; [key: string]: any;
constructor(opts: LoggerOptions = {}) { constructor(options: LoggerOptions = {}) {
let { let {
timestamp = true, timestamp = true,
file = 'console.log', file = 'console.log',
level = true, level = true,
logInDevMode = false logInDevMode = false
} = opts; } = options;
file = `${logDir}/${file}`; file = `${logDir}/${file}`;
if (timestamp === true) { if (timestamp === true) {
@@ -92,7 +92,7 @@ class Logger {
case typeof timestamp === 'function': case typeof timestamp === 'function':
args.unshift(timestamp() + ' |\t'); args.unshift(timestamp() + ' |\t');
case (level !== false): case (level):
args.unshift(type.toUpperCase() + ' |'); args.unshift(type.toUpperCase() + ' |');
case isDev || logInDevMode: case isDev || logInDevMode:
@@ -107,7 +107,7 @@ class Logger {
} }
setUpConsole(): void { setUpConsole(): void {
for (const type in browserConsole) { for (const type of Object.keys(browserConsole)) {
this.setupConsoleMethod(type); this.setupConsoleMethod(type);
} }
} }
@@ -151,5 +151,3 @@ class Logger {
}); });
} }
} }
export = Logger;

View File

@@ -1,11 +0,0 @@
// This util function returns the page params if they're present else returns null
export function isPageParams(): null | object {
let webpageParams = null;
try {
// eslint-disable-next-line no-undef, @typescript-eslint/camelcase
webpageParams = page_params;
} catch (_) {
webpageParams = null;
}
return webpageParams;
}

View File

@@ -1,142 +1,118 @@
'use strict'; import * as ConfigUtil from './config-util';
import url = require('url'); export interface ProxyRule {
import ConfigUtil = require('./config-util');
let instance: null | ProxyUtil = null;
interface ProxyRule {
hostname?: string; hostname?: string;
port?: number; port?: number;
} }
class ProxyUtil { // Return proxy to be used for a particular uri, to be used for request
constructor() { export function getProxy(_uri: string): ProxyRule | void {
if (instance) { let uri;
return instance; try {
} else { uri = new URL(_uri);
instance = this; } catch (err) {
} return;
return instance;
} }
// Return proxy to be used for a particular uri, to be used for request const proxyRules = ConfigUtil.getConfigItem('proxyRules', '').split(';');
getProxy(_uri: string): ProxyRule | void { // If SPS is on and system uses no proxy then request should not try to use proxy from
const parsedUri = url.parse(_uri); // environment. NO_PROXY = '*' makes request ignore all environment proxy variables.
if (parsedUri === null) { if (proxyRules[0] === '') {
return; process.env.NO_PROXY = '*';
} return;
const uri = parsedUri;
const proxyRules = ConfigUtil.getConfigItem('proxyRules', '').split(';');
// If SPS is on and system uses no proxy then request should not try to use proxy from
// environment. NO_PROXY = '*' makes request ignore all environment proxy variables.
if (proxyRules[0] === '') {
process.env.NO_PROXY = '*';
return;
}
const proxyRule: any = {};
if (uri.protocol === 'http:') {
proxyRules.forEach((proxy: string) => {
if (proxy.includes('http=')) {
proxyRule.hostname = proxy.split('http=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('http=')[1].trim().split(':')[1];
}
});
return proxyRule;
}
if (uri.protocol === 'https:') {
proxyRules.forEach((proxy: string) => {
if (proxy.includes('https=')) {
proxyRule.hostname = proxy.split('https=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('https=')[1].trim().split(':')[1];
}
});
return proxyRule;
}
} }
// TODO: Refactor to async function const proxyRule: any = {};
resolveSystemProxy(mainWindow: Electron.BrowserWindow): void { if (uri.protocol === 'http:') {
const page = mainWindow.webContents; proxyRules.forEach((proxy: string) => {
const ses = page.session; if (proxy.includes('http=')) {
const resolveProxyUrl = 'www.example.com'; proxyRule.hostname = proxy.split('http=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('http=')[1].trim().split(':')[1];
// Check HTTP Proxy
const httpProxy = new Promise(resolve => {
ses.resolveProxy('http://' + resolveProxyUrl, (proxy: string) => {
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: string) => {
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: string) => {
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: string) => {
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);
} }
}); });
return proxyRule;
}
if (uri.protocol === 'https:') {
proxyRules.forEach((proxy: string) => {
if (proxy.includes('https=')) {
proxyRule.hostname = proxy.split('https=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('https=')[1].trim().split(':')[1];
}
});
return proxyRule;
} }
} }
export = new ProxyUtil(); // TODO: Refactor to async function
export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Promise<void> {
const page = mainWindow.webContents;
const ses = page.session;
const resolveProxyUrl = 'www.example.com';
// Check HTTP Proxy
const httpProxy = (async () => {
const proxy = await ses.resolveProxy('http://' + resolveProxyUrl);
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] + ';';
}
}
return httpString;
})();
// Check HTTPS Proxy
const httpsProxy = (async () => {
const proxy = await ses.resolveProxy('https://' + resolveProxyUrl);
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] + ';';
}
}
return httpsString;
})();
// Check FTP Proxy
const ftpProxy = (async () => {
const proxy = await ses.resolveProxy('ftp://' + resolveProxyUrl);
let ftpString = '';
if (proxy !== 'DIRECT') {
if (proxy.includes('PROXY')) {
ftpString += 'ftp=' + proxy.split('PROXY')[1] + ';';
}
}
return ftpString;
})();
// Check SOCKS Proxy
const socksProxy = (async () => {
const proxy = await ses.resolveProxy('socks4://' + resolveProxyUrl);
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] + ';';
}
}
return socksString;
})();
const values = await Promise.all([httpProxy, httpsProxy, ftpProxy, socksProxy]);
let proxyString = '';
values.forEach(proxy => {
proxyString += proxy;
});
ConfigUtil.setConfigItem('systemProxyRules', proxyString);
const useSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
if (useSystemProxy) {
ConfigUtil.setConfigItem('proxyRules', proxyString);
}
}

View File

@@ -1,26 +1,24 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import backoff = require('backoff'); import type WebView from '../components/webview';
import request = require('request'); import backoff from 'backoff';
import Logger = require('./logger-util'); import request from 'request';
import RequestUtil = require('./request-util'); import Logger from './logger-util';
import DomainUtil = require('./domain-util'); import * as RequestUtil from './request-util';
import * as DomainUtil from './domain-util';
const logger = new Logger({ const logger = new Logger({
file: `domain-util.log`, file: 'domain-util.log',
timestamp: true timestamp: true
}); });
class ReconnectUtil { export default class ReconnectUtil {
// TODO: TypeScript - Figure out how to annotate webview webview: WebView;
// it should be WebView; maybe make it a generic so we can
// pass the class from main.ts
webview: any;
url: string; url: string;
alreadyReloaded: boolean; alreadyReloaded: boolean;
fibonacciBackoff: any; fibonacciBackoff: backoff.Backoff;
constructor(webview: any) { constructor(webview: WebView) {
this.webview = webview; this.webview = webview;
this.url = webview.props.url; this.url = webview.props.url;
this.alreadyReloaded = false; this.alreadyReloaded = false;
@@ -34,7 +32,7 @@ class ReconnectUtil {
}); });
} }
isOnline(): Promise<boolean> { async isOnline(): Promise<boolean> {
return new Promise(resolve => { return new Promise(resolve => {
try { try {
const ignoreCerts = DomainUtil.shouldIgnoreCerts(this.url); const ignoreCerts = DomainUtil.shouldIgnoreCerts(this.url);
@@ -60,43 +58,31 @@ class ReconnectUtil {
pollInternetAndReload(): void { pollInternetAndReload(): void {
this.fibonacciBackoff.backoff(); this.fibonacciBackoff.backoff();
this.fibonacciBackoff.on('ready', () => { this.fibonacciBackoff.on('ready', async () => {
this._checkAndReload().then(status => { if (await this._checkAndReload()) {
if (status) { this.fibonacciBackoff.reset();
this.fibonacciBackoff.reset();
} else {
this.fibonacciBackoff.backoff();
}
});
});
}
// TODO: Make this a async function
_checkAndReload(): Promise<boolean> {
return new Promise(resolve => {
if (!this.alreadyReloaded) { // eslint-disable-line no-negated-condition
this.isOnline()
.then((online: boolean) => {
if (online) {
ipcRenderer.send('forward-message', 'reload-viewer');
logger.log('You\'re back online.');
return resolve(true);
}
logger.log('There is no internet connection, try checking network cables, modem and router.');
const errMsgHolder = document.querySelector('#description');
if (errMsgHolder) {
errMsgHolder.innerHTML = `
<div>Your internet connection doesn't seem to work properly!</div>
<div>Verify that it works and then click try again.</div>`;
}
return resolve(false);
});
} else { } else {
return resolve(true); this.fibonacciBackoff.backoff();
} }
}); });
} }
}
export = ReconnectUtil; async _checkAndReload(): Promise<boolean> {
if (this.alreadyReloaded) {
return true;
}
if (await this.isOnline()) {
ipcRenderer.send('forward-message', 'reload-viewer');
logger.log('You\'re back online.');
return true;
}
logger.log('There is no internet connection, try checking network cables, modem and router.');
const errorMessageHolder = document.querySelector('#description');
if (errorMessageHolder) {
errorMessageHolder.innerHTML = `
<div>Your internet connection doesn't seem to work properly!</div>
<div>Verify that it works and then click try again.</div>`;
}
return false;
}
}

View File

@@ -1,87 +1,70 @@
import { remote } from 'electron'; import { remote } from 'electron';
import fs = require('fs'); import fs from 'fs';
import path = require('path'); import path from 'path';
import ConfigUtil = require('./config-util'); import * as ConfigUtil from './config-util';
import Logger = require('./logger-util'); import Logger from './logger-util';
import ProxyUtil = require('./proxy-util'); import * as ProxyUtil from './proxy-util';
import CertificateUtil = require('./certificate-util'); import * as CertificateUtil from './certificate-util';
import SystemUtil = require('./system-util'); import * as SystemUtil from './system-util';
const { app } = remote; const { app } = remote;
const logger = new Logger({ const logger = new Logger({
file: `request-util.log`, file: 'request-util.log',
timestamp: true timestamp: true
}); });
let instance: null | RequestUtil = null;
// TODO: TypeScript - Use ProxyRule for the proxy property
// we can do this now since we use export = ProxyUtil syntax
interface RequestUtilResponse { interface RequestUtilResponse {
ca: string; ca: string;
proxy: string | void | object; proxy: string | void | ProxyUtil.ProxyRule;
ecdhCurve: 'auto'; ecdhCurve: 'auto';
headers: { 'User-Agent': string }; headers: { 'User-Agent': string };
rejectUnauthorized: boolean; rejectUnauthorized: boolean;
} }
class RequestUtil { // ignoreCerts parameter helps in fetching server icon and
constructor() { // other server details when user chooses to ignore certificate warnings
if (!instance) { export function requestOptions(domain: string, ignoreCerts: boolean): RequestUtilResponse {
instance = this; domain = formatUrl(domain);
} const certificate = CertificateUtil.getCertificate(
encodeURIComponent(domain)
);
return instance; let certificateFile = null;
if (certificate?.includes('/')) {
// certificate saved using old app version
certificateFile = certificate;
} else if (certificate) {
certificateFile = path.join(`${app.getPath('userData')}/certificates`, certificate);
} }
// ignoreCerts parameter helps in fetching server icon and let certificateLocation = '';
// other server details when user chooses to ignore certificate warnings if (certificate) {
requestOptions(domain: string, ignoreCerts: boolean): RequestUtilResponse { // To handle case where certificate has been moved from the location in certificates.json
domain = this.formatUrl(domain); try {
const certificate = CertificateUtil.getCertificate( certificateLocation = fs.readFileSync(certificateFile, 'utf8');
encodeURIComponent(domain) } catch (err) {
); logger.warn(`Error while trying to get certificate: ${err}`);
let certificateFile = null;
if (certificate && certificate.includes('/')) {
// certificate saved using old app version
certificateFile = certificate;
} else if (certificate) {
certificateFile = path.join(`${app.getPath('userData')}/certificates`, certificate);
}
let certificateLocation = '';
if (certificate) {
// To handle case where certificate has been moved from the location in certificates.json
try {
certificateLocation = fs.readFileSync(certificateFile, 'utf8');
} catch (err) {
logger.warn(`Error while trying to get certificate: ${err}`);
}
}
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
// 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
// Add proxy as a parameter if it is being used.
return {
ca: certificateLocation ? certificateLocation : '',
proxy: proxyEnabled ? ProxyUtil.getProxy(domain) : '',
ecdhCurve: 'auto',
headers: { 'User-Agent': SystemUtil.getUserAgent() },
rejectUnauthorized: !ignoreCerts
};
}
formatUrl(domain: string): string {
const hasPrefix = domain.startsWith('http', 0);
if (hasPrefix) {
return domain;
} else {
return domain.includes('localhost:') ? `http://${domain}` : `https://${domain}`;
} }
} }
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
// 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
// Add proxy as a parameter if it is being used.
return {
ca: certificateLocation ? certificateLocation : '',
proxy: proxyEnabled ? ProxyUtil.getProxy(domain) : '',
ecdhCurve: 'auto',
headers: { 'User-Agent': SystemUtil.getUserAgent() },
rejectUnauthorized: !ignoreCerts
};
} }
const requestUtil = new RequestUtil(); function formatUrl(domain: string): string {
export = requestUtil; const hasPrefix = domain.startsWith('http', 0);
if (hasPrefix) {
return domain;
} else {
return domain.includes('localhost:') ? `http://${domain}` : `https://${domain}`;
}
}

View File

@@ -1,14 +1,11 @@
import { init } from '@sentry/electron'; import { init } from '@sentry/electron';
import isDev = require('electron-is-dev'); import isDev from 'electron-is-dev';
import path = require('path');
import dotenv = require('dotenv');
dotenv.config({ path: path.resolve(__dirname, '/../../../../.env') });
export const sentryInit = (): void => { export const sentryInit = (): void => {
if (!isDev) { if (!isDev) {
init({ init({
dsn: process.env.SENTRY_DSN, dsn: 'https://628dc2f2864243a08ead72e63f94c7b1@sentry.io/204668',
// We should ignore this error since it's harmless and we know the reason behind this // We should ignore this error since it's harmless and we know the reason behind this
// This error mainly comes from the console logs. // This error mainly comes from the console logs.
// This is a temp solution until Sentry supports disabling the console logs // This is a temp solution until Sentry supports disabling the console logs

View File

@@ -1,64 +1,45 @@
'use strict';
import { remote } from 'electron'; import { remote } from 'electron';
import os = require('os'); import os from 'os';
import ConfigUtil = require('./config-util'); import * as ConfigUtil from './config-util';
const { app } = remote; const { app } = remote;
let instance: null | SystemUtil = null;
class SystemUtil { export const connectivityERR: string[] = [
connectivityERR: string[]; 'ERR_INTERNET_DISCONNECTED',
'ERR_PROXY_CONNECTION_FAILED',
'ERR_CONNECTION_RESET',
'ERR_NOT_CONNECTED',
'ERR_NAME_NOT_RESOLVED',
'ERR_NETWORK_CHANGED'
];
userAgent: string | null; let userAgent: string | null = null;
constructor() { export function getOS(): string {
if (instance) { const platform = os.platform();
return instance; if (platform === 'darwin') {
return 'Mac';
} else if (platform === 'linux') {
return 'Linux';
} else if (platform === 'win32') {
if (parseFloat(os.release()) < 6.2) {
return 'Windows 7';
} else { } else {
instance = this; return 'Windows 10';
} }
} else {
this.connectivityERR = [ return '';
'ERR_INTERNET_DISCONNECTED',
'ERR_PROXY_CONNECTION_FAILED',
'ERR_CONNECTION_RESET',
'ERR_NOT_CONNECTED',
'ERR_NAME_NOT_RESOLVED',
'ERR_NETWORK_CHANGED'
];
this.userAgent = null;
return instance;
}
getOS(): string {
const platform = os.platform();
if (platform === 'darwin') {
return 'Mac';
} else if (platform === 'linux') {
return 'Linux';
} else if (platform === 'win32') {
if (parseFloat(os.release()) < 6.2) {
return 'Windows 7';
} else {
return 'Windows 10';
}
} else {
return '';
}
}
setUserAgent(webViewUserAgent: string): void {
this.userAgent = `ZulipElectron/${app.getVersion()} ${webViewUserAgent}`;
}
getUserAgent(): string | null {
if (!this.userAgent) {
this.setUserAgent(ConfigUtil.getConfigItem('userAgent', null));
}
return this.userAgent;
} }
} }
export = new SystemUtil(); export function setUserAgent(webViewUserAgent: string): void {
userAgent = `ZulipElectron/${app.getVersion()} ${webViewUserAgent}`;
}
export function getUserAgent(): string | null {
if (!userAgent) {
setUserAgent(ConfigUtil.getConfigItem('userAgent', null));
}
return userAgent;
}

View File

@@ -1,10 +1,7 @@
'use strict'; import path from 'path';
import electron from 'electron';
import i18n from 'i18n';
import path = require('path');
import electron = require('electron');
import i18n = require('i18n');
let instance: TranslationUtil = null;
let app: Electron.App = null; let app: Electron.App = null;
/* To make the util runnable in both main and renderer process */ /* To make the util runnable in both main and renderer process */
@@ -14,22 +11,10 @@ if (process.type === 'renderer') {
app = electron.app; app = electron.app;
} }
class TranslationUtil { i18n.configure({
constructor() { directory: path.join(__dirname, '../../../translations/')
if (instance) { });
return this;
}
instance = this; export function __(phrase: string): string {
i18n.configure({ return i18n.__({ phrase, locale: app.getLocale() ? app.getLocale() : 'en' });
directory: path.join(__dirname, '../../../translations/'),
register: this
});
}
__(phrase: string): string {
return i18n.__({ phrase, locale: app.getLocale() ? app.getLocale() : 'en' });
}
} }
export = new TranslationUtil();

View File

@@ -23,7 +23,4 @@
</div> </div>
</div> </div>
</body> </body>
<script>var exports = {};</script>
<script src="js/preload.js"></script>
<script>require('./js/shared/preventdrag.js')</script>
</html> </html>

Binary file not shown.

View File

@@ -3,50 +3,46 @@ interface DialogBoxError {
content: string; content: string;
} }
class Messages { export function invalidZulipServerError(domain: string): string {
invalidZulipServerError(domain: string): string { return `${domain} does not appear to be a valid Zulip server. Make sure that
return `${domain} does not appear to be a valid Zulip server. Make sure that \n • You can connect to that URL in a web browser.\
\n • You can connect to that URL in a web browser.\ \n • If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.\
\n • If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.\ \n • It's a Zulip server. (The oldest supported version is 1.6).\
\n • It's a Zulip server. (The oldest supported version is 1.6).\ \n • The server has a valid certificate. (You can add custom certificates in Settings > Organizations). \
\n • The server has a valid certificate. (You can add custom certificates in Settings > Organizations). \ \n • The SSL is correctly configured for the certificate. Check out the SSL troubleshooting guide -
\n • The SSL is correctly configured for the certificate. Check out the SSL troubleshooting guide - \n https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`;
\n https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`;
}
noOrgsError(domain: string): string {
return `${domain} does not have any organizations added.\
\nPlease contact your server administrator.`;
}
certErrorMessage(domain: string, error: string): string {
return `Do you trust certificate from ${domain}? \n ${error}`;
}
certErrorDetail(): string {
return `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.`;
}
enterpriseOrgError(length: number, domains: string[]): DialogBoxError {
let domainList = '';
for (const domain of domains) {
domainList += `${domain}\n`;
}
return {
title: `Could not add the following ${length === 1 ? `organization` : `organizations`}`,
content: `${domainList}\nPlease contact your system administrator.`
};
}
orgRemovalError(url: string): DialogBoxError {
return {
title: `Removing ${url} is a restricted operation.`,
content: `Please contact your system administrator.`
};
}
} }
export = new Messages(); export function noOrgsError(domain: string): string {
return `${domain} does not have any organizations added.\
\nPlease contact your server administrator.`;
}
export function certErrorMessage(domain: string, error: string): string {
return `Do you trust certificate from ${domain}? \n ${error}`;
}
export function certErrorDetail(): string {
return `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.`;
}
export function enterpriseOrgError(length: number, domains: string[]): DialogBoxError {
let domainList = '';
for (const domain of domains) {
domainList += `${domain}\n`;
}
return {
title: `Could not add the following ${length === 1 ? 'organization' : 'organizations'}`,
content: `${domainList}\nPlease contact your system administrator.`
};
}
export function orgRemovalError(url: string): DialogBoxError {
return {
title: `Removing ${url} is a restricted operation.`,
content: 'Please contact your system administrator.'
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 737 B

After

Width:  |  Height:  |  Size: 737 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -114,5 +114,9 @@
"Emoji & Symbols": "Emoji & Symbols", "Emoji & Symbols": "Emoji & Symbols",
"View Shortcuts": "View Shortcuts", "View Shortcuts": "View Shortcuts",
"Enter Full Screen": "Enter Full Screen", "Enter Full Screen": "Enter Full Screen",
"History Shortcuts": "History Shortcuts" "History Shortcuts": "History Shortcuts",
"Quit when the window is closed": "Quit when the window is closed",
"File": "File",
"Network and Proxy Settings": "Network and Proxy Settings",
"Ask where to save files before downloading": "Ask where to save files before downloading"
} }

View File

@@ -104,5 +104,16 @@
"Edit Shortcuts": "Edit Shortcuts", "Edit Shortcuts": "Edit Shortcuts",
"View Shortcuts": "View Shortcuts", "View Shortcuts": "View Shortcuts",
"History Shortcuts": "History Shortcuts", "History Shortcuts": "History Shortcuts",
"Window Shortcuts": "Window Shortcuts" "Window Shortcuts": "Window Shortcuts",
} "Quit when the window is closed": "Quit when the window is closed",
"Force social login in app instead of browser": "Force social login in app instead of browser",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Add a Zulip organization": "Add a Zulip organization",
"Connect": "Connect",
"OR": "OR",
"Create a new organization": "Create a new organization",
"Network and Proxy Settings": "Network and Proxy Settings",
"YES": "YES",
"NO": "NO",
"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?"
}

View File

@@ -114,5 +114,6 @@
"Zulip Help": "Zulip Help", "Zulip Help": "Zulip Help",
"keyboard shortcuts": "keyboard shortcuts", "keyboard shortcuts": "keyboard shortcuts",
"script": "script", "script": "script",
"Quit when the window is closed": "Quit when the window is closed" "Quit when the window is closed": "Quit when the window is closed",
"Ask where to save files before downloading": "Ask where to save files before downloading"
} }

View File

@@ -5,10 +5,10 @@ platform:
os: Previous Visual Studio 2015 os: Previous Visual Studio 2015
cache: cache:
- node_modules - node_modules -> appveyor.yml
install: install:
- ps: Install-Product node 8 x64 - ps: Install-Product node 10 x64
- git reset --hard HEAD - git reset --hard HEAD
- npm install npm -g - npm install npm -g
- node --version - node --version

View File

@@ -2,6 +2,66 @@
All notable changes to the Zulip desktop app are documented in this file. All notable changes to the Zulip desktop app are documented in this file.
### v5.0.0 --2020-03-30
**Security fixes**:
* CVE-2020-10856: Enable Electron context isolation. (Reported by Matt Austin.)
* CVE-2020-10857: Fix unsafe use of `shell.openExternal`/`shell.openItem`. (Reported by Matt Austin.)
* Downloaded files will no longer be opened directly; the previous option to show downloaded files in the file manager is now always on.
* CVE-2020-10858: Add permission request handler to guard against audio/video recording by a malicious server. (Reported by Matt Austin.)
**New features**:
* Add an option to prompt for the location to save each downloaded file.
**Fixes**:
* Fix automatic launching at startup.
* Fix Undo and Redo functionality on macOS.
**Dependencies**:
* Upgrade all dependencies, including Electron 8.0.3.
* Remove `assert`, `cp-file`, `dotenv`, `electron-debug`, and `wurl`.
**Deprecations**:
* Since Electron upstream has discontinued support for 32-bit Linux, we will only provide 32-bit Linux builds on a best effort basis, and they will likely be removed in a future release.
### v4.0.3 --2020-02-29
**Security fixes**:
* CVE-2020-9443: Do not disable web security in the Electron webview. (Reported by Matt Austin.)
### v4.0.2-beta --2019-11-13
**New features**:
* Add support for MSI installers.
* Show badge count on Linux.
* Sync system presence info to web app.
* Add option to open notification settings from the context menu of organization icons in the sidebar.
* Tackle network issues with each Zulip organization independently.
* Add option to quit on closing the window.
* Make certificate location dynamic.
* Add option to specify network settings when adding a new organization.
**Enhancements**:
* Load last active tab before others, speeding up user experience and eliminating flashing of server icons.
* Improve UX for notification settings.
* Set User-Agent from the main process for communication with the Zulip API.
* Add SSL troubleshooting guide in error message when adding an organization fails.
**Fixes**:
* Fix translations for `ru` locales.
* Fix trailing brackets in settings page.
* Fix broken icon issue faced by the snap package on Linux.
* Reactivate `network.js` script.
* Enable notarization for macOS Catalina.
**Documentation**:
* Document enterprise configuration features.
* Update the Electron tutorial guide.
* Explicitly address where to report bugs in `README.md`.
* Fix typo in the link to server/webapp repository in `README.md`.
* Add documentation for translation.
### v4.0.0 --2019-08-08 ### v4.0.0 --2019-08-08
**New features**: **New features**:

View File

@@ -9,7 +9,7 @@ To build and run the app from source, you'll need the following:
* [Git](http://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [Git](http://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* Use our [Git Guide](https://zulip.readthedocs.io/en/latest/git/setup.html) to get started with Git and GitHub. * Use our [Git Guide](https://zulip.readthedocs.io/en/latest/git/setup.html) to get started with Git and GitHub.
* [Node.js](https://nodejs.org) >= v6.9.0 * [Node.js](https://nodejs.org) >= v10.16.3
* [NPM](https://www.npmjs.com/get-npm) and * [NPM](https://www.npmjs.com/get-npm) and
[node-gyp](https://github.com/nodejs/node-gyp#installation), [node-gyp](https://github.com/nodejs/node-gyp#installation),
if they don't come bundled with your Node.js installation if they don't come bundled with your Node.js installation
@@ -111,4 +111,4 @@ operating system you're running on:
To generate all three types of files, you will need all three operating To generate all three types of files, you will need all three operating
systems. systems.
The output distributable packages appear in the `dist/` directory. The output distributable packages appear in the `dist/` directory.

View File

@@ -5,14 +5,14 @@ const electron = require('electron-connect').server.create({
}); });
const tape = require('gulp-tape'); const tape = require('gulp-tape');
const tapColorize = require('tap-colorize'); const tapColorize = require('tap-colorize');
const ts = require("gulp-typescript"); const ts = require('gulp-typescript');
const tsProject = ts.createProject("tsconfig.json"); const tsProject = ts.createProject('tsconfig.json');
const glob = require('glob'); const glob = require('glob');
const { execSync } = require('child_process'); const {execSync} = require('child_process');
const baseFilePattern = 'app/+(main|renderer)/**/*'; const baseFilePattern = 'app/+(main|renderer)/**/*';
const globOptions = { cwd: __dirname }; const globOptions = {cwd: __dirname};
const jsFiles = glob.sync(baseFilePattern + '.js', globOptions); const jsFiles = glob.sync(baseFilePattern + '.js', globOptions);
const tsFiles = glob.sync(baseFilePattern + '.ts', globOptions); const tsFiles = glob.sync(baseFilePattern + '.ts', globOptions);
if (jsFiles.length !== tsFiles.length) { if (jsFiles.length !== tsFiles.length) {
@@ -21,18 +21,18 @@ if (jsFiles.length !== tsFiles.length) {
execSync(`${npx} tsc`); execSync(`${npx} tsc`);
} }
gulp.task("compile", function () { gulp.task('compile', () => {
return tsProject.src() return tsProject.src()
.pipe(tsProject()) .pipe(tsProject())
.js.pipe(gulp.dest("app")); .js.pipe(gulp.dest('app'));
}); });
gulp.task('dev', () => { gulp.task('dev', () => {
// Start browser process // Start browser process
electron.start(); electron.start();
// Restart browser process // Restart browser process
gulp.watch('app/main/*.ts', gulp.series('compile', 'restart:browser')); gulp.watch('app/main/*.ts', gulp.series('compile', 'restart:browser'));
// Reload renderer process // Reload renderer process
gulp.watch('app/renderer/css/*.css', gulp.series('reload:renderer')); gulp.watch('app/renderer/css/*.css', gulp.series('reload:renderer'));
gulp.watch('app/renderer/*.html', gulp.series('reload:renderer')); gulp.watch('app/renderer/*.html', gulp.series('reload:renderer'));
gulp.watch('app/renderer/js/**/*.ts', gulp.series('compile', 'reload:renderer')); gulp.watch('app/renderer/js/**/*.ts', gulp.series('compile', 'reload:renderer'));
@@ -45,16 +45,16 @@ gulp.task('restart:browser', done => {
}); });
gulp.task('reload:renderer', done => { gulp.task('reload:renderer', done => {
// Reload renderer process // Reload renderer process
electron.reload(); electron.reload();
done(); done();
}); });
gulp.task('test-e2e', () => { gulp.task('test-e2e', () => {
return gulp.src('tests/*.js') return gulp.src('tests/*.js')
.pipe(tape({ .pipe(tape({
reporter: tapColorize() reporter: tapColorize()
})); }));
}); });
gulp.task('default', gulp.parallel('dev', 'test-e2e')); gulp.task('default', gulp.parallel('dev', 'test-e2e'));

12876
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "zulip", "name": "zulip",
"productName": "Zulip", "productName": "Zulip",
"version": "4.0.2-beta", "version": "5.0.0",
"main": "./app/main", "main": "./app/main",
"description": "Zulip Desktop App", "description": "Zulip Desktop App",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -18,18 +18,18 @@
"url": "https://github.com/zulip/zulip-desktop/issues" "url": "https://github.com/zulip/zulip-desktop/issues"
}, },
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=10.0.0"
}, },
"scripts": { "scripts": {
"start": "node tools/run-dev", "start": "node tools/run-dev",
"clean-ts-files": "git clean app/*.js -xf", "clean-ts-files": "git clean app/*.js -e node_modules -xf",
"watch-ts": "tsc -w", "watch-ts": "tsc -w",
"reinstall": "node ./tools/reinstall-node-modules.js", "reinstall": "node ./tools/reinstall-node-modules.js",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"lint-css": "stylelint app/renderer/css/*.css", "lint-css": "stylelint app/renderer/css/*.css",
"lint-html": "./node_modules/.bin/htmlhint \"app/renderer/*.html\" ", "lint-html": "./node_modules/.bin/htmlhint \"app/renderer/*.html\" ",
"lint-js": "xo", "lint-js": "xo",
"test": "npm run lint-html && npm run lint-css && npm run lint-js", "test": "tsc --noEmit && npm run lint-html && npm run lint-css && npm run lint-js",
"test-e2e": "gulp test-e2e", "test-e2e": "gulp test-e2e",
"compile": "gulp compile", "compile": "gulp compile",
"dev": "gulp dev && npm test", "dev": "gulp dev && npm test",
@@ -50,9 +50,8 @@
"**/*.node" "**/*.node"
], ],
"files": [ "files": [
"**/*", "app/**/*",
"!docs${/*}", "!**/node_modules/cld/deps/cld"
"!node_modules/@paulcbetts/cld/deps/cld${/*}"
], ],
"copyright": "©2019 Kandra Labs, Inc.", "copyright": "©2019 Kandra Labs, Inc.",
"mac": { "mac": {
@@ -143,97 +142,117 @@
"Desktop app", "Desktop app",
"InstantMessaging" "InstantMessaging"
], ],
"dependencies": {
"@electron-elements/send-feedback": "^1.0.8",
"@sentry/electron": "^1.3.0",
"adm-zip": "^0.4.14",
"auto-launch": "^5.0.5",
"backoff": "^2.5.0",
"electron-is-dev": "^1.1.0",
"electron-log": "^4.1.0",
"electron-spellchecker": "^2.2.1",
"electron-updater": "^4.2.5",
"electron-window-state": "^5.0.3",
"escape-html": "^1.0.3",
"fs-extra": "^9.0.0",
"i18n": "^0.8.6",
"node-json-db": "^1.0.3",
"request": "^2.88.2",
"rxjs": "^5.5.12",
"semver": "^7.1.3"
},
"optionalDependencies": {
"node-mac-notifier": "^1.1.0"
},
"devDependencies": { "devDependencies": {
"@types/adm-zip": "^0.4.32", "@types/adm-zip": "^0.4.32",
"@types/dotenv": "6.1.1", "@types/auto-launch": "^5.0.1",
"@typescript-eslint/eslint-plugin": "1.10.2", "@types/backoff": "^2.5.1",
"@typescript-eslint/parser": "1.10.2", "@types/electron-spellchecker": "^1.1.2",
"@vitalets/google-translate-api": "2.8.0", "@types/escape-html": "0.0.20",
"assert": "1.4.1", "@types/fs-extra": "^8.1.0",
"cp-file": "5.0.0", "@types/i18n": "^0.8.6",
"devtron": "1.4.0", "@types/node": "^12.12.31",
"electron": "3.1.10", "@types/request": "^2.48.4",
"electron-builder": "20.43.0", "@types/requestidlecallback": "^0.3.1",
"electron-connect": "0.6.2", "@typescript-eslint/eslint-plugin": "^2.25.0",
"electron-debug": "1.4.0", "@typescript-eslint/parser": "^2.25.0",
"electron-notarize": "0.2.0", "@vitalets/google-translate-api": "^3.0.0",
"eslint-config-xo-typescript": "0.14.0", "devtron": "^1.4.0",
"fs-extra": "8.1.0", "dotenv": "^8.2.0",
"gulp": "4.0.0", "electron": "^8.1.1",
"gulp-tape": "0.0.9", "electron-builder": "^22.4.1",
"gulp-typescript": "5.0.1", "electron-connect": "^0.6.3",
"htmlhint": "0.11.0", "electron-notarize": "^0.2.1",
"is-ci": "1.0.10", "eslint-config-xo-typescript": "^0.26.0",
"nodemon": "1.14.11", "glob": "^7.1.6",
"pre-commit": "1.2.2", "gulp": "^4.0.2",
"spectron": "5.0.0", "gulp-tape": "^1.0.0",
"stylelint": "9.10.1", "gulp-typescript": "^6.0.0-alpha.1",
"tap-colorize": "1.2.0", "htmlhint": "^0.11.0",
"tape": "4.8.0", "nodemon": "^2.0.2",
"typescript": "3.5.2", "pre-commit": "^1.2.2",
"xo": "0.24.0" "rimraf": "^3.0.2",
"spectron": "^10.0.1",
"stylelint": "^13.2.1",
"tap-colorize": "^1.2.0",
"tape": "^4.13.2",
"typescript": "^3.8.3",
"xo": "^0.28.0"
}, },
"xo": { "xo": {
"extends": "xo-typescript", "rules": {
"extensions": [ "@typescript-eslint/member-ordering": "off",
"ts" "@typescript-eslint/no-dynamic-delete": "off",
], "@typescript-eslint/no-unused-vars": "off",
"parserOptions": { "@typescript-eslint/restrict-plus-operands": "off",
"ecmaVersion": 6, "@typescript-eslint/restrict-template-expressions": "off",
"sourceType": "module", "capitalized-comments": "off",
"ecmaFeatures": { "import/no-mutable-exports": "off",
"globalReturn": true, "import/unambiguous": "error",
"modules": true "max-lines": [
} "warn",
}, {
"esnext": true, "max": 900,
"overrides": [ "skipBlankLines": true,
{ "skipComments": true
"files": "app/**/*.ts",
"rules": {
"max-lines": [
"warn",
{
"max": 800,
"skipBlankLines": true,
"skipComments": true
}
],
"no-warning-comments": 0,
"object-curly-spacing": 0,
"capitalized-comments": 0,
"no-else-return": 0,
"no-path-concat": 0,
"no-alert": 0,
"guard-for-in": 0,
"prefer-promise-reject-errors": 0,
"import/no-unresolved": 0,
"import/no-extraneous-dependencies": 0,
"no-prototype-builtins": 0,
"lines-between-class-members": 0,
"padding-line-between-statements": 0,
"quotes": 0,
"unicorn/catch-error-name": 0,
"unicorn/no-console-spaces": 0,
"node/no-deprecated-api": 0,
"@typescript-eslint/member-ordering": "never",
"@typescript-eslint/restrict-plus-operands": "never",
"import/default": 0,
"@typescript-eslint/no-unused-vars": 0
} }
} ],
], "no-alert": "off",
"ignore": [ "no-else-return": "off",
"tests/*.js", "no-warning-comments": "off",
"tools/locale-helper/*.js", "object-curly-spacing": "off",
"*/**/*.js", "padding-line-between-statements": "off",
"*.js", "strict": "error",
"typings.d.ts" "unicorn/catch-error-name": "off",
], "unicorn/string-content": "off"
},
"envs": [ "envs": [
"node", "node",
"browser", "browser"
"mocha" ],
"overrides": [
{
"files": [
"app/renderer/js/injected.ts",
"gulpfile.js",
"scripts/notarize.js",
"tests/**/*.js",
"tools/locale-helper/index.js",
"tools/reinstall-node-modules.js"
],
"parserOptions": {
"sourceType": "script"
}
},
{
"files": [
"**/*.d.ts"
],
"rules": {
"import/unambiguous": "off"
}
}
] ]
} }
} }

View File

@@ -1,22 +1,23 @@
'use strict';
const path = require('path'); const path = require('path');
const dotenv = require('dotenv'); const dotenv = require('dotenv');
dotenv.config({ path: path.join(__dirname, '/../.env') }); dotenv.config({path: path.join(__dirname, '/../.env')});
const { notarize } = require('electron-notarize'); const {notarize} = require('electron-notarize');
exports.default = async function notarizing(context) { exports.default = async function (context) {
const { electronPlatformName, appOutDir } = context; const {electronPlatformName, appOutDir} = context;
if (electronPlatformName !== 'darwin') { if (electronPlatformName !== 'darwin') {
return; return;
} }
const appName = context.packager.appInfo.productFilename; const appName = context.packager.appInfo.productFilename;
return await notarize({ return notarize({
appBundleId: 'org.zulip.zulip-electron', appBundleId: 'org.zulip.zulip-electron',
appPath: `${appOutDir}/${appName}.app`, appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID, appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASS, appleIdPassword: process.env.APPLE_ID_PASS
}); });
}; };

View File

@@ -1,7 +1,7 @@
const path = require('path') 'use strict';
const TEST_APP_PRODUCT_NAME = 'ZulipTest' const TEST_APP_PRODUCT_NAME = 'ZulipTest';
module.exports = { module.exports = {
TEST_APP_PRODUCT_NAME TEST_APP_PRODUCT_NAME
} };

View File

@@ -1,183 +0,0 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "2.4.0",
"main": "../../app/main",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"copyright": "Kandra Labs, Inc.",
"author": {
"name": "Kandra Labs, Inc.",
"email": "support@zulipchat.com"
},
"repository": {
"type": "git",
"url": "https://github.com/zulip/zulip-electron.git"
},
"bugs": {
"url": "https://github.com/zulip/zulip-electron/issues"
},
"engines": {
"node": ">=6.0.0"
},
"scripts": {
"start": "electron app --disable-http-cache --no-electron-connect",
"reinstall": "node ./tools/reinstall-node-modules.js",
"postinstall": "electron-builder install-app-deps",
"test": "xo",
"test-e2e": "gulp test-e2e",
"dev": "gulp dev & nodemon --watch app/main --watch app/renderer --exec 'npm test' -e html,css,js",
"pack": "electron-builder --dir",
"dist": "electron-builder",
"mas": "electron-builder --mac mas",
"travis": "cd ./scripts && ./travis-build-test.sh",
"build-locales": "node tools/locale-helper"
},
"pre-commit": [
"test"
],
"build": {
"appId": "org.zulip.zulip-electron",
"asar": true,
"files": [
"**/*",
"!docs${/*}",
"!node_modules/@paulcbetts/cld/deps/cld${/*}"
],
"copyright": "©2017 Kandra Labs, Inc.",
"mac": {
"category": "public.app-category.productivity",
"artifactName": "${productName}-${version}-${arch}.${ext}"
},
"linux": {
"category": "Chat;GNOME;GTK;Network;InstantMessaging",
"packageCategory": "GNOME;GTK;Network;InstantMessaging",
"description": "Zulip Desktop Client for Linux",
"target": [
"deb",
"zip",
"AppImage",
"snap"
],
"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": 100,
"contents": [
{
"x": 380,
"y": 280,
"type": "link",
"path": "/Applications"
},
{
"x": 110,
"y": 280,
"type": "file"
}
],
"window": {
"width": 500,
"height": 500
}
},
"win": {
"target": [
{
"target": "nsis-web",
"arch": [
"x64",
"ia32"
]
}
],
"icon": "build/icon.ico",
"publisherName": "Kandra Labs, Inc."
},
"nsis": {
"allowToChangeInstallationDirectory": true
}
},
"keywords": [
"Zulip",
"Group Chat app",
"electron-app",
"electron",
"Desktop app",
"InstantMessaging"
],
"devDependencies": {
"assert": "1.4.1",
"cp-file": "^5.0.0",
"devtron": "1.4.0",
"electron": "3.0.10",
"electron-builder": "20.38.4",
"electron-connect": "0.6.2",
"electron-debug": "1.4.0",
"google-translate-api": "2.3.0",
"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.8.0",
"tap-colorize": "^1.2.0",
"tape": "^4.8.0",
"xo": "0.18.2"
},
"xo": {
"parserOptions": {
"sourceType": "script",
"ecmaFeatures": {
"globalReturn": true
}
},
"esnext": true,
"overrides": [
{
"files": "app*/**/*.js",
"rules": {
"max-lines": [
"warn",
{
"max": 600,
"skipBlankLines": true,
"skipComments": true
}
],
"no-warning-comments": 0,
"object-curly-spacing": 0,
"capitalized-comments": 0,
"no-else-return": 0,
"no-path-concat": 0,
"no-alert": 0,
"guard-for-in": 0,
"prefer-promise-reject-errors": 0,
"import/no-unresolved": 0,
"import/no-extraneous-dependencies": 0,
"no-prototype-builtins": 0
}
}
],
"ignore": [
"tests/*.js",
"tools/locale-helper/*.js"
],
"envs": [
"node",
"browser",
"mocha"
]
}
}

View File

@@ -1,13 +1,17 @@
const test = require('tape') 'use strict';
const setup = require('./setup') const test = require('tape');
const setup = require('./setup');
test('app runs', function (t) { test('app runs', async t => {
t.timeoutAfter(10e3) t.timeoutAfter(10e3);
setup.resetTestDataDir() setup.resetTestDataDir();
const app = setup.createApp() const app = setup.createApp();
setup.waitForLoad(app, t) try {
.then(() => app.client.windowByIndex(1)) // focus on webview await setup.waitForLoad(app, t);
.then(() => app.client.waitForExist('//*[@id="connect"]')) // id of the connect button await app.client.windowByIndex(1); // Focus on webview
.then(() => setup.endTest(app, t), await app.client.waitForExist('//*[@id="connect"]'); // Id of the connect button
(err) => setup.endTest(app, t, err || 'error')) await setup.endTest(app, t);
}) } catch (error) {
await setup.endTest(app, t, error || 'error');
}
});

View File

@@ -1,99 +1,96 @@
const Application = require('spectron').Application 'use strict';
const cpFile = require('cp-file') const {Application} = require('spectron');
const fs = require('fs') const fs = require('fs');
const isCI = require('is-ci') const path = require('path');
const mkdirp = require('mkdirp') const rimraf = require('rimraf');
const path = require('path')
const rimraf = require('rimraf')
const config = require('./config') const config = require('./config');
module.exports = { module.exports = {
createApp, createApp,
endTest, endTest,
waitForLoad, waitForLoad,
wait, wait,
resetTestDataDir resetTestDataDir
} };
// Runs Zulip Desktop. // Runs Zulip Desktop.
// Returns a promise that resolves to a Spectron Application once the app has loaded. // Returns a promise that resolves to a Spectron Application once the app has loaded.
// Takes a Tape test. Makes some basic assertions to verify that the app loaded correctly. // Takes a Tape test. Makes some basic assertions to verify that the app loaded correctly.
function createApp (t) { function createApp() {
generateTestAppPackageJson() generateTestAppPackageJson();
return new Application({ return new Application({
path: path.join(__dirname, '..', 'node_modules', '.bin', path: path.join(__dirname, '..', 'node_modules', '.bin',
'electron' + (process.platform === 'win32' ? '.cmd' : '')), 'electron' + (process.platform === 'win32' ? '.cmd' : '')),
args: [path.join(__dirname)], // Ensure this dir has a package.json file with a 'main' entry piont args: [path.join(__dirname)], // Ensure this dir has a package.json file with a 'main' entry piont
env: {NODE_ENV: 'test'}, env: {NODE_ENV: 'test'},
waitTimeout: 10e3 waitTimeout: 10e3
}) });
} }
// Generates package.json for test app // Generates package.json for test app
// Reads app package.json and updates the productName to config.TEST_APP_PRODUCT_NAME // Reads app package.json and updates the productName to config.TEST_APP_PRODUCT_NAME
// We do this so that the app integration doesn't doesn't share the same appDataDir as the dev application // We do this so that the app integration doesn't doesn't share the same appDataDir as the dev application
function generateTestAppPackageJson () { function generateTestAppPackageJson() {
let packageJson = require(path.join(__dirname, '../package.json')) const packageJson = require(path.join(__dirname, '../package.json'));
packageJson.productName = config.TEST_APP_PRODUCT_NAME packageJson.productName = config.TEST_APP_PRODUCT_NAME;
packageJson.main = '../app/main' packageJson.main = '../app/main';
const testPackageJsonPath = path.join(__dirname, 'package.json') const testPackageJsonPath = path.join(__dirname, 'package.json');
fs.writeFileSync(testPackageJsonPath, JSON.stringify(packageJson, null, ' '), 'utf-8') fs.writeFileSync(testPackageJsonPath, JSON.stringify(packageJson, null, ' '), 'utf-8');
} }
// Starts the app, waits for it to load, returns a promise // Starts the app, waits for it to load, returns a promise
function waitForLoad (app, t, opts) { async function waitForLoad(app, t, options) {
if (!opts) opts = {} if (!options) {
return app.start().then(function () { options = {};
return app.client.waitUntilWindowLoaded() }
})
.then(function() { await app.start();
return app.client.pause(2000); await app.client.waitUntilWindowLoaded();
}) await app.client.pause(2000);
.then(function () { const title = await app.webContents.getTitle();
return app.webContents.getTitle() t.equal(title, 'Zulip', 'html title');
}).then(function (title) {
t.equal(title, 'Zulip', 'html title')
})
} }
// Returns a promise that resolves after 'ms' milliseconds. Default: 1 second // Returns a promise that resolves after 'ms' milliseconds. Default: 1 second
function wait (ms) { async function wait(ms) {
if (ms === undefined) ms = 1000 // Default: wait long enough for the UI to update if (ms === undefined) {
return new Promise(function (resolve, reject) { ms = 1000;
setTimeout(resolve, ms) } // Default: wait long enough for the UI to update
})
return new Promise((resolve => {
setTimeout(resolve, ms);
}));
} }
// Quit the app, end the test, either in success (!err) or failure (err) // Quit the app, end the test, either in success (!err) or failure (err)
function endTest (app, t, err) { async function endTest(app, t, err) {
return app.stop().then(function () { await app.stop();
t.end(err) t.end(err);
})
} }
function getAppDataDir () { function getAppDataDir() {
let base let base;
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
base = path.join(process.env.HOME, 'Library', 'Application Support') base = path.join(process.env.HOME, 'Library', 'Application Support');
} else if (process.platform === 'linux') { } else if (process.platform === 'linux') {
base = process.env.XDG_CONFIG_HOME ? base = process.env.XDG_CONFIG_HOME ?
process.env.XDG_CONFIG_HOME : path.join(process.env.HOME, '.config') process.env.XDG_CONFIG_HOME : path.join(process.env.HOME, '.config');
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
base = process.env.APPDATA base = process.env.APPDATA;
} else { } else {
console.log('Could not detect app data dir base. Exiting...') throw new Error('Could not detect app data dir base.');
process.exit(1) }
}
console.log('Detected App Data Dir base:', base) console.log('Detected App Data Dir base:', base);
return path.join(base, config.TEST_APP_PRODUCT_NAME) return path.join(base, config.TEST_APP_PRODUCT_NAME);
} }
// Resets the test directory, containing domain.json, window-state.json, etc // Resets the test directory, containing domain.json, window-state.json, etc
function resetTestDataDir () { function resetTestDataDir() {
appDataDir = getAppDataDir() const appDataDir = getAppDataDir();
rimraf.sync(appDataDir) rimraf.sync(appDataDir);
rimraf.sync(path.join(__dirname, 'package.json')) rimraf.sync(path.join(__dirname, 'package.json'));
} }

View File

@@ -1,19 +1,22 @@
const test = require('tape') 'use strict';
const setup = require('./setup') const test = require('tape');
const setup = require('./setup');
test('add-organization', function (t) {
t.timeoutAfter(50e3)
setup.resetTestDataDir()
const app = setup.createApp()
setup.waitForLoad(app, t)
.then(() => app.client.windowByIndex(1)) // focus on webview
.then(() => app.client.setValue('.setting-input-value', 'chat.zulip.org'))
.then(() => app.client.click('.server-save-action'))
.then(() => setup.wait(5000))
.then(() => app.client.windowByIndex(0)) // Switch focus back to main win
.then(() => app.client.windowByIndex(1)) // Switch focus back to org webview
.then(() => app.client.waitForExist('//*[@id="id_username"]'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})
test('add-organization', async t => {
t.timeoutAfter(50e3);
setup.resetTestDataDir();
const app = setup.createApp();
try {
await setup.waitForLoad(app, t);
await app.client.windowByIndex(1); // Focus on webview
await app.client.setValue('.setting-input-value', 'chat.zulip.org');
await app.client.click('#connect');
await setup.wait(5000);
await app.client.windowByIndex(0); // Switch focus back to main win
await app.client.windowByIndex(1); // Switch focus back to org webview
await app.client.waitForExist('//*[@id="id_username"]');
await setup.endTest(app, t);
} catch (error) {
await setup.endTest(app, t, error || 'error');
}
});

View File

@@ -1,17 +1,20 @@
const test = require('tape') 'use strict';
const setup = require('./setup') const test = require('tape');
const setup = require('./setup');
// Create new org link should open in the default browser [WIP] // Create new org link should open in the default browser [WIP]
test('new-org-link', function (t) { test('new-org-link', async t => {
t.timeoutAfter(50e3) t.timeoutAfter(50e3);
setup.resetTestDataDir() setup.resetTestDataDir();
const app = setup.createApp() const app = setup.createApp();
setup.waitForLoad(app, t) try {
.then(() => app.client.windowByIndex(1)) // focus on webview await setup.waitForLoad(app, t);
.then(() => app.client.click('#open-create-org-link')) // Click on new org link button await app.client.windowByIndex(1); // Focus on webview
.then(() => setup.wait(5000)) await app.client.click('#open-create-org-link'); // Click on new org link button
.then(() => setup.endTest(app, t), await setup.wait(5000);
(err) => setup.endTest(app, t, err || 'error')) await setup.endTest(app, t);
}) } catch (error) {
await setup.endTest(app, t, error || 'error');
}
});

View File

@@ -1,3 +1,4 @@
'use strict';
const translate = require('@vitalets/google-translate-api'); const translate = require('@vitalets/google-translate-api');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
@@ -5,28 +6,30 @@ const fs = require('fs');
const translationDir = path.resolve(__dirname, '../../app/translations'); const translationDir = path.resolve(__dirname, '../../app/translations');
function writeJSON(file, data) { function writeJSON(file, data) {
const filePath = path.resolve(translationDir, file); const filePath = path.resolve(translationDir, file);
fs.writeFileSync(filePath, `${JSON.stringify(data, null, '\t')}\n`, 'utf8'); fs.writeFileSync(filePath, `${JSON.stringify(data, null, '\t')}\n`, 'utf8');
} }
const { phrases } = require('./locale-template'); const {phrases} = require('./locale-template');
const supportedLocales = require('./supported-locales'); const supportedLocales = require('./supported-locales.json');
phrases.sort(); phrases.sort();
for (let locale in supportedLocales) { for (const [locale, name] of Object.entries(supportedLocales)) {
console.log(`fetching translation for: ${supportedLocales[locale]} - ${locale}..`); console.log(`fetching translation for: ${name} - ${locale}..`);
translate(phrases.join('\n'), { to: locale }) (async () => {
.then(res => { try {
const localeFile = `${locale}.json`; const result = await translate(phrases.join('\n'), {to: locale});
const translatedText = res.text.split('\n'); const localeFile = `${locale}.json`;
const translationJSON = {}; const translatedText = result.text.split('\n');
phrases.forEach((phrase, index) => { const translationJSON = {};
translationJSON[phrase] = translatedText[index]; phrases.forEach((phrase, index) => {
}); translationJSON[phrase] = translatedText[index];
});
writeJSON(localeFile, translationJSON); writeJSON(localeFile, translationJSON);
console.log(`create: ${localeFile}`); console.log(`create: ${localeFile}`);
}).catch(err => { } catch (error) {
console.error(err); console.error(error);
}); }
})();
} }

View File

@@ -1,4 +1,4 @@
module.exports = { {
"de": "Deutsch", "de": "Deutsch",
"pl": "polski", "pl": "polski",
"en": "English", "en": "English",
@@ -18,4 +18,4 @@ module.exports = {
"es": "español", "es": "español",
"ko": "한국어", "ko": "한국어",
"fr": "français" "fr": "français"
}; }

View File

@@ -2,9 +2,8 @@
set -e set -e
set -x set -x
echo "Removing node_modules and app/node_modules" echo "Removing node_modules"
rm -rf node_modules rm -rf node_modules
rm -rf app/node_modules
echo "node_modules removed reinstalling npm packages" echo "node_modules removed reinstalling npm packages"
npm i npm i

View File

@@ -1,8 +1,7 @@
@echo off @echo off
echo "Removing node_modules and app/node_modules" echo "Removing node_modules"
rmdir /s /q node_modules rmdir /s /q node_modules
rmdir /s /q app/node_modules
echo "node_modules removed reinstalling npm packages" echo "node_modules removed reinstalling npm packages"
npm i npm i

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict';
const {exec} = require('child_process'); const {exec} = require('child_process');
const path = require('path'); const path = require('path');

View File

@@ -80,7 +80,7 @@ async function main() {
} }
await Promise.all([ await Promise.all([
run('Electron app', 'npx electron app --disable-http-cache --no-electron-connect'), run('Electron app', 'npx electron . --disable-http-cache --no-electron-connect'),
run('TypeScript watch mode', 'npx tsc --watch --pretty') run('TypeScript watch mode', 'npx tsc --watch --pretty')
]); ]);
} }

View File

@@ -5,7 +5,7 @@
* Electron is more or less Chrome, you can get developer tools using `CMD+ALT+I` * Electron is more or less Chrome, you can get developer tools using `CMD+ALT+I`
### Error : ChecksumMismatchError ### Error : ChecksumMismatchError
- Try deleteing `node_modules` && `app/node_modules` directories. Re-install dependencies using `npm install` - Try deleting the `node_modules` directory and reinstalling dependencies using `npm install`
### Error : Module version mismatch. Expected 50, got 51 ### Error : Module version mismatch. Expected 50, got 51
- Make sure you have installed [node-gyp](https://github.com/nodejs/node-gyp#installation) dependencies properly - Make sure you have installed [node-gyp](https://github.com/nodejs/node-gyp#installation) dependencies properly

View File

@@ -17,13 +17,5 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": false, "noUnusedParameters": false,
"noImplicitReturns": true "noImplicitReturns": true
}, }
"include":[
"app/**/*",
"typings.d.ts"
],
"exclude":[
"node_modules",
"app/node_modules"
]
} }

54
typings.d.ts vendored
View File

@@ -1,56 +1,2 @@
declare module 'adm-zip';
declare module 'auto-launch';
declare module 'is-online';
declare module 'request';
declare module 'semver';
declare module '@electron-elements/send-feedback'; declare module '@electron-elements/send-feedback';
declare module 'node-mac-notifier'; declare module 'node-mac-notifier';
declare module 'electron-connect';
declare module 'electron-is-dev';
declare module 'electron-spellchecker';
declare module 'escape-html';
declare module 'fs-extra';
declare module 'wurl';
declare module 'i18n';
declare module 'backoff';
interface PageParamsObject {
realm_uri: string;
default_language: string;
}
declare var page_params: PageParamsObject;
// since requestIdleCallback didn't make it into lib.dom.d.ts yet
declare function requestIdleCallback(callback: Function, options?: object): void;
// Patch Notification object so we can implement our side
// of Notification classes which we export into zulip side through
// preload.js; if we don't do his extending Notification will throw error.
// Relevant code is in app/renderer/js/notification/default-notification.ts
// and the relevant function is requestPermission.
declare var PatchedNotification: {
prototype: Notification;
new(title: string, options?: NotificationOptions): Notification;
readonly maxActions: number;
readonly permission: NotificationPermission;
requestPermission(): void;
}
// This is mostly zulip side of code we access from window
interface Window {
$: any;
narrow: any
Notification: typeof PatchedNotification;
}
// typescript doesn't have up to date NotificationOptions yet
interface NotificationOptions {
silent?: boolean;
}
interface ZulipWebWindow extends Window {
electron_bridge: any;
tray: any;
$: any;
lightbox: any;
}