Compare commits

...

112 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
Akash Nimare
95a9568ece beta-release: New release v4.0.2-beta. 2019-11-13 14:56:34 +05:30
Kanishk Kakar
e7a885a1fb macos: Enable notarization for macOS Catalina.
This fixes the issue for Catalina users.
2019-11-13 14:55:11 +05:30
Akash Nimare
17d4d97e2e certificate: Make certificate location dynamic.
* certificate: Make certificate location dynamic.

* Update certificate location for old servers.
2019-11-01 19:56:22 +05:30
Akash Nimare
3b14684058 Add docs for translation 2019-10-30 02:10:38 +05:30
Akash Nimare
7d592a0a1c validation: Add SSL troubleshooting guide in error message. 2019-10-25 21:03:28 +05:30
Ross Brunton
eb1be7106b Added option to quit on closing the window.
This adds a configuration option to quit the app (rather than going to
the tray) when the "close" button is clicked.
2019-10-14 01:05:18 +05:30
Tim Abbott
1da6e5d51d README: Fix typo in link to server/webapp repository. 2019-10-07 11:51:57 -07:00
Tim Abbott
dae7089c7e README: Explicitly address where to report bugs. 2019-10-01 15:41:39 -07:00
Muskan Khedia
30b40e2ff2 network: Prompts Desktop App to ask for network setting in add-org page.
Fixes: #540.
2019-09-28 17:44:34 +05:30
Akash Nimare
b76f01349a docs: Update electron tutorial guide.
Fixes: #826.
2019-09-27 23:02:25 +05:30
Kanishk Kakar
8446deb673 sidebar: Improve UX for notification settings.
* sidebar: Disable notif settings if not logged in.

* sidebar: Activate relevant tab for notif settings.
2019-09-25 18:25:00 +05:30
Kanishk Kakar
d4b9663257 network: Tackle network issues independently.
Few changes -
* webview: Show connection failure per server.
* network: Try to reconnect diff servers.
* Fixes concern that some proxy networks may allow only specific servers
to be reachable.
* domains: Show network error on server invalidation.
* webview: Handle network errors in preload script.
Fixes: #591, #312.
2019-09-24 18:22:19 +05:30
Akash Nimare
77044fd9fa enterprise: Document the enterprise feature. 2019-09-18 23:26:44 +05:30
Kanishk Kakar
177b77f0b5 sidebar: Add option to open notification setting from the context menu. 2019-09-15 21:20:08 +05:30
Kanishk Kakar
99b154b8ae system-util: Set User-Agent from main process.
* Sets user-agent config item when the app's DOM is ready.
* App sends the right User-Agent to the server-settings API.
 
Fixes #817.
2019-09-11 16:39:56 +05:30
Kanishk Kakar
3fd8aedf81 network: Reactivate network.js script.
Fixes an issue introduced during TS migration that rendered network.ts
ineffective because exports were not defined.
2019-09-02 00:25:17 +05:30
Rhythm Sharma
b4d2e55c6f linux: Fix broken icon issue for snap package. 2019-08-29 02:20:12 +05:30
Kanishk Kakar
3c701ff518 sidebar: Load last active server before others.
Fixes: #551.
2019-08-28 14:12:01 +05:30
ViPuL
1f79a97b05 system-presence: Pass system active status to webapp.
We check user status every 15 seconds and update the status accordingly
to every organization connected. The webapp then uses this system presence data
we send to set the user status based on system activity.

Fixes #352.
2019-08-19 20:09:27 -04:00
Nikita
90e8e9a806 il8n: Fix translations for ru locales. 2019-08-17 10:52:36 -04:00
Kanishk Kakar
59ef505efd settings: Fix trailing brackets.
This accidentally slipped by in the translation work we recently did.
2019-08-17 10:49:24 -04:00
Akash Nimare
8d0a111c91 version: Update app to v4.0.1. 2019-08-17 01:41:15 +05:30
Akash Nimare
a10fa8f3ad badge-count: Show badge-count on Linux.
Added support for showing the badge counts in the Unity launcher.
This should work on elementary OS and Ubuntu.

More info -
https://github.com/electron/electron/issues/16001
https://github.com/signalapp/Signal-Desktop/issues/3387
2019-08-17 01:32:50 +05:30
Akash Nimare
39427091f5 linux: Add desktopName config for Linux.
This will help in creating .desktop file for Linux.
2019-08-17 01:31:31 +05:30
ViPuL
f8d93cf397 build: Add MSI installer support.
This PR adds MSI in the target option. MSI is useful for sysadmins. There is no support for auto-updates since it often manages by the admins. 
More info -
https://github.com/electron-userland/electron-builder/releases/tag/v19.41.0
https://github.com/electron-userland/electron-builder/issues/3322

Fixes: #641.
2019-08-15 00:35:00 +05:30
Priyank Patel
ab62b8b5bb Remove accidently commited translation-util.js file.
This file was accidently committed in 77a1fc0bd3
when migrating old PR to use typescript. This commit removes it.
2019-08-14 12:50:47 -04:00
105 changed files with 10269 additions and 11095 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

@@ -6,7 +6,7 @@ The following is a set of guidelines for contributing to Zulip's desktop Client.
## Getting Started ## Getting Started
Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If you are new to Electron, please head over to [this](https://jlord.dev/blog/essential-electron) great article. Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If you are new to Electron, please head over to [this](https://jlord.us/essential-electron) great article.
## Community ## Community

View File

@@ -18,6 +18,15 @@ Please see the [installation guide](https://zulipchat.com/help/desktop-app-insta
* Multi-language spell checker * Multi-language spell checker
* Automatic updates * Automatic updates
# Reporting issues
This desktop client shares most of its code with the Zulip webapp.
Issues in an individual organization's Zulip window should be reported
in the [Zulip server and webapp
project](https://github.com/zulip/zulip/issues/new). Other
issues in the desktop app and its settings should be reported [in this
project](https://github.com/zulip/zulip-desktop/issues/new).
# Contribute # Contribute
First, join us on the [Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html)! First, join us on the [Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html)!

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,15 +82,15 @@ 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) { if (response === 0) {
setTimeout(() => { setTimeout(() => {
autoUpdater.quitAndInstall(); autoUpdater.quitAndInstall();
@@ -100,7 +99,6 @@ Current Version: ${app.getVersion()}`
}, 1000); }, 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 } = 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,9 +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')) {
app.quit();
}
if (!isQuitting) { if (!isQuitting) {
e.preventDefault(); event.preventDefault();
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
app.hide(); app.hide();
@@ -116,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');
} }
}); });
@@ -144,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();
} }
}); });
@@ -158,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);
} }
@@ -182,6 +189,10 @@ app.on('ready', () => {
} else { } else {
mainWindow.show(); mainWindow.show();
} }
if (!ConfigUtil.isConfigItemExists('userAgent')) {
const userAgent = session.fromPartition('webview:persistsession').getUserAgent();
ConfigUtil.setConfigItem('userAgent', userAgent);
}
}); });
page.once('did-frame-finish-load', () => { page.once('did-frame-finish-load', () => {
@@ -191,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();
@@ -244,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) {
@@ -283,16 +311,29 @@ 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);
if (result.canceled) {
item.cancel();
return;
}
setFilePath = result.filePath;
shortFileName = path.basename(setFilePath);
} else {
const getTimeStamp = (): number => {
const date = new Date(); const date = new Date();
return date.getTime(); return date.getTime();
}; };
@@ -303,15 +344,17 @@ app.on('ready', () => {
return `${baseName}-${getTimeStamp()}${fileExtension}`; return `${baseName}-${getTimeStamp()}${fileExtension}`;
}; };
const filePath = path.join(downloadPath, item.getFilename());
// Update the name and path of the file if it already exists // Update the name and path of the file if it already exists
const updatedFilePath = path.join(downloadPath, formatFile(filePath)); const updatedFilePath = path.join(downloadPath, formatFile(filePath));
setFilePath = fs.existsSync(filePath) ? updatedFilePath : filePath;
const 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
@@ -330,38 +373,51 @@ 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);
}); });
// Update user idle status for each realm after every 15s
const idleCheckInterval = 15 * 1000; // 15 seconds
setInterval(() => {
// Set user idle if no activity in 1 second (idleThresholdSeconds)
const idleThresholdSeconds = 1; // 1 second
const idleState = electron.powerMonitor.getSystemIdleState(idleThresholdSeconds);
if (idleState === 'active') {
page.send('set-active');
} else {
page.send('set-idle');
}
}, idleCheckInterval);
}); });
app.on('before-quit', () => { app.on('before-quit', () => {

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',

View File

@@ -1,31 +1,30 @@
'use strict';
import { app, shell, BrowserWindow, Menu, dialog } from 'electron'; import { app, shell, BrowserWindow, Menu, dialog } from 'electron';
import { appUpdater } from './autoupdater'; import { appUpdater } from './autoupdater';
import AdmZip = require('adm-zip'); import AdmZip from 'adm-zip';
import fs = require('fs-extra'); import fs from 'fs-extra';
import path = require('path'); import path from 'path';
import DNDUtil = require('../renderer/js/utils/dnd-util'); import * as DNDUtil from '../renderer/js/utils/dnd-util';
import Logger = require('../renderer/js/utils/logger-util'); import Logger from '../renderer/js/utils/logger-util';
import ConfigUtil = require('../renderer/js/utils/config-util'); import * as ConfigUtil from '../renderer/js/utils/config-util';
import t = require('../renderer/js/utils/translation-util'); import * as LinkUtil from '../renderer/js/utils/link-util';
import * as t from '../renderer/js/utils/translation-util';
const appName = app.getName(); const appName = app.name;
const logger = new Logger({ const logger = new Logger({
file: 'errors.log', file: 'errors.log',
timestamp: true timestamp: true
}); });
class AppMenu { function getHistorySubmenu(enableMenu: boolean): Electron.MenuItemConstructorOptions[] {
getHistorySubmenu(enableMenu: boolean): Electron.MenuItemConstructorOptions[] {
return [{ return [{
label: t.__('Back'), label: t.__('Back'),
accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left', accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left',
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('back'); sendAction('back');
} }
} }
}, { }, {
@@ -34,23 +33,23 @@ class AppMenu {
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('forward'); sendAction('forward');
} }
} }
}]; }];
} }
getToolsSubmenu(): Electron.MenuItemConstructorOptions[] { function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
return [{ return [{
label: t.__(`Check for Updates`), label: t.__('Check for Updates'),
click() { click() {
AppMenu.checkForUpdate(); checkForUpdate();
} }
}, },
{ {
label: t.__(`Release Notes`), label: t.__('Release Notes'),
click() { click() {
shell.openExternal(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`); LinkUtil.openBrowser(new URL(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`));
} }
}, },
{ {
@@ -60,7 +59,7 @@ class AppMenu {
label: t.__('Factory Reset'), label: t.__('Factory Reset'),
accelerator: process.platform === 'darwin' ? 'Command+Shift+D' : 'Ctrl+Shift+D', accelerator: process.platform === 'darwin' ? 'Command+Shift+D' : 'Ctrl+Shift+D',
click() { click() {
AppMenu.resetAppSettings(); resetAppSettings();
} }
}, },
{ {
@@ -99,19 +98,19 @@ class AppMenu {
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U', accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('tab-devtools'); sendAction('tab-devtools');
} }
} }
}]; }];
} }
getViewSubmenu(): Electron.MenuItemConstructorOptions[] { function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
return [{ return [{
label: t.__('Reload'), label: t.__('Reload'),
accelerator: 'CommandOrControl+R', accelerator: 'CommandOrControl+R',
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('reload-current-viewer'); sendAction('reload-current-viewer');
} }
} }
}, { }, {
@@ -119,7 +118,7 @@ class AppMenu {
accelerator: 'CommandOrControl+Shift+R', accelerator: 'CommandOrControl+Shift+R',
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('hard-reload'); sendAction('hard-reload');
} }
} }
}, { }, {
@@ -129,26 +128,28 @@ class AppMenu {
role: 'togglefullscreen' role: 'togglefullscreen'
}, { }, {
label: t.__('Zoom In'), label: t.__('Zoom In'),
role: 'zoomin', role: 'zoomIn',
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('zoomIn'); sendAction('zoomIn');
} }
} }
}, { }, {
label: t.__('Zoom Out'), label: t.__('Zoom Out'),
role: 'zoomOut',
accelerator: 'CommandOrControl+-', accelerator: 'CommandOrControl+-',
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('zoomOut'); sendAction('zoomOut');
} }
} }
}, { }, {
label: t.__('Actual Size'), label: t.__('Actual Size'),
role: 'resetZoom',
accelerator: 'CommandOrControl+0', accelerator: 'CommandOrControl+0',
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('zoomActualSize'); sendAction('zoomActualSize');
} }
} }
}, { }, {
@@ -177,7 +178,7 @@ class AppMenu {
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem('autoHideMenubar'); const newValue = !ConfigUtil.getConfigItem('autoHideMenubar');
focusedWindow.setAutoHideMenuBar(newValue); focusedWindow.autoHideMenuBar = newValue;
focusedWindow.setMenuBarVisibility(!newValue); focusedWindow.setMenuBarVisibility(!newValue);
focusedWindow.webContents.send('toggle-autohide-menubar', newValue); focusedWindow.webContents.send('toggle-autohide-menubar', newValue);
ConfigUtil.setConfigItem('autoHideMenubar', newValue); ConfigUtil.setConfigItem('autoHideMenubar', newValue);
@@ -185,9 +186,9 @@ class AppMenu {
}, },
type: 'checkbox' type: 'checkbox'
}]; }];
} }
getHelpSubmenu(): Electron.MenuItemConstructorOptions[] { function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
return [ return [
{ {
label: `${appName + ' Desktop'} v${app.getVersion()}`, label: `${appName + ' Desktop'} v${app.getVersion()}`,
@@ -197,15 +198,15 @@ class AppMenu {
label: t.__('About Zulip'), label: t.__('About Zulip'),
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('open-about'); sendAction('open-about');
} }
} }
}, },
{ {
label: t.__(`Help Center`), label: t.__('Help Center'),
click(focusedWindow) { click(focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('open-help'); sendAction('open-help');
} }
} }
}, },
@@ -220,10 +221,10 @@ class AppMenu {
} }
} }
]; ];
} }
getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boolean): Electron.MenuItemConstructorOptions[] { function getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boolean): Electron.MenuItemConstructorOptions[] {
const initialSubmenu: any[] = [{ const initialSubmenu: Electron.MenuItemConstructorOptions[] = [{
label: t.__('Minimize'), label: t.__('Minimize'),
role: 'minimize' role: 'minimize'
}, { }, {
@@ -248,7 +249,7 @@ class AppMenu {
checked: tab.props.index === activeTabIndex, checked: tab.props.index === activeTabIndex,
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('switch-server-tab', tab.props.index); sendAction('switch-server-tab', tab.props.index);
} }
}, },
type: 'checkbox' type: 'checkbox'
@@ -259,39 +260,39 @@ class AppMenu {
}); });
initialSubmenu.push({ initialSubmenu.push({
label: t.__('Switch to Next Organization'), label: t.__('Switch to Next Organization'),
accelerator: `Ctrl+Tab`, accelerator: 'Ctrl+Tab',
enabled: tabs.length > 1, enabled: tabs.length > 1,
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('switch-server-tab', AppMenu.getNextServer(tabs, activeTabIndex)); sendAction('switch-server-tab', getNextServer(tabs, activeTabIndex));
} }
} }
}, { }, {
label: t.__('Switch to Previous Organization'), label: t.__('Switch to Previous Organization'),
accelerator: `Ctrl+Shift+Tab`, accelerator: 'Ctrl+Shift+Tab',
enabled: tabs.length > 1, enabled: tabs.length > 1,
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('switch-server-tab', AppMenu.getPreviousServer(tabs, activeTabIndex)); sendAction('switch-server-tab', getPreviousServer(tabs, activeTabIndex));
} }
} }
}); });
} }
return initialSubmenu; return initialSubmenu;
} }
getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] { function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
const { tabs, activeTabIndex, enableMenu } = props; const { tabs, activeTabIndex, enableMenu } = props;
return [{ return [{
label: `${app.getName()}`, label: app.name,
submenu: [{ submenu: [{
label: t.__('Add Organization'), label: t.__('Add Organization'),
accelerator: 'Cmd+Shift+N', accelerator: 'Cmd+Shift+N',
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('new-server'); sendAction('new-server');
} }
} }
}, { }, {
@@ -299,14 +300,14 @@ class AppMenu {
accelerator: 'Cmd+Shift+M', accelerator: 'Cmd+Shift+M',
click() { click() {
const dndUtil = DNDUtil.toggle(); const dndUtil = DNDUtil.toggle();
AppMenu.sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings); sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
} }
}, { }, {
label: t.__('Desktop Settings'), label: t.__('Desktop Settings'),
accelerator: 'Cmd+,', accelerator: 'Cmd+,',
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('open-settings'); sendAction('open-settings');
} }
} }
}, { }, {
@@ -315,7 +316,7 @@ class AppMenu {
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('shortcut'); sendAction('shortcut');
} }
} }
}, { }, {
@@ -326,7 +327,7 @@ class AppMenu {
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('copy-zulip-url'); sendAction('copy-zulip-url');
} }
} }
}, { }, {
@@ -335,7 +336,7 @@ class AppMenu {
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('log-out'); sendAction('log-out');
} }
} }
}, { }, {
@@ -351,7 +352,7 @@ class AppMenu {
role: 'hide' role: 'hide'
}, { }, {
label: t.__('Hide Others'), label: t.__('Hide Others'),
role: 'hideothers' role: 'hideOthers'
}, { }, {
label: t.__('Unhide'), label: t.__('Unhide'),
role: 'unhide' role: 'unhide'
@@ -371,10 +372,20 @@ class AppMenu {
label: t.__('Edit'), label: t.__('Edit'),
submenu: [{ submenu: [{
label: t.__('Undo'), label: t.__('Undo'),
role: 'undo' accelerator: 'Cmd+Z',
click(_item: any, focusedWindow: any) {
if (focusedWindow) {
sendAction('undo');
}
}
}, { }, {
label: t.__('Redo'), label: t.__('Redo'),
role: 'redo' accelerator: 'Cmd+Shift+Z',
click(_item: any, focusedWindow: any) {
if (focusedWindow) {
sendAction('redo');
}
}
}, { }, {
type: 'separator' type: 'separator'
}, { }, {
@@ -388,31 +399,31 @@ class AppMenu {
role: 'paste' role: 'paste'
}, { }, {
label: t.__('Paste and Match Style'), label: t.__('Paste and Match Style'),
role: 'pasteandmatchstyle' role: 'pasteAndMatchStyle'
}, { }, {
label: t.__('Select All'), label: t.__('Select All'),
role: 'selectall' role: 'selectAll'
}] }]
}, { }, {
label: t.__('View'), label: t.__('View'),
submenu: this.getViewSubmenu() submenu: getViewSubmenu()
}, { }, {
label: t.__('History'), label: t.__('History'),
submenu: this.getHistorySubmenu(enableMenu) submenu: getHistorySubmenu(enableMenu)
}, { }, {
label: t.__('Window'), label: t.__('Window'),
submenu: this.getWindowSubmenu(tabs, activeTabIndex, enableMenu) submenu: getWindowSubmenu(tabs, activeTabIndex, enableMenu)
}, { }, {
label: t.__('Tools'), label: t.__('Tools'),
submenu: this.getToolsSubmenu() submenu: getToolsSubmenu()
}, { }, {
label: t.__('Help'), label: t.__('Help'),
role: 'help', role: 'help',
submenu: this.getHelpSubmenu() submenu: getHelpSubmenu()
}]; }];
} }
getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] { function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
const { tabs, activeTabIndex, enableMenu } = props; const { tabs, activeTabIndex, enableMenu } = props;
return [{ return [{
label: t.__('File'), label: t.__('File'),
@@ -421,7 +432,7 @@ class AppMenu {
accelerator: 'Ctrl+Shift+N', accelerator: 'Ctrl+Shift+N',
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('new-server'); sendAction('new-server');
} }
} }
}, { }, {
@@ -431,14 +442,14 @@ class AppMenu {
accelerator: 'Ctrl+Shift+M', accelerator: 'Ctrl+Shift+M',
click() { click() {
const dndUtil = DNDUtil.toggle(); const dndUtil = DNDUtil.toggle();
AppMenu.sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings); sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
} }
}, { }, {
label: t.__('Desktop Settings'), label: t.__('Desktop Settings'),
accelerator: 'Ctrl+,', accelerator: 'Ctrl+,',
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('open-settings'); sendAction('open-settings');
} }
} }
}, { }, {
@@ -447,7 +458,7 @@ class AppMenu {
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('shortcut'); sendAction('shortcut');
} }
} }
}, { }, {
@@ -458,7 +469,7 @@ class AppMenu {
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('copy-zulip-url'); sendAction('copy-zulip-url');
} }
} }
}, { }, {
@@ -467,7 +478,7 @@ class AppMenu {
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item: any, focusedWindow: any) {
if (focusedWindow) { if (focusedWindow) {
AppMenu.sendAction('log-out'); sendAction('log-out');
} }
} }
}, { }, {
@@ -504,75 +515,75 @@ class AppMenu {
role: 'paste' role: 'paste'
}, { }, {
label: t.__('Paste and Match Style'), label: t.__('Paste and Match Style'),
role: 'pasteandmatchstyle' role: 'pasteAndMatchStyle'
}, { }, {
type: 'separator' type: 'separator'
}, { }, {
label: t.__('Select All'), label: t.__('Select All'),
role: 'selectall' role: 'selectAll'
}] }]
}, { }, {
label: t.__('View'), label: t.__('View'),
submenu: this.getViewSubmenu() submenu: getViewSubmenu()
}, { }, {
label: t.__('History'), label: t.__('History'),
submenu: this.getHistorySubmenu(enableMenu) submenu: getHistorySubmenu(enableMenu)
}, { }, {
label: t.__('Window'), label: t.__('Window'),
submenu: this.getWindowSubmenu(tabs, activeTabIndex, enableMenu) submenu: getWindowSubmenu(tabs, activeTabIndex, enableMenu)
}, { }, {
label: t.__('Tools'), label: t.__('Tools'),
submenu: this.getToolsSubmenu() submenu: getToolsSubmenu()
}, { }, {
label: t.__('Help'), label: t.__('Help'),
role: 'help', role: 'help',
submenu: this.getHelpSubmenu() submenu: getHelpSubmenu()
}]; }];
} }
static sendAction(action: any, ...params: any[]): void { function sendAction(action: string, ...parameters: any[]): void {
const win = BrowserWindow.getAllWindows()[0]; const win = BrowserWindow.getAllWindows()[0];
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
win.restore(); win.restore();
} }
win.webContents.send(action, ...params); win.webContents.send(action, ...parameters);
} }
static checkForUpdate(): void { function checkForUpdate(): void {
appUpdater(true); appUpdater(true);
} }
static getNextServer(tabs: any[], activeTabIndex: number): number { function getNextServer(tabs: any[], activeTabIndex: number): number {
do { do {
activeTabIndex = (activeTabIndex + 1) % tabs.length; activeTabIndex = (activeTabIndex + 1) % tabs.length;
} }
while (tabs[activeTabIndex].props.role !== 'server'); while (tabs[activeTabIndex].props.role !== 'server');
return activeTabIndex; return activeTabIndex;
} }
static getPreviousServer(tabs: any[], activeTabIndex: number): number { function getPreviousServer(tabs: any[], activeTabIndex: number): number {
do { do {
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length; activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
} }
while (tabs[activeTabIndex].props.role !== 'server'); while (tabs[activeTabIndex].props.role !== 'server');
return activeTabIndex; return activeTabIndex;
} }
static resetAppSettings(): void { async function resetAppSettings(): Promise<void> {
const resetAppSettingsMessage = 'By proceeding you will be removing all connected organizations and preferences from Zulip.'; const resetAppSettingsMessage = 'By proceeding you will be removing all connected organizations and preferences from Zulip.';
// We save App's settings/configurations in following files // We save App's settings/configurations in following files
const settingFiles = ['config/window-state.json', 'config/domain.json', 'config/settings.json', 'config/certificates.json']; const settingFiles = ['config/window-state.json', 'config/domain.json', 'config/settings.json', 'config/certificates.json'];
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: resetAppSettingsMessage detail: resetAppSettingsMessage
}, response => { });
if (response === 0) { if (response === 0) {
settingFiles.forEach(settingFileName => { settingFiles.forEach(settingFileName => {
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName); const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
@@ -582,20 +593,16 @@ class AppMenu {
logger.error(error); logger.error(error);
} else { } else {
fs.unlink(getSettingFilesPath, () => { fs.unlink(getSettingFilesPath, () => {
AppMenu.sendAction('clear-app-data'); sendAction('clear-app-data');
}); });
} }
}); });
}); });
} }
});
}
setMenu(props: any): void {
const tpl = process.platform === 'darwin' ? this.getDarwinTpl(props) : this.getOtherTpl(props);
const menu = Menu.buildFromTemplate(tpl);
Menu.setApplicationMenu(menu);
}
} }
export = new AppMenu(); export function setMenu(props: any): void {
const tpl = process.platform === 'darwin' ? getDarwinTpl(props) : getOtherTpl(props);
const menu = Menu.buildFromTemplate(tpl);
Menu.setApplicationMenu(menu);
}

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);
// setLoginItemSettings doesn't support linux
if (process.platform === 'linux') {
const ZulipAutoLauncher = new AutoLaunch({
name: 'Zulip',
isHidden: false
});
if (autoLaunchOption) { if (autoLaunchOption) {
ZulipAutoLauncher.enable(); ZulipAutoLauncher.enable();
} else { } else {
ZulipAutoLauncher.disable(); ZulipAutoLauncher.disable();
} }
} else {
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,50 +0,0 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "4.0.0",
"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",
"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",
"is-online": "7.0.0",
"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

@@ -17,27 +17,43 @@ body {
} }
#title { #title {
text-align: left;
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
margin: 20px 0; margin: 20px 0;
} }
#subtitle {
font-size: 20px;
text-align: left;
margin: 12px 0;
}
#description { #description {
text-align: left;
font-size: 16px; font-size: 16px;
list-style-position: inside;
} }
#reconnect { #reconnect {
float: left;
}
#settings {
margin-left: 116px;
}
.button {
font-size: 16px; font-size: 16px;
background: rgba(0, 150, 136, 1.000); background: rgba(0, 150, 136, 1.000);
color: rgba(255, 255, 255, 1.000); color: rgba(255, 255, 255, 1.000);
width: 84px; width: 96px;
height: 32px; height: 32px;
border-radius: 5px; border-radius: 5px;
line-height: 32px; line-height: 32px;
margin: 20px auto 0;
cursor: pointer; cursor: pointer;
} }
#reconnect:hover { .button:hover {
opacity: 0.8; opacity: 0.8;
} }

View File

@@ -642,6 +642,26 @@ input.toggle-round:checked + label::after {
cursor: pointer; cursor: pointer;
} }
.server-network-option {
font-weight: bold;
font-size: 1.1rem;
margin-top: 10px;
padding-top: 15px;
align-items: center;
text-align: center;
color: rgb(78, 191, 172);
width: 98%;
height: 46px;
cursor: pointer;
}
i.open-network-button {
font-size: 16px;
cursor: pointer;
padding-left: 5px;
position: absolute;
}
/* responsive grid */ /* responsive grid */
@media (max-width: 650px) { @media (max-width: 650px) {

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,48 +1,24 @@
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 {
const { url } = event;
const domainPrefix = DomainUtil.getDomain(this.props.index).url;
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
const {
isInternalUrl: isWhiteListURL,
isUploadsUrl: isUploadsURL
} = LinkUtil.isInternal(domainPrefix, url);
if (isWhiteListURL) {
event.preventDefault(); event.preventDefault();
// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream) const url = new URL(event.url);
// Show pdf attachments in a new window const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`);
// if (LinkUtil.isPDF(url) && isUploadsURL) {
// ipcRenderer.send('pdf-view', url);
// return;
// }
// download txt, mp3, mp4 etc.. by using downloadURL in the if (LinkUtil.isUploadsUrl(this.props.url, url)) {
// main process which allows the user to save the files to their desktop ipcRenderer.send('downloadFile', url.href, downloadPath);
// 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) => { ipcRenderer.once('downloadFileCompleted', (_event: Event, filePath: string, fileName: string) => {
const downloadNotification = new Notification('Download Complete', { const downloadNotification = new Notification('Download Complete', {
body: shouldShowInFolder ? `Click to show ${fileName} in folder` : `Click to open ${fileName}`, body: `Click to show ${fileName} in folder`,
silent: true // We'll play our own sound - ding.ogg silent: true // We'll play our own sound - ding.ogg
}); });
@@ -52,13 +28,8 @@ function handleExternalLink(this: any, event: any): void {
} }
downloadNotification.addEventListener('click', () => { downloadNotification.addEventListener('click', () => {
if (shouldShowInFolder) {
// Reveal file in download folder // Reveal file in download folder
shell.showItemInFolder(filePath); shell.showItemInFolder(filePath);
} else {
// Open file in the default native app
shell.openItem(filePath);
}
}); });
ipcRenderer.removeAllListeners('downloadFileFailed'); ipcRenderer.removeAllListeners('downloadFileFailed');
}); });
@@ -66,18 +37,20 @@ function handleExternalLink(this: any, event: any): void {
ipcRenderer.once('downloadFileFailed', () => { ipcRenderer.once('downloadFileFailed', () => {
// Automatic download failed, so show save dialog prompt and download // Automatic download failed, so show save dialog prompt and download
// through webview // through webview
this.$el.downloadURL(url); // 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'); ipcRenderer.removeAllListeners('downloadFileCompleted');
}); });
return;
}
// open internal urls inside the current webview.
this.$el.loadURL(url);
} 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;
@@ -25,6 +23,10 @@ class Tab extends BaseComponent {
this.$el.addEventListener('mouseout', this.props.onHoverOut); this.$el.addEventListener('mouseout', this.props.onHoverOut);
} }
showNetworkError(): void {
this.webview.forceLoad();
}
activate(): void { activate(): void {
this.$el.classList.add('active'); this.$el.classList.add('active');
this.webview.load(); this.webview.load();
@@ -40,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();
@@ -125,7 +127,9 @@ class WebView extends BaseComponent {
const hasConnectivityErr = SystemUtil.connectivityERR.includes(errorDescription); const hasConnectivityErr = SystemUtil.connectivityERR.includes(errorDescription);
if (hasConnectivityErr) { if (hasConnectivityErr) {
console.error('error', errorDescription); console.error('error', errorDescription);
this.props.onNetworkError(); if (!this.props.url.includes('network.html')) {
this.props.onNetworkError(this.props.index);
}
} }
}); });
@@ -152,6 +156,10 @@ class WebView extends BaseComponent {
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0; return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
} }
showNotificationSettings(): void {
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'show-notification-settings');
}
show(): void { show(): void {
// Do not show WebView if another tab was selected and this tab should be in background. // Do not show WebView if another tab was selected and this tab should be in background.
if (!this.props.isActive()) { if (!this.props.isActive()) {
@@ -184,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;
} }
@@ -232,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 {
@@ -279,9 +287,12 @@ class WebView extends BaseComponent {
this.$el.reload(); this.$el.reload();
} }
send(channel: string, ...param: any[]): void { forceLoad(): void {
this.$el.send(channel, ...param); this.init();
}
async send(channel: string, ...parameters: any[]): Promise<void> {
await this.domReady;
this.$el.send(channel, ...parameters);
} }
} }
export = WebView;

View File

@@ -1,22 +1,51 @@
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 send_notification_reply_message_supported: boolean;
// for the whole file. idle_on_system: boolean;
/* eslint-disable @typescript-eslint/camelcase */ last_active_on_system: number;
class ElectronBridge extends events {
send_notification_reply_message_supported = false; constructor() {
send_event(eventName: string | symbol, ...args: any[]): void { super();
this.emit(eventName, ...args); this.send_notification_reply_message_supported = false;
// Indicates if the user is idle or not
this.idle_on_system = false;
// Indicates the time at which user was last active
this.last_active_on_system = Date.now();
} }
on_event(eventName: string, listener: ListenerType): void { send_event = (eventName: string | symbol, ...args: any[]): void => {
this.emit(eventName, ...args);
};
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();
@@ -41,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,32 +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';
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
interface FunctionalTabProps { interface FunctionalTabProps {
name: string; name: string;
@@ -62,12 +56,18 @@ interface SettingsOptions {
flashTaskbarOnMessage?: boolean; flashTaskbarOnMessage?: boolean;
}; };
downloadsPath: string; downloadsPath: string;
showDownloadFolder: boolean; quitOnClose: boolean;
promptDownload: boolean;
flashTaskbarOnMessage?: boolean; flashTaskbarOnMessage?: boolean;
dockBouncing?: boolean; dockBouncing?: boolean;
loading?: AnyObject; loading?: AnyObject;
} }
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
const rendererDirectory = path.resolve(__dirname, '..'); const rendererDirectory = path.resolve(__dirname, '..');
type ServerOrFunctionalTab = ServerTab | FunctionalTab; type ServerOrFunctionalTab = ServerTab | FunctionalTab;
@@ -136,8 +136,8 @@ 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) {
@@ -146,11 +146,9 @@ class ServerManagerView {
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) {
@@ -163,20 +161,19 @@ class ServerManagerView {
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
// In case, user doesn't visit these section, those values set to be null automatically // In case, user doesn't visit these section, those values set to be null automatically
@@ -206,7 +203,8 @@ class ServerManagerView {
silent: false silent: false
}, },
downloadsPath: `${app.getPath('downloads')}`, downloadsPath: `${app.getPath('downloads')}`,
showDownloadFolder: false quitOnClose: false,
promptDownload: false
}; };
// Platform specific settings // Platform specific settings
@@ -226,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);
} }
} }
} }
@@ -276,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) { if (response === 0) {
ipcRenderer.send('reload-full-app'); ipcRenderer.send('reload-full-app');
} }
});
} else { } else {
ipcRenderer.send('reload-full-app'); ipcRenderer.send('reload-full-app');
} }
@@ -302,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');
} }
} }
} }
@@ -310,13 +306,26 @@ 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);
DomainUtil.updateSavedServer(servers[i].url, i);
this.activateTab(i);
} }
// Open last active tab // Open last active tab
this.activateTab(ConfigUtil.getConfigItem('lastActiveTab')); let lastActiveTab = ConfigUtil.getConfigItem('lastActiveTab');
if (lastActiveTab >= servers.length) {
lastActiveTab = 0;
}
// checkDomain() and webview.load() for lastActiveTab before the others
DomainUtil.updateSavedServer(servers[lastActiveTab].url, lastActiveTab);
this.activateTab(lastActiveTab);
for (const [i, server] of servers.entries()) {
// after the lastActiveTab is activated, we load the others in the background
// without activating them, to prevent flashing of server icons
if (i === lastActiveTab) {
continue;
}
DomainUtil.updateSavedServer(server.url, i);
this.tabs[i].webview.load();
}
// Remove focus from the settings icon at sidebar bottom // Remove focus from the settings icon at sidebar bottom
this.$settingsButton.classList.remove('active'); this.$settingsButton.classList.remove('active');
} else if (this.presetOrgs.length === 0) { } else if (this.presetOrgs.length === 0) {
@@ -332,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,
@@ -346,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;
}, },
@@ -357,7 +368,7 @@ class ServerManagerView {
} }
this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]); this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]);
}, },
onNetworkError: this.openNetworkTroubleshooting.bind(this), onNetworkError: (index: number) => this.openNetworkTroubleshooting(index),
onTitleChange: this.updateBadge.bind(this), onTitleChange: this.updateBadge.bind(this),
nodeIntegration: false, nodeIntegration: false,
preload: true preload: true
@@ -448,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);
@@ -523,7 +534,7 @@ class ServerManagerView {
} }
this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]); this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]);
}, },
onNetworkError: this.openNetworkTroubleshooting.bind(this), onNetworkError: (index: number) => this.openNetworkTroubleshooting(index),
onTitleChange: this.updateBadge.bind(this), onTitleChange: this.updateBadge.bind(this),
nodeIntegration: true, nodeIntegration: true,
preload: false preload: false
@@ -537,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 {
@@ -555,12 +566,11 @@ class ServerManagerView {
}); });
} }
openNetworkTroubleshooting(): void { openNetworkTroubleshooting(index: number): void {
this.openFunctionalTab({ const reconnectUtil = new ReconnectUtil(this.tabs[index].webview);
name: 'Network Troubleshooting', reconnectUtil.pollInternetAndReload();
materialIcon: 'network_check', this.tabs[index].webview.props.url = `file://${rendererDirectory}/network.html`;
url: `file://${rendererDirectory}/network.html` this.tabs[index].showNetworkError();
});
} }
activateLastTab(index: number): void { activateLastTab(index: number): void {
@@ -697,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);
} }
} }
@@ -719,19 +727,30 @@ class ServerManagerView {
this.$dndButton.querySelector('i').textContent = alert ? 'notifications_off' : 'notifications'; this.$dndButton.querySelector('i').textContent = alert ? 'notifications_off' : 'notifications';
} }
isLoggedIn(tabIndex: number): boolean {
const url = this.tabs[tabIndex].webview.$el.src;
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 (response === 0) {
if (DomainUtil.removeDomain(index)) { if (DomainUtil.removeDomain(index)) {
ipcRenderer.send('reload-full-app'); ipcRenderer.send('reload-full-app');
@@ -740,7 +759,15 @@ class ServerManagerView {
dialog.showErrorBox(title, content); dialog.showErrorBox(title, content);
} }
} }
}); }
},
{
label: 'Notification settings',
enabled: this.isLoggedIn(index),
click: () => {
// switch to tab whose icon was right-clicked
this.activateTab(index);
this.tabs[index].webview.showNotificationSettings();
} }
}, },
{ {
@@ -769,15 +796,42 @@ 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) => {
this.openNetworkTroubleshooting(index);
});
ipcRenderer.on('open-settings', (event: Event, settingNav: string) => { ipcRenderer.on('open-settings', (event: Event, settingNav: string) => {
this.openSettings(settingNav); this.openSettings(settingNav);
}); });
@@ -786,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));
@@ -810,14 +863,13 @@ 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) => {
// Toggle the left sidebar // Toggle the left sidebar
@@ -855,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);
}); });
@@ -865,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);
@@ -884,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,
icon: iconURL
});
const serverImgsSelector = '.tab .server-icons';
const serverImgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(serverImgsSelector); const serverImgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(serverImgsSelector);
serverImgs[index].src = localIconUrl; serverImgs[index].src = localIconUrl;
domain.icon = localIconUrl; domain.icon = localIconUrl;
DomainUtil.db.push(`/domains[${index}]`, domain, true); DomainUtil.db.push(`/domains[${index}]`, domain, true);
DomainUtil.reloadDB(); DomainUtil.reloadDB();
});
} }
}); });
}); });
@@ -961,22 +1011,39 @@ class ServerManagerView {
ipcRenderer.on('new-server', () => { ipcRenderer.on('new-server', () => {
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', () => {
const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => {
webview.send('set-active');
});
});
ipcRenderer.on('set-idle', () => {
const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => {
webview.send('set-idle');
});
});
ipcRenderer.on('open-network-settings', () => {
this.openSettings('Network');
});
} }
} }
window.addEventListener('load', () => { window.addEventListener('load', () => {
const serverManagerView = new ServerManagerView(); const serverManagerView = new ServerManagerView();
const reconnectUtil = new ReconnectUtil(serverManagerView);
serverManagerView.init(); serverManagerView.init();
window.addEventListener('online', () => {
reconnectUtil.pollInternetAndReload();
});
window.addEventListener('offline', () => {
reconnectUtil.clearState();
logger.log('No internet connection, you are offline.');
});
// only start electron-connect (auto reload on change) when its ran // only start electron-connect (auto reload on change) when its ran
// from `npm run dev` or `gulp dev` and not from `npm start` when // from `npm run dev` or `gulp dev` and not from `npm start` when
// app is started `npm start` main process's proces.argv will have // app is started `npm start` main process's proces.argv will have
@@ -987,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,17 +15,12 @@ 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')
.done((data: any) => {
const { members } = data;
members.forEach((membersRow: any) => { members.forEach((membersRow: any) => {
if (membersRow.is_bot) { if (membersRow.is_bot) {
const bot = `@${membersRow.full_name}`; const bot = `@${membersRow.full_name}`;
@@ -34,13 +29,9 @@ export function loadBots(sync = false): void {
} }
}); });
botsListLoaded = true; botsListLoaded = true;
}) } else {
.fail((error: any) => { logger.log('Load bots request failed: ', await response.text());
logger.log('Load bots request failed: ', error.responseText); logger.log('Load bots request status: ', response.status);
logger.log('Load bots request status: ', error.statusText);
});
if (sync) {
$.ajaxSetup({async: true});
} }
} }
@@ -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,21 +1,10 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
class NetworkTroubleshootingView { export function init($reconnectButton: Element, $settingsButton: Element): void {
$reconnectButton: Element; $reconnectButton.addEventListener('click', () => {
constructor() {
this.$reconnectButton = document.querySelector('#reconnect');
}
init(): void {
this.$reconnectButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer'); ipcRenderer.send('forward-message', 'reload-viewer');
}); });
} $settingsButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'open-settings');
});
} }
window.addEventListener('load', () => {
const networkTroubleshootingView = new NetworkTroubleshootingView();
networkTroubleshootingView.init();
});

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,49 +1,32 @@
'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 {
class BadgeSettings {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
return instance;
}
showBadgeCount(messageCount: number, mainWindow: electron.BrowserWindow): void {
if (process.platform === 'darwin') {
app.setBadgeCount(messageCount);
}
if (process.platform === 'win32') { if (process.platform === 'win32') {
this.updateOverlayIcon(messageCount, mainWindow); updateOverlayIcon(messageCount, mainWindow);
} } else {
app.badgeCount = messageCount;
} }
}
hideBadgeCount(mainWindow: electron.BrowserWindow): void { function hideBadgeCount(mainWindow: electron.BrowserWindow): void {
if (process.platform === 'darwin') {
app.setBadgeCount(0);
}
if (process.platform === 'win32') { if (process.platform === 'win32') {
mainWindow.setOverlayIcon(null, ''); mainWindow.setOverlayIcon(null, '');
}
}
updateBadge(badgeCount: number, mainWindow: electron.BrowserWindow): void {
if (ConfigUtil.getConfigItem('badgeOption', true)) {
this.showBadgeCount(badgeCount, mainWindow);
} else { } else {
this.hideBadgeCount(mainWindow); app.badgeCount = 0;
}
} }
}
updateOverlayIcon(messageCount: number, mainWindow: electron.BrowserWindow): void { 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()) { if (!mainWindow.isFocused()) {
mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage')); mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
} }
@@ -52,12 +35,9 @@ class BadgeSettings {
} else { } else {
mainWindow.webContents.send('render-taskbar-icon', messageCount); 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(); 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) {
@@ -42,7 +41,7 @@ class GeneralSection extends BaseSection {
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="dock-bounce-option" style= "display:${process.platform === 'darwin' ? '' : 'none'}"> <div class="setting-row" id="dock-bounce-option" style= "display:${process.platform === 'darwin' ? '' : 'none'}">
<div class="setting-description">${t.__('Bounce dock on new private message')})}</div> <div class="setting-description">${t.__('Bounce dock on new private message')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="flash-taskbar-option" style= "display:${process.platform === 'win32' ? '' : 'none'}"> <div class="setting-row" id="flash-taskbar-option" style= "display:${process.platform === 'win32' ? '' : 'none'}">
@@ -82,6 +81,10 @@ class GeneralSection extends BaseSection {
<div class="setting-description">${t.__('Always start minimized')}</div> <div class="setting-description">${t.__('Always start minimized')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="quitOnClose-option">
<div class="setting-description">${t.__('Quit when the window is closed')}</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="enable-spellchecker-option"> <div class="setting-row" id="enable-spellchecker-option">
<div class="setting-description">${t.__('Enable spellchecker (requires restart)')}</div> <div class="setting-description">${t.__('Enable spellchecker (requires restart)')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
@@ -93,10 +96,6 @@ class GeneralSection extends BaseSection {
<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-description">${t.__('Show downloaded files in file manager')}</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="add-custom-css"> <div class="setting-row" id="add-custom-css">
<div class="setting-description"> <div class="setting-description">
${t.__('Add custom CSS')} ${t.__('Add custom CSS')}
@@ -123,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">
@@ -154,7 +156,8 @@ class GeneralSection extends BaseSection {
this.showCustomCSSPath(); this.showCustomCSSPath();
this.removeCustomCSS(); this.removeCustomCSS();
this.downloadFolder(); this.downloadFolder();
this.showDownloadFolder(); this.updateQuitOnCloseOption();
this.updatePromptDownloadOption();
this.enableErrorReporting(); this.enableErrorReporting();
// Platform specific settings // Platform specific settings
@@ -276,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);
} }
}); });
} }
@@ -321,6 +322,18 @@ class GeneralSection extends BaseSection {
}); });
} }
updateQuitOnCloseOption(): void {
this.generateSettingOption({
$element: document.querySelector('#quitOnClose-option .setting-control'),
value: ConfigUtil.getConfigItem('quitOnClose', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('quitOnClose');
ConfigUtil.setConfigItem('quitOnClose', newValue);
this.updateQuitOnCloseOption();
}
});
}
enableSpellchecker(): void { enableSpellchecker(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#enable-spellchecker-option .setting-control'), $element: document.querySelector('#enable-spellchecker-option .setting-control'),
@@ -345,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) { if (response === 0) {
fs.remove(getAppPath); fs.remove(getAppPath);
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000); 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 {
@@ -414,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', () => {
@@ -440,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 } 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,19 +24,21 @@ 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">
<hr class="left"/>${t.__('OR')}<hr class="right" /> <hr class="left"/>${t.__('OR')}<hr class="right" />
</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 class="server-center">
<div class="server-network-option">
<span id="open-network-settings">${t.__('Network and Proxy Settings')}</span>
<i class="material-icons open-network-button">open_in_new</i>
</div>
</div> </div>
</div> </div>
`; `;
@@ -50,46 +51,50 @@ 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));
}); });
} }
networkSettingsLink(): void {
const networkSettingsId = document.querySelectorAll('.server-network-option')[0];
networkSettingsId.addEventListener('click', () => ipcRenderer.send('forward-message', 'open-network-settings'));
}
initActions(): void { initActions(): void {
this.$saveServerButton.addEventListener('click', () => { this.$saveServerButton.addEventListener('click', () => {
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();
} }
}); });
// open create new org link in default browser // open create new org link in default browser
this.openCreateNewOrgExternalLink(); this.openCreateNewOrgExternalLink();
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,13 +59,13 @@ 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 (response === 0) {
if (DomainUtil.removeDomain(this.props.index)) { if (DomainUtil.removeDomain(this.props.index)) {
ipcRenderer.send('reload-full-app'); ipcRenderer.send('reload-full-app');
@@ -77,7 +75,6 @@ class ServerInfoForm extends BaseComponent {
} }
} }
}); });
});
this.$openServerButton.addEventListener('click', () => { this.$openServerButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index); ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
@@ -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,39 +1,31 @@
'use strict'; import { contextBridge, ipcRenderer, webFrame } from 'electron';
import fs from 'fs';
import * as SetupSpellChecker from './spellchecker';
import { ipcRenderer, shell } from 'electron'; import isDev from 'electron-is-dev';
import SetupSpellChecker from './spellchecker';
import LinkUtil = require('./utils/link-util'); import * as NetworkError from './pages/network';
import params = require('./utils/params-util');
interface PatchedGlobal extends NodeJS.Global {
logout: () => void;
shortcut: () => 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);
// eslint-disable-next-line @typescript-eslint/camelcase ipcRenderer.on('logout', () => {
window.electron_bridge = require('./electron-bridge');
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
@@ -44,18 +36,26 @@ const shortcut = (): void => {
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle'); const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click(); dropdown.click();
} }
};
process.once('loaded', (): void => {
globalPatched.logout = logout;
globalPatched.shortcut = shortcut;
}); });
// To prevent failing this script on linux we need to load it after the document loaded ipcRenderer.on('show-notification-settings', () => {
document.addEventListener('DOMContentLoaded', (): void => { // Create the menu for the below
if (params.isPageParams()) { const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click();
const nodes: NodeListOf<HTMLElement> = document.querySelectorAll('.dropdown-menu li a');
nodes[2].click();
const notificationItem: NodeListOf<HTMLElement> = document.querySelectorAll('.normal-settings-list li div');
// wait until the notification dom element shows up
setTimeout(() => {
notificationItem[2].click();
}, 100);
});
electron_bridge.once('zulip-loaded', ({ serverLanguage }) => {
// 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, @typescript-eslint/camelcase
if (serverLanguage) { if (serverLanguage) {
// Init spellchecker // Init spellchecker
SetupSpellChecker.init(serverLanguage); SetupSpellChecker.init(serverLanguage);
@@ -67,28 +67,6 @@ document.addEventListener('DOMContentLoaded', (): void => {
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);
}
});
}
}); });
// Clean up spellchecker events after you navigate away from this page; // Clean up spellchecker events after you navigate away from this page;
@@ -97,6 +75,15 @@ window.addEventListener('beforeunload', (): void => {
SetupSpellChecker.unsubscribeSpellChecker(); SetupSpellChecker.unsubscribeSpellChecker();
}); });
window.addEventListener('load', (event: any): void => {
if (!event.target.URL.includes('app/renderer/network.html')) {
return;
}
const $reconnectButton = document.querySelector('#reconnect');
const $settingsButton = document.querySelector('#settings');
NetworkError.init($reconnectButton, $settingsButton);
});
// electron's globalShortcut can cause unexpected results // electron's globalShortcut can cause unexpected results
// so adding the reload shortcut in the old-school way // so adding the reload shortcut in the old-school way
// Zoom from numpad keys is not supported by electron, so adding it through listeners. // Zoom from numpad keys is not supported by electron, so adding it through listeners.
@@ -112,3 +99,24 @@ document.addEventListener('keydown', event => {
ipcRenderer.send('forward-message', 'zoomActualSize'); ipcRenderer.send('forward-message', 'zoomActualSize');
} }
}); });
// Set user as active and update the time of last activity
ipcRenderer.on('set-active', () => {
if (isDev) {
console.log('active');
}
electron_bridge.idle_on_system = false;
electron_bridge.last_active_on_system = Date.now();
});
// Set user as idle and time of last activity is left unchanged
ipcRenderer.on('set-idle', () => {
if (isDev) {
console.log('idle');
}
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 {
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
this.enableSpellChecker();
}
this.enableContextMenu(serverLanguage);
}
enableSpellChecker(): void { export function init(serverLanguage: string): void {
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
enableSpellChecker();
}
enableContextMenu(serverLanguage);
}
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,10 +50,9 @@ 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;
@@ -77,34 +83,25 @@ const renderCanvas = function (arg: number): Promise<HTMLCanvasElement> {
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))
.then(canvas => {
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG(); const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
return nativeImage.createFromBuffer(pngData, {
// 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 scaleFactor: config.pixelRatio
}));
}); });
}; };
@@ -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,51 +1,38 @@
'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];
getCertificate(server: string, defaultValue: any = null): any {
this.reloadDB();
const value = this.db.getData('/')[server];
if (value === undefined) { if (value === undefined) {
return defaultValue; return defaultValue;
} else { } else {
return value; return value;
} }
} }
// Function to copy the certificate to userData folder // Function to copy the certificate to userData folder
copyCertificate(_server: string, location: string, fileName: string): boolean { export function copyCertificate(_server: string, location: string, fileName: string): boolean {
let copied = false; let copied = false;
const filePath = `${certificatesDir}/${fileName}`; const filePath = `${certificatesDir}/${fileName}`;
try { try {
@@ -60,20 +47,20 @@ class CertificateUtil {
logger.error(err); logger.error(err);
} }
return copied; return copied;
} }
setCertificate(server: string, fileName: string): void { export function setCertificate(server: string, fileName: string): void {
const filePath = `${certificatesDir}/${fileName}`; const filePath = `${fileName}`;
this.db.push(`/${server}`, filePath, true); db.push(`/${server}`, filePath, true);
this.reloadDB(); reloadDB();
} }
removeCertificate(server: string): void { export function removeCertificate(server: string): void {
this.db.delete(`/${server}`); db.delete(`/${server}`);
this.reloadDB(); reloadDB();
} }
reloadDB(): void { function reloadDB(): void {
const settingsJsonPath = path.join(app.getPath('userData'), '/config/certificates.json'); const settingsJsonPath = path.join(app.getPath('userData'), '/config/certificates.json');
try { try {
const file = fs.readFileSync(settingsJsonPath, 'utf8'); const file = fs.readFileSync(settingsJsonPath, 'utf8');
@@ -89,8 +76,5 @@ class CertificateUtil {
logger.error(err); logger.error(err);
} }
} }
this.db = new JsonDB(settingsJsonPath, true, true); db = new JsonDB(settingsJsonPath, true, true);
}
} }
export = new CertificateUtil();

View File

@@ -1,25 +1,8 @@
'use strict'; // unescape already encoded/escaped strings
export function decodeString(stringInput: string): string {
let instance: null | CommonUtil = null;
class CommonUtil {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
return instance;
}
// unescape already encoded/escaped strings
decodeString(stringInput: string): string {
const parser = new DOMParser(); const parser = new DOMParser();
const dom = parser.parseFromString( const dom = parser.parseFromString(
'<!doctype html><body>' + stringInput, '<!doctype html><body>' + stringInput,
'text/html'); 'text/html');
return dom.body.textContent; 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,63 +24,53 @@ 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;
}
getConfigItem(key: string, defaultValue: any = null): any {
try { try {
this.db.reload(); db.reload();
} catch (err) { } catch (err) {
logger.error('Error while reloading settings.json: '); logger.error('Error while reloading settings.json: ');
logger.error(err); logger.error(err);
} }
const value = this.db.getData('/')[key]; const value = db.getData('/')[key];
if (value === undefined) { if (value === undefined) {
this.setConfigItem(key, defaultValue); setConfigItem(key, defaultValue);
return defaultValue; return defaultValue;
} else { } else {
return value; return value;
} }
} }
// This function returns whether a key exists in the configuration file (settings.json) // This function returns whether a key exists in the configuration file (settings.json)
isConfigItemExists(key: string): boolean { export function isConfigItemExists(key: string): boolean {
try { try {
this.db.reload(); db.reload();
} catch (err) { } catch (err) {
logger.error('Error while reloading settings.json: '); logger.error('Error while reloading settings.json: ');
logger.error(err); logger.error(err);
} }
const value = this.db.getData('/')[key]; const value = db.getData('/')[key];
return (value !== undefined); return (value !== undefined);
} }
setConfigItem(key: string, value: any, override? : boolean): void { export function setConfigItem(key: string, value: any, override? : boolean): void {
if (EnterpriseUtil.configItemExists(key) && !override) { if (EnterpriseUtil.configItemExists(key) && !override) {
// if item is in global config and we're not trying to override // if item is in global config and we're not trying to override
return; return;
} }
this.db.push(`/${key}`, value, true); db.push(`/${key}`, value, true);
this.db.save(); db.save();
} }
removeConfigItem(key: string): void { export function removeConfigItem(key: string): void {
this.db.delete(`/${key}`); db.delete(`/${key}`);
this.db.save(); db.save();
} }
reloadDB(): void { function reloadDB(): void {
const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.json'); const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.json');
try { try {
const file = fs.readFileSync(settingsJsonPath, 'utf8'); const file = fs.readFileSync(settingsJsonPath, 'utf8');
@@ -99,8 +87,5 @@ class ConfigUtil {
logger.reportSentry(err); logger.reportSentry(err);
} }
} }
this.db = new JsonDB(settingsJsonPath, true, true); db = new JsonDB(settingsJsonPath, true, true);
}
} }
export = new ConfigUtil();

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,115 +1,108 @@
'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 { app, dialog } = electron.remote; const { app, dialog } = 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) {
getDomains(): any {
this.reloadDB();
if (this.db.getData('/').domains === undefined) {
return []; return [];
} else { } else {
return this.db.getData('/domains'); return db.getData('/domains');
}
} }
}
getDomain(index: number): any { export function getDomain(index: number): ServerConf {
this.reloadDB(); reloadDB();
return this.db.getData(`/domains[${index}]`); return db.getData(`/domains[${index}]`);
} }
updateDomain(index: number, server: object): void { export function shouldIgnoreCerts(url: string): boolean {
this.reloadDB(); const domains = getDomains();
this.db.push(`/domains[${index}]`, server, true); for (const domain of domains) {
if (domain.url === url) {
return domain.ignoreCerts;
} }
}
return null;
}
addDomain(server: any): Promise<void> { function updateDomain(index: number, server: ServerConf): void {
reloadDB();
db.push(`/domains[${index}]`, server, true);
}
export async function addDomain(server: ServerConf): Promise<void> {
const { ignoreCerts } = server; const { ignoreCerts } = server;
return new Promise(resolve => {
if (server.icon) { if (server.icon) {
this.saveServerIcon(server, ignoreCerts).then(localIconUrl => { const localIconUrl = await saveServerIcon(server, ignoreCerts);
server.icon = localIconUrl; server.icon = localIconUrl;
this.db.push('/domains[]', server, true); db.push('/domains[]', server, true);
this.reloadDB(); reloadDB();
resolve();
});
} else { } else {
server.icon = defaultIconUrl; server.icon = defaultIconUrl;
this.db.push('/domains[]', server, true); db.push('/domains[]', server, true);
this.reloadDB(); reloadDB();
resolve();
}
});
} }
}
removeDomains(): void { export function removeDomains(): void {
this.db.delete('/domains'); db.delete('/domains');
this.reloadDB(); reloadDB();
} }
removeDomain(index: number): boolean { export function removeDomain(index: number): boolean {
if (EnterpriseUtil.isPresetOrg(this.getDomain(index).url)) { if (EnterpriseUtil.isPresetOrg(getDomain(index).url)) {
return false; return false;
} }
this.db.delete(`/domains[${index}]`); db.delete(`/domains[${index}]`);
this.reloadDB(); reloadDB();
return true; return true;
} }
// Check if domain is already added // Check if domain is already added
duplicateDomain(domain: any): boolean { export function duplicateDomain(domain: string): boolean {
domain = this.formatUrl(domain); domain = formatUrl(domain);
const servers = this.getDomains(); return getDomains().some(server => server.url === domain);
for (const i in servers) { }
if (servers[i].url === domain) {
return true;
}
}
return false;
}
async checkCertError(domain: any, serverConf: any, error: string, silent: boolean): Promise<string | object> { async function checkCertError(domain: string, serverConf: ServerConf, error: string, silent: boolean): Promise<ServerConf> {
if (silent) { if (silent) {
// since getting server settings has already failed // since getting server settings has already failed
return serverConf; return serverConf;
@@ -120,18 +113,18 @@ class DomainUtil {
const certErrorMessage = Messages.certErrorMessage(domain, error); const certErrorMessage = Messages.certErrorMessage(domain, error);
const certErrorDetail = Messages.certErrorDetail(); const certErrorDetail = Messages.certErrorDetail();
const response = await (dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'warning', type: 'warning',
buttons: ['Yes', 'No'], buttons: ['Yes', 'No'],
defaultId: 1, defaultId: 1,
message: certErrorMessage, message: certErrorMessage,
detail: certErrorDetail detail: certErrorDetail
}) as any); // TODO: TypeScript - Figure this out });
if (response === 0) { if (response === 0) {
// set ignoreCerts parameter to true in case user responds with yes // set ignoreCerts parameter to true in case user responds with yes
serverConf.ignoreCerts = true; serverConf.ignoreCerts = true;
try { try {
return await this.getServerSettings(domain, serverConf.ignoreCerts); return await getServerSettings(domain, serverConf.ignoreCerts);
} catch (_) { } catch (_) {
if (error === Messages.noOrgsError(domain)) { if (error === Messages.noOrgsError(domain)) {
throw new Error(error); throw new Error(error);
@@ -142,17 +135,17 @@ class DomainUtil {
throw new Error('Untrusted certificate.'); throw new Error('Untrusted certificate.');
} }
} }
} }
// ignoreCerts parameter helps in fetching server icon and // ignoreCerts parameter helps in fetching server icon and
// other server details when user chooses to ignore certificate warnings // other server details when user chooses to ignore certificate warnings
async checkDomain(domain: any, ignoreCerts = false, silent = false): Promise<any> { export async function checkDomain(domain: string, ignoreCerts = false, silent = false): Promise<ServerConf> {
if (!silent && this.duplicateDomain(domain)) { if (!silent && duplicateDomain(domain)) {
// Do not check duplicate in silent mode // Do not check duplicate in silent mode
throw new Error('This server has been added.'); throw new Error('This server has been added.');
} }
domain = this.formatUrl(domain); domain = formatUrl(domain);
const serverConf = { const serverConf = {
icon: defaultIconUrl, icon: defaultIconUrl,
@@ -162,27 +155,23 @@ class DomainUtil {
}; };
try { try {
return await this.getServerSettings(domain, serverConf.ignoreCerts); return await getServerSettings(domain, serverConf.ignoreCerts);
} catch (err) { } catch (err) {
// If the domain contains following strings we just bypass the server // Make sure that error is an error or string not undefined
const whitelistDomains = [
'zulipdev.org'
];
// make sure that error is an error or string not undefined
// so validation does not throw error. // so validation does not throw error.
const error = err || ''; const error = err || '';
const certsError = error.toString().includes('certificate'); const certsError = error.toString().includes('certificate');
if (domain.indexOf(whitelistDomains) >= 0 || certsError) { if (certsError) {
return this.checkCertError(domain, serverConf, error, silent); const result = await checkCertError(domain, serverConf, error, silent);
return result;
} else { } else {
throw Messages.invalidZulipServerError(domain); throw new Error(Messages.invalidZulipServerError(domain));
}
} }
} }
}
getServerSettings(domain: any, ignoreCerts = false): Promise<object | string> { async function getServerSettings(domain: string, ignoreCerts = false): Promise<ServerConf> {
const serverSettingsOptions = { const serverSettingsOptions = {
url: domain + '/api/v1/server_settings', url: domain + '/api/v1/server_settings',
...RequestUtil.requestOptions(domain, ignoreCerts) ...RequestUtil.requestOptions(domain, ignoreCerts)
@@ -192,7 +181,7 @@ class DomainUtil {
request(serverSettingsOptions, (error: string, response: any) => { request(serverSettingsOptions, (error: string, response: any) => {
if (!error && response.statusCode === 200) { if (!error && response.statusCode === 200) {
const data = JSON.parse(response.body); const data = JSON.parse(response.body);
if (data.hasOwnProperty('realm_icon') && data.realm_icon) { if (Object.prototype.hasOwnProperty.call(data, 'realm_icon') && data.realm_icon) {
resolve({ resolve({
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL // Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases // Following check handles both the cases
@@ -209,9 +198,9 @@ class DomainUtil {
} }
}); });
}); });
} }
saveServerIcon(server: any, ignoreCerts = false): Promise<string> { export async function saveServerIcon(server: ServerConf, ignoreCerts = false): Promise<string> {
const url = server.icon; const url = server.icon;
const domain = server.url; const domain = server.url;
@@ -222,7 +211,7 @@ class DomainUtil {
// The save will always succeed. If url is invalid, downgrade to default icon. // The save will always succeed. If url is invalid, downgrade to default icon.
return new Promise(resolve => { return new Promise(resolve => {
const filePath = this.generateFilePath(url); const filePath = generateFilePath(url);
const file = fs.createWriteStream(filePath); const file = fs.createWriteStream(filePath);
try { try {
request(serverIconOptions).on('response', (response: any) => { request(serverIconOptions).on('response', (response: any) => {
@@ -248,24 +237,26 @@ class DomainUtil {
resolve(defaultIconUrl); resolve(defaultIconUrl);
} }
}); });
} }
updateSavedServer(url: string, index: number): void { export async function updateSavedServer(url: string, index: number): Promise<void> {
// Does not promise successful update // Does not promise successful update
const oldIcon = this.getDomain(index).icon; const oldIcon = getDomain(index).icon;
const { ignoreCerts } = this.getDomain(index); const { ignoreCerts } = getDomain(index);
this.checkDomain(url, ignoreCerts, true).then(newServerConf => { try {
this.saveServerIcon(newServerConf, ignoreCerts).then(localIconUrl => { const newServerConf = await checkDomain(url, ignoreCerts, true);
const localIconUrl = await saveServerIcon(newServerConf, ignoreCerts);
if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') { if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') {
newServerConf.icon = localIconUrl; newServerConf.icon = localIconUrl;
this.updateDomain(index, newServerConf); updateDomain(index, newServerConf);
this.reloadDB(); reloadDB();
} }
}); } catch (err) {
}); ipcRenderer.send('forward-message', 'show-network-error', index);
} }
}
reloadDB(): void { export function reloadDB(): void {
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json'); const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
try { try {
const file = fs.readFileSync(domainJsonPath, 'utf8'); const file = fs.readFileSync(domainJsonPath, 'utf8');
@@ -283,18 +274,18 @@ class DomainUtil {
logger.reportSentry(err); logger.reportSentry(err);
} }
} }
this.db = new JsonDB(domainJsonPath, true, true); db = new JsonDB(domainJsonPath, true, true);
} }
generateFilePath(url: string): string { function generateFilePath(url: string): string {
const dir = `${app.getPath('userData')}/server-icons`; const dir = `${app.getPath('userData')}/server-icons`;
const extension = path.extname(url).split('?')[0]; const extension = path.extname(url).split('?')[0];
let hash = 5381; let hash = 5381;
let len = url.length; let { length } = url;
while (len) { while (length) {
hash = (hash * 33) ^ url.charCodeAt(--len); hash = (hash * 33) ^ url.charCodeAt(--length);
} }
// Create 'server-icons' directory if not existed // Create 'server-icons' directory if not existed
@@ -303,16 +294,14 @@ class DomainUtil {
} }
return `${dir}/${hash >>> 0}${extension}`; 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}`;
}
}
} }
export = new DomainUtil(); 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,29 +1,20 @@
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 {
}
reloadDB(): void {
let enterpriseFile = '/etc/zulip-desktop-config/global_config.json'; let enterpriseFile = '/etc/zulip-desktop-config/global_config.json';
if (process.platform === 'win32') { if (process.platform === 'win32') {
enterpriseFile = 'C:\\Program Files\\Zulip-Desktop-Config\\global_config.json'; enterpriseFile = 'C:\\Program Files\\Zulip-Desktop-Config\\global_config.json';
@@ -31,50 +22,47 @@ class EnterpriseUtil {
enterpriseFile = path.resolve(enterpriseFile); enterpriseFile = path.resolve(enterpriseFile);
if (fs.existsSync(enterpriseFile)) { if (fs.existsSync(enterpriseFile)) {
this.configFile = true; configFile = true;
try { try {
const file = fs.readFileSync(enterpriseFile, 'utf8'); const file = fs.readFileSync(enterpriseFile, 'utf8');
this.enterpriseSettings = JSON.parse(file); enterpriseSettings = JSON.parse(file);
} catch (err) { } catch (err) {
logger.log('Error while JSON parsing global_config.json: '); logger.log('Error while JSON parsing global_config.json: ');
logger.log(err); logger.log(err);
} }
} else { } else {
this.configFile = false; configFile = false;
}
} }
}
getConfigItem(key: string, defaultValue?: any): any { export function getConfigItem(key: string, defaultValue?: any): any {
this.reloadDB(); reloadDB();
if (!this.configFile) { if (!configFile) {
return defaultValue; return defaultValue;
} }
if (defaultValue === undefined) { if (defaultValue === undefined) {
defaultValue = null; defaultValue = null;
} }
return this.configItemExists(key) ? this.enterpriseSettings[key] : defaultValue; return configItemExists(key) ? enterpriseSettings[key] : defaultValue;
} }
configItemExists(key: string): boolean { export function configItemExists(key: string): boolean {
this.reloadDB(); reloadDB();
if (!this.configFile) { if (!configFile) {
return false; return false;
} }
return (this.enterpriseSettings[key] !== undefined); return (enterpriseSettings[key] !== undefined);
} }
isPresetOrg(url: string): boolean { export function isPresetOrg(url: string): boolean {
if (!this.configFile || !this.configItemExists('presetOrganizations')) { if (!configFile || !configItemExists('presetOrganizations')) {
return false; return false;
} }
const presetOrgs = this.enterpriseSettings.presetOrganizations; const presetOrgs = enterpriseSettings.presetOrganizations;
for (const org of presetOrgs) { for (const org of presetOrgs) {
if (url.includes(org)) { if (url.includes(org)) {
return true; return true;
} }
} }
return false; return false;
}
} }
export = new EnterpriseUtil();

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 {
instance = this; // For security, indirect links to non-whitelisted protocols
// through a real web browser via a local HTML file.
const dir = fs.mkdtempSync(
path.join(os.tmpdir(), 'zulip-redirect-')
);
const file = path.join(dir, 'redirect.html');
fs.writeFileSync(file, `\
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Refresh" content="0; url=${escape(url.href)}" />
<title>Redirecting</title>
<style>
html {
font-family: menu, "Helvetica Neue", sans-serif;
} }
</style>
return instance; </head>
} <body>
<p>Opening <a href="${escape(url.href)}">${escape(url.href)}</a>…</p>
isInternal(currentUrl: string, newUrl: string): IsInternalResponse { </body>
const currentDomain = wurl('hostname', currentUrl); </html>
const newDomain = wurl('hostname', newUrl); `);
shell.openItem(file);
const sameDomainUrl = (currentDomain === newDomain || newUrl === currentUrl + '/'); setTimeout(() => {
const isUploadsUrl = newUrl.includes(currentUrl + '/user_uploads/'); fs.unlinkSync(file);
const isInternalUrl = newUrl.includes('/#narrow') || isUploadsUrl; fs.rmdirSync(dir);
}, 15000);
return {
isInternalUrl: sameDomainUrl && isInternalUrl,
isUploadsUrl
};
}
isImage(url: string): boolean {
// test for images extension as well as urls like .png?s=100
const isImageUrl = /\.(bmp|gif|jpg|jpeg|png|webp)\?*.*$/i;
return isImageUrl.test(url);
}
isPDF(url: string): boolean {
// test for pdf extension
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,44 +15,33 @@ 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];
getUpdateItem(key: string, defaultValue: any = null): any {
this.reloadDB();
const value = this.db.getData('/')[key];
if (value === undefined) { if (value === undefined) {
this.setUpdateItem(key, defaultValue); setUpdateItem(key, defaultValue);
return defaultValue; return defaultValue;
} else { } else {
return value; return value;
} }
} }
setUpdateItem(key: string, value: any): void { export function setUpdateItem(key: string, value: any): void {
this.db.push(`/${key}`, value, true); db.push(`/${key}`, value, true);
this.reloadDB(); reloadDB();
} }
removeUpdateItem(key: string): void { export function removeUpdateItem(key: string): void {
this.db.delete(`/${key}`); db.delete(`/${key}`);
this.reloadDB(); reloadDB();
} }
reloadDB(): void { function reloadDB(): void {
const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/config/updates.json'); const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/config/updates.json');
try { try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8'); const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
@@ -69,8 +57,5 @@ class LinuxUpdateUtil {
logger.error(err); logger.error(err);
} }
} }
this.db = new JsonDB(linuxUpdateJsonPath, true, true); db = new JsonDB(linuxUpdateJsonPath, true, true);
}
} }
export = new LinuxUpdateUtil();

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,35 +1,19 @@
'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 instance;
}
// Return proxy to be used for a particular uri, to be used for request
getProxy(_uri: string): ProxyRule | void {
const parsedUri = url.parse(_uri);
if (parsedUri === null) {
return; return;
} }
const uri = parsedUri;
const proxyRules = ConfigUtil.getConfigItem('proxyRules', '').split(';'); const proxyRules = ConfigUtil.getConfigItem('proxyRules', '').split(';');
// If SPS is on and system uses no proxy then request should not try to use proxy from // 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. // environment. NO_PROXY = '*' makes request ignore all environment proxy variables.
@@ -58,17 +42,17 @@ class ProxyUtil {
}); });
return proxyRule; return proxyRule;
} }
} }
// TODO: Refactor to async function // TODO: Refactor to async function
resolveSystemProxy(mainWindow: Electron.BrowserWindow): void { export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Promise<void> {
const page = mainWindow.webContents; const page = mainWindow.webContents;
const ses = page.session; const ses = page.session;
const resolveProxyUrl = 'www.example.com'; const resolveProxyUrl = 'www.example.com';
// Check HTTP Proxy // Check HTTP Proxy
const httpProxy = new Promise(resolve => { const httpProxy = (async () => {
ses.resolveProxy('http://' + resolveProxyUrl, (proxy: string) => { const proxy = await ses.resolveProxy('http://' + resolveProxyUrl);
let httpString = ''; let httpString = '';
if (proxy !== 'DIRECT') { if (proxy !== 'DIRECT') {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY // in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
@@ -77,12 +61,11 @@ class ProxyUtil {
httpString = 'http=' + proxy.split('PROXY')[1] + ';'; httpString = 'http=' + proxy.split('PROXY')[1] + ';';
} }
} }
resolve(httpString); return httpString;
}); })();
});
// Check HTTPS Proxy // Check HTTPS Proxy
const httpsProxy = new Promise(resolve => { const httpsProxy = (async () => {
ses.resolveProxy('https://' + resolveProxyUrl, (proxy: string) => { const proxy = await ses.resolveProxy('https://' + resolveProxyUrl);
let httpsString = ''; let httpsString = '';
if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) { if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY // in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
@@ -91,26 +74,24 @@ class ProxyUtil {
httpsString += 'https=' + proxy.split('PROXY')[1] + ';'; httpsString += 'https=' + proxy.split('PROXY')[1] + ';';
} }
} }
resolve(httpsString); return httpsString;
}); })();
});
// Check FTP Proxy // Check FTP Proxy
const ftpProxy = new Promise(resolve => { const ftpProxy = (async () => {
ses.resolveProxy('ftp://' + resolveProxyUrl, (proxy: string) => { const proxy = await ses.resolveProxy('ftp://' + resolveProxyUrl);
let ftpString = ''; let ftpString = '';
if (proxy !== 'DIRECT') { if (proxy !== 'DIRECT') {
if (proxy.includes('PROXY')) { if (proxy.includes('PROXY')) {
ftpString += 'ftp=' + proxy.split('PROXY')[1] + ';'; ftpString += 'ftp=' + proxy.split('PROXY')[1] + ';';
} }
} }
resolve(ftpString); return ftpString;
}); })();
});
// Check SOCKS Proxy // Check SOCKS Proxy
const socksProxy = new Promise(resolve => { const socksProxy = (async () => {
ses.resolveProxy('socks4://' + resolveProxyUrl, (proxy: string) => { const proxy = await ses.resolveProxy('socks4://' + resolveProxyUrl);
let socksString = ''; let socksString = '';
if (proxy !== 'DIRECT') { if (proxy !== 'DIRECT') {
if (proxy.includes('SOCKS5')) { if (proxy.includes('SOCKS5')) {
@@ -121,11 +102,10 @@ class ProxyUtil {
socksString += 'socks=' + proxy.split('PROXY')[1] + ';'; socksString += 'socks=' + proxy.split('PROXY')[1] + ';';
} }
} }
resolve(socksString); return socksString;
}); })();
});
Promise.all([httpProxy, httpsProxy, ftpProxy, socksProxy]).then(values => { const values = await Promise.all([httpProxy, httpsProxy, ftpProxy, socksProxy]);
let proxyString = ''; let proxyString = '';
values.forEach(proxy => { values.forEach(proxy => {
proxyString += proxy; proxyString += proxy;
@@ -135,8 +115,4 @@ class ProxyUtil {
if (useSystemProxy) { if (useSystemProxy) {
ConfigUtil.setConfigItem('proxyRules', proxyString); ConfigUtil.setConfigItem('proxyRules', proxyString);
} }
});
}
} }
export = new ProxyUtil();

View File

@@ -1,67 +1,88 @@
import isOnline = require('is-online'); import { ipcRenderer } from 'electron';
import Logger = require('./logger-util');
import type WebView from '../components/webview';
import backoff from 'backoff';
import request from 'request';
import Logger from './logger-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 serverManagerView webview: WebView;
// it should be ServerManagerView; maybe make it a generic so we can url: string;
// pass the class from main.js
serverManagerView: any;
alreadyReloaded: boolean; alreadyReloaded: boolean;
fibonacciBackoff: backoff.Backoff;
constructor(serverManagerView: any) { constructor(webview: WebView) {
this.serverManagerView = serverManagerView; this.webview = webview;
this.url = webview.props.url;
this.alreadyReloaded = false; this.alreadyReloaded = false;
this.clearState();
} }
clearState(): void { clearState(): void {
this.alreadyReloaded = false; this.fibonacciBackoff = backoff.fibonacci({
initialDelay: 5000,
maxDelay: 300000
});
}
async isOnline(): Promise<boolean> {
return new Promise(resolve => {
try {
const ignoreCerts = DomainUtil.shouldIgnoreCerts(this.url);
if (ignoreCerts === null) {
return;
}
request(
{
url: `${this.url}/static/favicon.ico`,
...RequestUtil.requestOptions(this.url, ignoreCerts)
},
(error: Error, response: any) => {
const isValidResponse =
!error && response.statusCode >= 200 && response.statusCode < 400;
resolve(isValidResponse);
}
);
} catch (err) {
logger.log(err);
}
});
} }
pollInternetAndReload(): void { pollInternetAndReload(): void {
const pollInterval = setInterval(() => { this.fibonacciBackoff.backoff();
this._checkAndReload() this.fibonacciBackoff.on('ready', async () => {
.then(status => { if (await this._checkAndReload()) {
if (status) { this.fibonacciBackoff.reset();
this.alreadyReloaded = true; } else {
clearInterval(pollInterval); this.fibonacciBackoff.backoff();
} }
}); });
}, 1500);
} }
// TODO: Make this a async function async _checkAndReload(): Promise<boolean> {
_checkAndReload(): Promise<boolean> { if (this.alreadyReloaded) {
return new Promise(resolve => { return true;
if (!this.alreadyReloaded) { // eslint-disable-line no-negated-condition
isOnline()
.then((online: boolean) => {
if (online) {
if (!this.alreadyReloaded) {
this.serverManagerView.reloadCurrentView();
} }
if (await this.isOnline()) {
ipcRenderer.send('forward-message', 'reload-viewer');
logger.log('You\'re back online.'); logger.log('You\'re back online.');
return resolve(true); return true;
} }
logger.log('There is no internet connection, try checking network cables, modem and router.'); logger.log('There is no internet connection, try checking network cables, modem and router.');
const errMsgHolder = document.querySelector('#description'); const errorMessageHolder = document.querySelector('#description');
if (errMsgHolder) { if (errorMessageHolder) {
errMsgHolder.innerHTML = ` errorMessageHolder.innerHTML = `
<div>Your internet connection doesn't seem to work properly!</div> <div>Your internet connection doesn't seem to work properly!</div>
<div>Verify that it works and then click try again.</div>`; <div>Verify that it works and then click try again.</div>`;
} }
return resolve(false); return false;
});
} else {
return resolve(true);
}
});
} }
} }
export = ReconnectUtil;

View File

@@ -1,48 +1,49 @@
import fs = require('fs'); import { remote } from 'electron';
import ConfigUtil = require('./config-util');
import Logger = require('./logger-util'); import fs from 'fs';
import ProxyUtil = require('./proxy-util'); import path from 'path';
import CertificateUtil = require('./certificate-util'); import * as ConfigUtil from './config-util';
import SystemUtil = require('./system-util'); import Logger from './logger-util';
import * as ProxyUtil from './proxy-util';
import * as CertificateUtil from './certificate-util';
import * as SystemUtil from './system-util';
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);
}
return instance;
}
// ignoreCerts parameter helps in fetching server icon and
// other server details when user chooses to ignore certificate warnings
requestOptions(domain: string, ignoreCerts: boolean): RequestUtilResponse {
domain = this.formatUrl(domain);
const certificate = CertificateUtil.getCertificate( const certificate = CertificateUtil.getCertificate(
encodeURIComponent(domain) encodeURIComponent(domain)
); );
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);
}
let certificateLocation = ''; let certificateLocation = '';
if (certificate) { if (certificate) {
// To handle case where certificate has been moved from the location in certificates.json // To handle case where certificate has been moved from the location in certificates.json
try { try {
certificateLocation = fs.readFileSync(certificate, 'utf8'); certificateLocation = fs.readFileSync(certificateFile, 'utf8');
} catch (err) { } catch (err) {
logger.warn(`Error while trying to get certificate: ${err}`); logger.warn(`Error while trying to get certificate: ${err}`);
} }
@@ -57,17 +58,13 @@ class RequestUtil {
headers: { 'User-Agent': SystemUtil.getUserAgent() }, headers: { 'User-Agent': SystemUtil.getUserAgent() },
rejectUnauthorized: !ignoreCerts rejectUnauthorized: !ignoreCerts
}; };
} }
formatUrl(domain: string): string { function formatUrl(domain: string): string {
const hasPrefix = domain.startsWith('http', 0); const hasPrefix = domain.startsWith('http', 0);
if (hasPrefix) { if (hasPrefix) {
return domain; return domain;
} else { } else {
return domain.includes('localhost:') ? `http://${domain}` : `https://${domain}`; return domain.includes('localhost:') ? `http://${domain}` : `https://${domain}`;
} }
}
} }
const requestUtil = new RequestUtil();
export = requestUtil;

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,37 +1,22 @@
'use strict';
import { remote } from 'electron'; import { remote } from 'electron';
import os = require('os'); import os from 'os';
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[];
userAgent: string | null;
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
this.connectivityERR = [
'ERR_INTERNET_DISCONNECTED', 'ERR_INTERNET_DISCONNECTED',
'ERR_PROXY_CONNECTION_FAILED', 'ERR_PROXY_CONNECTION_FAILED',
'ERR_CONNECTION_RESET', 'ERR_CONNECTION_RESET',
'ERR_NOT_CONNECTED', 'ERR_NOT_CONNECTED',
'ERR_NAME_NOT_RESOLVED', 'ERR_NAME_NOT_RESOLVED',
'ERR_NETWORK_CHANGED' 'ERR_NETWORK_CHANGED'
]; ];
this.userAgent = null;
return instance; let userAgent: string | null = null;
}
getOS(): string { export function getOS(): string {
const platform = os.platform(); const platform = os.platform();
if (platform === 'darwin') { if (platform === 'darwin') {
return 'Mac'; return 'Mac';
@@ -46,15 +31,15 @@ class SystemUtil {
} else { } else {
return ''; return '';
} }
}
setUserAgent(webViewUserAgent: string): void {
this.userAgent = `ZulipElectron/${app.getVersion()} ${webViewUserAgent}`;
}
getUserAgent(): string | 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,29 +0,0 @@
'use strict';
const path = require("path");
const electron = require("electron");
const i18n = require("i18n");
let instance = null;
let app = null;
/* To make the util runnable in both main and renderer process */
if (process.type === 'renderer') {
app = electron.remote.app;
}
else {
app = electron.app;
}
class TranslationUtil {
constructor() {
if (instance) {
return this;
}
instance = this;
i18n.configure({
directory: path.join(__dirname, '../../../translations/'),
register: this
});
}
__(phrase) {
return i18n.__({ phrase, locale: app.getLocale() });
}
}
module.exports = new TranslationUtil();

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({
directory: path.join(__dirname, '../../../translations/'),
register: this
});
}
__(phrase: string): string {
return i18n.__({ phrase, locale: app.getLocale() ? app.getLocale() : 'en' }); return i18n.__({ phrase, locale: app.getLocale() ? app.getLocale() : 'en' });
}
} }
export = new TranslationUtil();

View File

@@ -9,14 +9,18 @@
<body> <body>
<div id="content"> <div id="content">
<div id="picture"><img src="img/zulip_network.png"></div> <div id="picture"><img src="img/zulip_network.png"></div>
<div id="title">Zulip can't connect</div> <div id="title">We can't connect to this organization</div>
<div id="description"> <div id="subtitle">This could be because</div>
<div>Your computer seems to be offline.</div> <ul id="description">
<div>We will keep trying to reconnect, or you can try now.</div> <li>You're not online or your proxy is misconfigured.</li>
<li>There is no Zulip organization hosted at this URL.</li>
<li>This Zulip organization is temporarily unavailable.</li>
<li>This Zulip organization has been moved or deleted.</li>
</ul>
<div id="buttons">
<div id="reconnect" class="button">Reconnect</div>
<div id="settings" class="button">Settings</div>
</div> </div>
<div id="reconnect">Try now</div>
</div> </div>
</body> </body>
<script src="js/pages/network.js"></script>
<script>require('./js/shared/preventdrag.js')</script>
</html> </html>

Binary file not shown.

View File

@@ -3,48 +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 https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`;
}
noOrgsError(domain: string): string { export function noOrgsError(domain: string): string {
return `${domain} does not have any organizations added.\ return `${domain} does not have any organizations added.\
\nPlease contact your server administrator.`; \nPlease contact your server administrator.`;
} }
certErrorMessage(domain: string, error: string): string { export function certErrorMessage(domain: string, error: string): string {
return `Do you trust certificate from ${domain}? \n ${error}`; return `Do you trust certificate from ${domain}? \n ${error}`;
} }
certErrorDetail(): string { 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. 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. \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. \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.`; \nYou can click here if you'd like to proceed with the connection.`;
} }
enterpriseOrgError(length: number, domains: string[]): DialogBoxError { export function enterpriseOrgError(length: number, domains: string[]): DialogBoxError {
let domainList = ''; let domainList = '';
for (const domain of domains) { for (const domain of domains) {
domainList += `${domain}\n`; domainList += `${domain}\n`;
} }
return { return {
title: `Could not add the following ${length === 1 ? `organization` : `organizations`}`, title: `Could not add the following ${length === 1 ? 'organization' : 'organizations'}`,
content: `${domainList}\nPlease contact your system administrator.` 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 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

@@ -0,0 +1,20 @@
# How to help translate Zulip Desktop
These are *generated* files (*) that contain translations of the strings in
the app.
You can help translate Zulip Desktop into your language! We do our
translations in Transifex, which is a nice web app for collaborating on
translations; a maintainer then syncs those translations into this repo.
To help out, [join the Zulip project on
Transifex](https://www.transifex.com/zulip/zulip/) and enter translations
there. More details in the [Zulip contributor docs](https://zulip.readthedocs.io/en/latest/translating/translating.html#translators-workflow).
Within that Transifex project, if you'd like to focus on Zulip Desktop, look
at `desktop.json`. The other resources there are for the Zulip web/mobile
app, where translations are also very welcome.
(*) One file is an exception: `en.json` is manually maintained as a
list of (English) messages in the source code, and is used when we upload to
Transifex a list of strings to be translated. It doesn't contain any
translations.

122
app/translations/en-GB.json Normal file
View File

@@ -0,0 +1,122 @@
{
"Add Organization": "Add Organization",
"Toggle Do Not Disturb": "Toggle Do Not Disturb",
"Desktop Settings": "Desktop Settings",
"Keyboard Shortcuts": "Keyboard Shortcuts",
"Copy Zulip URL": "Copy Zulip URL",
"Log Out of Organization": "Log Out of Organization",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"Minimize": "Minimize",
"Close": "Close",
"Quit": "Quit",
"Edit": "Edit",
"Undo": "Undo",
"Redo": "Redo",
"Cut": "Cut",
"Copy": "Copy",
"Paste": "Paste",
"Paste and Match Style": "Paste and Match Style",
"Select All": "Select All",
"View": "View",
"Reload": "Reload",
"Hard Reload": "Hard Reload",
"Toggle Full Screen": "Toggle Full Screen",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Actual Size": "Actual Size",
"Toggle Tray Icon": "Toggle Tray Icon",
"Toggle Sidebar": "Toggle Sidebar",
"Auto hide Menu bar": "Auto hide Menu bar",
"History": "History",
"Back": "Back",
"Forward": "Forward",
"Window": "Window",
"Tools": "Tools",
"Check for Updates": "Check for Updates",
"Release Notes": "Release Notes",
"Factory Reset": "Factory Reset",
"Download App Logs": "Download App Logs",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Help": "Help",
"About Zulip": "About Zulip",
"Help Center": "Help Center",
"Report an Issue": "Report an Issue",
"Switch to Next Organization": "Switch to Next Organization",
"Switch to Previous Organization": "Switch to Previous Organization",
"General": "General",
"Network": "Network",
"AddServer": "AddServer",
"Organizations": "Organizations",
"Shortcuts": "Shortcuts",
"Settings": "Settings",
"Add a Zulip organization": "Add a Zulip organization",
"Organization URL": "Organization URL",
"Connect": "Connect",
"OR": "OR",
"Create a new organization": "Create a new organization",
"Proxy": "Proxy",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"Manual proxy configuration": "Manual proxy configuration",
"script": "script",
"Proxy rules": "Proxy rules",
"Proxy bypass rules": "Proxy bypass rules",
"Save": "Save",
"Appearance": "Appearance",
"Show app icon in system tray": "Show app icon in system tray",
"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)",
"Show sidebar": "Show sidebar",
"Show app unread badge": "Show app unread badge",
"Bounce dock on new private message": "Bounce dock on new private message",
"Flash taskbar on new message": "Flash taskbar on new message",
"Desktop Notifications": "Desktop Notifications",
"Show desktop notifications": "Show desktop notifications",
"Mute all sounds from Zulip": "Mute all sounds from Zulip",
"App Updates": "App Updates",
"Enable auto updates": "Enable auto updates",
"Get beta updates": "Get beta updates",
"Functionality": "Functionality",
"Start app at login": "Start app at login",
"Always start minimized": "Always start minimized",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Advanced": "Advanced",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Add custom CSS": "Add custom CSS",
"Upload": "Upload",
"Delete": "Delete",
"Default download location": "Default download location",
"Change": "Change",
"Reset Application Data": "Reset Application Data",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Reset App Data": "Reset App Data",
"Connected organizations": "Connected organizations",
"All the connected orgnizations will appear here.": "All the connected orgnizations will appear here.",
"Connect to another organization": "Connect to another organization",
"Add Custom Certificates": "Add Custom Certificates",
"Find accounts by email": "Find accounts by email",
"All the connected orgnizations will appear here": "All the connected orgnizations will appear here",
"Disconnect": "Disconnect",
"Certificate file": "Certificate file",
"Find accounts": "Find accounts",
"Tip": "Tip",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"keyboard shortcuts": "keyboard shortcuts",
"Application Shortcuts": "Application Shortcuts",
"Reset App Settings": "Reset App Settings",
"Log Out": "Log Out",
"Hide Zulip": "Hide Zulip",
"Quit Zulip": "Quit Zulip",
"Edit Shortcuts": "Edit Shortcuts",
"Emoji & Symbols": "Emoji & Symbols",
"View Shortcuts": "View Shortcuts",
"Enter Full Screen": "Enter Full Screen",
"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

@@ -113,5 +113,7 @@
"Zoom Out": "Zoom Out", "Zoom Out": "Zoom Out",
"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",
"Ask where to save files before downloading": "Ask where to save files before downloading"
} }

View File

@@ -1,58 +1,58 @@
{ {
"About Zulip": "О Зулип", "About Zulip": "О Zulip",
"Actual Size": "Фактический размер", "Actual Size": "Фактический размер",
"Add Custom Certificates": "Добавить пользовательские сертификаты", "Add Custom Certificates": "Добавить пользовательские сертификаты",
"Add Organization": "Добавить организацию", "Add Organization": "Добавить организацию",
"Add a Zulip organization": "Добавить организацию Zulip", "Add a Zulip organization": "Добавить организацию Zulip",
"Add custom CSS": "Добавить собственный CSS", "Add custom CSS": "Добавить собственный CSS",
"Advanced": "продвинутый", "Advanced": "Расширенные",
"All the connected organizations will appear here": "Все связанные организации появятся здесь", "All the connected organizations will appear here": "Все связанные организации появятся здесь",
"Always start minimized": "Всегда начинайте сворачивать", "Always start minimized": "Всегда запускаться свернутым",
"App Updates": "Обновления приложений", "App Updates": "Обновления приложения",
"Appearance": "Внешность", "Appearance": "Внешний вид",
"Application Shortcuts": "Ярлыки приложений", "Application Shortcuts": "Горячие клавиши приложения",
"Are you sure you want to disconnect this organization?": "Вы уверены, что хотите отключить эту организацию?", "Are you sure you want to disconnect this organization?": "Вы уверены, что хотите отключить эту организацию?",
"Auto hide Menu bar": "Авто скрыть панель меню", "Auto hide Menu bar": "Авто скрыть панель меню",
"Auto hide menu bar (Press Alt key to display)": "Автоматическое скрытие строки меню (нажмите клавишу Alt для отображения)", "Auto hide menu bar (Press Alt key to display)": "Автоматическое скрытие строки меню (нажмите клавишу Alt для отображения)",
"Back": "назад", "Back": "Назад",
"Bounce dock on new private message": "Отскок док на новое личное сообщение", "Bounce dock on new private message": "Прыгающий док на новое личное сообщение",
"Certificate file": "Файл сертификата", "Certificate file": "Файл сертификата",
"Change": "+ Изменить", "Change": "+ Изменить",
"Check for Updates": "Проверить наличие обновлений", "Check for Updates": "Проверить наличие обновлений",
"Close": "близко", "Close": "Закрыть",
"Connect": "соединять", "Connect": "Подключиться",
"Connect to another organization": "Подключиться к другой организации", "Connect to another organization": "Подключиться к другой организации",
"Connected organizations": "Связанные организации", "Connected organizations": "Связанные организации",
"Copy": "копия", "Copy": "Копировать",
"Copy Zulip URL": "Скопируйте Zulip URL", "Copy Zulip URL": "Скопируйте Zulip URL",
"Create a new organization": "Создать новую организацию", "Create a new organization": "Создать новую организацию",
"Cut": "Резать", "Cut": "Вырезать",
"Default download location": "Местоположение загрузки по умолчанию", "Default download location": "Местоположение загрузок по умолчанию",
"Delete": "удалять", "Delete": "Удалить",
"Desktop App Settings": "Настройки настольного приложения", "Desktop App Settings": "Настройки приложения",
"Desktop Notifications": "Уведомления на рабочем столе", "Desktop Notifications": "Уведомления в приложении",
"Desktop Settings": "Настройки рабочего стола", "Desktop Settings": "Настройки приложения",
"Disconnect": "Отключить", "Disconnect": "Отключить",
"Download App Logs": "Скачать журналы приложений", "Download App Logs": "Скачать журналы приложения",
"Edit": "редактировать", "Edit": "Правка",
"Edit Shortcuts": "Изменить ярлыки", "Edit Shortcuts": "Изменить горячие клавиши",
"Enable auto updates": "Включить автообновления", "Enable auto updates": "Включить автообновления",
"Enable error reporting (requires restart)": "Включить отчет об ошибках (требуется перезагрузка)", "Enable error reporting (requires restart)": "Включить отчет об ошибках (требуется перезагрузка)",
"Enable spellchecker (requires restart)": "Включить проверку орфографии (требуется перезагрузка)", "Enable spellchecker (requires restart)": "Включить проверку орфографии (требуется перезагрузка)",
"Factory Reset": "Сброс к заводским настройкам", "Factory Reset": "Сброс к заводским настройкам",
"File": "файл", "File": "Файл",
"Find accounts": "Найти аккаунты", "Find accounts": "Найти аккаунты",
"Find accounts by email": "Найти аккаунты по электронной почте", "Find accounts by email": "Найти аккаунты по электронной почте",
"Flash taskbar on new message": "Flash-панель задач на новое сообщение", "Flash taskbar on new message": "Flash-панель задач на новое сообщение",
"Forward": "Вперед", "Forward": "Вперед",
"Functionality": "функциональность", "Functionality": "Функциональность",
"General": "генеральный", "General": "Общий",
"Get beta updates": "Получить бета-обновления", "Get beta updates": "Получить бета-обновления",
"Hard Reload": "Жесткая перезагрузка", "Hard Reload": "Принудительная перезагрузка",
"Help": "Помогите", "Help": "Помощь",
"Help Center": "Центр помощи", "Help Center": "Центр помощи",
"History": "история", "History": "История",
"History Shortcuts": "Ярлыки истории", "History Shortcuts": "Горячие клавиши истории",
"Keyboard Shortcuts": "Горячие клавиши", "Keyboard Shortcuts": "Горячие клавиши",
"Log Out": "Выйти", "Log Out": "Выйти",
"Log Out of Organization": "Выйти из организации", "Log Out of Organization": "Выйти из организации",
@@ -61,28 +61,28 @@
"Mute all sounds from Zulip": "Отключить все звуки от Zulip", "Mute all sounds from Zulip": "Отключить все звуки от Zulip",
"NO": "НЕТ", "NO": "НЕТ",
"Network": "сеть", "Network": "сеть",
"OR": "ИЛИ ЖЕ", "OR": "ИЛИ",
"Organization URL": "URL организации", "Organization URL": "URL организации",
"Organizations": "организации", "Organizations": "Организации",
"Paste": "Вставить", "Paste": "Вставить",
"Paste and Match Style": "Вставить и соответствовать стилю", "Paste and Match Style": "Вставить и соответствовать стилю",
"Proxy": "полномочие", "Proxy": "Прокси",
"Proxy bypass rules": "Правила обхода прокси", "Proxy bypass rules": "Правила обхода прокси",
"Proxy rules": "Правила прокси", "Proxy rules": "Правила проксирования",
"Quit": "Уволиться", "Quit": "Выйти",
"Quit Zulip": "Выйти из Zulip", "Quit Zulip": "Выйти из Zulip",
"Redo": "переделывать", "Redo": "Повторить",
"Release Notes": "Примечания к выпуску", "Release Notes": "Примечания к выпуску",
"Reload": "перезагружать", "Reload": "Перезагрузить",
"Report an Issue": "Сообщить о проблеме", "Report an Issue": "Сообщить о проблеме",
"Reset App Data": "Сбросить данные приложения", "Reset App Data": "Сбросить данные приложения",
"Reset App Settings": "Сбросить настройки приложения", "Reset App Settings": "Сбросить настройки приложения",
"Reset Application Data": "Сбросить данные приложения", "Reset Application Data": "Сбросить данные приложения",
"Save": "Сохранить", "Save": "Сохранить",
"Select All": "Выбрать все", "Select All": "Выбрать все",
"Settings": "настройки", "Settings": "Настройки",
"Shortcuts": "Ярлыки", "Shortcuts": "Горячие клавиши",
"Show App Logs": "Показать журналы приложений", "Show App Logs": "Показать журналы приложения",
"Show app icon in system tray": "Показать значок приложения в системном трее", "Show app icon in system tray": "Показать значок приложения в системном трее",
"Show app unread badge": "Показать непрочитанный значок приложения", "Show app unread badge": "Показать непрочитанный значок приложения",
"Show desktop notifications": "Показывать уведомления на рабочем столе", "Show desktop notifications": "Показывать уведомления на рабочем столе",
@@ -91,25 +91,25 @@
"Start app at login": "Запустить приложение при входе", "Start app at login": "Запустить приложение при входе",
"Switch to Next Organization": "Переключиться на следующую организацию", "Switch to Next Organization": "Переключиться на следующую организацию",
"Switch to Previous Organization": "Переключиться на предыдущую организацию", "Switch to Previous Organization": "Переключиться на предыдущую организацию",
"These desktop app shortcuts extend the Zulip webapp's": "Эти ярлыки настольных приложений расширяют возможности веб-приложения Zulip.", "These desktop app shortcuts extend the Zulip webapp's": "Эти горячие клавиши приложения расширяют возможности Zulip.",
"This will delete all application data including all added accounts and preferences": "Это удалит все данные приложения, включая все добавленные учетные записи и настройки", "This will delete all application data including all added accounts and preferences": "Это удалит все данные приложения, включая все добавленные учетные записи и настройки",
"Tip": "Совет", "Tip": "Совет",
"Toggle DevTools for Active Tab": "Переключить DevTools для активной вкладки", "Toggle DevTools for Active Tab": "Переключить DevTools для активной вкладки",
"Toggle DevTools for Zulip App": "Переключить DevTools для приложения Zulip", "Toggle DevTools for Zulip App": "Переключить DevTools для приложения Zulip",
"Toggle Do Not Disturb": "Переключить не беспокоить", "Toggle Do Not Disturb": "Переключить в режим не беспокоить",
"Toggle Full Screen": "Включить полноэкранный режим", "Toggle Full Screen": "Включить полноэкранный режим",
"Toggle Sidebar": "Переключить боковую панель", "Toggle Sidebar": "Переключить боковую панель",
"Toggle Tray Icon": "Значок переключателя в трее", "Toggle Tray Icon": "Переключить Значок в трее",
"Tools": "инструменты", "Tools": "Инструменты",
"Undo": "расстегивать", "Undo": "Повторить",
"Upload": "Загрузить", "Upload": "Загрузить",
"Use system proxy settings (requires restart)": "Использовать настройки системного прокси (требуется перезагрузка)", "Use system proxy settings (requires restart)": "Использовать настройки системного прокси (требуется перезагрузка)",
"View": "Посмотреть", "View": "Вид",
"View Shortcuts": "Посмотреть ярлыки", "View Shortcuts": "Посмотреть горячие клавиши окна",
"Window": "Окно", "Window": "Окно",
"Window Shortcuts": "Ярлыки окон", "Window Shortcuts": "Горячие клавиши окна",
"YES": "ДА", "YES": "ДА",
"Zoom In": "Приблизить", "Zoom In": "Увеличить",
"Zoom Out": "Уменьшить", "Zoom Out": "Уменьшить",
"Zulip Help": "Zulip Помощь", "Zulip Help": "Zulip Помощь",
"keyboard shortcuts": "горячие клавиши", "keyboard shortcuts": "горячие клавиши",

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

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

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

View File

@@ -24,4 +24,4 @@ If you'd like to remove organizations and have admin access, you'll need to chan
It also turns off automatic updates for every Zulip user on the same machine. It also turns off automatic updates for every Zulip user on the same machine.
> Currently, we only support `presetOrganizations` and `autoUpdate` settings. We are working on other settings as well, and will update this page when we add support for more. Currently, we only support `presetOrganizations` and `autoUpdate` settings. We are working on other settings as well, and will update this page when we add support for more.

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,10 +21,10 @@ 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', () => {

12959
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.0", "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",
@@ -43,24 +43,29 @@
"test" "test"
], ],
"build": { "build": {
"afterSign": "./scripts/notarize.js",
"appId": "org.zulip.zulip-electron", "appId": "org.zulip.zulip-electron",
"asar": true, "asar": true,
"asarUnpack": [ "asarUnpack": [
"**/*.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": {
"category": "public.app-category.productivity", "category": "public.app-category.productivity",
"darkModeSupport": true, "darkModeSupport": true,
"artifactName": "${productName}-${version}-${arch}.${ext}" "artifactName": "${productName}-${version}-${arch}.${ext}",
"hardenedRuntime": true,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist",
"gatekeeperAssess": false
}, },
"linux": { "linux": {
"category": "Chat;GNOME;GTK;Network;InstantMessaging", "category": "Chat;GNOME;GTK;Network;InstantMessaging",
"icon": "build/icon.icns",
"packageCategory": "GNOME;GTK;Network;InstantMessaging", "packageCategory": "GNOME;GTK;Network;InstantMessaging",
"description": "Zulip Desktop Client for Linux", "description": "Zulip Desktop Client for Linux",
"target": [ "target": [
@@ -110,6 +115,13 @@
"x64", "x64",
"ia32" "ia32"
] ]
},
{
"target": "msi",
"arch": [
"x64",
"ia32"
]
} }
], ],
"icon": "build/icon.ico", "icon": "build/icon.ico",
@@ -130,96 +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.40.2", "@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",
"eslint-config-xo-typescript": "0.14.0", "@vitalets/google-translate-api": "^3.0.0",
"fs-extra": "8.1.0", "devtron": "^1.4.0",
"gulp": "4.0.0", "dotenv": "^8.2.0",
"gulp-tape": "0.0.9", "electron": "^8.1.1",
"gulp-typescript": "5.0.1", "electron-builder": "^22.4.1",
"htmlhint": "0.11.0", "electron-connect": "^0.6.3",
"is-ci": "1.0.10", "electron-notarize": "^0.2.1",
"nodemon": "1.14.11", "eslint-config-xo-typescript": "^0.26.0",
"pre-commit": "1.2.2", "glob": "^7.1.6",
"spectron": "5.0.0", "gulp": "^4.0.2",
"stylelint": "9.10.1", "gulp-tape": "^1.0.0",
"tap-colorize": "1.2.0", "gulp-typescript": "^6.0.0-alpha.1",
"tape": "4.8.0", "htmlhint": "^0.11.0",
"typescript": "3.5.2", "nodemon": "^2.0.2",
"xo": "0.24.0" "pre-commit": "^1.2.2",
"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",
"extensions": [
"ts"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"globalReturn": true,
"modules": true
}
},
"esnext": true,
"overrides": [
{
"files": "app/**/*.ts",
"rules": { "rules": {
"@typescript-eslint/member-ordering": "off",
"@typescript-eslint/no-dynamic-delete": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"capitalized-comments": "off",
"import/no-mutable-exports": "off",
"import/unambiguous": "error",
"max-lines": [ "max-lines": [
"warn", "warn",
{ {
"max": 800, "max": 900,
"skipBlankLines": true, "skipBlankLines": true,
"skipComments": true "skipComments": true
} }
], ],
"no-warning-comments": 0, "no-alert": "off",
"object-curly-spacing": 0, "no-else-return": "off",
"capitalized-comments": 0, "no-warning-comments": "off",
"no-else-return": 0, "object-curly-spacing": "off",
"no-path-concat": 0, "padding-line-between-statements": "off",
"no-alert": 0, "strict": "error",
"guard-for-in": 0, "unicorn/catch-error-name": "off",
"prefer-promise-reject-errors": 0, "unicorn/string-content": "off"
"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
}
}
],
"ignore": [
"tests/*.js",
"tools/locale-helper/*.js",
"*/**/*.js",
"*.js",
"typings.d.ts"
],
"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"
}
}
] ]
} }
} }

23
scripts/notarize.js Normal file
View File

@@ -0,0 +1,23 @@
'use strict';
const path = require('path');
const dotenv = require('dotenv');
dotenv.config({path: path.join(__dirname, '/../.env')});
const {notarize} = require('electron-notarize');
exports.default = async function (context) {
const {electronPlatformName, appOutDir} = context;
if (electronPlatformName !== 'darwin') {
return;
}
const appName = context.packager.appInfo.productFilename;
return notarize({
appBundleId: 'org.zulip.zulip-electron',
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID,
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,12 +1,10 @@
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,
@@ -14,86 +12,85 @@ module.exports = {
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)
return path.join(base, config.TEST_APP_PRODUCT_NAME) console.log('Detected App Data Dir base:', base);
return path.join(base, config.TEST_APP_PRODUCT_NAME);
} }
// Resets the test directory, containing domain.json, window-state.json, etc // 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');
@@ -9,16 +10,17 @@ function writeJSON(file, data) {
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 result = await translate(phrases.join('\n'), {to: locale});
const localeFile = `${locale}.json`; const localeFile = `${locale}.json`;
const translatedText = res.text.split('\n'); const translatedText = result.text.split('\n');
const translationJSON = {}; const translationJSON = {};
phrases.forEach((phrase, index) => { phrases.forEach((phrase, index) => {
translationJSON[phrase] = translatedText[index]; translationJSON[phrase] = translatedText[index];
@@ -26,7 +28,8 @@ for (let locale in supportedLocales) {
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

Some files were not shown because too many files have changed in this diff Show More