Compare commits

...

130 Commits

Author SHA1 Message Date
Akash Nimare
dcc7cd118c release: 🎉 new release v2.3.82. 2018-10-11 21:53:25 +05:30
Abhigyan Khaund
d2649dd5d7 native-menus: Add tools submenu and reword a few options.
Check for Updates → Move to Tools
Reset App Settings → Move to Tools, rename “Factory reset”
Both Dev Tools → Move to Tools
Download App Logs → Move to Tools
What’s new → Move to Tools, and rename “Release notes”.
2018-10-10 13:53:25 +05:30
Akash Nimare
25e6f6d482 sentry: Update Sentry to v0.11.0.
This could potentially fix #577 since the issue is caused by Sentry.
2018-10-09 13:51:12 +05:30
Akash Nimare
bd805b8324 native-menus: Rename a few options of the submenus. 2018-10-07 19:17:31 +05:30
Abhigyan Khaund
e1d98e0d8e native-menus: Rename a few options of the submenus. 2018-10-07 17:41:57 +05:30
Akash Nimare
89e5d2892b readme: Replace the word team with organizations. 2018-10-04 00:37:49 +05:30
Akash Nimare
ab28b56377 tray: Remove about menu item and rename focus. 2018-10-03 20:47:24 +05:30
Abhigyan Khaund
71aa7e28ba readme: Replace word team with organizations. 2018-10-02 13:50:57 +05:30
Akash Nimare
ba2bfba459 settings: Rename custom css section. 2018-10-01 18:10:40 +05:30
Akash Nimare
d181b0c2e5 settings: Move custom css section to advanced. 2018-10-01 18:07:20 +05:30
Akash Nimare
bda5c62928 settings: Put shortcut tip on the top. 2018-10-01 17:53:11 +05:30
Akash Nimare
b2b4fd1003 settings: UI fixes in general settings. 2018-09-28 21:03:49 +05:30
Akash Nimare
19770f5fbf settings: Update wordings of org settings. 2018-09-28 13:28:47 +05:30
Akash Nimare
4592396c4b settings: Update wordings of general settings. 2018-09-28 13:18:06 +05:30
Akash Nimare
19d3c0f5b5 left-sidebar: Update the border-radius of org icon.
Org icon now have rounded squares.

Fixes: #574.
2018-09-28 13:06:19 +05:30
Akash Nimare
af87789c7f feedback: Upgrade send feedback to latest. 2018-09-27 19:43:18 +05:30
Priyank Patel
36e710dbfe gitattributes: Mark package-lock.json files binary.
So they don't show up during `git diff`.
2018-09-26 20:46:36 -04:00
Priyank Patel
a6e1c93d81 dependencies: Upgrade send-feedback module to v1.0.8.
Fixes: https://github.com/zulip/zulip-electron/issues/569
2018-09-26 20:46:36 -04:00
Akash Nimare
684d88dd7f menus: Rename a few menu items. 2018-09-26 13:44:51 +05:30
Akash Nimare
45aedbd9db tray: Add a separator in tray before quit item. 2018-09-26 13:44:01 +05:30
Akash Nimare
6f411a819c menu: Remove whitespace from app version. 2018-09-26 12:27:09 +05:30
Rishi Gupta
097738332b tray: Remove separators. 2018-09-25 12:43:32 -07:00
Rishi Gupta
b777f8a50f menus: Rename a few menu items. 2018-09-25 09:51:01 -07:00
Akash Nimare
50f06239bb Add release notes for v2.3.8. 2018-09-25 18:58:19 +05:30
Akash Nimare
94cbc786d6 release: 🎉 new release v2.3.8. 2018-09-24 16:45:50 +05:30
Akash Nimare
75abf38515 sentry: Update Sentry to v0.10.1. 2018-09-24 16:45:50 +05:30
Akash Nimare
c1af019182 sentry: Add ignore errors to sentry configuration.
We could ignore the validate realm  error since it's harmless and we know the reason behind this.
It mostly comes from the console logs. This is a temp solution until Sentry supports disabling
the console logs.
2018-09-24 16:43:32 +05:30
Akash Nimare
1ea8aea22a sentry: Update Sentry to v0.9.0. 2018-09-21 14:50:47 +05:30
Akash Nimare
a7e418885a Merge branch 'master' of github.com:zulip/zulip-electron 2018-09-20 23:11:19 +05:30
Akash Nimare
0ac823c85b Update package-lock.json. 2018-09-20 23:10:47 +05:30
Akash Nimare
1096188ddf Add v2.3.7-beta release notes. 2018-09-20 23:06:49 +05:30
Akash Nimare
8d0b68316c menubar: Auto hide menubar on Windows/Linux.
Fixes: #563.
2018-09-19 15:09:39 +05:30
Akash Nimare
c45ad7096f menubar: Document auto-hide menubar setting. 2018-09-18 17:59:51 +05:30
Akash Nimare
5ca7937e52 menubar: Fix menubar not hiding properly. 2018-09-18 17:36:55 +05:30
Akash Nimare
481b99a2ab menubar: Fix a bug in menubar settings. 2018-09-18 13:47:16 +05:30
Akash Nimare
7eb4adf722 menubar: Update menubar on changing the autoHideMenubar setting. 2018-09-18 13:41:47 +05:30
Akash Nimare
4d21a8f8d8 menubar: Auto hide menubar on Windows/Linux.
This PR adds a setting option to auto hide the menubar.

Fixes: #563.
2018-09-18 12:32:23 +05:30
Akash Nimare
94270af265 settings: Minor changes in font-color. 2018-09-17 21:30:50 +05:30
Akash Nimare
177bff6113 UI: Clean up design of setting page.
This commit cleans up the design of setting page and
remove the inconsistency in the page design.

Few changes -
* Update the border-radius to 4px for all the buttons
* Better button UI
* Improve the layout of the setting page
* Cleaner toggle switch
2018-09-17 16:42:23 +05:30
Akash Nimare
b4fa4cacb6 electron: Update electron to v2.0.9. 2018-09-14 22:47:38 +05:30
Akash Nimare
06abf6716a Add dev into master. 2018-09-14 19:19:19 +05:30
Priyank Patel
0ad4ea4fdf left-sidebar: Add initial character of Realm Name instead of default icon.
This PR adds a feature for showing the first char of realm name as the server icon when the
icon fails to load for some reasons.
2018-09-14 18:18:45 +05:30
Akash Nimare
2ece432d19 Linux: Add label for help menu item. 2018-09-13 14:31:36 +05:30
Akash Nimare
10cd7850ca linux: Fix ALT+SHIFT opening menu items on Linux.
It's an upstream bug where if you press ALT+SHIFT on
Linux, it opens up the app menu items. The fix is adding
the & to all the menu labels.

Fixes: #528.
2018-09-13 13:13:18 +05:30
bemyak
86e3d06e52 Improve tray icon behavior. 2018-09-12 18:25:55 +05:30
Abhigyan Khaund
6ebc9acbf4 realm name: Update sidebar realm name when it's changed in webapp.
This commit updates the realm name in the sidebar tooltip,
domains.json and Window menu when it is updated in the server
through electron_bridge.
This commit also removes the server.alias parameter from the
tooltip onHover function and inserts it in the innerHTML of the
tooltip DOM.

Fixes: #425
2018-09-11 21:03:33 +05:30
Akash Nimare
278dc686e1 file-attachments: Allow multiple downloads of same file name.
Previous flow used to overwrite the file if it has the same name.
Current flow is same as what Chrome uses (though we use timestamp
in the file name rather than increaments).

Fixes: #558.
2018-09-10 17:49:09 +05:30
Akash Nimare
cec98c030e lint-xo: Update the max lines in a file. 2018-09-07 01:26:05 +05:30
Akash Nimare
f10d6eb115 setting: Update silent setting option.
Correctly update the audio of the webview on toogle-silent event.
2018-09-07 01:23:17 +05:30
Akash Nimare
8c5e928e9c settings: Clean up toggle sidebar setting.
This removes the two copies of the same toggle-sidebar
ipc event.
2018-09-07 01:20:49 +05:30
Akash Nimare
8d142c8c3f ipc: Move IPC listners to different function.
Those IPCs were wrongly put in the activateTab function.
2018-09-07 00:56:19 +05:30
Akash Nimare
a09769c560 release: 🎉 new beta release v2.3.7-beta. 2018-09-03 15:30:35 +05:30
Abhigyan Khaund
bb99015fd2 config-util: Use package reload instead of custom reload.
This is a WIP commit to fix the errors while saving settings that occur from
reloadDB function.
2018-09-03 15:25:07 +05:30
Akash Nimare
24f5c9b226 builder: Update electron-builder to v20.28.3. 2018-09-03 15:25:07 +05:30
Akash Nimare
dc6c4be5fe updater: Update electron-updater to v3.1.2. 2018-09-03 15:25:07 +05:30
Akash Nimare
50647e330b server-name: Unescape server name in window menu item.
Escaping is necessary to avoid any security risk but we need
to unescape those strings in order to show them in the frontend
otherwise it will have ugly special characters.

We already escape server name in the db and unesacoe it in
the left-sidebar. This PR adds the decodeString function in
order to unescape strings in the menu items.

Fixes: #554.
2018-09-03 15:20:02 +05:30
Rishi Gupta
73dc3db436 readme: Add tray/dock integration to features. 2018-09-01 23:15:39 +05:30
Akash Nimare
09cf21bf49 pdf-viewer: Hide menubar in pdf window.
We don't want to show the menubar in a pdf window
so setting it to null.
2018-08-30 21:32:31 +05:30
Akash Nimare
c30d0cc77b Add Zulip chat badge. 2018-08-30 17:40:42 +05:30
Rishi Gupta
872ad4d3e7 Update README.md. 2018-08-30 17:38:07 +05:30
Akash Nimare
6fd9e1be8b pdf-viewer: Add a feature to show the pdf files.
This adds a feature of showing the pdf attachments in a
new window so that a user can quickly view the same.

Fixes: #547.
2018-08-29 23:19:38 +05:30
Harmon
76c7f24161 settings: Update reference to toggle sidebar shortcut in general section. 2018-08-27 08:49:48 -07:00
Akash Nimare
f9c270492c Fix a typo in changelog. 2018-08-27 15:00:09 +05:30
Akash Nimare
371c580934 Add v2.3.6 release notes. 2018-08-27 14:59:11 +05:30
Akash Nimare
3eec4c2209 release: 🎉 new release v2.3.6. 2018-08-23 20:10:18 +05:30
Akash Nimare
debbfb6b7d electron: Update electron to v2.0.8. 2018-08-23 18:56:36 +05:30
Akash Nimare
8bd1492586 left-sidebar: Do not escape realm name.
We escape the realm name whenever user adds a realm + on app startup.
That's why we don't need to do the double escaping for already added
servers.

Fixes: #541.
2018-08-23 18:48:06 +05:30
Akash Nimare
1115c6d5c3 docs: Remove help docs from the repo.
Content is now maintained in the /help docs.

Fixes: #543.
2018-08-23 12:32:25 +05:30
Abhigyan Khaund
9ba279213c proxy: Add proxy details in request module parameters.
This commit adds proxy details to request module paramters from
the proxyRules so that the request module can use these rules while
sending a request. In case of no system proxy, set environment
variable NO_PROXY to handle all links.

Fixes: #534.
2018-08-21 00:26:37 +05:30
Akash Nimare
89c35cb1d4 electron: Update electron to v2.0.7. 2018-08-17 14:16:00 +05:30
Akash Nimare
21d6eb52c5 sentry: Update Sentry to v0.8.1.
This fixes the youtube video not playing in the lightbox issue.
Youtube video stopped playing in the lightbox when we added the
Sentry support. The exact reason behind the issue is still unknown
but we're guessing that previous version of Sentry doesn't exit
process on oncaught errors which might have caused lightbox to break.
The issue was fixed in the latest release of the Sentry v0.8.1.

Fixes: #537.
2018-08-14 18:02:10 +05:30
Akash Nimare
aa1538837b Add v2.3.5 release notes. 2018-08-03 17:39:48 +05:30
Akash Nimare
ea103380b6 release: 🎉 new release v2.3.5. 2018-08-02 20:27:29 +05:30
aashish-ak
60d10d88d1 preload: use page_params only when it exists.
This PR adds a params-util.js file which checks wheather the
page_params exists or not.

Fixes: #517.
2018-08-02 20:00:42 +05:30
Abhigyan Khaund
124a842bbd electron-bridge: Implement electron bridge.
This PR adds a bridge to communicate with the webapp in real time. As of now, the bridge listens for following events -

* When realm name changes
* When realm icon changes
* When the unread count changes

Partially fixes #425.
2018-08-02 01:05:29 +05:30
Akash Nimare
7130103999 file-attachment: Update notification message. 2018-08-02 00:11:47 +05:30
Akash Nimare
26a144e1c2 Merge branch 'master' of github.com:zulip/zulip-electron 2018-08-01 23:50:21 +05:30
Akash Nimare
a5c1ae8726 file-attachment: Add a setting option to show downloaded file in file manager. 2018-08-01 23:50:02 +05:30
Abhigyan Khaund
ed5096840d attachments: Fix failing attached file downloads.
This commit fixes the failing download files that had occurred due to the session not being the same in the browserWindow and the webview. This made the uploaded files unavailable to browserWindow for download. This fix adds the persist session to the browserWindow.

Fixes: #523.
2018-07-31 19:38:39 +05:30
Abhigyan Khaund
3f6d256910 gulpfile: Update syntax and methods for gulp v4.x.
This fixes the broken e2e-test.
2018-07-25 16:14:04 +05:30
Akash Nimare
28421992ba Update changelog.md 2018-07-24 19:31:25 +05:30
Akash Nimare
82199dd1c3 release: new beta release v2.3.4. 2018-07-23 23:16:18 +05:30
Akash Nimare
952baf1f42 electron: Downgrade electron to v2.0.1.
Looks like the latest release of electron has some
bugs and it's breaking the app. Downgrading until they
fix this in the upstream.
2018-07-23 23:12:03 +05:30
Abhigyan Khaund
3342d7da91 attachement: Handle downloading files failure.
In case of any failure, the app will fall back to the previous download
functionality which is to show a download dialog when a user clicks on 
file attachments.
2018-07-23 21:07:51 +05:30
Abhigyan Khaund
c0ec292090 fixes: Fix null of downloadPath when settings.json fails. 2018-07-23 21:04:49 +05:30
Akash Nimare
8f6fba0b97 electron: Update electron to v2.0.5. 2018-07-21 16:44:54 +05:30
Akash Nimare
d3453a3fe1 Update changelog.md 2018-07-14 21:31:24 +05:30
Akash Nimare
18e14fd3f8 Update changelog.md 2018-07-14 21:30:31 +05:30
Akash Nimare
c52bfd6a45 release: 🎉 v2.3.3. 2018-07-13 18:18:25 +05:30
Akash Nimare
428087110a tools: Add scripts to push/reset to pull request. 2018-07-13 17:05:46 +05:30
Abhigyan Khaund
781883a898 crash-reporter: Remove crash-reporter file and its use, as it is replaced by sentry. 2018-07-13 12:57:48 +05:30
Abhigyan Khaund
7299cd51e6 sentry: Initialize sentry for main and renderer process using logger-util.
captureExceptions are used to report expected exceptions from domain-util.js and
config-util.js.
All files that include logger-util automatically includes sentry for those files
and report unexpected exceptions to sentry.
2018-07-13 12:57:48 +05:30
Akash Nimare
a45254cff0 builder: Update electron-builder to v20.20.4. 2018-07-13 10:38:01 +05:30
Abhigyan Khaund
4095c121ba certificates: Make default option "No" on enter key press to reject certificate. 2018-07-12 20:32:33 +05:30
Abhigyan Khaund
6787b8d557 file-attachments: Download file-attachments to Downloads folder.
This PR adds a feature to download file-attachments to "Downloads" folder and shows a
notification when the download is complete. On clicking the notification user can
open the downloaded file with system default app. A user can set/change the download location
from the settings > general.
2018-07-12 18:39:43 +05:30
Akash Nimare
306e0f3b5e macOS: Add dock bouncing effect on macOS.
This PR adds a new setting option of controlling the
dock bouncing feature on macOS.

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

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

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

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

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

This updates rc dependecies, to fix a vulnerability

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

* dependencies: Upgrade stringstream to the depth of 6.

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

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

* gulp: Upgrade gulp to v4.0.0.

This solves, 5 vulnerabilities.

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

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

* dependencies: Update request to the depth of 6.

This solves 5 vulnerabilities.

stringstream - Out-of-bounds Read
https://nodesecurity.io/advisories/664
2018-05-21 02:17:18 +05:30
Akash Nimare
0ac3e3f6d3 New release v2.3.0. 2018-05-17 00:59:39 +05:30
Akash Nimare
d69c1339e6 security: Do proper HTML escaping for server data.
This commit fixes a security bug which was caused by
using innerHTML and not doing proper HTML escaping.
Ideally, we should be doing proper HTML escaping for
any data we get from the server to avoid XSS attack.
We already handle realm icon and url very well, the
realm description was not handled previously but this commit
now fixes this in a right way.
2018-05-16 18:25:03 +05:30
Akash Nimare
fe56a20334 setting: Add an option to disable the auto-updates. (#493)
Fixes: #491.
2018-05-16 16:00:21 +05:30
Akash Nimare
cfc97c9b73 Update electron-builder. 2018-05-15 19:01:33 +05:30
Abhigyan Khaund
2e70b515da menu: Add option to check for updates.
Fixes: #479.
2018-05-15 17:20:40 +05:30
52 changed files with 14249 additions and 503 deletions

5
.gitattributes vendored
View File

@@ -1 +1,4 @@
* text=auto
* text=auto
package-lock.json binary
app/package-lock.json binary

View File

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

View File

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

View File

@@ -2,31 +2,26 @@
[![Build Status](https://travis-ci.org/zulip/zulip-electron.svg?branch=master)](https://travis-ci.org/zulip/zulip-electron)
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/zulip/zulip-electron?branch=master&svg=true)](https://ci.appveyor.com/project/akashnimare/zulip-electron/branch/master)
[![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)
[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://chat.zulip.org)
Desktop client for Zulip. Available for Mac, Linux and Windows.
Desktop client for Zulip. Available for Mac, Linux, and Windows.
<img src="http://i.imgur.com/ChzTq4F.png"/>
# Download
Please see [installation guide](https://zulipchat.com/help/desktop-app-install-guide).
Please see the [installation guide](https://zulipchat.com/help/desktop-app-install-guide).
# Features
* Sign in to multiple teams
* Desktop Notifications with inline reply support
* Multilanguage SpellChecker
* OSX/Win/Linux installers
* Automatic Updates (macOS/Windows/Linux)
* Keyboard shortcuts
# Development
Please see our [development guide](./development.md) to get started and run app locally.
* Sign in to multiple organizations
* Desktop notifications with inline reply
* Tray/dock integration
* Multi-language spell checker
* Automatic updates
# Contribute
If you want to contribute please make sure to read [our documentation about contributing](./CONTRIBUTING.md) first.
* [Issue Tracker](https://github.com/zulip/zulip-electron/issues)
* [Source Code](https://github.com/zulip/zulip-electron/)
First, join us on the [Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html)!
Also see our [contribution guidelines](./CONTRIBUTING.md) and our [development guide](./development.md).
# License
Released under the [Apache-2.0](./LICENSE) license.

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
'use strict';
const path = require('path');
const fs = require('fs');
const electron = require('electron');
const windowStateKeeper = require('electron-window-state');
const isDev = require('electron-is-dev');
const appMenu = require('./menu');
const { appUpdater } = require('./autoupdater');
const { crashHandler } = require('./crash-reporter');
const { setAutoLaunch } = require('./startup');
@@ -13,6 +14,8 @@ const { app, ipcMain } = electron;
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js');
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
const ProxyUtil = require('./../renderer/js/utils/proxy-util.js');
const { sentryInit } = require('./../renderer/js/utils/sentry-util.js');
// Adds debug features like hotkeys for triggering dev tools and reload
// in development mode
@@ -53,7 +56,8 @@ function createMainWindow() {
// Load the previous state with fallback to defaults
const mainWindowState = windowStateKeeper({
defaultWidth: 1100,
defaultHeight: 720
defaultHeight: 720,
path: `${app.getPath('userData')}/config`
});
// Let's keep the window position global so that we can access it in other process
@@ -71,7 +75,8 @@ function createMainWindow() {
minHeight: 400,
webPreferences: {
plugins: true,
nodeIntegration: true
nodeIntegration: true,
partition: 'persist:webviewsession'
},
show: false
});
@@ -131,6 +136,10 @@ function createMainWindow() {
// Decrease load on GPU (experimental)
app.disableHardwareAcceleration();
// Temporary fix for Electron render colors differently
// More info here - https://github.com/electron/electron/issues/10732
app.commandLine.appendSwitch('force-color-profile', 'srgb');
// eslint-disable-next-line max-params
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
event.preventDefault();
@@ -149,6 +158,22 @@ app.on('ready', () => {
});
mainWindow = createMainWindow();
// Auto-hide menu bar on Windows + Linux
if (process.platform !== 'darwin') {
const shouldHideMenu = ConfigUtil.getConfigItem('autoHideMenubar') || false;
mainWindow.setAutoHideMenuBar(shouldHideMenu);
mainWindow.setMenuBarVisibility(!shouldHideMenu);
}
// Initialize sentry for main process
sentryInit();
const isSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
if (isSystemProxy) {
ProxyUtil.resolveSystemProxy(mainWindow);
}
const page = mainWindow.webContents;
page.on('dom-ready', () => {
@@ -160,9 +185,10 @@ app.on('ready', () => {
});
page.once('did-frame-finish-load', () => {
// Initate auto-updates on MacOS and Windows
appUpdater();
crashHandler();
// Initiate auto-updates on MacOS and Windows
if (ConfigUtil.getConfigItem('autoUpdate')) {
appUpdater();
}
});
// Temporarily remove this event
@@ -179,6 +205,32 @@ app.on('ready', () => {
app.quit();
});
// Show pdf in a new BrowserWindow
ipcMain.on('pdf-view', (event, url) => {
// Paddings for pdfWindow so that it fits into the main browserWindow
const paddingWidth = 55;
const paddingHeight = 22;
// Get the config of main browserWindow
const mainWindowState = global.mainWindowState;
// Window to view the pdf file
const pdfWindow = new electron.BrowserWindow({
x: mainWindowState.x + paddingWidth,
y: mainWindowState.y + paddingHeight,
width: mainWindowState.width - paddingWidth,
height: mainWindowState.height - paddingHeight,
webPreferences: {
plugins: true,
partition: 'persist:webviewsession'
}
});
pdfWindow.loadURL(url);
// We don't want to have the menu bar in pdf window
pdfWindow.setMenu(null);
});
// Reload full app not just webview, useful in debugging
ipcMain.on('reload-full-app', () => {
mainWindow.reload();
@@ -192,10 +244,10 @@ app.on('ready', () => {
});
ipcMain.on('toggle-app', () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
mainWindow.show();
} else {
mainWindow.hide();
}
});
@@ -203,6 +255,11 @@ app.on('ready', () => {
BadgeSettings.updateBadge(badgeCount, mainWindow);
});
ipcMain.on('toggle-menubar', (event, showMenubar) => {
mainWindow.setAutoHideMenuBar(showMenubar);
mainWindow.setMenuBarVisibility(!showMenubar);
});
ipcMain.on('update-badge', (event, messageCount) => {
badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow);
@@ -224,6 +281,72 @@ app.on('ready', () => {
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => {
setAutoLaunch(AutoLaunchValue);
});
ipcMain.on('downloadFile', (event, url, downloadPath) => {
page.downloadURL(url);
page.session.once('will-download', (event, item) => {
const filePath = path.join(downloadPath, item.getFilename());
const getTimeStamp = () => {
const date = new Date();
return date.getTime();
};
const formatFile = filePath => {
const fileExtension = path.extname(filePath);
const baseName = path.basename(filePath, fileExtension);
return `${baseName}-${getTimeStamp()}${fileExtension}`;
};
// Update the name and path of the file if it already exists
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
const setFilePath = fs.existsSync(filePath) ? updatedFilePath : filePath;
item.setSavePath(setFilePath);
item.on('updated', (event, state) => {
switch (state) {
case 'interrupted': {
// Can interrupted to due to network error, cancel download then
console.log('Download interrupted, cancelling and fallback to dialog download.');
item.cancel();
break;
}
case 'progressing': {
if (item.isPaused()) {
item.cancel();
}
// This event can also be used to show progress in percentage in future.
break;
}
default: {
console.info('Unknown updated state of download item');
}
}
});
item.once('done', (event, state) => {
const getFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename();
if (state === 'completed') {
page.send('downloadFileCompleted', item.getSavePath(), getFileName);
} else {
console.log('Download failed state: ', state);
page.send('downloadFileFailed');
}
// To stop item for listening to updated events of this file
item.removeAllListeners('updated');
});
});
});
ipcMain.on('realm-name-changed', (event, serverURL, realmName) => {
page.send('update-realm-name', serverURL, realmName);
});
ipcMain.on('realm-icon-changed', (event, serverURL, iconURL) => {
page.send('update-realm-icon', serverURL, iconURL);
});
});
app.on('before-quit', () => {

View File

@@ -4,20 +4,30 @@ const { Notification } = require('electron');
const request = require('request');
const semver = require('semver');
const ConfigUtil = require('../renderer/js/utils/config-util');
const ProxyUtil = require('../renderer/js/utils/proxy-util');
const LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util');
const Logger = require('../renderer/js/utils/logger-util');
const logger = new Logger({
file: 'linux-update-util.log',
timestamp: true
});
function linuxUpdateNotification() {
let url = 'https://api.github.com/repos/zulip/zulip-electron/releases';
url = ConfigUtil.getConfigItem('betaUpdate') ? url : url + '/latest';
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
const options = {
url,
headers: {'User-Agent': 'request'}
headers: {'User-Agent': 'request'},
proxy: proxyEnabled ? ProxyUtil.getProxy(url) : ''
};
request(options, (error, response, body) => {
if (error) {
console.log('Error:', error);
logger.error('Linux update error.');
logger.error(error);
return;
}
if (response.statusCode < 400) {
@@ -32,7 +42,7 @@ function linuxUpdateNotification() {
}
}
} else {
console.log('Status:', response.statusCode);
logger.log('Linux update response status: ', response.statusCode);
}
});
}

View File

@@ -4,12 +4,20 @@ const path = require('path');
const { app, shell, BrowserWindow, Menu, dialog } = require('electron');
const fs = require('fs-extra');
const AdmZip = require('adm-zip');
const { appUpdater } = require('./autoupdater');
const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');
const DNDUtil = require(__dirname + '/../renderer/js/utils/dnd-util.js');
const Logger = require(__dirname + '/../renderer/js/utils/logger-util.js');
const appName = app.getName();
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
class AppMenu {
getHistorySubmenu() {
return [{
@@ -31,6 +39,65 @@ class AppMenu {
}];
}
getToolsSubmenu() {
return [{
label: `Check for Updates`,
click() {
AppMenu.checkForUpdate();
}
},
{
label: `Release Notes`,
click() {
shell.openExternal(`https://github.com/zulip/zulip-electron/releases/tag/v${app.getVersion()}`);
}
}, {
type: 'separator'
}, {
label: 'Factory Reset',
accelerator: process.platform === 'darwin' ? 'Command+Shift+D' : 'Ctrl+Shift+D',
click() {
AppMenu.resetAppSettings();
}
}, {
label: 'Download App Logs',
click() {
const zip = new AdmZip();
let date = new Date();
date = date.toLocaleDateString().replace(/\//g, '-');
// Create a zip file of all the logs and config data
zip.addLocalFolder(`${app.getPath('appData')}/${appName}/Logs`);
zip.addLocalFolder(`${app.getPath('appData')}/${appName}/config`);
// Put the log file in downloads folder
const logFilePath = `${app.getPath('downloads')}/Zulip-logs-${date}.zip`;
zip.writeZip(logFilePath);
// Open and select the log file
shell.showItemInFolder(logFilePath);
}
}, {
type: 'separator'
}, {
label: 'Toggle DevTools for Zulip App',
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.toggleDevTools();
}
}
}, {
label: 'Toggle DevTools for Active Tab',
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('tab-devtools');
}
}
}];
}
getViewSubmenu() {
return [{
label: 'Reload',
@@ -95,52 +162,33 @@ class AppMenu {
ConfigUtil.setConfigItem('showSidebar', newValue);
}
}
}, {
label: 'Toggle DevTools for Zulip App',
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.toggleDevTools();
}
}
}, {
label: 'Toggle DevTools for Active Tab',
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('tab-devtools');
}
}
}];
}
getHelpSubmenu() {
return [
{
label: `${appName + ' Desktop-'} v${app.getVersion()}`,
label: `${appName + ' Desktop'} v${app.getVersion()}`,
enabled: false
},
{
label: `What's New...`,
click() {
shell.openExternal(`https://github.com/zulip/zulip-electron/releases/tag/v${app.getVersion()}`);
label: 'About Zulip',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('open-about');
}
}
},
{
label: `${appName} Help`,
label: `Help Center`,
click() {
shell.openExternal('https://zulipchat.com/help/');
}
}, {
label: 'Show App Logs',
label: 'Report an Issue',
click() {
shell.openItem(app.getPath('userData'));
}
}, {
label: 'Report an Issue...',
click() {
// the goal is to notify the main.html BrowserWindow
// which may not be the focused window.
// the goal is to notify the main.html BrowserWindow
// which may not be the focused window.
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('open-feedback-modal');
});
@@ -189,16 +237,7 @@ class AppMenu {
return [{
label: `${app.getName()}`,
submenu: [{
label: 'About Zulip',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('open-about');
}
}
}, {
type: 'separator'
}, {
label: 'Desktop App Settings',
label: 'Desktop Settings',
accelerator: 'Cmd+,',
click(item, focusedWindow) {
if (focusedWindow) {
@@ -222,12 +261,6 @@ class AppMenu {
const dndUtil = DNDUtil.toggle();
AppMenu.sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
}
}, {
label: 'Reset App Settings',
accelerator: 'Command+Shift+D',
click() {
AppMenu.resetAppSettings();
}
}, {
label: 'Log Out',
accelerator: 'Cmd+L',
@@ -270,8 +303,6 @@ class AppMenu {
role: 'paste'
}, {
role: 'pasteandmatchstyle'
}, {
role: 'delete'
}, {
role: 'selectall'
}]
@@ -284,6 +315,9 @@ class AppMenu {
}, {
label: 'Window',
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
}, {
label: 'Tools',
submenu: this.getToolsSubmenu()
}, {
role: 'help',
submenu: this.getHelpSubmenu()
@@ -294,18 +328,9 @@ class AppMenu {
const { tabs, activeTabIndex } = props;
return [{
label: 'File',
label: '&File',
submenu: [{
label: 'About Zulip',
click(item, focusedWindow) {
if (focusedWindow) {
AppMenu.sendAction('open-about');
}
}
}, {
type: 'separator'
}, {
label: 'Desktop App Settings',
label: 'Desktop Settings',
accelerator: 'Ctrl+,',
click(item, focusedWindow) {
if (focusedWindow) {
@@ -331,12 +356,6 @@ class AppMenu {
const dndUtil = DNDUtil.toggle();
AppMenu.sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
}
}, {
label: 'Reset App Settings',
accelerator: 'Ctrl+Shift+D',
click() {
AppMenu.resetAppSettings();
}
}, {
label: 'Log Out',
accelerator: 'Ctrl+L',
@@ -352,7 +371,7 @@ class AppMenu {
accelerator: 'Ctrl+Q'
}]
}, {
label: 'Edit',
label: '&Edit',
submenu: [{
role: 'undo'
}, {
@@ -367,23 +386,25 @@ class AppMenu {
role: 'paste'
}, {
role: 'pasteandmatchstyle'
}, {
role: 'delete'
}, {
type: 'separator'
}, {
role: 'selectall'
}]
}, {
label: 'View',
label: '&View',
submenu: this.getViewSubmenu()
}, {
label: 'History',
label: '&History',
submenu: this.getHistorySubmenu()
}, {
label: 'Window',
label: '&Window',
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
}, {
label: '&Tools',
submenu: this.getToolsSubmenu()
}, {
label: '&Help',
role: 'help',
submenu: this.getHelpSubmenu()
}];
@@ -399,11 +420,14 @@ class AppMenu {
win.webContents.send(action, ...params);
}
static checkForUpdate() {
appUpdater(true);
}
static resetAppSettings() {
const resetAppSettingsMessage = 'By proceeding you will be removing all connected organizations and preferences from Zulip.';
// We save App's settings/configurations in following files
const settingFiles = ['window-state.json', 'domain.json', 'settings.json'];
const settingFiles = ['config/window-state.json', 'config/domain.json', 'config/settings.json', 'config/certificates.json'];
dialog.showMessageBox({
type: 'warning',
@@ -417,7 +441,8 @@ class AppMenu {
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
fs.access(getSettingFilesPath, error => {
if (error) {
console.log(error);
logger.error('Error while resetting app settings.');
logger.error(error);
} else {
fs.unlink(getSettingFilesPath, () => {
AppMenu.sendAction('clear-app-data');

1658
app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "2.2.0-beta",
"version": "2.3.82",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"copyright": "Kandra Labs, Inc.",
@@ -26,13 +26,16 @@
"InstantMessaging"
],
"dependencies": {
"@electron-elements/send-feedback": "1.0.7",
"@electron-elements/send-feedback": "1.0.8",
"@sentry/electron": "0.11.0",
"adm-zip": "0.4.11",
"auto-launch": "5.0.5",
"electron-is-dev": "0.3.0",
"electron-log": "2.2.14",
"electron-spellchecker": "1.1.2",
"electron-updater": "2.21.10",
"electron-updater": "3.1.2",
"electron-window-state": "4.1.1",
"escape-html": "1.0.3",
"is-online": "7.0.0",
"node-json-db": "0.7.3",
"request": "2.85.0",

View File

@@ -151,7 +151,7 @@ body {
}
.tab:first-child {
margin-top: 8px;
margin-top: 9px;
}
.tab {
@@ -165,25 +165,33 @@ body {
}
.tab .server-icons {
border-radius: 50%;
width: 30px;
padding: 3px;
height: 30px;
width: 35px;
vertical-align: top;
border-radius: 4px;
}
.tab .server-tab {
width: 100%;
height: 35px;
position: relative;
margin: 5px 0 2px 0;
margin-top: 5px;
z-index: 11;
line-height: 31px;
color: #eee;
text-align: center;
overflow: hidden;
opacity: 0.6;
padding: 2px 0;
padding: 6px 0;
}
.server-tab .alt-icon {
font-family: Verdana;
font-weight: 600;
font-size: 22px;
border: 2px solid #222c31;
margin-left: 17%;
width: 35px;
border-radius: 4px;
}
.tab .server-tab:hover {
@@ -358,7 +366,7 @@ webview.focus {
left: 56px;
padding: 10px 20px;
position: fixed;
margin-top: 8px;
margin-top: 11px;
z-index: 5000 !important;
color: #fff;
border-radius: 4px;

View File

@@ -7,14 +7,16 @@ body {
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
color: #333;
background: #efefef;
letter-spacing: -.08px;
line-height: 18px;
color: #8b8e8f;
}
kbd {
display: inline-block;
border: 1px solid #ccc;
border-radius: 3px;
border-radius: 4px;
font-size: 15px;
font-family: Courier New, Courier, monospace;
font-weight: bold;
@@ -109,7 +111,7 @@ td:nth-child(odd) {
}
.nav {
padding: 5px 0;
padding: 7px 0;
color: #999;
cursor: pointer;
}
@@ -129,7 +131,6 @@ td:nth-child(odd) {
content: '';
}
/* We don't want to show this in nav item since we have the + button for adding an Organization */
#nav-AddServer {
@@ -187,7 +188,7 @@ td:nth-child(odd) {
img.server-info-icon {
width: 36px;
height: 36px;
padding: 4px;
padding: 4px 4px 4px 10px;
cursor: pointer;
vertical-align: middle;
}
@@ -246,7 +247,7 @@ img.server-info-icon {
.setting-input-value {
flex-grow: 1;
font-size: 14px;
border-radius: 3px;
border-radius: 4px;
padding: 13px;
border: #ededed 2px solid;
outline-width: 0;
@@ -258,8 +259,8 @@ img.server-info-icon {
border: #4EBFAC 2px solid;
}
.setting-block {
width: 100%;
.manual-proxy-block {
width: 96%;
}
.actions-container {
@@ -306,7 +307,7 @@ img.server-info-icon {
padding: 12px 30px;
margin: 10px 0 20px 0;
background: #fff;
width: 70%;
width: 80%;
transition: all 0.2s;
}
@@ -323,20 +324,38 @@ img.server-info-icon {
color: #ef5350;
padding: 8px;
border: rgba(239, 83, 80, 0.5) solid 1px;
border-radius: 4px;
border-color: #fff;
}
.red:hover {
color: #e63431;
border: rgba(239, 83, 80, 0.7) solid 1px;
;
background-color: rgba(230, 52, 49, 0.5);
color: #fff;
}
.blue {
color: #ffffff;
.green {
color: #fff;
background: #4EBFAC;
padding: 3px;
padding-right: 10px;
padding-left: 10px;
border-radius: 4px;
display: inline-block;
border: none;
padding: 10px;
width: 100px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: background-color 0.2s ease;
text-decoration: none;
text-align: center;
}
.green:hover {
background-color: #3c9f8d;
color: #fff;
}
.w-150 {
width: 150px;
}
.grey {
@@ -358,36 +377,19 @@ img.server-info-icon {
}
i.open-tab-button {
padding-left: 2px;
padding-left: 5px;
font-size: 19px;
cursor: pointer;
}
.reset-data-button,
.custom-css-button {
display: inline-block;
border: none;
padding: 10px;
width: 120px;
cursor: pointer;
font-size: 13px;
transition: background-color 0.2s ease;
text-decoration: none;
}
.css-delete-action {
margin-bottom: 10px;
}
.reset-data-button:hover {
background-color: #3c9f8d;
color: #fff;
}
.selected-css-path {
.selected-css-path,
.download-folder-path {
background: #eeeeee;
padding: 10px;
margin-top: 10px;
padding: 5px 10px;
margin-right: 10px;
display: flex;
width: 90%;
@@ -449,9 +451,12 @@ input.toggle-round+label:after {
}
input.toggle-round+label:before {
right: 2px;
background-color: #f1f1f1;
border-radius: 25px;
top: 0;
right: 0px;
left: 0px;
bottom: 0px;
}
input.toggle-round+label:after {
@@ -463,13 +468,16 @@ input.toggle-round+label:after {
input.toggle-round:checked+label:before {
background-color: #4EBFAC;
top: 0;
right: 0px;
left: 0px;
bottom: 0px;
}
input.toggle-round:checked+label:after {
margin-left: 25px;
}
/* Add new server modal */
.add-server-modal {
@@ -487,7 +495,6 @@ input.toggle-round:checked+label:after {
overflow: auto;
}
/* Modal Content */
.modal-container {
@@ -545,7 +552,7 @@ input.toggle-round:checked+label:after {
border: none;
width: 98%;
height: 46px;
border-radius: 3px;
border-radius: 4px;
cursor: pointer;
}
@@ -557,11 +564,54 @@ input.toggle-round:checked+label:after {
background: #329588;
}
.certificates-card {
width: 80%
}
.certificate-input {
width: 100%;
margin-top: 10px;
display: inline-flex;
}
.certificate-input div {
align-self: center;
}
.certificate-input .setting-input-value {
margin-left: 10px;
max-width: 100%;
}
#add-certificate-button {
margin: 10px 10px 10px 37px;
}
.tip {
background: none;
padding: 0;
}
.tip:hover{
box-shadow: none;
}
.md-14 {
font-size: 14px;
vertical-align: middle;
padding-right: 6px;
}
#open-hotkeys-link {
text-decoration: underline;
cursor: pointer;
}
/* responsive grid */
@media (max-width: 650px) {
.selected-css-path {
.selected-css-path,
.download-folder-path {
margin-right: 15px;
}
#css-delete-action {
@@ -611,13 +661,11 @@ input.toggle-round:checked+label:after {
flex-direction: column;
align-items: center;
}
.server-info-right {
flex-direction: column;
align-items: center;
}
.action {
margin-top: 10px;
}
}
}

View File

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

View File

@@ -8,7 +8,7 @@ const {ipcRenderer} = require('electron');
class ServerTab extends Tab {
template() {
return `<div class="tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tooltip" style="display:none"></div>
<div class="server-tooltip" style="display:none">${this.props.name}</div>
<div class="server-tab-badge"></div>
<div class="server-tab">
<img class="server-icons" src='${this.props.icon}'/>

View File

@@ -33,6 +33,7 @@ class WebView extends BaseComponent {
disablewebsecurity
${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession"
name="${this.props.name}"
webpreferences="allowRunningInsecureContent, javascript=yes">
</webview>`;
}
@@ -75,11 +76,16 @@ class WebView extends BaseComponent {
this.$el.addEventListener('page-favicon-updated', event => {
const { favicons } = event;
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') {
// This api is only supported on macOS
app.dock.setBadge('●');
// bounce the dock
if (ConfigUtil.getConfigItem('dockBouncing')) {
app.dock.bounce();
}
}
});

View File

@@ -0,0 +1,40 @@
const events = require('events');
const { ipcRenderer } = require('electron');
// we have and will have some non camelcase stuff
// while working with zulip so just turning the rule off
// for the whole file.
/* eslint-disable camelcase */
class ElectronBridge extends events {
send_event(...args) {
this.emit(...args);
}
on_event(...args) {
this.on(...args);
}
}
const electron_bridge = new ElectronBridge();
electron_bridge.on('total_unread_count', (...args) => {
ipcRenderer.send('unread-count', ...args);
});
electron_bridge.on('realm_name', realmName => {
const serverURL = location.origin;
ipcRenderer.send('realm-name-changed', serverURL, realmName);
});
electron_bridge.on('realm_icon_url', iconURL => {
const serverURL = location.origin;
iconURL = iconURL.includes('http') ? iconURL : `${serverURL}${iconURL}`;
ipcRenderer.send('realm-icon-changed', serverURL, iconURL);
});
// this follows node's idiomatic implementation of event
// emitters to make event handling more simpler instead of using
// 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
// a listener for the event.
module.exports = electron_bridge;

View File

@@ -3,7 +3,8 @@
const { ipcRenderer, remote } = require('electron');
const isDev = require('electron-is-dev');
const { session } = remote;
const { session, app } = remote;
const escape = require('escape-html');
require(__dirname + '/js/tray.js');
const DomainUtil = require(__dirname + '/js/utils/domain-util.js');
@@ -13,8 +14,16 @@ const FunctionalTab = require(__dirname + '/js/components/functional-tab.js');
const ConfigUtil = require(__dirname + '/js/utils/config-util.js');
const DNDUtil = require(__dirname + '/js/utils/dnd-util.js');
const ReconnectUtil = require(__dirname + '/js/utils/reconnect-util.js');
const Logger = require(__dirname + '/js/utils/logger-util.js');
const CommonUtil = require(__dirname + '/js/utils/common-util.js');
const { feedbackHolder } = require(__dirname + '/js/feedback.js');
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
class ServerManagerView {
constructor() {
this.$addServerButton = document.getElementById('add-tab');
@@ -58,7 +67,17 @@ class ServerManagerView {
loadProxy() {
return new Promise(resolve => {
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
// To change proxyEnable to useManualProxy in older versions
const proxyEnabledOld = ConfigUtil.isConfigItemExists('useProxy');
if (proxyEnabledOld) {
const proxyEnableOldState = ConfigUtil.getConfigItem('useProxy');
if (proxyEnableOldState) {
ConfigUtil.setConfigItem('useManualProxy', true);
}
ConfigUtil.removeConfigItem('useProxy');
}
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
if (proxyEnabled) {
session.fromPartition('persist:webviewsession').setProxy({
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
@@ -82,13 +101,15 @@ class ServerManagerView {
// Default settings which should be respected
const settingOptions = {
trayIcon: true,
useProxy: false,
useManualProxy: false,
useSystemProxy: false,
showSidebar: true,
badgeOption: true,
startAtLogin: false,
startMinimized: false,
enableSpellchecker: true,
showNotification: true,
autoUpdate: true,
betaUpdate: false,
silent: false,
lastActiveTab: 0,
@@ -96,7 +117,9 @@ class ServerManagerView {
dndPreviousSettings: {
showNotification: true,
silent: false
}
},
downloadsPath: `${app.getPath('downloads')}`,
showDownloadFolder: false
};
// Platform specific settings
@@ -107,6 +130,15 @@ class ServerManagerView {
settingOptions.dndPreviousSettings.flashTaskbarOnMessage = true;
}
if (process.platform === 'darwin') {
// Only available on macOS
settingOptions.dockBouncing = true;
}
if (process.platform !== 'darwin') {
settingOptions.autoHideMenubar = false;
}
for (const i in settingOptions) {
if (ConfigUtil.getConfigItem(i) === null) {
ConfigUtil.setConfigItem(i, settingOptions[i]);
@@ -141,18 +173,19 @@ class ServerManagerView {
this.tabs.push(new ServerTab({
role: 'server',
icon: server.icon,
name: server.alias,
$root: this.$tabsContainer,
onClick: this.activateLastTab.bind(this, index),
index,
tabIndex,
onHover: this.onHover.bind(this, index, server.alias),
onHover: this.onHover.bind(this, index),
onHoverOut: this.onHoverOut.bind(this, index),
webview: new WebView({
$root: this.$webviewsContainer,
index,
tabIndex,
url: server.url,
name: server.alias,
name: CommonUtil.decodeString(server.alias),
isActive: () => {
return index === this.activeTabIndex;
},
@@ -185,8 +218,12 @@ class ServerManagerView {
const $serverImgs = document.querySelectorAll('.server-icons');
$serverImgs.forEach($serverImg => {
if ($serverImg.src.includes('img/icon.png')) {
this.displayInitalCharLogo($serverImg);
}
$serverImg.addEventListener('error', () => {
$serverImg.src = 'img/icon.png';
this.displayInitalCharLogo($serverImg);
});
});
@@ -208,6 +245,27 @@ class ServerManagerView {
return currentIndex;
}
displayInitalCharLogo($img) {
const $altIcon = document.createElement('div');
const $parent = $img.parentElement;
const $container = $parent.parentElement;
const webviewId = $container.dataset.tabId;
const $webview = document.querySelector(`webview[data-tab-id="${webviewId}"]`);
const realmName = $webview.getAttribute('name');
if (realmName === null) {
$img.src = '/img/icon.png';
return;
}
$altIcon.textContent = realmName.charAt(0) || 'Z';
$altIcon.classList.add('server-icon');
$altIcon.classList.add('alt-icon');
$parent.removeChild($img);
$parent.appendChild($altIcon);
}
sidebarHoverEvent(SidebarButton, SidebarTooltip, addServer = false) {
SidebarButton.addEventListener('mouseover', () => {
SidebarTooltip.removeAttribute('style');
@@ -225,8 +283,9 @@ class ServerManagerView {
});
}
onHover(index, serverName) {
this.$serverIconTooltip[index].innerHTML = serverName;
onHover(index) {
// this.$serverIconTooltip[index].innerHTML already has realm name, so we are just
// removing the style.
this.$serverIconTooltip[index].removeAttribute('style');
// To handle position of servers' tooltip due to scrolling of list of organizations
// This could not be handled using CSS, hence the top of the tooltip is made same
@@ -340,36 +399,6 @@ class ServerManagerView {
tabs: this.tabs,
activeTabIndex: this.activeTabIndex
});
ipcRenderer.on('toggle-sidebar', (event, state) => {
const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector);
const webContents = webview.getWebContents();
webContents.send('toggle-sidebar', state);
});
ipcRenderer.on('toggle-silent', (event, state) => {
const webviews = document.querySelectorAll('webview');
webviews.forEach(webview => {
try {
webview.setAudioMuted(state);
} catch (err) {
// webview is not ready yet
webview.addEventListener('dom-ready', () => {
webview.isAudioMuted();
});
}
});
});
ipcRenderer.on('toggle-dnd', (event, state, newSettings) => {
this.toggleDNDButton(state);
ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent);
const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector);
const webContents = webview.getWebContents();
webContents.send('toggle-dnd', state, newSettings);
});
}
destroyTab(name, index) {
@@ -494,12 +523,80 @@ class ServerManagerView {
this.loadProxy().then(() => {
if (showAlert) {
alert('Proxy settings saved!');
ipcRenderer.send('reload-full-app');
}
});
});
ipcRenderer.on('toggle-sidebar', (event, show) => {
// Toggle the left sidebar
this.toggleSidebar(show);
// Toggle sidebar switch in the general settings
const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector);
const webContents = webview.getWebContents();
webContents.send('toggle-sidebar-setting', show);
});
ipcRenderer.on('toggle-silent', (event, state) => {
const webviews = document.querySelectorAll('webview');
webviews.forEach(webview => {
try {
webview.setAudioMuted(state);
} catch (err) {
// webview is not ready yet
webview.addEventListener('dom-ready', () => {
webview.setAudioMuted(state);
});
}
});
});
ipcRenderer.on('toggle-dnd', (event, state, newSettings) => {
this.toggleDNDButton(state);
ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent);
const selector = 'webview:not([class*=disabled])';
const webview = document.querySelector(selector);
const webContents = webview.getWebContents();
webContents.send('toggle-dnd', state, newSettings);
});
ipcRenderer.on('update-realm-name', (event, serverURL, realmName) => {
DomainUtil.getDomains().forEach((domain, index) => {
if (domain.url.includes(serverURL)) {
const serverTooltipSelector = `.tab .server-tooltip`;
const serverTooltips = document.querySelectorAll(serverTooltipSelector);
serverTooltips[index].innerHTML = escape(realmName);
this.tabs[index].props.name = escape(realmName);
this.tabs[index].webview.props.name = realmName;
domain.alias = escape(realmName);
DomainUtil.db.push(`/domains[${index}]`, domain, true);
DomainUtil.reloadDB();
// Update the realm name also on the Window menu
ipcRenderer.send('update-menu', {
tabs: this.tabs,
activeTabIndex: this.activeTabIndex
});
}
});
});
ipcRenderer.on('update-realm-icon', (event, serverURL, iconURL) => {
DomainUtil.getDomains().forEach((domain, index) => {
if (domain.url.includes(serverURL)) {
DomainUtil.saveServerIcon(iconURL).then(localIconUrl => {
const serverImgsSelector = `.tab .server-icons`;
const serverImgs = document.querySelectorAll(serverImgsSelector);
serverImgs[index].src = localIconUrl;
domain.icon = localIconUrl;
DomainUtil.db.push(`/domains[${index}]`, domain, true);
DomainUtil.reloadDB();
});
}
});
});
ipcRenderer.on('enter-fullscreen', () => {
@@ -568,7 +665,7 @@ window.onload = () => {
window.addEventListener('offline', () => {
reconnectUtil.clearState();
console.log('No internet connection, you are offline.');
logger.log('No internet connection, you are offline.');
});
// only start electron-connect (auto reload on change) when its ran

View File

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

View File

@@ -4,6 +4,7 @@ const {
remote: { app }
} = require('electron');
const params = require('../utils/params-util.js');
const DefaultNotification = require('./default-notification');
const { appId, loadBots } = require('./helpers');
@@ -19,9 +20,8 @@ if (process.platform === 'darwin') {
}
window.addEventListener('load', () => {
// Call this function only when user is logged in
// eslint-disable-next-line no-undef, camelcase
if (page_params.realm_uri) {
if (params.isPageParams() && page_params.realm_uri) {
loadBots();
}
});

View File

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

View File

@@ -3,6 +3,7 @@
const BaseSection = require(__dirname + '/base-section.js');
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
const ServerInfoForm = require(__dirname + '/server-info-form.js');
const AddCertificate = require(__dirname + '/add-certificate.js');
class ConnectedOrgSection extends BaseSection {
constructor(props) {
@@ -16,6 +17,9 @@ class ConnectedOrgSection extends BaseSection {
<div class="page-title">Connected organizations</div>
<div class="title" id="existing-servers">All the connected orgnizations will appear here.</div>
<div id="server-info-container"></div>
<div class="page-title">Add Custom Certificates</div>
<div id="add-certificate-container"></div>
</div>
`;
}
@@ -44,6 +48,15 @@ class ConnectedOrgSection extends BaseSection {
onChange: this.reloadApp
}).init();
}
this.$addCertificateContainer = document.getElementById('add-certificate-container');
this.initAddCertificate();
}
initAddCertificate() {
new AddCertificate({
$root: this.$addCertificateContainer
}).init();
}
}

View File

@@ -23,20 +23,28 @@ class GeneralSection extends BaseSection {
<div class="setting-description">Show app icon in system tray</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="menubar-option" style= "display:${process.platform === 'darwin' ? 'none' : ''}">
<div class="setting-description">Auto hide Menubar (Press Alt key to display)</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="sidebar-option">
<div class="setting-description">Show sidebar (<span class="code">Cmd Or Ctrl+S</span>)</div>
<div class="setting-description">Show sidebar (<span class="code">Cmd/Ctrl+S</span>)</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="badge-option">
<div class="setting-description">Show app unread badge</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="dock-bounce-option" style= "display:${process.platform === 'darwin' ? '' : 'none'}">
<div class="setting-description">Bounce dock on new private message</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="flash-taskbar-option" style= "display:${process.platform === 'win32' ? '' : 'none'}">
<div class="setting-description">Flash taskbar on new message</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">Desktop Notification</div>
<div class="title">Desktop Notifications</div>
<div class="settings-card">
<div class="setting-row" id="show-notification-option">
<div class="setting-description">Show Desktop Notifications</div>
@@ -48,7 +56,11 @@ class GeneralSection extends BaseSection {
</div>
</div>
<div class="title">App Updates</div>
<div class="settings-card">
<div class="settings-card">
<div class="setting-row" id="autoupdate-option">
<div class="setting-description">Enable auto updates</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="betaupdate-option">
<div class="setting-description">Get beta updates</div>
<div class="setting-control"></div>
@@ -65,34 +77,50 @@ class GeneralSection extends BaseSection {
<div class="setting-control"></div>
</div>
<div class="setting-row" id="enable-spellchecker-option">
<div class="setting-description">Enable Spellchecker (requires restart)</div>
<div class="setting-description">Enable spellchecker (requires restart)</div>
<div class="setting-control"></div>
</div>
</div>
<div class="title">Add custom CSS</div>
<div class="title">Advanced</div>
<div class="settings-card">
<div class="setting-row" id="add-custom-css">
<div class="setting-row" id="show-download-folder">
<div class="setting-description">Show downloaded files in file manager</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="add-custom-css">
<div class="setting-description">
Add custom CSS
</div>
<button class="custom-css-button green">Upload</button>
</div>
<div class="setting-row" id="remove-custom-css">
<div class="setting-description">
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div>
</div>
<div class="action red" id="css-delete-action">
<i class="material-icons">indeterminate_check_box</i>
<span>Delete</span>
</div>
</div>
<div class="setting-row" id="download-folder">
<div class="setting-description">
This will inject the selected css stylesheet in all the added accounts
Default download location
</div>
<button class="custom-css-button blue">Add</button>
<button class="download-folder-button green">Change</button>
</div>
<div class="setting-row" id="remove-custom-css">
<div class="setting-row">
<div class="setting-description">
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div>
</div>
<div class="action red" id="css-delete-action">
<i class="material-icons">indeterminate_check_box</i>
<span>Delete</span>
<div class="download-folder-path">${ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`)}</div>
</div>
</div>
</div>
<div class="title">Reset Application Data</div>
<div class="settings-card">
<div class="setting-row" id="resetdata-option">
<div class="setting-description">This will delete all application data including all added accounts and preferences
</div>
<button class="reset-data-button blue">Reset App Data</button>
<button class="reset-data-button green w-150">Reset App Data</button>
</div>
</div>
</div>
@@ -104,7 +132,8 @@ class GeneralSection extends BaseSection {
this.updateTrayOption();
this.updateBadgeOption();
this.updateSilentOption();
this.updateUpdateOption();
this.autoUpdateOption();
this.betaUpdateOption();
this.updateSidebarOption();
this.updateStartAtLoginOption();
this.updateResetDataOption();
@@ -114,12 +143,24 @@ class GeneralSection extends BaseSection {
this.addCustomCSS();
this.showCustomCSSPath();
this.removeCustomCSS();
this.downloadFolder();
this.showDownloadFolder();
// Platform specific settings
// Flashing taskbar on Windows
if (process.platform === 'win32') {
this.updateFlashTaskbar();
}
// Dock bounce on macOS
if (process.platform === 'darwin') {
this.updateDockBouncing();
}
// Auto hide menubar on Windows and Linux
if (process.platform !== 'darwin') {
this.updateMenubarOption();
}
}
updateTrayOption() {
@@ -135,6 +176,19 @@ class GeneralSection extends BaseSection {
});
}
updateMenubarOption() {
this.generateSettingOption({
$element: document.querySelector('#menubar-option .setting-control'),
value: ConfigUtil.getConfigItem('autoHideMenubar', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('autoHideMenubar');
ConfigUtil.setConfigItem('autoHideMenubar', newValue);
ipcRenderer.send('toggle-menubar', newValue);
this.updateMenubarOption();
}
});
}
updateBadgeOption() {
this.generateSettingOption({
$element: document.querySelector('#badge-option .setting-control'),
@@ -148,6 +202,18 @@ class GeneralSection extends BaseSection {
});
}
updateDockBouncing() {
this.generateSettingOption({
$element: document.querySelector('#dock-bounce-option .setting-control'),
value: ConfigUtil.getConfigItem('dockBouncing', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('dockBouncing');
ConfigUtil.setConfigItem('dockBouncing', newValue);
this.updateDockBouncing();
}
});
}
updateFlashTaskbar() {
this.generateSettingOption({
$element: document.querySelector('#flash-taskbar-option .setting-control'),
@@ -160,14 +226,26 @@ class GeneralSection extends BaseSection {
});
}
updateUpdateOption() {
autoUpdateOption() {
this.generateSettingOption({
$element: document.querySelector('#autoupdate-option .setting-control'),
value: ConfigUtil.getConfigItem('autoUpdate', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('autoUpdate');
ConfigUtil.setConfigItem('autoUpdate', newValue);
this.autoUpdateOption();
}
});
}
betaUpdateOption() {
this.generateSettingOption({
$element: document.querySelector('#betaupdate-option .setting-control'),
value: ConfigUtil.getConfigItem('betaUpdate', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
ConfigUtil.setConfigItem('betaUpdate', newValue);
this.updateUpdateOption();
this.betaUpdateOption();
}
});
}
@@ -310,6 +388,40 @@ class GeneralSection extends BaseSection {
});
}
downloadFolderDialog() {
const showDialogOptions = {
title: 'Select Download Location',
defaultId: 1,
properties: ['openDirectory']
};
dialog.showOpenDialog(showDialogOptions, selectedFolder => {
if (selectedFolder) {
ConfigUtil.setConfigItem('downloadsPath', selectedFolder[0]);
const downloadFolderPath = document.querySelector('.download-folder-path');
downloadFolderPath.innerText = selectedFolder[0];
}
});
}
downloadFolder() {
const downloadFolder = document.querySelector('#download-folder .download-folder-button');
downloadFolder.addEventListener('click', () => {
this.downloadFolderDialog();
});
}
showDownloadFolder() {
this.generateSettingOption({
$element: document.querySelector('#show-download-folder .setting-control'),
value: ConfigUtil.getConfigItem('showDownloadFolder', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('showDownloadFolder');
ConfigUtil.setConfigItem('showDownloadFolder', newValue);
this.showDownloadFolder();
}
});
}
}
module.exports = GeneralSection;

View File

@@ -16,11 +16,15 @@ class NetworkSection extends BaseSection {
<div class="settings-pane">
<div class="title">Proxy</div>
<div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="use-proxy-option">
<div class="setting-description">Connect servers through a proxy</div>
<div class="setting-row" id="use-system-settings">
<div class="setting-description">Use system proxy settings (requires restart)</div>
<div class="setting-control"></div>
</div>
<div class="setting-block">
<div class="setting-row" id="use-manual-settings">
<div class="setting-description">Manual proxy configuration</div>
<div class="setting-control"></div>
</div>
<div class="manual-proxy-block">
<div class="setting-row" id="proxy-pac-option">
<span class="setting-input-key">PAC script</span>
<input class="setting-input-value" placeholder="e.g. foobar.com/pacfile.js"/>
@@ -34,8 +38,7 @@ class NetworkSection extends BaseSection {
<input class="setting-input-value" placeholder="e.g. foobar.com"/>
</div>
<div class="setting-row">
<div class="action blue" id="proxy-save-action">
<i class="material-icons">check_box</i>
<div class="action green" id="proxy-save-action">
<span>Save</span>
</div>
</div>
@@ -51,7 +54,7 @@ class NetworkSection extends BaseSection {
this.$proxyRules = document.querySelector('#proxy-rules-option .setting-input-value');
this.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value');
this.$proxySaveAction = document.getElementById('proxy-save-action');
this.$settingBlock = this.props.$root.querySelector('.setting-block');
this.$manualProxyBlock = this.props.$root.querySelector('.manual-proxy-block');
this.initProxyOption();
this.$proxyPAC.value = ConfigUtil.getConfigItem('proxyPAC', '');
@@ -68,31 +71,54 @@ class NetworkSection extends BaseSection {
}
initProxyOption() {
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
this.toggleProxySettings(proxyEnabled);
const manualProxyEnabled = ConfigUtil.getConfigItem('useManualProxy', false);
this.toggleManualProxySettings(manualProxyEnabled);
this.updateProxyOption();
}
toggleProxySettings(option) {
toggleManualProxySettings(option) {
if (option) {
this.$settingBlock.classList.remove('hidden');
this.$manualProxyBlock.classList.remove('hidden');
} else {
this.$settingBlock.classList.add('hidden');
this.$manualProxyBlock.classList.add('hidden');
}
}
updateProxyOption() {
this.generateSettingOption({
$element: document.querySelector('#use-proxy-option .setting-control'),
value: ConfigUtil.getConfigItem('useProxy', false),
$element: document.querySelector('#use-system-settings .setting-control'),
value: ConfigUtil.getConfigItem('useSystemProxy', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('useProxy');
ConfigUtil.setConfigItem('useProxy', newValue);
this.toggleProxySettings(newValue);
if (newValue === false) {
// Reload proxy if the proxy is turned off
ipcRenderer.send('forward-message', 'reload-proxy', false);
const newValue = !ConfigUtil.getConfigItem('useSystemProxy');
const manualProxyValue = ConfigUtil.getConfigItem('useManualProxy');
if (manualProxyValue && newValue) {
ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue);
this.toggleManualProxySettings(!manualProxyValue);
}
if (newValue === false) {
// Remove proxy system proxy settings
ConfigUtil.setConfigItem('proxyRules', '');
ipcRenderer.send('forward-message', 'reload-proxy', true);
}
ConfigUtil.setConfigItem('useSystemProxy', newValue);
this.updateProxyOption();
}
});
this.generateSettingOption({
$element: document.querySelector('#use-manual-settings .setting-control'),
value: ConfigUtil.getConfigItem('useManualProxy', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('useManualProxy');
const systemProxyValue = ConfigUtil.getConfigItem('useSystemProxy');
this.toggleManualProxySettings(newValue);
if (systemProxyValue && newValue) {
ConfigUtil.setConfigItem('useSystemProxy', !systemProxyValue);
}
ConfigUtil.setConfigItem('proxyRules', '');
ConfigUtil.setConfigItem('useManualProxy', newValue);
// Reload app only when turning manual proxy off, hence !newValue
ipcRenderer.send('forward-message', 'reload-proxy', !newValue);
this.updateProxyOption();
}
});

View File

@@ -90,7 +90,7 @@ class PreferenceView extends BaseComponent {
this.handleNavigation(navItem);
});
ipcRenderer.on('toggle-sidebar', (event, state) => {
ipcRenderer.on('toggle-sidebar-setting', (event, state) => {
this.handleToggle('sidebar-option', state);
});

View File

@@ -1,6 +1,7 @@
'use strict';
const BaseSection = require(__dirname + '/base-section.js');
const shell = require('electron').shell;
class ShortcutsSection extends BaseSection {
constructor(props) {
@@ -13,6 +14,7 @@ class ShortcutsSection extends BaseSection {
return `
<div class="settings-pane">
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>Tip: </b>These desktop app shortcuts extend the Zulip webapp's <span id="open-hotkeys-link"> keyboard shortcuts</span>.</p></div>
<div class="title">Application Shortcuts</div>
<div class="settings-card">
<table>
@@ -298,13 +300,22 @@ class ShortcutsSection extends BaseSection {
</table>
<div class="setting-control"></div>
</div>
<div class="tip"><b><i class="material-icons md-14">lightbulb_outline</i>Tip: </b>These desktop app shortcuts extend the Zulip webapp's <span id="open-hotkeys-link">keyboard shortcuts</span>.</div>
</div>
`;
}
openHotkeysExternalLink() {
const link = 'https://zulipchat.com/help/keyboard-shortcuts';
const externalCreateNewOrgEl = document.getElementById('open-hotkeys-link');
externalCreateNewOrgEl.addEventListener('click', () => {
shell.openExternal(link);
});
}
init() {
this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin();
this.openHotkeysExternalLink();
}
}

View File

@@ -1,10 +1,11 @@
'use strict';
const { ipcRenderer } = require('electron');
const { ipcRenderer, shell } = require('electron');
const SetupSpellChecker = require('./spellchecker');
const ConfigUtil = require(__dirname + '/utils/config-util.js');
const LinkUtil = require(__dirname + '/utils/link-util.js');
const params = require(__dirname + '/utils/params-util.js');
// eslint-disable-next-line import/no-unassigned-import
require('./notification');
@@ -12,6 +13,9 @@ require('./notification');
// Prevent drag and drop event in main process which prevents remote code executaion
require(__dirname + '/shared/preventdrag.js');
// eslint-disable-next-line camelcase
window.electron_bridge = require('./electron-bridge');
const logout = () => {
// Create the menu for the below
document.querySelector('.dropdown-toggle').click();
@@ -39,41 +43,44 @@ process.once('loaded', () => {
// To prevent failing this script on linux we need to load it after the document loaded
document.addEventListener('DOMContentLoaded', () => {
if (params.isPageParams()) {
// Get the default language of the server
const serverLanguage = page_params.default_language; // eslint-disable-line no-undef, camelcase
const serverLanguage = page_params.default_language; // eslint-disable-line no-undef, camelcase
if (serverLanguage) {
// Set spellcheker language
ConfigUtil.setConfigItem('spellcheckerLanguage', serverLanguage);
// Init spellchecker
SetupSpellChecker.init();
}
// redirect users to network troubleshooting page
const getRestartButton = document.querySelector('.restart_get_events_button');
if (getRestartButton) {
getRestartButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
}
// Open image attachment link in the lightbox instead of opening in the default browser
const { $, lightbox } = window;
$('#main_div').on('click', '.message_content p a', function (e) {
const url = $(this).attr('href');
if (serverLanguage) {
// Set spellcheker language
ConfigUtil.setConfigItem('spellcheckerLanguage', serverLanguage);
// Init spellchecker
SetupSpellChecker.init();
}
if (LinkUtil.isImage(url)) {
const $img = $(this).parent().siblings('.message_inline_image').find('img');
// redirect users to network troubleshooting page
const getRestartButton = document.querySelector('.restart_get_events_button');
if (getRestartButton) {
getRestartButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
// 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);
}
});
}
// Open image attachment link in the lightbox instead of opening in the default browser
const { $, lightbox } = window;
$('#main_div').on('click', '.message_content p a', function (e) {
const url = $(this).attr('href');
if (LinkUtil.isImage(url)) {
const $img = $(this).parent().siblings('.message_inline_image').find('img');
// prevent the image link from opening in a new page.
e.preventDefault();
// prevent the message compose dialog from happening.
e.stopPropagation();
lightbox.open($img);
}
});
});
// Clean up spellchecker events after you navigate away from this page;

View File

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

View File

@@ -3,9 +3,9 @@ const path = require('path');
const electron = require('electron');
const {ipcRenderer, remote} = electron;
const { ipcRenderer, remote } = electron;
const {Tray, Menu, nativeImage, BrowserWindow} = remote;
const { Tray, Menu, nativeImage, BrowserWindow } = remote;
const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray');
@@ -115,50 +115,36 @@ function sendAction(action) {
const createTray = function () {
window.tray = new Tray(iconPath());
const contextMenu = Menu.buildFromTemplate([{
label: 'About',
click() {
// We need to focus the main window first
ipcRenderer.send('focus-app');
sendAction('open-about');
const contextMenu = Menu.buildFromTemplate([
{
label: 'Zulip',
click() {
ipcRenderer.send('focus-app');
}
},
{
label: 'Settings',
click() {
ipcRenderer.send('focus-app');
sendAction('open-settings');
}
},
{
type: 'separator'
},
{
label: 'Quit',
click() {
ipcRenderer.send('quit-app');
}
}
},
{
type: 'separator'
},
{
label: 'Focus',
click() {
ipcRenderer.send('focus-app');
}
},
{
type: 'separator'
},
{
label: 'Settings',
click() {
ipcRenderer.send('focus-app');
sendAction('open-settings');
}
},
{
type: 'separator'
},
{
label: 'Quit',
click() {
ipcRenderer.send('quit-app');
}
}
]);
window.tray.setContextMenu(contextMenu);
window.tray.on('click', () => {
// Click event only works on Windows
if (process.platform === 'win32') {
if (process.platform === 'linux' || process.platform === 'win32') {
window.tray.on('click', () => {
ipcRenderer.send('toggle-app');
}
});
});
}
};
ipcRenderer.on('destroytray', event => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,8 +5,14 @@ const fs = require('fs');
const path = require('path');
const JsonDB = require('node-json-db');
const request = require('request');
const escape = require('escape-html');
const Logger = require('./logger-util');
const CertificateUtil = require(__dirname + '/certificate-util.js');
const ProxyUtil = require(__dirname + '/proxy-util.js');
const ConfigUtil = require(__dirname + '/config-util.js');
const logger = new Logger({
file: `domain-util.log`,
timestamp: true
@@ -104,7 +110,27 @@ class DomainUtil {
domain = this.formatUrl(domain);
const checkDomain = domain + '/static/audio/zulip.ogg';
const certificate = CertificateUtil.getCertificate(encodeURIComponent(domain));
let certificateLocation = '';
if (certificate) {
// To handle case where certificate has been moved from the location in certificates.json
try {
certificateLocation = fs.readFileSync(certificate);
} catch (err) {
logger.warn('Error while trying to get certificate: ' + err);
}
}
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
// If certificate for the domain exists add it as a ca key in the request's parameter else consider only domain as the parameter for request
// Add proxy as a parameter if it sbeing used.
const checkDomain = {
url: domain + '/static/audio/zulip.ogg',
ca: (certificateLocation) ? certificateLocation : '',
proxy: proxyEnabled ? ProxyUtil.getProxy(domain) : ''
};
const serverConf = {
icon: defaultIconUrl,
@@ -114,21 +140,16 @@ class DomainUtil {
return new Promise((resolve, reject) => {
request(checkDomain, (error, response) => {
const certsError =
[
'Error: self signed certificate',
'Error: unable to verify the first certificate',
'Error: unable to get local issuer certificate'
];
// If the domain contains following strings we just bypass the server
const whitelistDomains = [
'zulipdev.org'
];
// make sure that error is a error or string not undefined
// make sure that error is an error or string not undefined
// so validation does not throw error.
error = error || '';
const certsError = error.toString().includes('certificate');
if (!error && response.statusCode < 400) {
// Correct
this.getServerSettings(domain).then(serverSettings => {
@@ -136,7 +157,7 @@ class DomainUtil {
}, () => {
resolve(serverConf);
});
} else if (domain.indexOf(whitelistDomains) >= 0 || certsError.indexOf(error.toString()) >= 0) {
} else if (domain.indexOf(whitelistDomains) >= 0 || certsError) {
if (silent) {
this.getServerSettings(domain).then(serverSettings => {
resolve(serverSettings);
@@ -144,15 +165,19 @@ class DomainUtil {
resolve(serverConf);
});
} else {
// Report error to sentry to get idea of possible certificate errors
// users get when adding the servers
logger.reportSentry(new Error(error));
const certErrorMessage = `Do you trust certificate from ${domain}? \n ${error}`;
const certErrorDetail = `The server you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
\n Unless you have a good reason to believe otherwise, you should not proceed.
\n You can click here if you'd like to proceed with the connection.`;
const certErrorDetail = `The organization you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
\nIf you have a valid certificate please add it from Settings>Organizations and try to add the organization again.
\nUnless you have a good reason to believe otherwise, you should not proceed.
\nYou can click here if you'd like to proceed with the connection.`;
dialog.showMessageBox({
type: 'warning',
buttons: ['Yes', 'No'],
defaultId: 0,
defaultId: 1,
message: certErrorMessage,
detail: certErrorDetail
}, response => {
@@ -169,7 +194,8 @@ class DomainUtil {
}
} else {
const invalidZulipServerError = `${domain} does not appear to be a valid Zulip server. Make sure that \
\n(1) you can connect to that URL in a web browser and \n (2) if you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings \n (3) its a zulip server`;
\n (1) you can connect to that URL in a web browser and \n (2) if you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings \n (3) its a zulip server \
\n (4) the server has a valid certificate, you can add custom certificates in Settings>Organizations`;
reject(invalidZulipServerError);
}
});
@@ -177,9 +203,13 @@ class DomainUtil {
}
getServerSettings(domain) {
const serverSettingsUrl = domain + '/api/v1/server_settings';
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
const serverSettingsOptions = {
url: domain + '/api/v1/server_settings',
proxy: proxyEnabled ? ProxyUtil.getProxy(domain) : ''
};
return new Promise((resolve, reject) => {
request(serverSettingsUrl, (error, response) => {
request(serverSettingsOptions, (error, response) => {
if (!error && response.statusCode === 200) {
const data = JSON.parse(response.body);
if (data.hasOwnProperty('realm_icon') && data.realm_icon) {
@@ -188,7 +218,7 @@ class DomainUtil {
// Following check handles both the cases
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
url: data.realm_uri,
alias: data.realm_name
alias: escape(data.realm_name)
});
}
} else {
@@ -199,25 +229,36 @@ class DomainUtil {
}
saveServerIcon(url) {
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
const serverIconOptions = {
url,
proxy: proxyEnabled ? ProxyUtil.getProxy(url) : ''
};
// The save will always succeed. If url is invalid, downgrade to default icon.
return new Promise(resolve => {
const filePath = this.generateFilePath(url);
const file = fs.createWriteStream(filePath);
try {
request(url).on('response', response => {
request(serverIconOptions).on('response', response => {
response.on('error', err => {
console.log(err);
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
});
response.pipe(file).on('finish', () => {
resolve(filePath);
});
}).on('error', err => {
console.log(err);
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
});
} catch (err) {
console.log(err);
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
}
});
@@ -235,7 +276,7 @@ class DomainUtil {
}
reloadDB() {
const domainJsonPath = path.join(app.getPath('userData'), '/domain.json');
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
try {
const file = fs.readFileSync(domainJsonPath, 'utf8');
JSON.parse(file);
@@ -249,6 +290,7 @@ class DomainUtil {
);
logger.error('Error while JSON parsing domain.json: ');
logger.error(err);
logger.reportSentry(err);
}
}
this.db = new JsonDB(domainJsonPath, true, true);

View File

@@ -34,6 +34,12 @@ class LinkUtil {
const isImageUrl = /\.(bmp|gif|jpg|jpeg|png|webp)\?*.*$/i;
return isImageUrl.test(url);
}
isPDF(url) {
// test for pdf extension
const isPDFUrl = /\.(pdf)\?*.*$/i;
return isPDFUrl.test(url);
}
}
module.exports = new LinkUtil();

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
// This util function returns the page params if they're present else returns null
function isPageParams() {
let webpageParams = null;
try {
// eslint-disable-next-line no-undef, camelcase
webpageParams = page_params;
} catch (err) {
webpageParams = null;
}
return webpageParams;
}
module.exports = {
isPageParams
};

View File

@@ -0,0 +1,130 @@
'use strict';
const url = require('url');
const ConfigUtil = require('./config-util.js');
let instance = null;
class ProxyUtil {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
return instance;
}
// Return proxy to be used for a particular uri, to be used for request
getProxy(uri) {
uri = url.parse(uri);
const proxyRules = ConfigUtil.getConfigItem('proxyRules', '').split(';');
// If SPS is on and system uses no proxy then request should not try to use proxy from
// environment. NO_PROXY = '*' makes request ignore all environment proxy variables.
if (proxyRules[0] === '') {
process.env.NO_PROXY = '*';
return;
}
const proxyRule = {};
if (uri.protocol === 'http:') {
proxyRules.forEach(proxy => {
if (proxy.includes('http=')) {
proxyRule.hostname = proxy.split('http=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('http=')[1].trim().split(':')[1];
}
});
return proxyRule;
}
if (uri.protocol === 'https:') {
proxyRules.forEach(proxy => {
if (proxy.includes('https=')) {
proxyRule.hostname = proxy.split('https=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('https=')[1].trim().split(':')[1];
}
});
return proxyRule;
}
}
resolveSystemProxy(mainWindow) {
const page = mainWindow.webContents;
const ses = page.session;
const resolveProxyUrl = 'www.google.com';
// Check HTTP Proxy
const httpProxy = new Promise(resolve => {
ses.resolveProxy('http://' + resolveProxyUrl, proxy => {
let httpString = '';
if (proxy !== 'DIRECT') {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
httpString = 'http=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(httpString);
});
});
// Check HTTPS Proxy
const httpsProxy = new Promise(resolve => {
ses.resolveProxy('https://' + resolveProxyUrl, proxy => {
let httpsString = '';
if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY' || proxy.includes('HTTPS'))) {
httpsString += 'https=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(httpsString);
});
});
// Check FTP Proxy
const ftpProxy = new Promise(resolve => {
ses.resolveProxy('ftp://' + resolveProxyUrl, proxy => {
let ftpString = '';
if (proxy !== 'DIRECT') {
if (proxy.includes('PROXY')) {
ftpString += 'ftp=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(ftpString);
});
});
// Check SOCKS Proxy
const socksProxy = new Promise(resolve => {
ses.resolveProxy('socks4://' + resolveProxyUrl, proxy => {
let socksString = '';
if (proxy !== 'DIRECT') {
if (proxy.includes('SOCKS5')) {
socksString += 'socks=' + proxy.split('SOCKS5')[1] + ';';
} else if (proxy.includes('SOCKS4')) {
socksString += 'socks=' + proxy.split('SOCKS4')[1] + ';';
} else if (proxy.includes('PROXY')) {
socksString += 'socks=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(socksString);
});
});
Promise.all([httpProxy, httpsProxy, ftpProxy, socksProxy]).then(values => {
let proxyString = '';
values.forEach(proxy => {
proxyString += proxy;
});
ConfigUtil.setConfigItem('systemProxyRules', proxyString);
const useSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
if (useSystemProxy) {
ConfigUtil.setConfigItem('proxyRules', proxyString);
}
});
}
}
module.exports = new ProxyUtil();

View File

@@ -1,4 +1,10 @@
const isOnline = require('is-online');
const Logger = require('./logger-util');
const logger = new Logger({
file: `domain-util.log`,
timestamp: true
});
class ReconnectUtil {
constructor(serverManagerView) {
@@ -31,11 +37,11 @@ class ReconnectUtil {
if (!this.alreadyReloaded) {
this.serverManagerView.reloadView();
}
console.log('You\'re back online.');
logger.log('You\'re back online.');
return resolve(true);
}
console.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');
if (errMsgHolder) {
errMsgHolder.innerHTML = `

View File

@@ -0,0 +1,20 @@
const { init, captureException } = require('@sentry/electron');
const isDev = require('electron-is-dev');
const sentryInit = () => {
if (!isDev) {
init({
dsn: 'SENTRY_DSN',
// 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 is a temp solution until Sentry supports disabling the console logs
ignoreErrors: ['does not appear to be a valid Zulip server'],
sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second
});
}
};
module.exports = {
sentryInit,
captureException
};

Binary file not shown.

View File

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

755
changelog.md Normal file
View File

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

View File

@@ -10,11 +10,11 @@ gulp.task('dev', () => {
// Start browser process
electron.start();
// Restart browser process
gulp.watch('app/main/*.js', ['restart:browser']);
gulp.watch('app/main/*.js', gulp.series('restart:browser'));
// Reload renderer process
gulp.watch('app/renderer/css/*.css', ['reload:renderer']);
gulp.watch('app/renderer/*.html', ['reload:renderer']);
gulp.watch('app/renderer/js/**/*.js', ['reload:renderer']);
gulp.watch('app/renderer/css/*.css', gulp.series('reload:renderer'));
gulp.watch('app/renderer/*.html', gulp.series('reload:renderer'));
gulp.watch('app/renderer/js/**/*.js', gulp.series('reload:renderer'));
});
gulp.task('restart:browser', done => {
@@ -36,4 +36,4 @@ gulp.task('test-e2e', () => {
}));
});
gulp.task('default', ['dev', 'test-e2e']);
gulp.task('default', gulp.parallel('dev', 'test-e2e'));

105
help.md
View File

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

10057
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "2.2.0-beta",
"version": "2.3.82",
"main": "./app/main",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
@@ -88,8 +88,8 @@
}
],
"window": {
"width": 500,
"height": 500
"width": 500,
"height": 500
}
},
"win": {
@@ -123,12 +123,12 @@
"assert": "1.4.1",
"cp-file": "^5.0.0",
"devtron": "1.4.0",
"electron": "2.0.0",
"electron-builder": "20.11.1",
"electron": "2.0.9",
"electron-builder": "20.28.3",
"electron-connect": "0.6.2",
"electron-debug": "1.4.0",
"google-translate-api": "2.3.0",
"gulp": "3.9.1",
"gulp": "^4.0.0",
"gulp-tape": "0.0.9",
"is-ci": "^1.0.10",
"nodemon": "^1.14.11",
@@ -153,7 +153,7 @@
"max-lines": [
"warn",
{
"max": 500,
"max": 600,
"skipBlankLines": true,
"skipComments": true
}
@@ -182,4 +182,4 @@
"mocha"
]
}
}
}

View File

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

185
tests/e2e/package.json Normal file
View File

@@ -0,0 +1,185 @@
{
"name": "zulip",
"productName": "ZulipTest",
"version": "2.3.3",
"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": {
"perMachine": true,
"oneClick": false,
"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": "2.0.1",
"electron-builder": "20.20.4",
"electron-connect": "0.6.2",
"electron-debug": "1.4.0",
"google-translate-api": "2.3.0",
"gulp": "^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": 500,
"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/e2e/*.js",
"tools/locale-helper/*.js"
],
"envs": [
"node",
"browser",
"mocha"
]
}
}

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

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

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

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