mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-24 08:33:36 +00:00
Compare commits
420 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ed253d2e1 | ||
|
|
5f672fe404 | ||
|
|
10372787ac | ||
|
|
68acf2ec64 | ||
|
|
afd24035f4 | ||
|
|
4f28f6b935 | ||
|
|
52bd600690 | ||
|
|
c7ce2a8a99 | ||
|
|
9760b1bf98 | ||
|
|
d579c267f0 | ||
|
|
21f01d268a | ||
|
|
33782e0492 | ||
|
|
8d20568b7a | ||
|
|
06f38e92ce | ||
|
|
855e96e40e | ||
|
|
bb68720ab7 | ||
|
|
044d007a0c | ||
|
|
4ea95fe8e8 | ||
|
|
d17c685e4d | ||
|
|
bb174da59a | ||
|
|
a028b80adb | ||
|
|
3683511e60 | ||
|
|
bf359db7f4 | ||
|
|
949d786f1f | ||
|
|
5cd0c1ca4e | ||
|
|
86674991c1 | ||
|
|
8492cda092 | ||
|
|
647303c001 | ||
|
|
fe34f8adad | ||
|
|
f142a2eb4e | ||
|
|
43eaa3dd63 | ||
|
|
a3142713f7 | ||
|
|
876936125a | ||
|
|
7261fb6cef | ||
|
|
fba52e6dff | ||
|
|
23ac347fb9 | ||
|
|
082aebb1e0 | ||
|
|
3b18dc4df0 | ||
|
|
39d30b92fa | ||
|
|
733209e04e | ||
|
|
a68d697fc5 | ||
|
|
e52ece57df | ||
|
|
65681c7da9 | ||
|
|
52b6209905 | ||
|
|
0e37925418 | ||
|
|
1a31d2e431 | ||
|
|
c2fab2c7bc | ||
|
|
965b55ba5f | ||
|
|
2c7bb36f8e | ||
|
|
48b829b771 | ||
|
|
f13c28db73 | ||
|
|
19157c77e4 | ||
|
|
161cd80d38 | ||
|
|
65db4c8f39 | ||
|
|
d600d3a6ed | ||
|
|
5fdd971966 | ||
|
|
d77d39d6ad | ||
|
|
bf35db26bd | ||
|
|
4d8c8e08a0 | ||
|
|
b34681ece8 | ||
|
|
58f97038b1 | ||
|
|
6030d4c62a | ||
|
|
206f726e4a | ||
|
|
9a36fffaac | ||
|
|
30af4277e0 | ||
|
|
98d23aaae1 | ||
|
|
20feb9bd38 | ||
|
|
56090151c2 | ||
|
|
84a69ce455 | ||
|
|
2424f7a995 | ||
|
|
c3c60c98d6 | ||
|
|
24017631c0 | ||
|
|
82a9d13a3c | ||
|
|
c064322234 | ||
|
|
32542d500a | ||
|
|
5e8a971789 | ||
|
|
a7a9e96a58 | ||
|
|
347f6e50eb | ||
|
|
b873b358fe | ||
|
|
caf4545902 | ||
|
|
9c2b7eeb27 | ||
|
|
d25d71cb91 | ||
|
|
289417e5a9 | ||
|
|
fd549e44a6 | ||
|
|
ae1ffe7ccc | ||
|
|
a04e14545e | ||
|
|
9b928a16b7 | ||
|
|
b54cedbdbb | ||
|
|
3f9ba0a2bb | ||
|
|
9e8ec3b6d4 | ||
|
|
c3072854fd | ||
|
|
197e9a0520 | ||
|
|
ff84792374 | ||
|
|
a0ae29b34a | ||
|
|
f880683d9c | ||
|
|
d80b4a813c | ||
|
|
3567f6be6c | ||
|
|
03358b1e50 | ||
|
|
b098f9e616 | ||
|
|
d2fc5bd5e8 | ||
|
|
f5e15e3c85 | ||
|
|
f7002ecdf3 | ||
|
|
48e5396092 | ||
|
|
6901b5f128 | ||
|
|
df0adb373d | ||
|
|
1c0cf148f8 | ||
|
|
c644fa2778 | ||
|
|
7962ccf19a | ||
|
|
3ad73a1eaa | ||
|
|
4445baafa9 | ||
|
|
63cfcbbaf1 | ||
|
|
769972fc4b | ||
|
|
2c8cf2b959 | ||
|
|
068fe249ea | ||
|
|
d8c08c1c5b | ||
|
|
56189806a9 | ||
|
|
0f6e48c65f | ||
|
|
fa50243dbb | ||
|
|
9e4e5e9bfd | ||
|
|
c413a65f07 | ||
|
|
53c91e890a | ||
|
|
6e3017d5e7 | ||
|
|
1e57daa8bf | ||
|
|
f1ed6695fb | ||
|
|
c5d9eceb6d | ||
|
|
f2a7ce188d | ||
|
|
3dd9c89a0e | ||
|
|
697948c9d8 | ||
|
|
5150b7c57c | ||
|
|
ce88da3de9 | ||
|
|
0617f41a2d | ||
|
|
975bcbbe31 | ||
|
|
e44af311ce | ||
|
|
297ada5565 | ||
|
|
1fe64cb8d7 | ||
|
|
6557a7d606 | ||
|
|
9a0f6648ff | ||
|
|
3cd4890e60 | ||
|
|
dc287e7e57 | ||
|
|
ac0d998804 | ||
|
|
1d38ebdf05 | ||
|
|
d70783600d | ||
|
|
527196cdbb | ||
|
|
ffeb960851 | ||
|
|
a36b39ec73 | ||
|
|
2deb63b557 | ||
|
|
fcd97f3a32 | ||
|
|
67dc1e2a11 | ||
|
|
c37ae73392 | ||
|
|
9e667e3cb0 | ||
|
|
505fea0e91 | ||
|
|
70ff8db756 | ||
|
|
d6975f7b2a | ||
|
|
6cbea1acba | ||
|
|
d4decfb6af | ||
|
|
5bbf710529 | ||
|
|
b057bffe42 | ||
|
|
4e04c85258 | ||
|
|
bfbd9d4578 | ||
|
|
6594da6ddd | ||
|
|
38abf2deab | ||
|
|
0004152e18 | ||
|
|
e8be57d710 | ||
|
|
64e8419410 | ||
|
|
f35a3df63b | ||
|
|
9e15ed2699 | ||
|
|
cdc99cda26 | ||
|
|
7e08af5ced | ||
|
|
0ee85bea16 | ||
|
|
592584fcf4 | ||
|
|
8e1b7a0289 | ||
|
|
715cf8d86f | ||
|
|
0dc20cc66c | ||
|
|
c860832a73 | ||
|
|
27a29aeba6 | ||
|
|
d0bd7e1f1c | ||
|
|
72974b075f | ||
|
|
947bab657f | ||
|
|
5a2975ca4d | ||
|
|
499743e99d | ||
|
|
6bd4c44893 | ||
|
|
5b82a82313 | ||
|
|
0208e407f0 | ||
|
|
e3a622fc07 | ||
|
|
f39839618d | ||
|
|
3f93a3346f | ||
|
|
9ab75b9800 | ||
|
|
f600c4db0e | ||
|
|
4e5816697e | ||
|
|
dc3c446a46 | ||
|
|
4cd8efa396 | ||
|
|
541ba335ae | ||
|
|
3e226400c4 | ||
|
|
890d7caea5 | ||
|
|
45e7993d0c | ||
|
|
8210a7c472 | ||
|
|
9e6bb1b48f | ||
|
|
d3c2da7961 | ||
|
|
a5017456f2 | ||
|
|
76cd62d0c8 | ||
|
|
6b68217494 | ||
|
|
8148d83448 | ||
|
|
a55cda3b1f | ||
|
|
e381960206 | ||
|
|
091b641abb | ||
|
|
af0ec80998 | ||
|
|
5653a38d9b | ||
|
|
01ea3beb99 | ||
|
|
cd387aaf9c | ||
|
|
24b5e0412b | ||
|
|
8174f7b37e | ||
|
|
d3a5eceaf9 | ||
|
|
31f285eba9 | ||
|
|
b1365f9669 | ||
|
|
21351125fa | ||
|
|
50b9ec7220 | ||
|
|
46cf5c9a5c | ||
|
|
35a42f3873 | ||
|
|
14b9fcf8d7 | ||
|
|
47cfe86a06 | ||
|
|
57ad2d63e0 | ||
|
|
2fcc5d9649 | ||
|
|
45523f41aa | ||
|
|
15b3af7b97 | ||
|
|
81d3aa8a1b | ||
|
|
321860a232 | ||
|
|
1c290fc2cd | ||
|
|
a74b17b989 | ||
|
|
ffba6b68f8 | ||
|
|
b553b29328 | ||
|
|
d7b44b23d1 | ||
|
|
b991fac136 | ||
|
|
cda8aa3b09 | ||
|
|
1e60643ae9 | ||
|
|
d9fbcaf38e | ||
|
|
9e5a67c36b | ||
|
|
a16181be33 | ||
|
|
cd0a7741b1 | ||
|
|
6da523a18b | ||
|
|
86302308a9 | ||
|
|
b0294db133 | ||
|
|
252586cf71 | ||
|
|
52ea1a48fd | ||
|
|
98e73f807c | ||
|
|
f96dd6e6bc | ||
|
|
1511ce4610 | ||
|
|
c2db6fc0f0 | ||
|
|
937a193a61 | ||
|
|
3823ac7f78 | ||
|
|
878cc3fe82 | ||
|
|
eace637f29 | ||
|
|
316b5fda9e | ||
|
|
e3b8d5ea2a | ||
|
|
c8434cfd21 | ||
|
|
fbc048e8cb | ||
|
|
8a4483da80 | ||
|
|
8252c9ae6c | ||
|
|
5beb425f1c | ||
|
|
44337dd04c | ||
|
|
bb76e2c2f4 | ||
|
|
71305bca4e | ||
|
|
9cff5c5a4d | ||
|
|
f3cf2229c6 | ||
|
|
a62fc3d3bf | ||
|
|
e538543512 | ||
|
|
f85bca9879 | ||
|
|
f548a0ae53 | ||
|
|
7192dc69f6 | ||
|
|
47eec89a9b | ||
|
|
48ff506344 | ||
|
|
c2e6e9603f | ||
|
|
f4f4836887 | ||
|
|
02bc5e41a5 | ||
|
|
68c90434b3 | ||
|
|
7f8d933ab7 | ||
|
|
935b37705a | ||
|
|
798235fb06 | ||
|
|
743d689281 | ||
|
|
4c188bbdc8 | ||
|
|
26e0543ae2 | ||
|
|
463701c5f8 | ||
|
|
4c33f0779c | ||
|
|
4888efb9f2 | ||
|
|
ea332a9ff3 | ||
|
|
f1b2fdcf99 | ||
|
|
9f73160f74 | ||
|
|
b18e3ad5d2 | ||
|
|
ed6013fb5d | ||
|
|
39b436819c | ||
|
|
3e74fc9b0a | ||
|
|
0708519816 | ||
|
|
84808313fe | ||
|
|
e7e55596c6 | ||
|
|
1b8eb099ab | ||
|
|
1fa276a400 | ||
|
|
0751f6ac72 | ||
|
|
3d8da55648 | ||
|
|
318a729a4a | ||
|
|
aab581f204 | ||
|
|
c0075b4f1c | ||
|
|
7bd2e751c5 | ||
|
|
5ff2492c79 | ||
|
|
a26df708f4 | ||
|
|
57df256a4a | ||
|
|
7f1890d8a1 | ||
|
|
ff12b041a1 | ||
|
|
606e407aee | ||
|
|
b0db81095a | ||
|
|
2208b03612 | ||
|
|
7cf2422d76 | ||
|
|
57d4e5c930 | ||
|
|
deafa315df | ||
|
|
4c09da791c | ||
|
|
e3490dbfa5 | ||
|
|
f6358a06fd | ||
|
|
3ef346495f | ||
|
|
64fcc51c7e | ||
|
|
a5299a6973 | ||
|
|
154aa323ab | ||
|
|
c52318f5d6 | ||
|
|
bcd4048709 | ||
|
|
f6cb262d4c | ||
|
|
ca21912374 | ||
|
|
472ba9a199 | ||
|
|
f3a4d4225d | ||
|
|
62178b6035 | ||
|
|
2068ac8905 | ||
|
|
51352be1f6 | ||
|
|
8c3bfcbdd5 | ||
|
|
06ed522714 | ||
|
|
d5526944fe | ||
|
|
616ed89d90 | ||
|
|
92362653d3 | ||
|
|
06a09574a5 | ||
|
|
c19a7f81c2 | ||
|
|
635b6f6128 | ||
|
|
996084cd36 | ||
|
|
6a403e52e1 | ||
|
|
aed58ed1a8 | ||
|
|
0412702e35 | ||
|
|
cf0086e324 | ||
|
|
d156ba99c8 | ||
|
|
7d71e2e04d | ||
|
|
546abb28c0 | ||
|
|
a43d008aa9 | ||
|
|
777ed5c561 | ||
|
|
61db04c574 | ||
|
|
93123401fe | ||
|
|
dca4debee2 | ||
|
|
bef1503b9e | ||
|
|
f9d430b2d2 | ||
|
|
0d551861c7 | ||
|
|
4ced663f01 | ||
|
|
06f15eba2d | ||
|
|
3008cd1f24 | ||
|
|
ea449965af | ||
|
|
06ecdb678c | ||
|
|
dd99560426 | ||
|
|
cebea28ba8 | ||
|
|
da3e7e39b9 | ||
|
|
4b4e3a3d01 | ||
|
|
aff4632927 | ||
|
|
eeb9ee512f | ||
|
|
11785d78d2 | ||
|
|
f8d8d0ce2e | ||
|
|
b5af5d413d | ||
|
|
d962bd6e60 | ||
|
|
0271ada591 | ||
|
|
42fedf2d73 | ||
|
|
0b17dbb014 | ||
|
|
ae3c595d82 | ||
|
|
de34a22740 | ||
|
|
865553fa45 | ||
|
|
23fd7ba2b3 | ||
|
|
e43b651060 | ||
|
|
c0fc7718aa | ||
|
|
3fe23e84b3 | ||
|
|
6b29139805 | ||
|
|
0bfa202763 | ||
|
|
8c494f329b | ||
|
|
35bf2b0012 | ||
|
|
5ac4ea71c9 | ||
|
|
f7eb4128cb | ||
|
|
f9f21cd626 | ||
|
|
4a84f17d86 | ||
|
|
d5a92110db | ||
|
|
e9cf591559 | ||
|
|
ba60c04452 | ||
|
|
f4567c762d | ||
|
|
b2aec4b27e | ||
|
|
0fc58b4cc9 | ||
|
|
e9d7bfe48b | ||
|
|
070eb099d0 | ||
|
|
e35661993d | ||
|
|
f6234cd2f6 | ||
|
|
66475cf46c | ||
|
|
06734d2e56 | ||
|
|
1c012c7a28 | ||
|
|
7fdccd278b | ||
|
|
9a4556a59c | ||
|
|
92b9388f9a | ||
|
|
869600e3d4 | ||
|
|
3c22d5462d | ||
|
|
92ff92f501 | ||
|
|
47b3dd04fb | ||
|
|
9ed09c9e1c | ||
|
|
239cce8a4c | ||
|
|
d6a35408b8 | ||
|
|
35e6f7dcdd | ||
|
|
a38c933bc8 | ||
|
|
720ccf5d00 | ||
|
|
8dc87d0485 | ||
|
|
cd9f1c0c47 | ||
|
|
331452edbb | ||
|
|
d18885ecc9 | ||
|
|
1aef53ef94 | ||
|
|
11086210de | ||
|
|
e0d693fa19 | ||
|
|
81c71b1f83 | ||
|
|
cf8c83e3cf |
27
.gitignore
vendored
27
.gitignore
vendored
@@ -1,9 +1,26 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
domain.json
|
||||
dist
|
||||
config.gypi
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# npm cache directory
|
||||
.npm
|
||||
|
||||
# Compiled binary build directory
|
||||
dist/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
// osx garbage
|
||||
*.DS_Store
|
||||
.DS_Store
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# miscellaneous
|
||||
.idea
|
||||
config.gypi
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
6.9.4
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
2.7.9
|
||||
@@ -2,44 +2,46 @@
|
||||
|
||||
Thanks for taking the time to contribute!
|
||||
|
||||
The following is a set of guidelines for contributing to zulip-electron. These are just guidelines, not rules, use your best judgement and feel free to propose changes to this document in a pull request.
|
||||
The following is a set of guidelines for contributing to Zulip Electron Desktop Client. These are just guidelines, not rules, so use your best judgement and feel free to propose changes to this document in a pull request.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If you are new to Electron please head over to [this](http://jlord.us/essential-electron/) great article.
|
||||
Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If you are new to Electron, please head over to [this](http://jlord.us/essential-electron/) great article.
|
||||
|
||||
## Community
|
||||
|
||||
* The whole zulip documentation such as development environment, setting up with zulip project, testing, code of conduct can be read [here](https://zulip.readthedocs.io).
|
||||
* 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 [here](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/electron).
|
||||
|
||||
## 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 problem, [open a new one](https://github.com/zulip/zulip-electron/issues/new). Please pay attention to following points while opening an 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).
|
||||
|
||||
The [Zulipbot](https://github.com/zulip/zulipbot) helps to claim the issue by commenting the following in the comment section: "**@zulipbot** claim". **@zulipbot** will assign you to the issue and label the issue as **in progress**. For more details, check out [**@zulipbot**](https://github.com/zulip/zulipbot).
|
||||
The [zulipbot](https://github.com/zulip/zulipbot) helps to claim an issue by commenting the following in the comment section: "**@zulipbot** claim". **@zulipbot** will assign you to the issue and label the issue as **in progress**. For more details, check out [**@zulipbot**](https://github.com/zulip/zulipbot).
|
||||
|
||||
Please pay attention to the following points while opening an issue.
|
||||
|
||||
### Does it happen on web browsers? (especially Chrome)
|
||||
Zulip-Desktop is based on Electron, which integrates the Chrome engine within a standalone application.
|
||||
If the problem you encounter can be reproduced on web browsers, it may be an issue with Zulip web app.
|
||||
Zulip's desktop client is based on Electron, which integrates the Chrome engine within a standalone application.
|
||||
If the problem you encounter can be reproduced on web browsers, it may be an issue with [Zulip web app](https://github.com/zulip/zulip).
|
||||
|
||||
### Write detailed information
|
||||
Detailed information is very helpful to understand the problem.
|
||||
Detailed information is very helpful to understand an issue.
|
||||
|
||||
For example:
|
||||
* How to reproduce, step-by-step
|
||||
* Expected behavior (or what is wrong)
|
||||
* Screenshots (for GUI issues)
|
||||
* Application version
|
||||
* Operating system
|
||||
* Zulip-Desktop version
|
||||
* How to reproduce the issue, step-by-step.
|
||||
* The expected behavior (or what is wrong).
|
||||
* Screenshots for GUI issues.
|
||||
* The application version.
|
||||
* The operating system.
|
||||
* The Zulip-Desktop version.
|
||||
|
||||
|
||||
## Pull Requests
|
||||
Pull Requests are welcome.
|
||||
Pull Requests are always welcome.
|
||||
|
||||
1. When you edit the code, please run `npm run test` to check formatting of your code before git commit.
|
||||
1. When you edit the code, please run `npm run test` to check the formatting of your code before you `git commit`.
|
||||
2. Ensure the PR description clearly describes the problem and solution. It should include:
|
||||
* Operating System version on which you tested
|
||||
* Zulip-Desktop version on which you tested
|
||||
* The relevant issue number if applicable
|
||||
* The operating system on which you tested.
|
||||
* The Zulip-Desktop version on which you tested.
|
||||
* The relevant issue number, if applicable.
|
||||
|
||||
81
README.md
81
README.md
@@ -1,82 +1,39 @@
|
||||
# Zulip Desktop Client
|
||||
# Zulip Desktop Client
|
||||
[](https://travis-ci.org/zulip/zulip-electron)
|
||||
[](https://ci.appveyor.com/project/akashnimare/zulip-electron/branch/master)
|
||||
[](https://github.com/sindresorhus/xo)
|
||||
|
||||
This is an experimental replacement for the [Zulip Desktop
|
||||
app](https://github.com/zulip/zulip-desktop) implemented in
|
||||
[Electron](http://electron.atom.io/).
|
||||
Desktop client for Zulip. Available for Mac, Linux and Windows.
|
||||
|
||||
The goal is to achieve feature-compatibility with the old desktop app
|
||||
and then start adding cool features like easy support for
|
||||
multi-account, auto-updates etc.
|
||||
<img src="http://i.imgur.com/ChzTq4F.png"/>
|
||||
|
||||
## Prerequisites
|
||||
* node >= v6.3.1
|
||||
> Use [nvm](https://github.com/creationix/nvm) to install the current stable version of node
|
||||
# Download
|
||||
Please see [installation guide](./how-to-install.md).
|
||||
|
||||
|
||||
* python (v2.7.x recommended)
|
||||
* If you're on Debian or Ubuntu, you'll need to install following packages:
|
||||
```sh
|
||||
$ sudo apt-get install build-essential libxext-dev libxtst-dev libxkbfile-dev
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Clone the source locally:
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/zulip/zulip-electron
|
||||
$ cd zulip-electron
|
||||
```
|
||||
|
||||
Install project dependencies:
|
||||
|
||||
```sh
|
||||
$ npm install
|
||||
```
|
||||
Start the app:
|
||||
|
||||
```sh
|
||||
$ npm start
|
||||
```
|
||||
|
||||
Start and watch changes
|
||||
|
||||
```sh
|
||||
$ npm run dev
|
||||
```
|
||||
# Making a release
|
||||
|
||||
To package app into an installer use command:
|
||||
```
|
||||
npm run dist
|
||||
```
|
||||
It will start the packaging process for operating system you are running this command on. Ready for distribution file (e.g. dmg, windows installer, deb package) will be outputted to `dist` directory.
|
||||
|
||||
You can create Windows installer only when running on Windows, the same is true for Linux and OSX. So to generate all three installers you need all three operating systems.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Native Notifications
|
||||
- [x] SpellChecker
|
||||
- [x] OSX/Win/Linux installer
|
||||
- [x] Automatic Updates (macOS/Windows)
|
||||
- [x] Keyboard shortcuts
|
||||
# Features
|
||||
* Sign in to multiple teams
|
||||
* Native desktop Notifications
|
||||
* SpellChecker
|
||||
* OSX/Win/Linux installers
|
||||
* Automatic Updates (macOS/Windows)
|
||||
* Keyboard shortcuts
|
||||
|
||||
Description | Keys
|
||||
-----------------------| -----------------------
|
||||
Default shortcuts | <kbd>Cmd/Ctrl</kbd> <kbd>k</kbd>
|
||||
Change Zulip Server | <kbd>Cmd/Ctrl</kbd> <kbd>,</kbd>
|
||||
Manage Zulip Servers | <kbd>Cmd/Ctrl</kbd> <kbd>,</kbd>
|
||||
Back | <kbd>Cmd/Ctrl</kbd> <kbd>[</kbd>
|
||||
Forward | <kbd>Cmd/Ctrl</kbd> <kbd>]</kbd>
|
||||
|
||||
# Development
|
||||
Please see our [development guide](./development.md) to get started and run app locally.
|
||||
|
||||
## Contribute
|
||||
# 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/)
|
||||
|
||||
# License
|
||||
Released under the [Apache-2.0](./LICENSE) license.
|
||||
|
||||
@@ -1,40 +1,52 @@
|
||||
'use strict';
|
||||
const {app, dialog} = require('electron');
|
||||
const {autoUpdater} = require('electron-updater');
|
||||
const fs = require('fs');
|
||||
const { app, dialog } = require('electron');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
|
||||
|
||||
function appUpdater() {
|
||||
// Don't initiate auto-updates in development and on Linux system
|
||||
// since autoUpdater doesn't work on Linux
|
||||
if (isDev || process.platform === 'linux') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Logs directory
|
||||
const LogsDir = `${app.getPath('userData')}/Logs`;
|
||||
|
||||
if (!fs.existsSync(LogsDir)) {
|
||||
fs.mkdirSync(LogsDir);
|
||||
}
|
||||
|
||||
// Log whats happening
|
||||
const log = require('electron-log');
|
||||
|
||||
log.transports.file.file = `${LogsDir}/updates.log`;
|
||||
log.transports.file.level = 'info';
|
||||
autoUpdater.logger = log;
|
||||
/*
|
||||
AutoUpdater.on('error', err => log.info(err));
|
||||
autoUpdater.on('checking-for-update', () => log.info('checking-for-update'));
|
||||
autoUpdater.on('update-available', () => log.info('update-available'));
|
||||
autoUpdater.on('update-not-available', () => log.info('update-not-available'));
|
||||
*/
|
||||
|
||||
// Handle auto updates for beta/pre releases
|
||||
autoUpdater.allowPrerelease = ConfigUtil.getConfigItem('betaUpdate') || false;
|
||||
|
||||
// Ask the user if update is available
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
autoUpdater.on('update-downloaded', (event, info) => {
|
||||
// Let message = app.getName() + ' ' + info.releaseName + ' is now available. It will be installed the next time you restart the application.';
|
||||
// if (info.releaseNotes) {
|
||||
// const splitNotes = info.releaseNotes.split(/[^\r]\n/);
|
||||
// message += '\n\nRelease notes:\n';
|
||||
// splitNotes.forEach(notes => {
|
||||
// message += notes + '\n\n';
|
||||
// });
|
||||
// }
|
||||
autoUpdater.on('update-downloaded', event => {
|
||||
// Ask user to update the app
|
||||
dialog.showMessageBox({
|
||||
type: 'question',
|
||||
buttons: ['Install and Relaunch', 'Later'],
|
||||
buttons: ['Install and Relaunch', 'Install Later'],
|
||||
defaultId: 0,
|
||||
message: 'A new version of ' + app.getName() + ' has been downloaded',
|
||||
message: `A new update ${event.version} has been downloaded`,
|
||||
detail: 'It will be installed the next time you restart the application'
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
setTimeout(() => autoUpdater.quitAndInstall(), 1);
|
||||
setTimeout(() => {
|
||||
autoUpdater.quitAndInstall();
|
||||
// force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
|
||||
app.quit();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,128 +1,27 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const electron = require('electron');
|
||||
const {app} = require('electron');
|
||||
const ipc = require('electron').ipcMain;
|
||||
const {dialog} = require('electron');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const electronLocalshortcut = require('electron-localshortcut');
|
||||
const Configstore = require('electron-config');
|
||||
const JsonDB = require('node-json-db');
|
||||
const isDev = require('electron-is-dev');
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
const appMenu = require('./menu');
|
||||
const {linkIsInternal, skipImages} = require('./link-helper');
|
||||
const {appUpdater} = require('./autoupdater');
|
||||
const { appUpdater } = require('./autoupdater');
|
||||
const { setAutoLaunch } = require('./startup');
|
||||
|
||||
const db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
|
||||
const data = db.getData('/');
|
||||
const { app, ipcMain } = electron;
|
||||
|
||||
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js');
|
||||
|
||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||
require('electron-debug')();
|
||||
|
||||
const conf = new Configstore();
|
||||
|
||||
function userOS() {
|
||||
if (os.platform() === 'darwin') {
|
||||
return 'Mac';
|
||||
}
|
||||
if (os.platform() === 'linux') {
|
||||
return 'Linux';
|
||||
}
|
||||
if (os.platform() === 'win32' || os.platform() === 'win64') {
|
||||
if (parseFloat(os.release()) < 6.2) {
|
||||
return 'Windows 7';
|
||||
} else {
|
||||
return 'Windows 10';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setting userAgent so that server-side code can identify the desktop app
|
||||
const isUserAgent = 'ZulipElectron/' + app.getVersion() + ' ' + userOS();
|
||||
|
||||
// Prevent window being garbage collected
|
||||
let mainWindow;
|
||||
let targetLink;
|
||||
let badgeCount;
|
||||
|
||||
let isQuitting = false;
|
||||
|
||||
// Load this url in main window
|
||||
const staticURL = 'file://' + path.join(__dirname, '../renderer', 'index.html');
|
||||
|
||||
const targetURL = function () {
|
||||
if (data.domain === undefined) {
|
||||
return staticURL;
|
||||
}
|
||||
return data.domain;
|
||||
};
|
||||
|
||||
function serverError(targetURL) {
|
||||
if (targetURL.indexOf('localhost:') < 0 && data.domain) {
|
||||
const req = https.request(targetURL + '/static/audio/zulip.ogg', res => {
|
||||
console.log('Server StatusCode:', res.statusCode);
|
||||
console.log('You are connected to:', res.req._headers.host);
|
||||
if (res.statusCode >= 500 && res.statusCode <= 599) {
|
||||
return dialog.showErrorBox('SERVER IS DOWN!', 'We are getting a ' + res.statusCode + ' error status from the server ' + res.req._headers.host + '. Please try again after some time or you may switch server.');
|
||||
}
|
||||
});
|
||||
req.on('error', e => {
|
||||
if (e.toString().indexOf('Error: self signed certificate') >= 0) {
|
||||
const url = targetURL.replace(/^https?:\/\//, '');
|
||||
console.log('Server StatusCode:', 200);
|
||||
console.log('You are connected to:', url);
|
||||
} else {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
req.end();
|
||||
} else if (data.domain) {
|
||||
const req = http.request(targetURL + '/static/audio/zulip.ogg', res => {
|
||||
console.log('Server StatusCode:', res.statusCode);
|
||||
console.log('You are connected to:', res.req._headers.host);
|
||||
});
|
||||
req.on('error', e => {
|
||||
console.error(e);
|
||||
});
|
||||
req.end();
|
||||
}
|
||||
}
|
||||
|
||||
function checkConnectivity() {
|
||||
return dialog.showMessageBox({
|
||||
title: 'Internet connection problem',
|
||||
message: 'No internet available! Try again?',
|
||||
type: 'warning',
|
||||
buttons: ['Try again', 'Close'],
|
||||
defaultId: 0
|
||||
}, index => {
|
||||
if (index === 0) {
|
||||
mainWindow.webContents.reload();
|
||||
mainWindow.webContents.send('destroytray');
|
||||
}
|
||||
if (index === 1) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
const connectivityERR = [
|
||||
'ERR_INTERNET_DISCONNECTED',
|
||||
'ERR_PROXY_CONNECTION_FAILED',
|
||||
'ERR_CONNECTION_RESET',
|
||||
'ERR_NOT_CONNECTED',
|
||||
'ERR_NAME_NOT_RESOLVED'
|
||||
];
|
||||
|
||||
function checkConnection() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => {
|
||||
const hasConnectivityErr = (connectivityERR.indexOf(errorDescription) >= 0);
|
||||
if (hasConnectivityErr) {
|
||||
console.error('error', errorDescription);
|
||||
checkConnectivity();
|
||||
}
|
||||
});
|
||||
}
|
||||
const mainURL = 'file://' + path.join(__dirname, '../renderer', 'main.html');
|
||||
|
||||
const isAlreadyRunning = app.makeSingleInstance(() => {
|
||||
if (mainWindow) {
|
||||
@@ -135,18 +34,7 @@ const isAlreadyRunning = app.makeSingleInstance(() => {
|
||||
});
|
||||
|
||||
if (isAlreadyRunning) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
function checkWindowURL() {
|
||||
if (data.domain !== undefined) {
|
||||
return data.domain;
|
||||
}
|
||||
return targetLink;
|
||||
}
|
||||
|
||||
function isWindowsOrmacOS() {
|
||||
return process.platform === 'darwin' || process.platform === 'win32';
|
||||
return app.quit();
|
||||
}
|
||||
|
||||
const APP_ICON = path.join(__dirname, '../resources', 'Icon');
|
||||
@@ -155,96 +43,64 @@ const iconPath = () => {
|
||||
return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
|
||||
};
|
||||
|
||||
function onClosed() {
|
||||
// Dereference the window
|
||||
// For multiple windows, store them in an array
|
||||
mainWindow = null;
|
||||
}
|
||||
|
||||
function updateDockBadge(title) {
|
||||
if (title.indexOf('Zulip') === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let messageCount = (/\(([0-9]+)\)/).exec(title);
|
||||
messageCount = messageCount ? Number(messageCount[1]) : 0;
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
app.setBadgeCount(messageCount);
|
||||
}
|
||||
mainWindow.webContents.send('tray', messageCount);
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1000,
|
||||
defaultHeight: 600
|
||||
});
|
||||
const win = new electron.BrowserWindow({
|
||||
// This settings needs to be saved in config
|
||||
title: 'Zulip',
|
||||
width: conf.get('width') || 1000,
|
||||
height: conf.get('height') || 600,
|
||||
icon: iconPath(),
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 600,
|
||||
minHeight: 400,
|
||||
minHeight: 500,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../renderer/js/preload.js'),
|
||||
plugins: true,
|
||||
allowDisplayingInsecureContent: true,
|
||||
nodeIntegration: false
|
||||
nodeIntegration: true
|
||||
},
|
||||
show: false
|
||||
});
|
||||
|
||||
win.on('focus', () => {
|
||||
win.webContents.send('focus');
|
||||
});
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
win.show();
|
||||
});
|
||||
|
||||
serverError(targetURL());
|
||||
win.loadURL(mainURL);
|
||||
|
||||
win.loadURL(targetURL(), {
|
||||
userAgent: isUserAgent + ' ' + win.webContents.getUserAgent()
|
||||
// Keep the app running in background on close event
|
||||
win.on('close', e => {
|
||||
if (!isQuitting) {
|
||||
e.preventDefault();
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
app.hide();
|
||||
} else {
|
||||
win.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister all the shortcuts so that they don't interfare with other apps
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
});
|
||||
|
||||
win.on('closed', onClosed);
|
||||
win.setTitle('Zulip');
|
||||
|
||||
// Let's save browser window position
|
||||
if (conf.get('x') || conf.get('y')) {
|
||||
win.setPosition(conf.get('x'), conf.get('y'));
|
||||
}
|
||||
|
||||
if (conf.get('maximize')) {
|
||||
win.maximize();
|
||||
}
|
||||
|
||||
// Handle sizing events so we can persist them.
|
||||
win.on('maximize', () => {
|
||||
conf.set('maximize', true);
|
||||
win.on('enter-full-screen', () => {
|
||||
win.webContents.send('enter-fullscreen');
|
||||
});
|
||||
|
||||
win.on('unmaximize', () => {
|
||||
conf.set('maximize', false);
|
||||
});
|
||||
|
||||
win.on('resize', function () {
|
||||
const size = this.getSize();
|
||||
conf.set({
|
||||
width: size[0],
|
||||
height: size[1]
|
||||
});
|
||||
});
|
||||
|
||||
// On osx it's 'moved'
|
||||
win.on('move', function () {
|
||||
const pos = this.getPosition();
|
||||
conf.set({
|
||||
x: pos[0],
|
||||
y: pos[1]
|
||||
});
|
||||
});
|
||||
|
||||
// Stop page to update it's title
|
||||
win.on('page-title-updated', (e, title) => {
|
||||
e.preventDefault();
|
||||
updateDockBadge(title);
|
||||
win.on('leave-full-screen', () => {
|
||||
win.webContents.send('leave-fullscreen');
|
||||
});
|
||||
|
||||
// To destroy tray icon when navigate to a new URL
|
||||
@@ -254,33 +110,30 @@ function createMainWindow() {
|
||||
}
|
||||
});
|
||||
|
||||
// Let us register listeners on the window, so we can update the state
|
||||
// automatically (the listeners will be removed when the window is closed)
|
||||
// and restore the maximized or full screen state
|
||||
mainWindowState.manage(win);
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
// TODO - fix certificate errors
|
||||
|
||||
// app.commandLine.appendSwitch('ignore-certificate-errors', 'true');
|
||||
|
||||
// For self-signed certificate
|
||||
ipc.on('certificate-err', (e, domain) => {
|
||||
const detail = `URL: ${domain} \n Error: Self-Signed Certificate`;
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
title: 'Certificate error',
|
||||
message: `Do you trust certificate from ${domain}?`,
|
||||
// eslint-disable-next-line object-shorthand
|
||||
detail: detail,
|
||||
type: 'warning',
|
||||
buttons: ['Yes', 'No'],
|
||||
cancelId: 1
|
||||
// eslint-disable-next-line object-shorthand
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
// eslint-disable-next-line object-shorthand
|
||||
db.push('/domain', domain);
|
||||
mainWindow.loadURL(domain);
|
||||
}
|
||||
function registerLocalShortcuts(page) {
|
||||
// Somehow, reload action cannot be overwritten by the menu item
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
|
||||
page.send('reload-viewer');
|
||||
});
|
||||
});
|
||||
|
||||
// Also adding these shortcuts because some users might want to use it instead of CMD/Left-Right
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => {
|
||||
page.send('back');
|
||||
});
|
||||
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+]', () => {
|
||||
page.send('forward');
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||
event.preventDefault();
|
||||
@@ -290,9 +143,6 @@ app.on('certificate-error', (event, webContents, url, error, certificate, callba
|
||||
app.on('window-all-closed', () => {
|
||||
// Unregister all the shortcuts so that they don't interfare with other apps
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
@@ -302,52 +152,90 @@ app.on('activate', () => {
|
||||
});
|
||||
|
||||
app.on('ready', () => {
|
||||
electron.Menu.setApplicationMenu(appMenu);
|
||||
appMenu.setMenu({
|
||||
tabs: []
|
||||
});
|
||||
mainWindow = createMainWindow();
|
||||
// Not using for now // tray.create();
|
||||
|
||||
const page = mainWindow.webContents;
|
||||
|
||||
// TODO - use global shortcut instead
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
|
||||
mainWindow.reload();
|
||||
mainWindow.webContents.send('destroytray');
|
||||
});
|
||||
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => {
|
||||
if (page.canGoBack()) {
|
||||
page.goBack();
|
||||
}
|
||||
});
|
||||
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+]', () => {
|
||||
if (page.canGoForward()) {
|
||||
page.goForward();
|
||||
}
|
||||
});
|
||||
registerLocalShortcuts(page);
|
||||
|
||||
page.on('dom-ready', () => {
|
||||
page.insertCSS(fs.readFileSync(path.join(__dirname, '../renderer/css/preload.css'), 'utf8'));
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
page.on('new-window', (event, url) => {
|
||||
if (linkIsInternal(checkWindowURL(), url) && url.match(skipImages) === null) {
|
||||
event.preventDefault();
|
||||
return mainWindow.loadURL(url);
|
||||
}
|
||||
event.preventDefault();
|
||||
electron.shell.openExternal(url);
|
||||
page.once('did-frame-finish-load', () => {
|
||||
// Initate auto-updates on MacOS and Windows
|
||||
appUpdater();
|
||||
});
|
||||
|
||||
page.once('did-frame-finish-load', () => {
|
||||
const checkOS = isWindowsOrmacOS();
|
||||
if (checkOS && !isDev) {
|
||||
// Initate auto-updates on MacOS and Windows
|
||||
appUpdater();
|
||||
electron.powerMonitor.on('resume', () => {
|
||||
page.send('reload-viewer');
|
||||
});
|
||||
|
||||
ipcMain.on('focus-app', () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
ipcMain.on('quit-app', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// Reload full app not just webview, useful in debugging
|
||||
ipcMain.on('reload-full-app', () => {
|
||||
mainWindow.reload();
|
||||
page.send('destroytray');
|
||||
});
|
||||
|
||||
ipcMain.on('toggle-app', () => {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
checkConnection();
|
||||
|
||||
ipcMain.on('toggle-badge-option', () => {
|
||||
BadgeSettings.updateBadge(badgeCount, mainWindow);
|
||||
});
|
||||
|
||||
ipcMain.on('update-badge', (event, messageCount) => {
|
||||
badgeCount = messageCount;
|
||||
BadgeSettings.updateBadge(badgeCount, mainWindow);
|
||||
page.send('tray', messageCount);
|
||||
});
|
||||
|
||||
ipcMain.on('update-taskbar-icon', (event, data, text) => {
|
||||
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
|
||||
});
|
||||
|
||||
ipcMain.on('forward-message', (event, listener, ...params) => {
|
||||
page.send(listener, ...params);
|
||||
});
|
||||
|
||||
ipcMain.on('update-menu', (event, props) => {
|
||||
appMenu.setMenu(props);
|
||||
});
|
||||
|
||||
ipcMain.on('register-server-tab-shortcut', (event, index) => {
|
||||
electronLocalshortcut.register(mainWindow, `CommandOrControl+${index}`, () => {
|
||||
// Array index == Shown index - 1
|
||||
page.send('switch-server-tab', index - 1);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('local-shortcuts', (event, enable) => {
|
||||
if (enable) {
|
||||
registerLocalShortcuts(page);
|
||||
} else {
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => {
|
||||
setAutoLaunch(AutoLaunchValue);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
@@ -355,20 +243,12 @@ app.on('will-quit', () => {
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
});
|
||||
|
||||
ipc.on('new-domain', (e, domain) => {
|
||||
// MainWindow.loadURL(domain);
|
||||
if (!mainWindow) {
|
||||
mainWindow = createMainWindow();
|
||||
mainWindow.loadURL(domain);
|
||||
mainWindow.webContents.send('destroytray');
|
||||
} else if (mainWindow.isMinimized()) {
|
||||
mainWindow.webContents.send('destroytray');
|
||||
mainWindow.loadURL(domain);
|
||||
mainWindow.show();
|
||||
} else {
|
||||
mainWindow.webContents.send('destroytray');
|
||||
mainWindow.loadURL(domain);
|
||||
serverError(domain);
|
||||
}
|
||||
targetLink = domain;
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true;
|
||||
});
|
||||
|
||||
// Send crash reports
|
||||
process.on('uncaughtException', err => {
|
||||
console.error(err);
|
||||
console.error(err.stack);
|
||||
});
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
const wurl = require('wurl');
|
||||
|
||||
// Check link if it's internal/external
|
||||
function linkIsInternal(currentUrl, newUrl) {
|
||||
const currentDomain = wurl('hostname', currentUrl);
|
||||
const newDomain = wurl('hostname', newUrl);
|
||||
return currentDomain === newDomain;
|
||||
}
|
||||
|
||||
// We'll be needing this to open images in default browser
|
||||
const skipImages = '.jpg|.gif|.png|.jpeg|.JPG|.PNG';
|
||||
|
||||
module.exports = {
|
||||
linkIsInternal,
|
||||
skipImages
|
||||
};
|
||||
588
app/main/menu.js
588
app/main/menu.js
@@ -1,363 +1,381 @@
|
||||
'use strict';
|
||||
const os = require('os');
|
||||
const electron = require('electron');
|
||||
const {dialog, app, shell, BrowserWindow, Menu} = require('electron');
|
||||
|
||||
const {dialog} = require('electron');
|
||||
const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');
|
||||
|
||||
const app = electron.app;
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
const shell = electron.shell;
|
||||
const appName = app.getName();
|
||||
// Const tray = require('./tray');
|
||||
|
||||
const {addDomain, about} = require('./windowmanager');
|
||||
|
||||
function sendAction(action) {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
win.restore();
|
||||
}
|
||||
|
||||
win.webContents.send(action);
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
const ses = win.webContents.session;
|
||||
ses.clearCache(() => {
|
||||
dialog.showMessageBox({type: 'info', buttons: [], message: 'Cache cleared!'});
|
||||
});
|
||||
}
|
||||
|
||||
const viewSubmenu = [
|
||||
{
|
||||
label: 'Reload',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.reload();
|
||||
focusedWindow.webContents.send('destroytray');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'togglefullscreen'
|
||||
},
|
||||
{
|
||||
label: 'Zoom In',
|
||||
accelerator: 'CommandOrControl+=',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('zoomIn');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Zoom Out',
|
||||
accelerator: 'CommandOrControl+-',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('zoomOut');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Actual Size',
|
||||
accelerator: 'CommandOrControl+0',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('zoomActualSize');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Toggle Tray Icon',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.send('toggletray');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.toggleDevTools();
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const helpSubmenu = [
|
||||
{
|
||||
label: `${appName} Website`,
|
||||
click() {
|
||||
shell.openExternal('https://zulip.org');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: `${appName + 'Desktop'} - ${app.getVersion()}`,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: 'Report an Issue...',
|
||||
click() {
|
||||
const body = `
|
||||
<!-- Please succinctly describe your issue and steps to reproduce it. -->
|
||||
-
|
||||
${app.getName()} ${app.getVersion()}
|
||||
Electron ${process.versions.electron}
|
||||
${process.platform} ${process.arch} ${os.release()}`;
|
||||
|
||||
shell.openExternal(`https://github.com/zulip/zulip-electron/issues/new?body=${encodeURIComponent(body)}`);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const darwinTpl = [
|
||||
|
||||
{
|
||||
label: `${app.getName()}`,
|
||||
submenu: [
|
||||
{
|
||||
label: 'Zulip desktop',
|
||||
click() {
|
||||
about();
|
||||
class AppMenu {
|
||||
getHistorySubmenu() {
|
||||
return [{
|
||||
label: 'Back',
|
||||
accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('back');
|
||||
}
|
||||
},
|
||||
{
|
||||
}
|
||||
}, {
|
||||
label: 'Forward',
|
||||
accelerator: process.platform === 'darwin' ? 'Command+Right' : 'Alt+Right',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('forward');
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
getViewSubmenu() {
|
||||
return [{
|
||||
label: 'Reload',
|
||||
accelerator: 'CommandOrControl+R',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('reload-viewer');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Hard Reload',
|
||||
accelerator: 'CommandOrControl+Shift+R',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('hard-reload');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
role: 'togglefullscreen'
|
||||
}, {
|
||||
label: 'Zoom In',
|
||||
accelerator: 'CommandOrControl+=',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('zoomIn');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Zoom Out',
|
||||
accelerator: 'CommandOrControl+-',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('zoomOut');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Actual Size',
|
||||
accelerator: 'CommandOrControl+0',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('zoomActualSize');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Toggle Tray Icon',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.send('toggletray');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Toggle Sidebar',
|
||||
accelerator: 'CommandOrControl+S',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
const newValue = !ConfigUtil.getConfigItem('showSidebar');
|
||||
focusedWindow.webContents.send('toggle-sidebar', newValue);
|
||||
ConfigUtil.setConfigItem('showSidebar', newValue);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Toggle DevTools for Zulip App',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.toggleDevTools();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Toggle DevTools for Active Tab',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('tab-devtools');
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
getHelpSubmenu() {
|
||||
return [{
|
||||
label: `${appName} Website`,
|
||||
click() {
|
||||
shell.openExternal('https://zulipchat.com/help/');
|
||||
}
|
||||
}, {
|
||||
label: `${appName + 'Desktop'} - ${app.getVersion()}`,
|
||||
enabled: false
|
||||
}, {
|
||||
label: 'Report an Issue...',
|
||||
click() {
|
||||
const body = `
|
||||
<!-- Please succinctly describe your issue and steps to reproduce it. -->
|
||||
-
|
||||
${app.getName()} ${app.getVersion()}
|
||||
Electron ${process.versions.electron}
|
||||
${process.platform} ${process.arch} ${os.release()}`;
|
||||
shell.openExternal(`https://github.com/zulip/zulip-electron/issues/new?body=${encodeURIComponent(body)}`);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
getWindowSubmenu(tabs, activeTabIndex) {
|
||||
const initialSubmenu = [{
|
||||
role: 'minimize'
|
||||
}, {
|
||||
role: 'close'
|
||||
}];
|
||||
|
||||
if (tabs.length > 0) {
|
||||
const ShortcutKey = process.platform === 'darwin' ? 'Cmd' : 'Ctrl';
|
||||
initialSubmenu.push({
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Change Zulip Server',
|
||||
accelerator: 'Cmd+,',
|
||||
click() {
|
||||
addDomain();
|
||||
});
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
initialSubmenu.push({
|
||||
label: tabs[i].webview.props.name,
|
||||
accelerator: tabs[i].props.role === 'function' ? '' : `${ShortcutKey} + ${tabs[i].props.index + 1}`,
|
||||
checked: tabs[i].props.index === activeTabIndex,
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('switch-server-tab', tabs[i].props.index);
|
||||
}
|
||||
},
|
||||
type: 'radio'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return initialSubmenu;
|
||||
}
|
||||
|
||||
getDarwinTpl(props) {
|
||||
const {tabs, activeTabIndex} = props;
|
||||
|
||||
return [{
|
||||
label: `${app.getName()}`,
|
||||
submenu: [{
|
||||
label: 'Zulip Desktop',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('open-about');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Keyboard shortcuts',
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Settings',
|
||||
accelerator: 'Cmd+,',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('open-settings');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Keyboard Shortcuts',
|
||||
accelerator: 'Cmd+K',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('shortcut');
|
||||
AppMenu.sendAction('shortcut');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
label: 'Clear Cache',
|
||||
click() {
|
||||
clearCache();
|
||||
AppMenu.clearCache();
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
label: 'Log Out',
|
||||
accelerator: 'Cmd+L',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('log-out');
|
||||
AppMenu.sendAction('log-out');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'services',
|
||||
submenu: []
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'hide'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'hideothers'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'unhide'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'quit'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{
|
||||
}]
|
||||
}, {
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
role: 'undo'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'redo'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'cut'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'copy'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'paste'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'pasteandmatchstyle'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'delete'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'selectall'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: viewSubmenu
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{
|
||||
role: 'minimize'
|
||||
},
|
||||
{
|
||||
role: 'close'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'front'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: helpSubmenu
|
||||
}]
|
||||
}, {
|
||||
label: 'View',
|
||||
submenu: this.getViewSubmenu()
|
||||
}, {
|
||||
label: 'History',
|
||||
submenu: this.getHistorySubmenu()
|
||||
}, {
|
||||
label: 'Window',
|
||||
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
|
||||
}, {
|
||||
role: 'help',
|
||||
submenu: this.getHelpSubmenu()
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
const otherTpl = [
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Zulip desktop',
|
||||
click() {
|
||||
about();
|
||||
getOtherTpl(props) {
|
||||
const {tabs, activeTabIndex} = props;
|
||||
|
||||
return [{
|
||||
label: 'File',
|
||||
submenu: [{
|
||||
label: 'Zulip Desktop',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('open-about');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Change Zulip Server',
|
||||
}, {
|
||||
label: 'Settings',
|
||||
accelerator: 'Ctrl+,',
|
||||
click() {
|
||||
addDomain();
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('open-settings');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Keyboard shortcuts',
|
||||
}, {
|
||||
label: 'Keyboard Shortcuts',
|
||||
accelerator: 'Ctrl+K',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('shortcut');
|
||||
AppMenu.sendAction('shortcut');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
label: 'Clear Cache',
|
||||
click() {
|
||||
clearCache();
|
||||
AppMenu.clearCache();
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
label: 'Log Out',
|
||||
accelerator: 'Ctrl+L',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('log-out');
|
||||
AppMenu.sendAction('log-out');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'quit'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{
|
||||
}, {
|
||||
role: 'quit',
|
||||
accelerator: 'Ctrl+Q'
|
||||
}]
|
||||
}, {
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
role: 'undo'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'redo'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'cut'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'copy'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'paste'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'pasteandmatchstyle'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'delete'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
}, {
|
||||
role: 'selectall'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: viewSubmenu
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: helpSubmenu
|
||||
}]
|
||||
}, {
|
||||
label: 'View',
|
||||
submenu: this.getViewSubmenu()
|
||||
}, {
|
||||
label: 'History',
|
||||
submenu: this.getHistorySubmenu()
|
||||
}, {
|
||||
label: 'Window',
|
||||
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
|
||||
}, {
|
||||
role: 'help',
|
||||
submenu: this.getHelpSubmenu()
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
const tpl = process.platform === 'darwin' ? darwinTpl : otherTpl;
|
||||
static sendAction(action, ...params) {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
module.exports = electron.Menu.buildFromTemplate(tpl);
|
||||
if (process.platform === 'darwin') {
|
||||
win.restore();
|
||||
}
|
||||
|
||||
win.webContents.send(action, ...params);
|
||||
}
|
||||
|
||||
static clearCache() {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
const ses = win.webContents.session;
|
||||
ses.clearCache(() => {
|
||||
dialog.showMessageBox({type: 'info', buttons: [], message: 'Cache cleared!'});
|
||||
});
|
||||
}
|
||||
|
||||
setMenu(props) {
|
||||
const tpl = process.platform === 'darwin' ? this.getDarwinTpl(props) : this.getOtherTpl(props);
|
||||
const menu = Menu.buildFromTemplate(tpl);
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AppMenu();
|
||||
|
||||
34
app/main/startup.js
Normal file
34
app/main/startup.js
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
const { app } = require('electron');
|
||||
const AutoLaunch = require('auto-launch');
|
||||
const isDev = require('electron-is-dev');
|
||||
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
|
||||
|
||||
const setAutoLaunch = AutoLaunchValue => {
|
||||
// Don't run this in development
|
||||
if (isDev) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On Mac, work around a bug in auto-launch where it opens a Terminal window
|
||||
// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
|
||||
|
||||
const appPath = process.platform === 'darwin' ? app.getPath('exe').replace(/\.app\/Content.*/, '.app') : undefined; // Use the default
|
||||
|
||||
const ZulipAutoLauncher = new AutoLaunch({
|
||||
name: 'Zulip',
|
||||
path: appPath,
|
||||
isHidden: false
|
||||
});
|
||||
const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue);
|
||||
|
||||
if (autoLaunchOption) {
|
||||
ZulipAutoLauncher.enable();
|
||||
} else {
|
||||
ZulipAutoLauncher.disable();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setAutoLaunch
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const electron = require('electron');
|
||||
const ipc = require('electron').ipcMain;
|
||||
|
||||
const APP_ICON = path.join(__dirname, '../resources', 'Icon');
|
||||
|
||||
const iconPath = () => {
|
||||
return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
|
||||
};
|
||||
let domainWindow;
|
||||
let aboutWindow;
|
||||
|
||||
function onClosed() {
|
||||
// Dereference the window
|
||||
domainWindow = null;
|
||||
aboutWindow = null;
|
||||
}
|
||||
|
||||
// Change Zulip server Window
|
||||
function createdomainWindow() {
|
||||
const domainwin = new electron.BrowserWindow({
|
||||
title: 'Switch Server',
|
||||
frame: false,
|
||||
height: 300,
|
||||
resizable: false,
|
||||
width: 400,
|
||||
show: false,
|
||||
icon: iconPath()
|
||||
|
||||
});
|
||||
const domainURL = 'file://' + path.join(__dirname, '../renderer', 'pref.html');
|
||||
domainwin.loadURL(domainURL);
|
||||
domainwin.on('closed', onClosed);
|
||||
|
||||
return domainwin;
|
||||
}
|
||||
// Call this window onClick addDomain in tray
|
||||
function addDomain() {
|
||||
domainWindow = createdomainWindow();
|
||||
domainWindow.once('ready-to-show', () => {
|
||||
domainWindow.show();
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (domainWindow !== null) {
|
||||
if (!domainWindow.isDestroyed()) {
|
||||
domainWindow.destroy();
|
||||
}
|
||||
}
|
||||
}, 15000);
|
||||
}
|
||||
// About window
|
||||
function createAboutWindow() {
|
||||
const aboutwin = new electron.BrowserWindow({
|
||||
width: 500,
|
||||
height: 500,
|
||||
title: 'About Zulip Desktop',
|
||||
show: false,
|
||||
center: true,
|
||||
fullscreen: false,
|
||||
fullscreenable: false,
|
||||
resizable: false
|
||||
});
|
||||
const aboutURL = 'file://' + path.join(__dirname, '../renderer', 'about.html');
|
||||
aboutwin.loadURL(aboutURL);
|
||||
aboutwin.on('closed', onClosed);
|
||||
|
||||
// Stop page to update it's title
|
||||
aboutwin.on('page-title-updated', e => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
aboutwin.on('closed', onClosed);
|
||||
|
||||
return aboutwin;
|
||||
}
|
||||
|
||||
// Call this onClick About in tray
|
||||
function about() {
|
||||
aboutWindow = createAboutWindow();
|
||||
aboutWindow.once('ready-to-show', () => {
|
||||
aboutWindow.show();
|
||||
});
|
||||
}
|
||||
|
||||
ipc.on('trayabout', event => {
|
||||
if (event) {
|
||||
about();
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on('traychangeserver', event => {
|
||||
if (event) {
|
||||
addDomain();
|
||||
}
|
||||
});
|
||||
module.exports = {
|
||||
addDomain,
|
||||
about
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "zulip",
|
||||
"productName": "Zulip",
|
||||
"version": "0.5.10",
|
||||
"version": "1.4.0",
|
||||
"description": "Zulip Desktop App",
|
||||
"license": "Apache-2.0",
|
||||
"email": "<svnitakash@gmail.com>",
|
||||
"copyright": "©2017 Kandra Labs, Inc.",
|
||||
"author": {
|
||||
"name": "Akash Nimare",
|
||||
"name": "Kandra Labs, Inc.",
|
||||
"email": "svnitakash@gmail.com"
|
||||
},
|
||||
"repository": {
|
||||
@@ -27,16 +27,16 @@
|
||||
"InstantMessaging"
|
||||
],
|
||||
"dependencies": {
|
||||
"electron-config":"0.2.1",
|
||||
"electron-debug": "1.1.0",
|
||||
"electron-is-dev": "0.1.2",
|
||||
"electron-localshortcut": "1.0.0",
|
||||
"electron-log": "1.3.0",
|
||||
"electron-spellchecker": "1.0.5",
|
||||
"electron-updater": "1.11.2",
|
||||
"https": "^1.0.0",
|
||||
"electron-debug": "1.4.0",
|
||||
"electron-is-dev": "0.3.0",
|
||||
"electron-localshortcut": "2.0.2",
|
||||
"electron-log": "2.2.7",
|
||||
"electron-spellchecker": "1.2.0",
|
||||
"electron-updater": "2.8.9",
|
||||
"node-json-db": "0.7.3",
|
||||
"request": "2.79.0",
|
||||
"wurl": "2.1.0"
|
||||
"request": "2.81.0",
|
||||
"wurl": "2.5.0",
|
||||
"electron-window-state": "4.1.1",
|
||||
"auto-launch": "5.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,143 +1,321 @@
|
||||
* {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Helvetica, Arial, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
color: #444;
|
||||
/*******************
|
||||
* General rules *
|
||||
*******************/
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: #eee url(../img/ic_loading.gif) no-repeat;
|
||||
background-size: 60px 60px;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
background: #222c31;
|
||||
width: 54px;
|
||||
padding: 27px 0 20px 0;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-webkit-app-region: drag;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'), local('MaterialIcons-Regular'), url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
|
||||
/*******************
|
||||
* Left Sidebar *
|
||||
*******************/
|
||||
|
||||
#tabs-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
/* Preferred icon size */
|
||||
font-size: 24px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-button i {
|
||||
color: #6c8592;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.action-button:hover i {
|
||||
color: #98a9b3;
|
||||
}
|
||||
|
||||
.tab:first-child {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab .server-icons {
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
padding: 3px;
|
||||
height: 30px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-image: url(../img/topography.png);
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
header {
|
||||
.tab .server-tab {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
position: relative;
|
||||
margin: 5px 0 2px 0;
|
||||
z-index: 11;
|
||||
line-height: 31px;
|
||||
color: #eee;
|
||||
text-align: center;
|
||||
margin: 0px 0px 30px 0px;
|
||||
overflow: hidden;
|
||||
opacity: 0.6;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
header img {
|
||||
width: 50px;
|
||||
.tab .server-tab:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
display: inline-block;
|
||||
margin: 10px 0px 10px 10px;
|
||||
|
||||
font-weight: 500;
|
||||
.tab.functional-tab {
|
||||
height: 46px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
section.server {
|
||||
display: inline-block;
|
||||
padding: 50px;
|
||||
margin-top: calc(50vh - 135px);
|
||||
|
||||
text-align: left;
|
||||
|
||||
box-shadow: 0px 0px 100px rgba(0,0,0,0.15);
|
||||
|
||||
background-color: #fff;
|
||||
|
||||
animation-name: pulse;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
.tab.functional-tab.active .server-tab {
|
||||
padding: 2px 0;
|
||||
height: 40px;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
section.shake {
|
||||
animation-name: shake;
|
||||
animation-duration: 0.5s;
|
||||
animation-iteration-count: 1;
|
||||
.tab.functional-tab .server-tab i {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
form label {
|
||||
.tab.active .server-tab {
|
||||
opacity: 1;
|
||||
background-color: #648478;
|
||||
}
|
||||
|
||||
.tab .server-tab-badge.active {
|
||||
border-radius: 9px;
|
||||
min-width: 11px;
|
||||
padding: 0 3px;
|
||||
height: 17px;
|
||||
background-color: #f44336;
|
||||
font-size: 10px;
|
||||
font-family: sans-serif;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
z-index: 15;
|
||||
top: 6px;
|
||||
float: right;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 17px;
|
||||
display: block;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
font-weight: 600;
|
||||
.tab .server-tab-badge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab .server-tab-badge.close-button {
|
||||
width: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tab .server-tab-badge.close-button i {
|
||||
font-size: 13px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
#add-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab .server-tab-shortcut {
|
||||
color: #648478;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-family: sans-serif;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
form input[type=text] {
|
||||
width: 300px;
|
||||
border: 2px solid #ccc;
|
||||
padding: 10px 10px;
|
||||
|
||||
outline: none;
|
||||
/*******************
|
||||
* Webview Area *
|
||||
*******************/
|
||||
|
||||
transition: all 0.2s ease;
|
||||
#webviews-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form input[type=text]:hover,
|
||||
form input[type=text]:focus {
|
||||
border-color: #aaa;
|
||||
}
|
||||
|
||||
form button {
|
||||
padding: 10px 10px;
|
||||
|
||||
background-color: #52c2af;
|
||||
color: #fff;
|
||||
|
||||
border: 2px solid #52c2af;
|
||||
outline: none;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
form button:hover {
|
||||
background-color: #30b09a;
|
||||
border-color: #30b09a;
|
||||
}
|
||||
|
||||
form button:active {
|
||||
background-color: #14957f;
|
||||
border-color: #14957f;
|
||||
}
|
||||
|
||||
form #error {
|
||||
margin: 5px 0px 0px;
|
||||
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
|
||||
color: rgb(201, 107, 107);
|
||||
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
|
||||
height: 15px;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
form #error.show {
|
||||
webview {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
transition: opacity 0.3s ease-in;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* -- generic components -- */
|
||||
.center {
|
||||
webview.onload {
|
||||
transition: opacity 1s cubic-bezier(0.95, 0.05, 0.795, 0.035);
|
||||
}
|
||||
|
||||
webview.disabled {
|
||||
flex: 0 1;
|
||||
height: 0;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
webview:focus {
|
||||
outline: 0px solid transparent;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip styling */
|
||||
|
||||
#reload-tooltip,
|
||||
#setting-tooltip {
|
||||
font-family: sans-serif;
|
||||
background: #222c31;
|
||||
margin-left: 68px;
|
||||
padding: 6px 8px;
|
||||
position: absolute;
|
||||
margin-top: 0px;
|
||||
z-index: 1000;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: 55px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0px 0px 100px rgba(0,0,0,0.15); }
|
||||
50% { box-shadow: 0px 0px 100px rgba(0,0,0,0.30); }
|
||||
100% { box-shadow: 0px 0px 100px rgba(0,0,0,0.15); }
|
||||
#reload-tooltip:after,
|
||||
#setting-tooltip:after {
|
||||
content: " ";
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-right: 8px solid #222c31;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 68px;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% { transform: translate3d(-1px, 0, 0); }
|
||||
20%, 80% { transform: translate3d(2px, 0, 0); }
|
||||
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
|
||||
40%, 60% { transform: translate3d(4px, 0, 0); }
|
||||
#collapse-button {
|
||||
bottom: 30px;
|
||||
left: 20px;
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #222c31;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
box-shadow: #999 1px 1px;
|
||||
}
|
||||
|
||||
#collapse-button i {
|
||||
color: #efefef;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Full screen Popup container */
|
||||
.popup .popuptext {
|
||||
visibility: hidden;
|
||||
background-color: #555;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 9px 0;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
font-family: arial;
|
||||
width: 240px;
|
||||
top: 15px;
|
||||
height: 20px;
|
||||
left: 43%;
|
||||
}
|
||||
|
||||
.popup .show {
|
||||
visibility: visible;
|
||||
animation: cssAnimation 0s ease-in 5s forwards;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes cssAnimation {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
42
app/renderer/css/network.css
Normal file
42
app/renderer/css/network.css
Normal file
@@ -0,0 +1,42 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
user-select:none;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: "Trebuchet MS", Helvetica, sans-serif;
|
||||
margin: 100px 200px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#description {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#reconnect {
|
||||
font-size: 16px;
|
||||
background: #009688;
|
||||
color: #fff;
|
||||
width: 84px;
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
line-height: 32px;
|
||||
margin: 20px auto 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#reconnect:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
body{
|
||||
background-color: #6BB6C7;
|
||||
}
|
||||
|
||||
.form {
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
width: 300px;
|
||||
left: 9%;
|
||||
}
|
||||
|
||||
.close {
|
||||
background: transparent url('../img/close.png') no-repeat 4px 4px;
|
||||
background-size: 24px 24px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
text-indent: -10000px;
|
||||
top: 6px;
|
||||
width: 32px;
|
||||
z-index: 1;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-family: sans-serif;
|
||||
font-size: 18px;
|
||||
appearance: none;
|
||||
box-shadow: none;
|
||||
border-radius: none;
|
||||
color: #646464;
|
||||
}
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form input[type="text"] {
|
||||
padding: 10px;
|
||||
border: solid 1px #dcdcdc;
|
||||
transition: box-shadow 0.3s, border 0.3s;
|
||||
}
|
||||
.form input[type="text"]:focus,
|
||||
.form input[type="text"].focus {
|
||||
border: solid 1px #707070;
|
||||
box-shadow: 0 0 5px 1px #969696;
|
||||
}
|
||||
button {
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 12px 32px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
margin-left: 107px;
|
||||
margin-top: 24px;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
background: #137b86;
|
||||
}
|
||||
button:focus {
|
||||
outline: 0;
|
||||
}
|
||||
#urladded {
|
||||
font-size: 20px;
|
||||
position: absolute;
|
||||
font-family: 'opensans';
|
||||
top: -61%;
|
||||
left: 25%;
|
||||
text-align: center;
|
||||
}
|
||||
250
app/renderer/css/preference.css
Normal file
250
app/renderer/css/preference.css
Normal file
@@ -0,0 +1,250 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
/* Preferred icon size */
|
||||
font-size: 24px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 80px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#nav-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding: 5px 0;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav.active {
|
||||
color: #464e5a;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav.active::before {
|
||||
background: #464e5a;
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
#settings-header {
|
||||
font-size: 22px;
|
||||
color: #5c6166;
|
||||
}
|
||||
|
||||
#settings-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 30px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#new-server-container {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 4px 0 6px 0;
|
||||
font-weight: bold;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
padding: 4px 0 6px 0;
|
||||
font-weight: bold;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
img.server-info-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.server-info-left {
|
||||
margin: 10px 20px 0 0;
|
||||
}
|
||||
|
||||
.server-info-right {
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.server-info-row {
|
||||
display: flex;
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.server-info-alias {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-input-key {
|
||||
font-size: 14px;
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
flex-wrap: nowrap;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.setting-input-value {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-bottom: #ededed 1px solid;
|
||||
outline-width: 0;
|
||||
background: transparent;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.setting-input-value:focus {
|
||||
border-bottom: #7cb980 1px solid;
|
||||
}
|
||||
|
||||
.setting-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: #235d3a;
|
||||
vertical-align: middle;
|
||||
margin: 10px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
border-radius: 2px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.action i {
|
||||
margin-right: 5px;
|
||||
font-size: 18px;
|
||||
line-height: 27px;
|
||||
}
|
||||
|
||||
.settings-pane {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.action:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action.disabled:hover {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.action.disabled {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 30px;
|
||||
margin: 10px 0 20px 0;
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
width: 540px;
|
||||
box-shadow: 1px 2px 4px #bcbcbc;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
margin: 5px !important;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #ef5350;
|
||||
background: #ffebee;
|
||||
border: 1px solid #ef5350;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: #388E3C;
|
||||
background: #E8F5E9;
|
||||
border: 1px solid #388E3C;
|
||||
}
|
||||
|
||||
.grey {
|
||||
color: #9E9E9E;
|
||||
background: #FAFAFA;
|
||||
border: 1px solid #9E9E9E;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: Courier New, Courier, monospace;
|
||||
}
|
||||
|
||||
i.open-tab-button {
|
||||
padding: 0 5px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
/* We'll be overriding default styling so that app look more native * /
|
||||
/* Override css rules */
|
||||
|
||||
.portico-wrap>.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.portico-container>.footer {
|
||||
display: none;
|
||||
}
|
||||
BIN
app/renderer/fonts/MaterialIcons-Regular.ttf
Normal file
BIN
app/renderer/fonts/MaterialIcons-Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 803 B |
BIN
app/renderer/img/ic_loading.gif
Normal file
BIN
app/renderer/img/ic_loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
app/renderer/img/ic_server_tab_default.png
Normal file
BIN
app/renderer/img/ic_server_tab_default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
app/renderer/img/icon.png
Normal file
BIN
app/renderer/img/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
BIN
app/renderer/img/zulip_network.png
Normal file
BIN
app/renderer/img/zulip_network.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -1,30 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="responsive desktop">
|
||||
<!--<![endif]-->
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Login - Zulip</title>
|
||||
<link rel="stylesheet" href="css/main.css" type="text/css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<div class="center">
|
||||
<section class="server">
|
||||
<header>
|
||||
<img src="../resources/zulip.png" id="logo"/>
|
||||
<h1>Zulip Login</h1>
|
||||
</header>
|
||||
<div class="container">
|
||||
<form id="frm-signInForm" class="form-large" onsubmit="addDomain(); return false">
|
||||
<label for="url">
|
||||
Zulip Server URL
|
||||
</label>
|
||||
<input type="text" id="url" autofocus="autofocus" spellcheck="false" placeholder="Server URL">
|
||||
<button type="submit" id="main" class="btn-primary btn-large" value="Submit">Connect</button>
|
||||
<p id="error"></p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
11
app/renderer/js/components/base.js
Normal file
11
app/renderer/js/components/base.js
Normal file
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
class BaseComponent {
|
||||
generateNodeFromTemplate(template) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = template;
|
||||
return wrapper.firstElementChild;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseComponent;
|
||||
43
app/renderer/js/components/functional-tab.js
Normal file
43
app/renderer/js/components/functional-tab.js
Normal file
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
const Tab = require(__dirname + '/../components/tab.js');
|
||||
|
||||
class FunctionalTab extends Tab {
|
||||
template() {
|
||||
return `<div class="tab functional-tab">
|
||||
<div class="server-tab-badge close-button">
|
||||
<i class="material-icons">close</i>
|
||||
</div>
|
||||
<div class="server-tab">
|
||||
<i class="material-icons">${this.props.materialIcon}</i>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$el = this.generateNodeFromTemplate(this.template());
|
||||
this.props.$root.appendChild(this.$el);
|
||||
|
||||
this.$closeButton = this.$el.getElementsByClassName('server-tab-badge')[0];
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
super.registerListeners();
|
||||
|
||||
this.$el.addEventListener('mouseover', () => {
|
||||
this.$closeButton.classList.add('active');
|
||||
});
|
||||
|
||||
this.$el.addEventListener('mouseout', () => {
|
||||
this.$closeButton.classList.remove('active');
|
||||
});
|
||||
|
||||
this.$closeButton.addEventListener('click', e => {
|
||||
this.props.onDestroy();
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FunctionalTab;
|
||||
58
app/renderer/js/components/server-tab.js
Normal file
58
app/renderer/js/components/server-tab.js
Normal file
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
const Tab = require(__dirname + '/../components/tab.js');
|
||||
const SystemUtil = require(__dirname + '/../utils/system-util.js');
|
||||
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
class ServerTab extends Tab {
|
||||
template() {
|
||||
return `<div class="tab">
|
||||
<div class="server-tab-badge"></div>
|
||||
<div class="server-tab">
|
||||
<img class="server-icons" src='${this.props.icon}'/>
|
||||
</div>
|
||||
<div class="server-tab-shortcut">${this.generateShortcutText()}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.$badge = this.$el.getElementsByClassName('server-tab-badge')[0];
|
||||
}
|
||||
|
||||
updateBadge(count) {
|
||||
if (count > 0) {
|
||||
const formattedCount = count > 999 ? '1K+' : count;
|
||||
|
||||
this.$badge.innerHTML = formattedCount;
|
||||
this.$badge.classList.add('active');
|
||||
} else {
|
||||
this.$badge.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
generateShortcutText() {
|
||||
// Only provide shortcuts for server [0..10]
|
||||
if (this.props.index >= 10) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const shownIndex = this.props.index + 1;
|
||||
|
||||
let shortcutText = '';
|
||||
|
||||
if (SystemUtil.getOS() === 'Mac') {
|
||||
shortcutText = `⌘ ${shownIndex}`;
|
||||
} else {
|
||||
shortcutText = `Ctrl+${shownIndex}`;
|
||||
}
|
||||
|
||||
ipcRenderer.send('register-server-tab-shortcut', shownIndex);
|
||||
|
||||
return shortcutText;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServerTab;
|
||||
46
app/renderer/js/components/tab.js
Normal file
46
app/renderer/js/components/tab.js
Normal file
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
const BaseComponent = require(__dirname + '/../components/base.js');
|
||||
|
||||
class Tab extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
|
||||
this.props = props;
|
||||
this.webview = this.props.webview;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$el = this.generateNodeFromTemplate(this.template());
|
||||
this.props.$root.appendChild(this.$el);
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
this.$el.addEventListener('click', this.props.onClick);
|
||||
}
|
||||
|
||||
isLoading() {
|
||||
return this.webview.isLoading;
|
||||
}
|
||||
|
||||
activate() {
|
||||
this.$el.classList.add('active');
|
||||
this.webview.load();
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
this.$el.classList.remove('active');
|
||||
this.webview.hide();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.$el.parentNode.removeChild(this.$el);
|
||||
this.webview.$el.parentNode.removeChild(this.webview.$el);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Tab;
|
||||
183
app/renderer/js/components/webview.js
Normal file
183
app/renderer/js/components/webview.js
Normal file
@@ -0,0 +1,183 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const DomainUtil = require(__dirname + '/../utils/domain-util.js');
|
||||
const SystemUtil = require(__dirname + '/../utils/system-util.js');
|
||||
const LinkUtil = require(__dirname + '/../utils/link-util.js');
|
||||
const {shell} = require('electron').remote;
|
||||
|
||||
const BaseComponent = require(__dirname + '/../components/base.js');
|
||||
|
||||
class WebView extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
|
||||
this.props = props;
|
||||
|
||||
this.zoomFactor = 1.0;
|
||||
this.loading = false;
|
||||
this.badgeCount = 0;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `<webview
|
||||
class="disabled"
|
||||
src="${this.props.url}"
|
||||
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
|
||||
disablewebsecurity
|
||||
${this.props.preload ? 'preload="js/preload.js"' : ''}
|
||||
partition="persist:webviewsession"
|
||||
webpreferences="allowRunningInsecureContent, javascript=yes">
|
||||
</webview>`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$el = this.generateNodeFromTemplate(this.template());
|
||||
this.props.$root.appendChild(this.$el);
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
this.$el.addEventListener('new-window', event => {
|
||||
const {url} = event;
|
||||
const domainPrefix = DomainUtil.getDomain(this.props.index).url;
|
||||
|
||||
if (LinkUtil.isInternal(domainPrefix, url) || url === (domainPrefix + '/')) {
|
||||
event.preventDefault();
|
||||
this.$el.loadURL(url);
|
||||
} else {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
}
|
||||
});
|
||||
|
||||
this.$el.addEventListener('page-title-updated', event => {
|
||||
const {title} = event;
|
||||
this.badgeCount = this.getBadgeCount(title);
|
||||
this.props.onTitleChange();
|
||||
});
|
||||
|
||||
this.$el.addEventListener('dom-ready', () => {
|
||||
if (this.props.role === 'server') {
|
||||
this.$el.classList.add('onload');
|
||||
}
|
||||
this.show();
|
||||
});
|
||||
|
||||
this.$el.addEventListener('did-fail-load', event => {
|
||||
const {errorDescription} = event;
|
||||
const hasConnectivityErr = (SystemUtil.connectivityERR.indexOf(errorDescription) >= 0);
|
||||
if (hasConnectivityErr) {
|
||||
console.error('error', errorDescription);
|
||||
this.props.onNetworkError();
|
||||
}
|
||||
});
|
||||
|
||||
this.$el.addEventListener('did-start-loading', () => {
|
||||
let userAgent = SystemUtil.getUserAgent();
|
||||
if (!userAgent) {
|
||||
SystemUtil.setUserAgent(this.$el.getUserAgent());
|
||||
userAgent = SystemUtil.getUserAgent();
|
||||
}
|
||||
this.$el.setUserAgent(userAgent);
|
||||
});
|
||||
}
|
||||
|
||||
getBadgeCount(title) {
|
||||
const messageCountInTitle = (/\(([0-9]+)\)/).exec(title);
|
||||
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
|
||||
}
|
||||
|
||||
show() {
|
||||
// Do not show WebView if another tab was selected and this tab should be in background.
|
||||
if (!this.props.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$el.classList.remove('disabled');
|
||||
setTimeout(() => {
|
||||
if (this.props.role === 'server') {
|
||||
this.$el.classList.remove('onload');
|
||||
}
|
||||
}, 1000);
|
||||
this.focus();
|
||||
this.loading = false;
|
||||
this.props.onTitleChange();
|
||||
// Injecting preload css in webview to override some css rules
|
||||
this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8'));
|
||||
}
|
||||
|
||||
focus() {
|
||||
// focus Webview and it's contents when Window regain focus.
|
||||
const webContents = this.$el.getWebContents();
|
||||
if (webContents && !webContents.isFocused()) {
|
||||
this.$el.focus();
|
||||
webContents.focus();
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$el.classList.add('disabled');
|
||||
}
|
||||
|
||||
load() {
|
||||
if (this.$el) {
|
||||
this.show();
|
||||
} else {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn() {
|
||||
this.zoomFactor += 0.1;
|
||||
this.$el.setZoomFactor(this.zoomFactor);
|
||||
}
|
||||
|
||||
zoomOut() {
|
||||
this.zoomFactor -= 0.1;
|
||||
this.$el.setZoomFactor(this.zoomFactor);
|
||||
}
|
||||
|
||||
zoomActualSize() {
|
||||
this.zoomFactor = 1.0;
|
||||
this.$el.setZoomFactor(this.zoomFactor);
|
||||
}
|
||||
|
||||
logOut() {
|
||||
this.$el.executeJavaScript('logout()');
|
||||
}
|
||||
|
||||
showShortcut() {
|
||||
this.$el.executeJavaScript('shortcut()');
|
||||
}
|
||||
|
||||
openDevTools() {
|
||||
this.$el.openDevTools();
|
||||
}
|
||||
|
||||
back() {
|
||||
if (this.$el.canGoBack()) {
|
||||
this.$el.goBack();
|
||||
}
|
||||
}
|
||||
|
||||
forward() {
|
||||
if (this.$el.canGoForward()) {
|
||||
this.$el.goForward();
|
||||
}
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.hide();
|
||||
this.$el.reload();
|
||||
}
|
||||
|
||||
send(...param) {
|
||||
this.$el.send(...param);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebView;
|
||||
@@ -1,80 +0,0 @@
|
||||
const {app} = require('electron').remote;
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
const JsonDB = require('node-json-db');
|
||||
const request = require('request');
|
||||
|
||||
const db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
|
||||
|
||||
window.addDomain = function () {
|
||||
const el = sel => {
|
||||
return document.querySelector(sel);
|
||||
};
|
||||
|
||||
const $el = {
|
||||
error: el('#error'),
|
||||
main: el('#main'),
|
||||
section: el('section')
|
||||
};
|
||||
|
||||
const event = sel => {
|
||||
return {
|
||||
on: (event, callback) => {
|
||||
document.querySelector(sel).addEventListener(event, callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const displayError = msg => {
|
||||
$el.error.innerText = msg;
|
||||
$el.error.classList.add('show');
|
||||
$el.section.classList.add('shake');
|
||||
};
|
||||
|
||||
let newDomain = document.getElementById('url').value;
|
||||
newDomain = newDomain.replace(/^https?:\/\//, '');
|
||||
if (newDomain === '') {
|
||||
displayError('Please input a valid URL.');
|
||||
} else {
|
||||
el('#main').innerHTML = 'Checking...';
|
||||
if (newDomain.indexOf('localhost:') >= 0) {
|
||||
const domain = 'http://' + newDomain;
|
||||
const checkDomain = domain + '/static/audio/zulip.ogg';
|
||||
request(checkDomain, (error, response) => {
|
||||
if (!error && response.statusCode !== 404) {
|
||||
document.getElementById('main').innerHTML = 'Connect';
|
||||
db.push('/domain', domain);
|
||||
ipcRenderer.send('new-domain', domain);
|
||||
} else {
|
||||
$el.main.innerHTML = 'Connect';
|
||||
displayError('Not a valid Zulip local server');
|
||||
}
|
||||
});
|
||||
// });
|
||||
} else {
|
||||
const domain = 'https://' + newDomain;
|
||||
const checkDomain = domain + '/static/audio/zulip.ogg';
|
||||
|
||||
request(checkDomain, (error, response) => {
|
||||
if (!error && response.statusCode !== 404) {
|
||||
$el.main.innerHTML = 'Connect';
|
||||
db.push('/domain', domain);
|
||||
ipcRenderer.send('new-domain', domain);
|
||||
} else if (error.toString().indexOf('Error: self signed certificate') >= 0) {
|
||||
$el.main.innerHTML = 'Connect';
|
||||
ipcRenderer.send('certificate-err', domain);
|
||||
} else {
|
||||
$el.main.innerHTML = 'Connect';
|
||||
displayError('Not a valid Zulip server');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
event('#url').on('input', () => {
|
||||
el('#error').classList.remove('show');
|
||||
});
|
||||
|
||||
event('section').on('animationend', function () {
|
||||
this.classList.remove('shake');
|
||||
});
|
||||
};
|
||||
367
app/renderer/js/main.js
Normal file
367
app/renderer/js/main.js
Normal file
@@ -0,0 +1,367 @@
|
||||
'use strict';
|
||||
|
||||
require(__dirname + '/js/tray.js');
|
||||
const {ipcRenderer, remote} = require('electron');
|
||||
|
||||
const {session} = remote;
|
||||
|
||||
const DomainUtil = require(__dirname + '/js/utils/domain-util.js');
|
||||
const WebView = require(__dirname + '/js/components/webview.js');
|
||||
const ServerTab = require(__dirname + '/js/components/server-tab.js');
|
||||
const FunctionalTab = require(__dirname + '/js/components/functional-tab.js');
|
||||
const ConfigUtil = require(__dirname + '/js/utils/config-util.js');
|
||||
|
||||
class ServerManagerView {
|
||||
constructor() {
|
||||
this.$addServerButton = document.getElementById('add-tab');
|
||||
this.$tabsContainer = document.getElementById('tabs-container');
|
||||
|
||||
const $actionsContainer = document.getElementById('actions-container');
|
||||
this.$reloadButton = $actionsContainer.querySelector('#reload-action');
|
||||
this.$settingsButton = $actionsContainer.querySelector('#settings-action');
|
||||
this.$webviewsContainer = document.getElementById('webviews-container');
|
||||
|
||||
this.$reloadTooltip = $actionsContainer.querySelector('#reload-tooltip');
|
||||
this.$settingsTooltip = $actionsContainer.querySelector('#setting-tooltip');
|
||||
this.$sidebar = document.getElementById('sidebar');
|
||||
|
||||
this.$fullscreenPopup = document.getElementById('fullscreen-popup');
|
||||
this.$fullscreenEscapeKey = process.platform === 'darwin' ? '^⌘F' : 'F11';
|
||||
this.$fullscreenPopup.innerHTML = `Press ${this.$fullscreenEscapeKey} to exit full screen`;
|
||||
|
||||
this.activeTabIndex = -1;
|
||||
this.tabs = [];
|
||||
this.functionalTabs = {};
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadProxy().then(() => {
|
||||
this.initSidebar();
|
||||
this.initTabs();
|
||||
this.initActions();
|
||||
this.registerIpcs();
|
||||
});
|
||||
}
|
||||
|
||||
loadProxy() {
|
||||
return new Promise(resolve => {
|
||||
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
|
||||
if (proxyEnabled) {
|
||||
session.fromPartition('persist:webviewsession').setProxy({
|
||||
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
|
||||
proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
|
||||
proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
|
||||
}, resolve);
|
||||
} else {
|
||||
session.fromPartition('persist:webviewsession').setProxy({
|
||||
pacScript: '',
|
||||
proxyRules: '',
|
||||
proxyBypassRules: ''
|
||||
}, resolve);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initSidebar() {
|
||||
const showSidebar = ConfigUtil.getConfigItem('showSidebar', true);
|
||||
this.toggleSidebar(showSidebar);
|
||||
}
|
||||
|
||||
initTabs() {
|
||||
const servers = DomainUtil.getDomains();
|
||||
if (servers.length > 0) {
|
||||
for (let i = 0; i < servers.length; i++) {
|
||||
this.initServer(servers[i], i);
|
||||
DomainUtil.updateSavedServer(servers[i].url, i);
|
||||
this.activateTab(i);
|
||||
}
|
||||
this.activateTab(0);
|
||||
} else {
|
||||
this.openSettings('Servers');
|
||||
}
|
||||
|
||||
ipcRenderer.send('local-shortcuts', true);
|
||||
}
|
||||
|
||||
initServer(server, index) {
|
||||
this.tabs.push(new ServerTab({
|
||||
role: 'server',
|
||||
icon: server.icon,
|
||||
$root: this.$tabsContainer,
|
||||
onClick: this.activateTab.bind(this, index),
|
||||
index,
|
||||
webview: new WebView({
|
||||
$root: this.$webviewsContainer,
|
||||
index,
|
||||
url: server.url,
|
||||
name: server.alias,
|
||||
isActive: () => {
|
||||
return index === this.activeTabIndex;
|
||||
},
|
||||
onNetworkError: this.openNetworkTroubleshooting.bind(this),
|
||||
onTitleChange: this.updateBadge.bind(this),
|
||||
nodeIntegration: false,
|
||||
preload: true
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
initActions() {
|
||||
this.$reloadButton.addEventListener('click', () => {
|
||||
this.tabs[this.activeTabIndex].webview.reload();
|
||||
});
|
||||
this.$addServerButton.addEventListener('click', () => {
|
||||
this.openSettings('Servers');
|
||||
});
|
||||
this.$settingsButton.addEventListener('click', () => {
|
||||
this.openSettings('General');
|
||||
});
|
||||
this.$reloadButton.addEventListener('mouseover', () => {
|
||||
this.$reloadTooltip.removeAttribute('style');
|
||||
});
|
||||
this.$reloadButton.addEventListener('mouseout', () => {
|
||||
this.$reloadTooltip.style.display = 'none';
|
||||
});
|
||||
this.$settingsButton.addEventListener('mouseover', () => {
|
||||
this.$settingsTooltip.removeAttribute('style');
|
||||
});
|
||||
this.$settingsButton.addEventListener('mouseout', () => {
|
||||
this.$settingsTooltip.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
openFunctionalTab(tabProps) {
|
||||
if (this.functionalTabs[tabProps.name] !== undefined) {
|
||||
this.activateTab(this.functionalTabs[tabProps.name]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.functionalTabs[tabProps.name] = this.tabs.length;
|
||||
|
||||
this.tabs.push(new FunctionalTab({
|
||||
role: 'function',
|
||||
materialIcon: tabProps.materialIcon,
|
||||
$root: this.$tabsContainer,
|
||||
index: this.functionalTabs[tabProps.name],
|
||||
onClick: this.activateTab.bind(this, this.functionalTabs[tabProps.name]),
|
||||
onDestroy: this.destroyTab.bind(this, tabProps.name, this.functionalTabs[tabProps.name]),
|
||||
webview: new WebView({
|
||||
$root: this.$webviewsContainer,
|
||||
index: this.functionalTabs[tabProps.name],
|
||||
url: tabProps.url,
|
||||
name: tabProps.name,
|
||||
isActive: () => {
|
||||
return this.functionalTabs[tabProps.name] === this.activeTabIndex;
|
||||
},
|
||||
onNetworkError: this.openNetworkTroubleshooting.bind(this),
|
||||
onTitleChange: this.updateBadge.bind(this),
|
||||
nodeIntegration: true,
|
||||
preload: false
|
||||
})
|
||||
}));
|
||||
|
||||
this.activateTab(this.functionalTabs[tabProps.name]);
|
||||
}
|
||||
|
||||
openSettings(nav = 'General') {
|
||||
this.openFunctionalTab({
|
||||
name: 'Settings',
|
||||
materialIcon: 'settings',
|
||||
url: `file://${__dirname}/preference.html#${nav}`
|
||||
});
|
||||
this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav);
|
||||
}
|
||||
|
||||
openAbout() {
|
||||
this.openFunctionalTab({
|
||||
name: 'About',
|
||||
materialIcon: 'sentiment_very_satisfied',
|
||||
url: `file://${__dirname}/about.html`
|
||||
});
|
||||
}
|
||||
|
||||
openNetworkTroubleshooting() {
|
||||
this.openFunctionalTab({
|
||||
name: 'Network Troubleshooting',
|
||||
materialIcon: 'network_check',
|
||||
url: `file://${__dirname}/network.html`
|
||||
});
|
||||
}
|
||||
|
||||
activateTab(index, hideOldTab = true) {
|
||||
if (this.tabs[index].webview.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.activeTabIndex !== -1) {
|
||||
if (this.activeTabIndex === index) {
|
||||
return;
|
||||
} else if (hideOldTab) {
|
||||
this.tabs[this.activeTabIndex].deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
this.activeTabIndex = index;
|
||||
this.tabs[index].activate();
|
||||
|
||||
ipcRenderer.send('update-menu', {
|
||||
tabs: this.tabs,
|
||||
activeTabIndex: this.activeTabIndex
|
||||
});
|
||||
}
|
||||
|
||||
destroyTab(name, index) {
|
||||
if (this.tabs[index].webview.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabs[index].destroy();
|
||||
|
||||
delete this.tabs[index];
|
||||
delete this.functionalTabs[name];
|
||||
|
||||
// Issue #188: If the functional tab was not focused, do not activate another tab.
|
||||
if (this.activeTabIndex === index) {
|
||||
this.activateTab(0, false);
|
||||
}
|
||||
}
|
||||
|
||||
destroyView() {
|
||||
// Clear global variables
|
||||
this.activeTabIndex = -1;
|
||||
this.tabs = [];
|
||||
this.functionalTabs = {};
|
||||
|
||||
// Clear DOM elements
|
||||
this.$tabsContainer.innerHTML = '';
|
||||
this.$webviewsContainer.innerHTML = '';
|
||||
|
||||
// Destroy shortcuts
|
||||
ipcRenderer.send('local-shortcuts', false);
|
||||
}
|
||||
|
||||
reloadView() {
|
||||
this.destroyView();
|
||||
this.initTabs();
|
||||
}
|
||||
|
||||
updateBadge() {
|
||||
let messageCountAll = 0;
|
||||
for (let i = 0; i < this.tabs.length; i++) {
|
||||
if (this.tabs[i] && this.tabs[i].updateBadge) {
|
||||
const count = this.tabs[i].webview.badgeCount;
|
||||
messageCountAll += count;
|
||||
this.tabs[i].updateBadge(count);
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.send('update-badge', messageCountAll);
|
||||
}
|
||||
|
||||
toggleSidebar(show) {
|
||||
if (show) {
|
||||
this.$sidebar.classList.remove('hidden');
|
||||
} else {
|
||||
this.$sidebar.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
registerIpcs() {
|
||||
const webviewListeners = {
|
||||
'webview-reload': 'reload',
|
||||
back: 'back',
|
||||
focus: 'focus',
|
||||
forward: 'forward',
|
||||
zoomIn: 'zoomIn',
|
||||
zoomOut: 'zoomOut',
|
||||
zoomActualSize: 'zoomActualSize',
|
||||
'log-out': 'logOut',
|
||||
shortcut: 'showShortcut',
|
||||
'tab-devtools': 'openDevTools'
|
||||
};
|
||||
|
||||
for (const key in webviewListeners) {
|
||||
ipcRenderer.on(key, () => {
|
||||
const activeWebview = this.tabs[this.activeTabIndex].webview;
|
||||
if (activeWebview) {
|
||||
activeWebview[webviewListeners[key]]();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ipcRenderer.on('open-settings', (event, settingNav) => {
|
||||
this.openSettings(settingNav);
|
||||
});
|
||||
|
||||
ipcRenderer.on('open-about', this.openAbout.bind(this));
|
||||
|
||||
ipcRenderer.on('reload-viewer', this.reloadView.bind(this));
|
||||
|
||||
ipcRenderer.on('hard-reload', () => {
|
||||
ipcRenderer.send('reload-full-app');
|
||||
});
|
||||
|
||||
ipcRenderer.on('switch-server-tab', (event, index) => {
|
||||
this.activateTab(index);
|
||||
});
|
||||
|
||||
ipcRenderer.on('reload-proxy', (event, showAlert) => {
|
||||
this.loadProxy().then(() => {
|
||||
if (showAlert) {
|
||||
alert('Proxy settings saved!');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggle-sidebar', (event, show) => {
|
||||
this.toggleSidebar(show);
|
||||
});
|
||||
|
||||
ipcRenderer.on('enter-fullscreen', () => {
|
||||
this.$fullscreenPopup.classList.add('show');
|
||||
this.$fullscreenPopup.classList.remove('hidden');
|
||||
});
|
||||
|
||||
ipcRenderer.on('leave-fullscreen', () => {
|
||||
this.$fullscreenPopup.classList.remove('show');
|
||||
});
|
||||
|
||||
ipcRenderer.on('render-taskbar-icon', (event, messageCount) => {
|
||||
// Create a canvas from unread messagecounts
|
||||
function createOverlayIcon(messageCount) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = 128;
|
||||
canvas.width = 128;
|
||||
canvas.style.letterSpacing = '-5px';
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#f42020';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = 'white';
|
||||
if (messageCount > 99) {
|
||||
ctx.font = '65px Helvetica';
|
||||
ctx.fillText('99+', 64, 85);
|
||||
} else if (messageCount < 10) {
|
||||
ctx.font = '90px Helvetica';
|
||||
ctx.fillText(String(Math.min(99, messageCount)), 64, 96);
|
||||
} else {
|
||||
ctx.font = '85px Helvetica';
|
||||
ctx.fillText(String(Math.min(99, messageCount)), 64, 90);
|
||||
}
|
||||
return canvas;
|
||||
}
|
||||
ipcRenderer.send('update-taskbar-icon', createOverlayIcon(messageCount).toDataURL(), String(messageCount));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
const serverManagerView = new ServerManagerView();
|
||||
serverManagerView.init();
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
serverManagerView.reloadView();
|
||||
});
|
||||
};
|
||||
22
app/renderer/js/notification.js
Normal file
22
app/renderer/js/notification.js
Normal file
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const {remote} = require('electron');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/utils/config-util.js');
|
||||
|
||||
const app = remote.app;
|
||||
|
||||
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
|
||||
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
|
||||
app.setAppUserModelId('org.zulip.zulip-electron');
|
||||
|
||||
const NativeNotification = window.Notification;
|
||||
|
||||
class SilentNotification extends NativeNotification {
|
||||
constructor(title, opts) {
|
||||
opts.silent = ConfigUtil.getConfigItem('silent') || false;
|
||||
super(title, opts);
|
||||
}
|
||||
}
|
||||
|
||||
window.Notification = SilentNotification;
|
||||
20
app/renderer/js/pages/network.js
Normal file
20
app/renderer/js/pages/network.js
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
class NetworkTroubleshootingView {
|
||||
constructor() {
|
||||
this.$reconnectButton = document.getElementById('reconnect');
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$reconnectButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
const networkTroubleshootingView = new NetworkTroubleshootingView();
|
||||
networkTroubleshootingView.init();
|
||||
};
|
||||
63
app/renderer/js/pages/preference/badge-settings.js
Normal file
63
app/renderer/js/pages/preference/badge-settings.js
Normal file
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
const electron = require('electron');
|
||||
const { app } = require('electron');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
|
||||
|
||||
let instance = null;
|
||||
|
||||
class BadgeSettings {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
showBadgeCount(messageCount, mainWindow) {
|
||||
if (process.platform === 'darwin') {
|
||||
app.setBadgeCount(messageCount);
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
this.updateOverlayIcon(messageCount, mainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
hideBadgeCount(mainWindow) {
|
||||
if (process.platform === 'darwin') {
|
||||
app.setBadgeCount(0);
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
}
|
||||
}
|
||||
|
||||
updateBadge(badgeCount, mainWindow) {
|
||||
if (ConfigUtil.getConfigItem('badgeOption', true)) {
|
||||
this.showBadgeCount(badgeCount, mainWindow);
|
||||
} else {
|
||||
this.hideBadgeCount(mainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
updateOverlayIcon(messageCount, mainWindow) {
|
||||
if (!mainWindow.isFocused()) {
|
||||
mainWindow.flashFrame(true);
|
||||
}
|
||||
if (messageCount === 0) {
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
} else {
|
||||
mainWindow.webContents.send('render-taskbar-icon', messageCount);
|
||||
}
|
||||
}
|
||||
|
||||
updateTaskbarIcon(data, text, mainWindow) {
|
||||
const img = electron.nativeImage.createFromDataURL(data);
|
||||
mainWindow.setOverlayIcon(img, text);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new BadgeSettings();
|
||||
40
app/renderer/js/pages/preference/base-section.js
Normal file
40
app/renderer/js/pages/preference/base-section.js
Normal file
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
const BaseComponent = require(__dirname + '/../../components/base.js');
|
||||
|
||||
class BaseSection extends BaseComponent {
|
||||
generateSettingOption(props) {
|
||||
const {$element, value, clickHandler} = props;
|
||||
|
||||
$element.innerHTML = '';
|
||||
|
||||
const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value));
|
||||
$element.appendChild($optionControl);
|
||||
|
||||
$optionControl.addEventListener('click', clickHandler);
|
||||
}
|
||||
|
||||
generateOptionTemplate(settingOption) {
|
||||
if (settingOption) {
|
||||
return `
|
||||
<div class="action green">
|
||||
<span>On</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="action red">
|
||||
<span>Off</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
reloadApp() {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseSection;
|
||||
144
app/renderer/js/pages/preference/general-section.js
Normal file
144
app/renderer/js/pages/preference/general-section.js
Normal file
@@ -0,0 +1,144 @@
|
||||
'use strict';
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
const BaseSection = require(__dirname + '/base-section.js');
|
||||
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
|
||||
|
||||
class GeneralSection extends BaseSection {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-pane">
|
||||
<div class="title">Appearance</div>
|
||||
<div id="appearance-option-settings" class="settings-card">
|
||||
<div class="setting-row" id="tray-option">
|
||||
<div class="setting-description">Show app icon in system tray</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="sidebar-option">
|
||||
<div class="setting-description">Show sidebar (<span class="code">CmdOrCtrl+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>
|
||||
<div class="title">Desktop Notification</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="silent-option">
|
||||
<div class="setting-description">Mute all sounds from Zulip (requires reload)</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">App Updates</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="betaupdate-option">
|
||||
<div class="setting-description">Get beta updates</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">Functionality</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="startAtLogin-option">
|
||||
<div class="setting-description">Start app at login</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.updateTrayOption();
|
||||
this.updateBadgeOption();
|
||||
this.updateUpdateOption();
|
||||
this.updateSilentOption();
|
||||
this.updateSidebarOption();
|
||||
this.updateStartAtLoginOption();
|
||||
}
|
||||
|
||||
updateTrayOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#tray-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('trayIcon', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('trayIcon');
|
||||
ConfigUtil.setConfigItem('trayIcon', newValue);
|
||||
ipcRenderer.send('forward-message', 'toggletray');
|
||||
this.updateTrayOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateBadgeOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#badge-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('badgeOption', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('badgeOption');
|
||||
ConfigUtil.setConfigItem('badgeOption', newValue);
|
||||
ipcRenderer.send('toggle-badge-option', newValue);
|
||||
this.updateBadgeOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateUpdateOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#betaupdate-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('betaUpdate', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
|
||||
ConfigUtil.setConfigItem('betaUpdate', newValue);
|
||||
this.updateUpdateOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSilentOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#silent-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('silent', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('silent', true);
|
||||
ConfigUtil.setConfigItem('silent', newValue);
|
||||
this.updateSilentOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSidebarOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#sidebar-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('showSidebar', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('showSidebar');
|
||||
ConfigUtil.setConfigItem('showSidebar', newValue);
|
||||
ipcRenderer.send('forward-message', 'toggle-sidebar', newValue);
|
||||
this.updateSidebarOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateStartAtLoginOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#startAtLogin-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('startAtLogin', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('startAtLogin');
|
||||
ConfigUtil.setConfigItem('startAtLogin', newValue);
|
||||
ipcRenderer.send('toggleAutoLauncher', newValue);
|
||||
this.updateStartAtLoginOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = GeneralSection;
|
||||
67
app/renderer/js/pages/preference/nav.js
Normal file
67
app/renderer/js/pages/preference/nav.js
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
const BaseComponent = require(__dirname + '/../../components/base.js');
|
||||
|
||||
class PreferenceNav extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
|
||||
this.props = props;
|
||||
|
||||
this.navItems = ['General', 'Network', 'Servers'];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
template() {
|
||||
let navItemsTemplate = '';
|
||||
for (const navItem of this.navItems) {
|
||||
navItemsTemplate += `<div class="nav" id="nav-${navItem}">${navItem}</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div>
|
||||
<div id="settings-header">Settings</div>
|
||||
<div id="nav-container">${navItemsTemplate}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$el = this.generateNodeFromTemplate(this.template());
|
||||
this.props.$root.appendChild(this.$el);
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
for (const navItem of this.navItems) {
|
||||
const $item = document.getElementById(`nav-${navItem}`);
|
||||
$item.addEventListener('click', () => {
|
||||
this.props.onItemSelected(navItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
select(navItemToSelect) {
|
||||
for (const navItem of this.navItems) {
|
||||
if (navItem === navItemToSelect) {
|
||||
this.activate(navItem);
|
||||
} else {
|
||||
this.deactivate(navItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activate(navItem) {
|
||||
const $item = document.getElementById(`nav-${navItem}`);
|
||||
$item.classList.add('active');
|
||||
}
|
||||
|
||||
deactivate(navItem) {
|
||||
const $item = document.getElementById(`nav-${navItem}`);
|
||||
$item.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PreferenceNav;
|
||||
102
app/renderer/js/pages/preference/network-section.js
Normal file
102
app/renderer/js/pages/preference/network-section.js
Normal file
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
const BaseSection = require(__dirname + '/base-section.js');
|
||||
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
|
||||
|
||||
class NetworkSection extends BaseSection {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-pane">
|
||||
<div class="title">Proxy</div>
|
||||
<div id="appearance-option-settings" class="settings-card">
|
||||
<div class="setting-row" id="use-proxy-option">
|
||||
<div class="setting-description">Connect servers through a proxy</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-block">
|
||||
<div class="setting-row" id="proxy-pac-option">
|
||||
<span class="setting-input-key">PAC script</span>
|
||||
<input class="setting-input-value" placeholder="e.g. foobar.com/pacfile.js"/>
|
||||
</div>
|
||||
<div class="setting-row" id="proxy-rules-option">
|
||||
<span class="setting-input-key">Proxy rules</span>
|
||||
<input class="setting-input-value" placeholder="e.g. http=foopy:80;ftp=foopy2"/>
|
||||
</div>
|
||||
<div class="setting-row" id="proxy-bypass-option">
|
||||
<span class="setting-input-key">Proxy bypass rules</span>
|
||||
<input class="setting-input-value" placeholder="e.g. foobar.com"/>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="action green" id="proxy-save-action">
|
||||
<i class="material-icons">check_box</i>
|
||||
<span>Save</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.$proxyPAC = document.querySelector('#proxy-pac-option .setting-input-value');
|
||||
this.$proxyRules = document.querySelector('#proxy-rules-option .setting-input-value');
|
||||
this.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value');
|
||||
this.$proxySaveAction = document.getElementById('proxy-save-action');
|
||||
this.$settingBlock = this.props.$root.querySelector('.setting-block');
|
||||
this.initProxyOption();
|
||||
|
||||
this.$proxyPAC.value = ConfigUtil.getConfigItem('proxyPAC', '');
|
||||
this.$proxyRules.value = ConfigUtil.getConfigItem('proxyRules', '');
|
||||
this.$proxyBypass.value = ConfigUtil.getConfigItem('proxyBypass', '');
|
||||
|
||||
this.$proxySaveAction.addEventListener('click', () => {
|
||||
ConfigUtil.setConfigItem('proxyPAC', this.$proxyPAC.value);
|
||||
ConfigUtil.setConfigItem('proxyRules', this.$proxyRules.value);
|
||||
ConfigUtil.setConfigItem('proxyBypass', this.$proxyBypass.value);
|
||||
|
||||
ipcRenderer.send('forward-message', 'reload-proxy', true);
|
||||
});
|
||||
}
|
||||
|
||||
initProxyOption() {
|
||||
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
|
||||
this.toggleProxySettings(proxyEnabled);
|
||||
this.updateProxyOption();
|
||||
}
|
||||
|
||||
toggleProxySettings(option) {
|
||||
if (option) {
|
||||
this.$settingBlock.classList.remove('hidden');
|
||||
} else {
|
||||
this.$settingBlock.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
updateProxyOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#use-proxy-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('useProxy', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('useProxy');
|
||||
ConfigUtil.setConfigItem('useProxy', newValue);
|
||||
this.toggleProxySettings(newValue);
|
||||
if (newValue === false) {
|
||||
// Reload proxy if the proxy is turned off
|
||||
ipcRenderer.send('forward-message', 'reload-proxy', false);
|
||||
}
|
||||
this.updateProxyOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NetworkSection;
|
||||
68
app/renderer/js/pages/preference/new-server-form.js
Normal file
68
app/renderer/js/pages/preference/new-server-form.js
Normal file
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
const BaseComponent = require(__dirname + '/../../components/base.js');
|
||||
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
|
||||
|
||||
class NewServerForm extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-card">
|
||||
<div class="server-info-right">
|
||||
<div class="server-info-row">
|
||||
<input class="setting-input-value" autofocus placeholder="Entert the URL of your Zulip organization..."/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<div class="action green server-save-action">
|
||||
<i class="material-icons">check_box</i>
|
||||
<span>Save</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initForm();
|
||||
this.initActions();
|
||||
}
|
||||
|
||||
initForm() {
|
||||
this.$newServerForm = this.generateNodeFromTemplate(this.template());
|
||||
this.$saveServerButton = this.$newServerForm.getElementsByClassName('server-save-action')[0];
|
||||
this.props.$root.innerHTML = '';
|
||||
this.props.$root.appendChild(this.$newServerForm);
|
||||
|
||||
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0];
|
||||
}
|
||||
|
||||
submitFormHandler() {
|
||||
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
|
||||
DomainUtil.addDomain(serverConf).then(() => {
|
||||
this.props.onChange(this.props.index);
|
||||
});
|
||||
}, errorMessage => {
|
||||
alert(errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
initActions() {
|
||||
this.$saveServerButton.addEventListener('click', () => {
|
||||
this.submitFormHandler();
|
||||
});
|
||||
this.$newServerUrl.addEventListener('keypress', event => {
|
||||
const EnterkeyCode = event.keyCode;
|
||||
// Submit form when Enter key is pressed
|
||||
if (EnterkeyCode === 13) {
|
||||
this.submitFormHandler();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NewServerForm;
|
||||
75
app/renderer/js/pages/preference/preference.js
Normal file
75
app/renderer/js/pages/preference/preference.js
Normal file
@@ -0,0 +1,75 @@
|
||||
'use strict';
|
||||
|
||||
const BaseComponent = require(__dirname + '/js/components/base.js');
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
const Nav = require(__dirname + '/js/pages/preference/nav.js');
|
||||
const ServersSection = require(__dirname + '/js/pages/preference/servers-section.js');
|
||||
const GeneralSection = require(__dirname + '/js/pages/preference/general-section.js');
|
||||
const NetworkSection = require(__dirname + '/js/pages/preference/network-section.js');
|
||||
|
||||
class PreferenceView extends BaseComponent {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.$sidebarContainer = document.getElementById('sidebar');
|
||||
this.$settingsContainer = document.getElementById('settings-container');
|
||||
}
|
||||
|
||||
init() {
|
||||
this.nav = new Nav({
|
||||
$root: this.$sidebarContainer,
|
||||
onItemSelected: this.handleNavigation.bind(this)
|
||||
});
|
||||
|
||||
this.setDefaultView();
|
||||
this.registerIpcs();
|
||||
}
|
||||
|
||||
setDefaultView() {
|
||||
let nav = 'General';
|
||||
const hasTag = window.location.hash;
|
||||
if (hasTag) {
|
||||
nav = hasTag.substring(1);
|
||||
}
|
||||
this.handleNavigation(nav);
|
||||
}
|
||||
|
||||
handleNavigation(navItem) {
|
||||
this.nav.select(navItem);
|
||||
switch (navItem) {
|
||||
case 'Servers': {
|
||||
this.section = new ServersSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'General': {
|
||||
this.section = new GeneralSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'Network': {
|
||||
this.section = new NetworkSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
this.section.init();
|
||||
window.location.hash = `#${navItem}`;
|
||||
}
|
||||
|
||||
registerIpcs() {
|
||||
ipcRenderer.on('switch-settings-nav', (event, navItem) => {
|
||||
this.handleNavigation(navItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
const preferenceView = new PreferenceView();
|
||||
preferenceView.init();
|
||||
};
|
||||
77
app/renderer/js/pages/preference/server-info-form.js
Normal file
77
app/renderer/js/pages/preference/server-info-form.js
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
const {dialog} = require('electron').remote;
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
const BaseComponent = require(__dirname + '/../../components/base.js');
|
||||
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
|
||||
|
||||
class ServerInfoForm extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-card">
|
||||
<div class="server-info-left">
|
||||
<img class="server-info-icon" src="${this.props.server.icon}"/>
|
||||
</div>
|
||||
<div class="server-info-right">
|
||||
<div class="server-info-row">
|
||||
<span class="server-info-alias">${this.props.server.alias}</span>
|
||||
<i class="material-icons open-tab-button">open_in_new</i>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<input class="setting-input-value" disabled value="${this.props.server.url}"/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<div class="action red server-delete-action">
|
||||
<i class="material-icons">indeterminate_check_box</i>
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initForm();
|
||||
this.initActions();
|
||||
}
|
||||
|
||||
initForm() {
|
||||
this.$serverInfoForm = this.generateNodeFromTemplate(this.template());
|
||||
this.$serverInfoAlias = this.$serverInfoForm.getElementsByClassName('server-info-alias')[0];
|
||||
this.$deleteServerButton = this.$serverInfoForm.getElementsByClassName('server-delete-action')[0];
|
||||
this.$openServerButton = this.$serverInfoForm.getElementsByClassName('open-tab-button')[0];
|
||||
this.props.$root.appendChild(this.$serverInfoForm);
|
||||
}
|
||||
|
||||
initActions() {
|
||||
this.$deleteServerButton.addEventListener('click', () => {
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['YES', 'NO'],
|
||||
defaultId: 0,
|
||||
message: 'Are you sure you want to delete this server?'
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
DomainUtil.removeDomain(this.props.index);
|
||||
this.props.onChange(this.props.index);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.$openServerButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
|
||||
});
|
||||
|
||||
this.$serverInfoAlias.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServerInfoForm;
|
||||
62
app/renderer/js/pages/preference/servers-section.js
Normal file
62
app/renderer/js/pages/preference/servers-section.js
Normal file
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
const BaseSection = require(__dirname + '/base-section.js');
|
||||
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
|
||||
const ServerInfoForm = require(__dirname + '/server-info-form.js');
|
||||
const NewServerForm = require(__dirname + '/new-server-form.js');
|
||||
|
||||
class ServersSection extends BaseSection {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-pane" id="server-settings-pane">
|
||||
<div class="title">Add Server</div>
|
||||
<div id="new-server-container"></div>
|
||||
<div class="title" id="existing-servers"></div>
|
||||
<div id="server-info-container"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initServers();
|
||||
}
|
||||
|
||||
initServers() {
|
||||
this.props.$root.innerHTML = '';
|
||||
|
||||
const servers = DomainUtil.getDomains();
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.$serverInfoContainer = document.getElementById('server-info-container');
|
||||
this.$existingServers = document.getElementById('existing-servers');
|
||||
this.$newServerContainer = document.getElementById('new-server-container');
|
||||
this.$newServerButton = document.getElementById('new-server-action');
|
||||
|
||||
this.$serverInfoContainer.innerHTML = servers.length ? '' : 'Add your first server to get started!';
|
||||
// Show Existing servers if servers are there otherwise hide it
|
||||
this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Existing Servers';
|
||||
this.initNewServerForm();
|
||||
|
||||
for (let i = 0; i < servers.length; i++) {
|
||||
new ServerInfoForm({
|
||||
$root: this.$serverInfoContainer,
|
||||
server: servers[i],
|
||||
index: i,
|
||||
onChange: this.reloadApp
|
||||
}).init();
|
||||
}
|
||||
}
|
||||
|
||||
initNewServerForm() {
|
||||
new NewServerForm({
|
||||
$root: this.$newServerContainer,
|
||||
onChange: this.reloadApp
|
||||
}).init();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServersSection;
|
||||
@@ -1,69 +0,0 @@
|
||||
'use strict';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const {remote} = require('electron');
|
||||
|
||||
const prefWindow = remote.getCurrentWindow();
|
||||
|
||||
document.getElementById('close-button').addEventListener('click', () => {
|
||||
prefWindow.close();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape' || event.keyCode === 27) {
|
||||
prefWindow.close();
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
window.prefDomain = function () {
|
||||
const request = require('request');
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
const JsonDB = require('node-json-db');
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const {app} = require('electron').remote;
|
||||
|
||||
const db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
|
||||
|
||||
let newDomain = document.getElementById('url').value;
|
||||
newDomain = newDomain.replace(/^https?:\/\//, '');
|
||||
newDomain = newDomain.replace(/^http?:\/\//, '');
|
||||
|
||||
if (newDomain === '') {
|
||||
document.getElementById('urladded').innerHTML = 'Please input a value';
|
||||
} else {
|
||||
document.getElementById('main').innerHTML = 'Checking...';
|
||||
if (newDomain.indexOf('localhost:') >= 0) {
|
||||
const domain = 'http://' + newDomain;
|
||||
const checkDomain = domain + '/static/audio/zulip.ogg';
|
||||
request(checkDomain, (error, response) => {
|
||||
if (!error && response.statusCode !== 404) {
|
||||
document.getElementById('main').innerHTML = 'Switch';
|
||||
document.getElementById('urladded').innerHTML = 'Switched to ' + newDomain;
|
||||
db.push('/domain', domain);
|
||||
ipcRenderer.send('new-domain', domain);
|
||||
} else {
|
||||
document.getElementById('main').innerHTML = 'Switch';
|
||||
document.getElementById('urladded').innerHTML = 'Not a valid Zulip Local Server.';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const domain = 'https://' + newDomain;
|
||||
const checkDomain = domain + '/static/audio/zulip.ogg';
|
||||
request(checkDomain, (error, response) => {
|
||||
if (!error && response.statusCode !== 404) {
|
||||
document.getElementById('main').innerHTML = 'Switch';
|
||||
document.getElementById('urladded').innerHTML = 'Switched to ' + newDomain;
|
||||
db.push('/domain', domain);
|
||||
ipcRenderer.send('new-domain', domain);
|
||||
} else if (error.toString().indexOf('Error: self signed certificate') >= 0) {
|
||||
document.getElementById('main').innerHTML = 'Switch';
|
||||
ipcRenderer.send('certificate-err', domain);
|
||||
document.getElementById('urladded').innerHTML = 'Switched to ' + newDomain;
|
||||
} else {
|
||||
document.getElementById('main').innerHTML = 'Switch';
|
||||
document.getElementById('urladded').innerHTML = 'Not a valid Zulip Server.';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,56 +1,19 @@
|
||||
'use strict';
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
const {webFrame} = require('electron');
|
||||
const {spellChecker} = require('./spellchecker');
|
||||
|
||||
const _setImmediate = setImmediate;
|
||||
const _clearImmediate = clearImmediate;
|
||||
process.once('loaded', () => {
|
||||
global.setImmediate = _setImmediate;
|
||||
global.clearImmediate = _clearImmediate;
|
||||
});
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const { spellChecker } = require('./spellchecker');
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
require('./domain');
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
require('./tray.js');
|
||||
// Calling Tray.js in renderer process everytime app window loads
|
||||
require('./notification');
|
||||
|
||||
// Handle zooming functionality
|
||||
const zoomIn = () => {
|
||||
webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
|
||||
};
|
||||
|
||||
const zoomActualSize = () => {
|
||||
webFrame.setZoomFactor(1);
|
||||
};
|
||||
|
||||
// Get zooming actions from main process
|
||||
ipcRenderer.on('zoomIn', () => {
|
||||
zoomIn();
|
||||
});
|
||||
|
||||
ipcRenderer.on('zoomOut', () => {
|
||||
zoomOut();
|
||||
});
|
||||
|
||||
ipcRenderer.on('zoomActualSize', () => {
|
||||
zoomActualSize();
|
||||
});
|
||||
|
||||
ipcRenderer.on('log-out', () => {
|
||||
const logout = () => {
|
||||
// Create the menu for the below
|
||||
document.querySelector('.dropdown-toggle').click();
|
||||
|
||||
const nodes = document.querySelectorAll('.dropdown-menu li:last-child a');
|
||||
nodes[nodes.length - 1].click();
|
||||
});
|
||||
};
|
||||
|
||||
ipcRenderer.on('shortcut', () => {
|
||||
const shortcut = () => {
|
||||
// Create the menu for the below
|
||||
const node = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
|
||||
// Additional check
|
||||
@@ -60,10 +23,23 @@ ipcRenderer.on('shortcut', () => {
|
||||
// Atleast click the dropdown
|
||||
document.querySelector('.dropdown-toggle').click();
|
||||
}
|
||||
};
|
||||
|
||||
process.once('loaded', () => {
|
||||
global.logout = logout;
|
||||
global.shortcut = shortcut;
|
||||
});
|
||||
|
||||
// To prevent failing this script on linux we need to load it after the document loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Init spellchecker
|
||||
spellChecker();
|
||||
|
||||
// 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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const {SpellCheckHandler, ContextMenuListener, ContextMenuBuilder} = require('electron-spellchecker');
|
||||
|
||||
function spellChecker() {
|
||||
|
||||
@@ -5,10 +5,12 @@ const electron = require('electron');
|
||||
|
||||
const {ipcRenderer, remote} = electron;
|
||||
|
||||
const {Tray, Menu, nativeImage} = remote;
|
||||
const {Tray, Menu, nativeImage, BrowserWindow} = remote;
|
||||
|
||||
const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/utils/config-util.js');
|
||||
|
||||
const iconPath = () => {
|
||||
if (process.platform === 'linux') {
|
||||
return APP_ICON + 'linux.png';
|
||||
@@ -45,7 +47,8 @@ const config = {
|
||||
|
||||
const renderCanvas = function (arg) {
|
||||
config.unreadCount = arg;
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
return new Promise(resolve => {
|
||||
const SIZE = config.size * config.pixelRatio;
|
||||
const PADDING = SIZE * 0.05;
|
||||
const CENTER = SIZE / 2;
|
||||
@@ -83,8 +86,6 @@ const renderCanvas = function (arg) {
|
||||
}
|
||||
|
||||
resolve(canvas);
|
||||
} else {
|
||||
reject(canvas);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -102,31 +103,43 @@ const renderNativeImage = function (arg) {
|
||||
});
|
||||
};
|
||||
|
||||
function sendAction(action) {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
win.restore();
|
||||
}
|
||||
|
||||
win.webContents.send(action);
|
||||
}
|
||||
|
||||
const createTray = function () {
|
||||
window.tray = new Tray(iconPath());
|
||||
const contextMenu = Menu.buildFromTemplate([{
|
||||
label: 'About',
|
||||
click() {
|
||||
ipcRenderer.send('trayabout');
|
||||
// We need to focus the main window first
|
||||
ipcRenderer.send('focus-app');
|
||||
sendAction('open-about');
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Change Zulip server',
|
||||
label: 'Focus',
|
||||
click() {
|
||||
ipcRenderer.send('traychangeserver');
|
||||
ipcRenderer.send('focus-app');
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
label: 'Settings',
|
||||
click() {
|
||||
remote.getCurrentWindow().reload();
|
||||
window.tray.destroy();
|
||||
ipcRenderer.send('focus-app');
|
||||
sendAction('open-settings');
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -135,14 +148,24 @@ const createTray = function () {
|
||||
{
|
||||
label: 'Quit',
|
||||
click() {
|
||||
remote.getCurrentWindow().close();
|
||||
ipcRenderer.send('quit-app');
|
||||
}
|
||||
}
|
||||
]);
|
||||
window.tray.setContextMenu(contextMenu);
|
||||
window.tray.on('click', () => {
|
||||
// Click event only works on Windows
|
||||
if (process.platform === 'win32') {
|
||||
ipcRenderer.send('toggle-app');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ipcRenderer.on('destroytray', event => {
|
||||
if (!window.tray) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.tray.destroy();
|
||||
if (window.tray.isDestroyed()) {
|
||||
window.tray = null;
|
||||
@@ -154,35 +177,47 @@ ipcRenderer.on('destroytray', event => {
|
||||
});
|
||||
|
||||
ipcRenderer.on('tray', (event, arg) => {
|
||||
if (arg === 0) {
|
||||
unread = arg;
|
||||
// Message Count // console.log("message count is zero.");
|
||||
window.tray.setImage(iconPath());
|
||||
window.tray.setToolTip('No unread messages');
|
||||
} else {
|
||||
unread = arg;
|
||||
renderNativeImage(arg).then(image => {
|
||||
window.tray.setImage(image);
|
||||
window.tray.setToolTip(arg + ' unread messages');
|
||||
});
|
||||
if (!window.tray) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggletray', event => {
|
||||
if (event) {
|
||||
if (window.tray) {
|
||||
window.tray.destroy();
|
||||
if (window.tray.isDestroyed()) {
|
||||
window.tray = null;
|
||||
}
|
||||
// We don't want to create tray from unread messages on windows and macOS since these systems already have dock badges and taskbar overlay icon.
|
||||
if (process.platform === 'linux') {
|
||||
if (arg === 0) {
|
||||
unread = arg;
|
||||
// Message Count // console.log("message count is zero.");
|
||||
window.tray.setImage(iconPath());
|
||||
window.tray.setToolTip('No unread messages');
|
||||
} else {
|
||||
createTray();
|
||||
renderNativeImage(unread).then(image => {
|
||||
unread = arg;
|
||||
renderNativeImage(arg).then(image => {
|
||||
window.tray.setImage(image);
|
||||
window.tray.setToolTip(unread + ' unread messages');
|
||||
window.tray.setToolTip(arg + ' unread messages');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
createTray();
|
||||
function toggleTray() {
|
||||
if (window.tray) {
|
||||
window.tray.destroy();
|
||||
if (window.tray.isDestroyed()) {
|
||||
window.tray = null;
|
||||
}
|
||||
ConfigUtil.setConfigItem('trayIcon', false);
|
||||
} else {
|
||||
createTray();
|
||||
if (process.platform === 'linux') {
|
||||
renderNativeImage(unread).then(image => {
|
||||
window.tray.setImage(image);
|
||||
window.tray.setToolTip(unread + ' unread messages');
|
||||
});
|
||||
}
|
||||
ConfigUtil.setConfigItem('trayIcon', true);
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.on('toggletray', toggleTray);
|
||||
|
||||
if (ConfigUtil.getConfigItem('trayIcon', true)) {
|
||||
createTray();
|
||||
}
|
||||
|
||||
54
app/renderer/js/utils/config-util.js
Normal file
54
app/renderer/js/utils/config-util.js
Normal file
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
const process = require('process');
|
||||
const JsonDB = require('node-json-db');
|
||||
|
||||
let instance = null;
|
||||
let app = null;
|
||||
|
||||
/* To make the util runnable in both main and renderer process */
|
||||
if (process.type === 'renderer') {
|
||||
app = require('electron').remote.app;
|
||||
} else {
|
||||
app = require('electron').app;
|
||||
}
|
||||
|
||||
class ConfigUtil {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
this.reloadDB();
|
||||
return instance;
|
||||
}
|
||||
|
||||
getConfigItem(key, defaultValue = null) {
|
||||
this.reloadDB();
|
||||
const value = this.db.getData('/')[key];
|
||||
if (value === undefined) {
|
||||
this.setConfigItem(key, defaultValue);
|
||||
return defaultValue;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
setConfigItem(key, value) {
|
||||
this.db.push(`/${key}`, value, true);
|
||||
this.reloadDB();
|
||||
}
|
||||
|
||||
removeConfigItem(key) {
|
||||
this.db.delete(`/${key}`);
|
||||
this.reloadDB();
|
||||
}
|
||||
|
||||
reloadDB() {
|
||||
this.db = new JsonDB(app.getPath('userData') + '/settings.json', true, true);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ConfigUtil();
|
||||
250
app/renderer/js/utils/domain-util.js
Normal file
250
app/renderer/js/utils/domain-util.js
Normal file
@@ -0,0 +1,250 @@
|
||||
'use strict';
|
||||
|
||||
const { app, dialog } = require('electron').remote;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const JsonDB = require('node-json-db');
|
||||
const request = require('request');
|
||||
|
||||
let instance = null;
|
||||
|
||||
const defaultIconUrl = '../renderer/img/icon.png';
|
||||
|
||||
class DomainUtil {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
this.reloadDB();
|
||||
// Migrate from old schema
|
||||
if (this.db.getData('/').domain) {
|
||||
this.addDomain({
|
||||
alias: 'Zulip',
|
||||
url: this.db.getData('/domain')
|
||||
});
|
||||
this.db.delete('/domain');
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
getDomains() {
|
||||
this.reloadDB();
|
||||
if (this.db.getData('/').domains === undefined) {
|
||||
return [];
|
||||
} else {
|
||||
return this.db.getData('/domains');
|
||||
}
|
||||
}
|
||||
|
||||
getDomain(index) {
|
||||
this.reloadDB();
|
||||
return this.db.getData(`/domains[${index}]`);
|
||||
}
|
||||
|
||||
updateDomain(index, server) {
|
||||
this.reloadDB();
|
||||
this.db.push(`/domains[${index}]`, server, true);
|
||||
}
|
||||
|
||||
addDomain(server) {
|
||||
return new Promise(resolve => {
|
||||
if (server.icon) {
|
||||
this.saveServerIcon(server.icon).then(localIconUrl => {
|
||||
server.icon = localIconUrl;
|
||||
this.db.push('/domains[]', server, true);
|
||||
this.reloadDB();
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
server.icon = defaultIconUrl;
|
||||
this.db.push('/domains[]', server, true);
|
||||
this.reloadDB();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeDomains() {
|
||||
this.db.delete('/domains');
|
||||
this.reloadDB();
|
||||
}
|
||||
|
||||
removeDomain(index) {
|
||||
this.db.delete(`/domains[${index}]`);
|
||||
this.reloadDB();
|
||||
}
|
||||
|
||||
// Check if domain is already added
|
||||
duplicateDomain(domain) {
|
||||
domain = this.formatUrl(domain);
|
||||
const servers = this.getDomains();
|
||||
for (const i in servers) {
|
||||
if (servers[i].url === domain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
checkDomain(domain, silent = false) {
|
||||
if (!silent && this.duplicateDomain(domain)) {
|
||||
// Do not check duplicate in silent mode
|
||||
alert('This server has been added.');
|
||||
return;
|
||||
}
|
||||
|
||||
domain = this.formatUrl(domain);
|
||||
|
||||
const checkDomain = domain + '/static/audio/zulip.ogg';
|
||||
|
||||
const serverConf = {
|
||||
icon: defaultIconUrl,
|
||||
url: domain,
|
||||
alias: domain
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(checkDomain, (error, response) => {
|
||||
const certsError =
|
||||
['Error: self signed certificate',
|
||||
'Error: unable to verify the first certificate'
|
||||
];
|
||||
if (!error && response.statusCode !== 404) {
|
||||
// Correct
|
||||
this.getServerSettings(domain).then(serverSettings => {
|
||||
resolve(serverSettings);
|
||||
}, () => {
|
||||
resolve(serverConf);
|
||||
});
|
||||
} else if (certsError.indexOf(error.toString()) >= 0) {
|
||||
if (silent) {
|
||||
this.getServerSettings(domain).then(serverSettings => {
|
||||
resolve(serverSettings);
|
||||
}, () => {
|
||||
resolve(serverConf);
|
||||
});
|
||||
} else {
|
||||
const certErrorMessage = `Do you trust certificate from ${domain}? \n ${error}`;
|
||||
const certErrorDetail = `The server you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
|
||||
\n Unless you have a good reason to believe otherwise, you should not proceed.
|
||||
\n You can click here if you'd like to proceed with the connection.`;
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['Yes', 'No'],
|
||||
defaultId: 0,
|
||||
message: certErrorMessage,
|
||||
detail: certErrorDetail
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
this.getServerSettings(domain).then(serverSettings => {
|
||||
resolve(serverSettings);
|
||||
}, () => {
|
||||
resolve(serverConf);
|
||||
});
|
||||
} else {
|
||||
reject('Untrusted Certificate.');
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject('Not a valid Zulip server');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getServerSettings(domain) {
|
||||
const serverSettingsUrl = domain + '/api/v1/server_settings';
|
||||
return new Promise((resolve, reject) => {
|
||||
request(serverSettingsUrl, (error, response) => {
|
||||
if (!error && response.statusCode === 200) {
|
||||
const data = JSON.parse(response.body);
|
||||
if (data.hasOwnProperty('realm_icon') && data.realm_icon) {
|
||||
resolve({
|
||||
icon: data.realm_uri + data.realm_icon,
|
||||
url: data.realm_uri,
|
||||
alias: data.realm_name
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject('Zulip server version < 1.6.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
saveServerIcon(url) {
|
||||
// The save will always succeed. If url is invalid, downgrade to default icon.
|
||||
return new Promise(resolve => {
|
||||
const filePath = this.generateFilePath(url);
|
||||
const file = fs.createWriteStream(filePath);
|
||||
try {
|
||||
request(url).on('response', response => {
|
||||
response.on('error', err => {
|
||||
console.log(err);
|
||||
resolve(defaultIconUrl);
|
||||
});
|
||||
response.pipe(file).on('finish', () => {
|
||||
resolve(filePath);
|
||||
});
|
||||
}).on('error', err => {
|
||||
console.log(err);
|
||||
resolve(defaultIconUrl);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
resolve(defaultIconUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSavedServer(url, index) {
|
||||
// Does not promise successful update
|
||||
this.checkDomain(url, true).then(newServerConf => {
|
||||
this.saveServerIcon(newServerConf.icon).then(localIconUrl => {
|
||||
newServerConf.icon = localIconUrl;
|
||||
this.updateDomain(index, newServerConf);
|
||||
this.reloadDB();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
reloadDB() {
|
||||
this.db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
|
||||
}
|
||||
|
||||
generateFilePath(url) {
|
||||
const dir = `${app.getPath('userData')}/server-icons`;
|
||||
const extension = path.extname(url).split('?')[0];
|
||||
|
||||
let hash = 5381;
|
||||
let len = url.length;
|
||||
|
||||
while (len) {
|
||||
hash = (hash * 33) ^ url.charCodeAt(--len);
|
||||
}
|
||||
|
||||
// Create 'server-icons' directory if not existed
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir);
|
||||
}
|
||||
|
||||
return `${dir}/${hash >>> 0}${extension}`;
|
||||
}
|
||||
|
||||
formatUrl(domain) {
|
||||
const hasPrefix = (domain.indexOf('http') === 0);
|
||||
if (hasPrefix) {
|
||||
return domain;
|
||||
} else {
|
||||
return (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new DomainUtil();
|
||||
26
app/renderer/js/utils/link-util.js
Normal file
26
app/renderer/js/utils/link-util.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const wurl = require('wurl');
|
||||
|
||||
let instance = null;
|
||||
|
||||
class LinkUtil {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
isInternal(currentUrl, newUrl) {
|
||||
const currentDomain = wurl('hostname', currentUrl);
|
||||
const newDomain = wurl('hostname', newUrl);
|
||||
|
||||
return (currentDomain === newDomain) && newUrl.includes('/#narrow');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new LinkUtil();
|
||||
55
app/renderer/js/utils/system-util.js
Normal file
55
app/renderer/js/utils/system-util.js
Normal file
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
const {app} = require('electron').remote;
|
||||
|
||||
const os = require('os');
|
||||
|
||||
let instance = null;
|
||||
|
||||
class SystemUtil {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
this.connectivityERR = [
|
||||
'ERR_INTERNET_DISCONNECTED',
|
||||
'ERR_PROXY_CONNECTION_FAILED',
|
||||
'ERR_CONNECTION_RESET',
|
||||
'ERR_NOT_CONNECTED',
|
||||
'ERR_NAME_NOT_RESOLVED',
|
||||
'ERR_NETWORK_CHANGED'
|
||||
];
|
||||
this.userAgent = null;
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
getOS() {
|
||||
if (os.platform() === 'darwin') {
|
||||
return 'Mac';
|
||||
}
|
||||
if (os.platform() === 'linux') {
|
||||
return 'Linux';
|
||||
}
|
||||
if (os.platform() === 'win32' || os.platform() === 'win64') {
|
||||
if (parseFloat(os.release()) < 6.2) {
|
||||
return 'Windows 7';
|
||||
} else {
|
||||
return 'Windows 10';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setUserAgent(webViewUserAgent) {
|
||||
this.userAgent = 'ZulipElectron/' + app.getVersion() + ' ' + webViewUserAgent;
|
||||
}
|
||||
|
||||
getUserAgent() {
|
||||
return this.userAgent;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SystemUtil();
|
||||
43
app/renderer/main.html
Normal file
43
app/renderer/main.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="responsive desktop">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip</title>
|
||||
<link rel="stylesheet" href="css/main.css" type="text/css" media="screen">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="content">
|
||||
<div class="popup">
|
||||
<span class="popuptext hidden" id="fullscreen-popup"></span>
|
||||
</div>
|
||||
<div id="sidebar">
|
||||
<div id="view-controls-container">
|
||||
<div id="tabs-container"></div>
|
||||
<div id="add-tab" class="tab functional-tab">
|
||||
<div class="server-tab" id="add-action">
|
||||
<i class="material-icons">add</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="actions-container">
|
||||
<div class="action-button" id="reload-action">
|
||||
<i class="material-icons md-48">refresh</i>
|
||||
<span id="reload-tooltip" style="display:none">Reload</span>
|
||||
</div>
|
||||
<div class="action-button" id="settings-action">
|
||||
<i class="material-icons md-48">settings</i>
|
||||
<span id="setting-tooltip" style="display:none">Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-container">
|
||||
<div id="webviews-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
</html>
|
||||
21
app/renderer/network.html
Normal file
21
app/renderer/network.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="responsive desktop">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip - Network Troubleshooting</title>
|
||||
<link rel="stylesheet" href="css/network.css" type="text/css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div id="picture"><img src="img/zulip_network.png"></div>
|
||||
<div id="title">Zulip can't connect</div>
|
||||
<div id="description">
|
||||
<div>Your computer seems to be offline.</div>
|
||||
<div>We will keep trying to reconnect, or you can try now.</div>
|
||||
</div>
|
||||
<div id="reconnect">Try now</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/pages/network.js"></script>
|
||||
</html>
|
||||
@@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="css/pref.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="close" id="close-button">Close</div>
|
||||
<div class="form">
|
||||
<form onsubmit="prefDomain(); return false">
|
||||
<input id="url" type="text" placeholder="Server URL">
|
||||
<button type="submit" id="main" value="Submit">
|
||||
Switch</button>
|
||||
</form>
|
||||
<p id="urladded"><p>
|
||||
</div>
|
||||
<script src="js/pref.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
16
app/renderer/preference.html
Normal file
16
app/renderer/preference.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="responsive desktop">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip - Settings</title>
|
||||
<link rel="stylesheet" href="css/preference.css" type="text/css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div id="sidebar"></div>
|
||||
<div id="settings-container"></div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/pages/preference/preference.js"></script>
|
||||
</html>
|
||||
16
build/entitlements.mas.plist
Normal file
16
build/entitlements.mas.plist
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
84
development.md
Normal file
84
development.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Development setup
|
||||
|
||||
This is a guide to running the Zulip desktop app from a source tree,
|
||||
in order to contribute to developing it.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To build and run the app from source, you'll need the following:
|
||||
|
||||
* [Git](http://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [Node.js](https://nodejs.org) >= v6.9.0
|
||||
* [NPM](https://www.npmjs.com/get-npm) and
|
||||
[node-gyp](https://github.com/nodejs/node-gyp#installation),
|
||||
if they don't come bundled with your Node.js installation
|
||||
* [Python](https://www.python.org/downloads/release/python-2713/)
|
||||
(v2.7.x recommended)
|
||||
* A C++ compiler compatible with C++11
|
||||
* Development headers for the libXext, libXtst, and libxkbfile libraries
|
||||
|
||||
### Debian/Ubuntu and friends
|
||||
|
||||
On a system running Debian, Ubuntu, or another Debian-based Linux
|
||||
distribution, you can install all dependencies through the package
|
||||
manager (see [here][nodesource-install] for more on the first command):
|
||||
|
||||
```sh
|
||||
$ curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
|
||||
$ sudo apt install git nodejs python build-essential libxext-dev libxtst-dev libxkbfile-dev
|
||||
```
|
||||
|
||||
[nodesource-install]: https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions
|
||||
|
||||
### Other OSes
|
||||
|
||||
Other developers run the app on Windows, macOS, and possibly other OSes.
|
||||
PRs to add specific instructions to this doc are welcome!
|
||||
|
||||
On Windows, your C++ compiler should be Visual Studio 2015 or later.
|
||||
|
||||
## Download, build, and run
|
||||
|
||||
Clone the source locally:
|
||||
```sh
|
||||
$ git clone https://github.com/zulip/zulip-electron
|
||||
$ cd zulip-electron
|
||||
```
|
||||
|
||||
Install project dependencies:
|
||||
```sh
|
||||
$ npm install
|
||||
```
|
||||
|
||||
Start the app:
|
||||
```sh
|
||||
$ npm start
|
||||
```
|
||||
|
||||
Start and watch changes:
|
||||
```sh
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you have any problems running the app, see the [most common
|
||||
issues](./troubleshooting.md).
|
||||
|
||||
## Making a release
|
||||
|
||||
To package the app into an installer:
|
||||
```
|
||||
npm run dist
|
||||
```
|
||||
|
||||
This command will produce distributable packages or installers for the
|
||||
operating system you're running on:
|
||||
* on Windows, a Windows installer file
|
||||
* on macOS, a `.dmg` file
|
||||
* on Linux, a plain `.zip` file as well as a `.deb` file and an
|
||||
`AppImage` file.
|
||||
To generate all three types, you will need all three operating
|
||||
systems.
|
||||
|
||||
The output files appear in the `dist/` directory.
|
||||
2
docs/Home.md
Normal file
2
docs/Home.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Installation instructions
|
||||
* [[Windows]]
|
||||
1
docs/Windows.md
Normal file
1
docs/Windows.md
Normal file
@@ -0,0 +1 @@
|
||||
** Windows Set up instructions **
|
||||
3
docs/_Footer.md
Normal file
3
docs/_Footer.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Want to contribute to this Wiki?
|
||||
|
||||
[Edit `/docs` files and send a pull request.](https://github.com/zulip/zulip-electron/tree/master/docs)
|
||||
26
docs/desktop-release.md
Normal file
26
docs/desktop-release.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# New release checklist -
|
||||
|
||||
## We need to cross check following things before pushing a new release + after updating electron version. This is just to make sure that nothing gets broken.
|
||||
## - Desktop notifications
|
||||
## - Spellchecker
|
||||
## - Auto updates
|
||||
**Check for the logs in -**
|
||||
- **on Linux:** `~/.config/Zulip/log.log`
|
||||
- **on OS X:** `~/Library/Logs/Zulip/log.log`
|
||||
- **on Windows:** `%USERPROFILE%\AppData\Roaming\Zulip\log.log`
|
||||
## - All the installer i.e.
|
||||
- Linux (.deb, AppImage)
|
||||
- Mac - (.dmg)
|
||||
- Windows - (web installer for 32/64bit)
|
||||
## - Check for errors in console (if any)
|
||||
## - Code signing verification on Mac and Windows
|
||||
## - Tray and menu options
|
||||
# We need to cross check all these things on -
|
||||
- Windows 7
|
||||
- Windows 8
|
||||
- Windows 10
|
||||
- Ubuntu 14.04/16.04
|
||||
- macOSX
|
||||
|
||||
|
||||
|
||||
105
help.md
Normal file
105
help.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# User Guide
|
||||
|
||||
> Welcome! This guide will walk you through the basics of using Zulip Desktop.
|
||||
|
||||
|
||||
## Get Zulip Desktop
|
||||
|
||||
|
||||
|
||||
## Connect to a Server
|
||||
|
||||
### Connect through a proxy
|
||||
|
||||
It's possible to connect to your server through a proxy.
|
||||
You can enter the proxy settings in the `Network` section of App Settings.
|
||||
There are three fields provided:
|
||||
* `PAC script` - The URL associated with the PAC file.
|
||||
* `Proxy rules` - Rules indicating which proxies to use.
|
||||
* `Proxy bypass rules` - Rules indicating which URLs should
|
||||
bypass the proxy settings.
|
||||
|
||||
For a typical setup where internet access is required to use an HTTP proxy,
|
||||
but URLs on the local network should be accessed directly, configure as follows:
|
||||
|
||||
`Proxy rules = proxy.example.com`
|
||||
|
||||
Your HTTP proxy server
|
||||
`Proxy bypass rules = *.example.com;10.0.0.0/8`
|
||||
|
||||
Directly connect to your own domain and private IP subnet
|
||||
for more complex setups, read below to configure complex proxy rules and proxy bypass rules.
|
||||
|
||||
### Sets the proxy settings.
|
||||
|
||||
When `PAC script` and `Proxy rules` are provided together, the `Proxy rules`
|
||||
option is ignored and `PAC script` configuration is applied.
|
||||
|
||||
The `Proxy rules` has to follow the rules below:
|
||||
|
||||
```
|
||||
proxyRules = schemeProxies[";"<schemeProxies>]
|
||||
schemeProxies = [<urlScheme>"="]<proxyURIList>
|
||||
urlScheme = "http" | "https" | "ftp" | "socks"
|
||||
proxyURIList = <proxyURL>[","<proxyURIList>]
|
||||
proxyURL = [<proxyScheme>"://"]<proxyHost>[":"<proxyPort>]
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
* `http=foopy:80;ftp=foopy2` - Use HTTP proxy `foopy:80` for `http://` URLs, and
|
||||
HTTP proxy `foopy2:80` for `ftp://` URLs.
|
||||
* `foopy:80` - Use HTTP proxy `foopy:80` for all URLs.
|
||||
* `foopy:80,bar,direct://` - Use HTTP proxy `foopy:80` for all URLs, failing
|
||||
over to `bar` if `foopy:80` is unavailable, and after that using no proxy.
|
||||
* `socks4://foopy` - Use SOCKS v4 proxy `foopy:1080` for all URLs.
|
||||
* `http=foopy,socks5://bar.com` - Use HTTP proxy `foopy` for http URLs, and fail
|
||||
over to the SOCKS5 proxy `bar.com` if `foopy` is unavailable.
|
||||
* `http=foopy,direct://` - Use HTTP proxy `foopy` for http URLs, and use no
|
||||
proxy if `foopy` is unavailable.
|
||||
* `http=foopy;socks=foopy2` - Use HTTP proxy `foopy` for http URLs, and use
|
||||
`socks4://foopy2` for all other URLs.
|
||||
|
||||
The `Proxy bypass rules` is a comma separated list of rules described below:
|
||||
|
||||
* `[ URL_SCHEME "://" ] HOSTNAME_PATTERN [ ":" <port> ]`
|
||||
|
||||
Match all hostnames that match the pattern HOSTNAME_PATTERN.
|
||||
|
||||
Examples:
|
||||
"foobar.com", "*foobar.com", "*.foobar.com", "*foobar.com:99",
|
||||
"https://x.*.y.com:99"
|
||||
|
||||
* `"." HOSTNAME_SUFFIX_PATTERN [ ":" PORT ]`
|
||||
|
||||
Match a particular domain suffix.
|
||||
|
||||
Examples:
|
||||
".google.com", ".com", "http://.google.com"
|
||||
|
||||
* `[ SCHEME "://" ] IP_LITERAL [ ":" PORT ]`
|
||||
|
||||
Match URLs which are IP address literals.
|
||||
|
||||
Examples:
|
||||
"127.0.1", "[0:0::1]", "[::1]", "http://[::1]:99"
|
||||
|
||||
* `IP_LITERAL "/" PREFIX_LENGHT_IN_BITS`
|
||||
|
||||
Match any URL that is to an IP literal that falls between the
|
||||
given range. IP range is specified using CIDR notation.
|
||||
|
||||
Examples:
|
||||
"192.168.1.1/16", "fefe:13::abc/33".
|
||||
|
||||
* `<local>`
|
||||
|
||||
Match local addresses. The meaning of `<local>` is whether the
|
||||
host matches one of: "127.0.0.1", "::1", "localhost".
|
||||
|
||||
|
||||
## Change App Preferences
|
||||
|
||||
|
||||
|
||||
## Reporting an Issue
|
||||
68
how-to-install.md
Normal file
68
how-to-install.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# How to install
|
||||
|
||||
**Note:** If you download from the [releases page](https://github.com/zulip/zulip-electron/releases), be careful what version you pick. Releases that end with `-beta` are beta releases and the rest are stable.
|
||||
- **beta:** these releases are the right balance between getting new features early while staying away from nasty bugs.
|
||||
- **stable:** these releases are more thoroughly tested; they receive new features later, but there's a lower chance that things will go wrong.
|
||||
|
||||
[LR]: https://github.com/zulip/zulip-electron/releases
|
||||
|
||||
## OS X
|
||||
|
||||
**DMG or zip**:
|
||||
|
||||
1. Download [Zulip-x.x.x.dmg][LR] or [Zulip-x.x.x-mac.zip][LR]
|
||||
2. Open or unzip the file and drag the app into the `Applications` folder
|
||||
3. Done! The app will update automatically
|
||||
|
||||
**Using brew**:
|
||||
|
||||
1. Run `brew cask install zulip` in your terminal
|
||||
2. The app will be installed in your `Applications`
|
||||
3. Done! The app will update automatically (you can also use `brew update && brew upgrade zulip`)
|
||||
|
||||
## Windows
|
||||
|
||||
**Installer (recommended)**:
|
||||
|
||||
1. Download [Zulip-Web-Setup-x.x.x.exe][LR]
|
||||
2. Run the installer, wait until it finishes
|
||||
3. Done! The app will update automatically
|
||||
|
||||
**Portable**:
|
||||
|
||||
1. Download [zulip-x.x.x-arch.nsis.7z][LR] [*here arch = ia32 (32-bit), x64 (64-bit)*]
|
||||
2. Extract the zip wherever you want (e.g. a flash drive) and run the app from there
|
||||
|
||||
## Linux
|
||||
|
||||
**Ubuntu, Debian 8+ (deb package)**:
|
||||
|
||||
1. Download [Zulip-x.x.x-amd64.deb][LR]
|
||||
2. Double click and install, or run `dpkg -i Zulip-x.x.x-amd64.deb` in the terminal
|
||||
3. Start the app with your app launcher or by running `zulip` in a terminal
|
||||
4. Done! The app will NOT update automatically, but you can still check for updates
|
||||
|
||||
**Other distros (Fedora, CentOS, Arch Linux etc)** :
|
||||
1. Download Zulip-x.x.x-x86_64.AppImage[LR]
|
||||
2. Make it executable using chmod a+x Zulip-x.x.x-x86_64.AppImage
|
||||
3. Start the app with your app launcher
|
||||
|
||||
**You can also use `apt-get` (recommended)**:
|
||||
|
||||
* First download our signing key to make sure the deb you download is correct:
|
||||
|
||||
```
|
||||
sudo apt-key adv --keyserver pool.sks-keyservers.net --recv 69AD12704E71A4803DCA3A682424BE5AE9BD10D9
|
||||
```
|
||||
|
||||
* Add the repo to your apt source list :
|
||||
```
|
||||
echo "deb https://dl.bintray.com/zulip/debian/ beta main" |
|
||||
sudo tee -a /etc/apt/sources.list.d/zulip.list
|
||||
```
|
||||
|
||||
* Now install the client :
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install zulip
|
||||
```
|
||||
74
package.json
74
package.json
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "zulip",
|
||||
"productName": "Zulip",
|
||||
"version": "0.5.10",
|
||||
"version": "1.4.0",
|
||||
"main": "./app/main",
|
||||
"description": "Zulip Desktop App",
|
||||
"license": "Apache-2.0",
|
||||
"email": "<svnitakash@gmail.com>",
|
||||
"copyright": "©2017 Kandra Labs, Inc.",
|
||||
"author": {
|
||||
"name": "Akash Nimare",
|
||||
"name": "Kandra Labs, Inc.",
|
||||
"email": "svnitakash@gmail.com"
|
||||
},
|
||||
"repository": {
|
||||
@@ -19,20 +19,24 @@
|
||||
"url": "https://github.com/zulip/zulip-electron/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "electron ./app/main",
|
||||
"postinstall": "install-app-deps",
|
||||
"test": "gulp test && xo",
|
||||
"start": "electron app --disable-http-cache",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"test": "xo",
|
||||
"dev": "gulp dev",
|
||||
"pack": "build --dir",
|
||||
"dist": "build",
|
||||
"build:win": "build --win nsis-web --ia32 --x64",
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "electron-builder",
|
||||
"mas": "electron-builder --mac mas",
|
||||
"travis": "cd ./scripts && ./travis-build-test.sh"
|
||||
},
|
||||
"pre-commit": [
|
||||
"test"
|
||||
],
|
||||
"build": {
|
||||
"appId": "org.zulip.zulip-electron",
|
||||
"asar": true,
|
||||
"files": [
|
||||
"**/*",
|
||||
"!docs${/*}",
|
||||
"!node_modules/@paulcbetts/cld/deps/cld${/*}"
|
||||
],
|
||||
"copyright": "©2017 Kandra Labs, Inc.",
|
||||
@@ -40,11 +44,12 @@
|
||||
"category": "public.app-category.productivity"
|
||||
},
|
||||
"linux": {
|
||||
"category": "",
|
||||
"category": "Chat;GNOME;GTK;Network;InstantMessaging",
|
||||
"packageCategory": "GNOME;GTK;Network;InstantMessaging",
|
||||
"description": "Zulip Desktop Client for Linux",
|
||||
"target": [
|
||||
"deb",
|
||||
"zip",
|
||||
"AppImage"
|
||||
],
|
||||
"maintainer": "Akash Nimare <svnitakash@gmail.com>"
|
||||
@@ -71,12 +76,22 @@
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "build/icon.ico"
|
||||
},
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis-web",
|
||||
"arch": [
|
||||
"x64",
|
||||
"ia32"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "build/icon.ico",
|
||||
"publisherName": "Kandra Labs, Inc."
|
||||
},
|
||||
"nsis": {
|
||||
"perMachine": true,
|
||||
"oneClick": false
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
@@ -90,29 +105,44 @@
|
||||
"devDependencies": {
|
||||
"assert": "1.4.1",
|
||||
"devtron": "1.4.0",
|
||||
"electron-builder": "16.6.0",
|
||||
"electron": "1.4.15",
|
||||
"electron-connect": "0.4.8",
|
||||
"electron-builder": "19.27.3",
|
||||
"electron": "1.6.11",
|
||||
"electron-connect": "0.6.2",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-mocha": "3.0.1",
|
||||
"spectron": "3.6.1",
|
||||
"xo": "0.18.1"
|
||||
"gulp-mocha": "4.3.1",
|
||||
"chai-as-promised": "7.1.1",
|
||||
"chai": "4.1.1",
|
||||
"spectron": "3.7.2",
|
||||
"xo": "0.18.2",
|
||||
"pre-commit": "1.2.2"
|
||||
},
|
||||
"xo": {
|
||||
"parserOptions": {
|
||||
"sourceType": "script",
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
},
|
||||
"esnext": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "app/main/*.js",
|
||||
"files": "app*/**/*.js",
|
||||
"rules": {
|
||||
"max-lines": [
|
||||
"warn",
|
||||
500
|
||||
],
|
||||
"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
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"no-prototype-builtins": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -125,4 +155,4 @@
|
||||
"mocha"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
const assert = require('assert')
|
||||
const Application = require('spectron').Application
|
||||
const chai = require('chai')
|
||||
const { expect } = chai
|
||||
const chaiAsPromised = require('chai-as-promised')
|
||||
|
||||
chai.should()
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
describe('application launch', function () {
|
||||
this.timeout(15000)
|
||||
@@ -7,11 +13,15 @@ describe('application launch', function () {
|
||||
beforeEach(function () {
|
||||
this.app = new Application({
|
||||
path: require('electron'),
|
||||
args: [__dirname + '/../app/renderer/index.html']
|
||||
args: [__dirname + '/../app/renderer/main.html']
|
||||
})
|
||||
return this.app.start()
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
chaiAsPromised.transferPromiseness = this.app.transferPromiseness
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
if (this.app && this.app.isRunning()) {
|
||||
return this.app.stop()
|
||||
@@ -19,9 +29,53 @@ describe('application launch', function () {
|
||||
})
|
||||
|
||||
it('shows an initial window', function () {
|
||||
return this.app.client.getWindowCount().then(function (count) {
|
||||
assert.equal(count, 1)
|
||||
})
|
||||
return this.app.client.waitUntilWindowLoaded(5000)
|
||||
.getWindowCount().should.eventually.equal(2)
|
||||
.browserWindow.isMinimized().should.eventually.be.false
|
||||
.browserWindow.isDevToolsOpened().should.eventually.be.false
|
||||
.browserWindow.isVisible().should.eventually.be.true
|
||||
.browserWindow.isFocused().should.eventually.be.true
|
||||
.browserWindow.getBounds().should.eventually.have.property('width').and.be.above(0)
|
||||
.browserWindow.getBounds().should.eventually.have.property('height').and.be.above(0)
|
||||
})
|
||||
|
||||
it('sets up a default organization', function () {
|
||||
let app = this.app
|
||||
let self = this
|
||||
app.client.execute(() => {
|
||||
window.confirm = function () { return true }
|
||||
})
|
||||
|
||||
function createOrg (client, name, url, winIndex) {
|
||||
return client
|
||||
// Focus on settings webview
|
||||
.then(switchToWebviewAtIndex.bind(null, self.app.client, winIndex))
|
||||
.pause(1000) // wait for settings to load
|
||||
|
||||
// Fill settings form
|
||||
.click('#new-server-action')
|
||||
.setValue('input[id="server-info-name"]', name)
|
||||
.setValue('input[id="server-info-url"]', url)
|
||||
.click('#save-server-action')
|
||||
.pause(500) // Need to pause while server verification takes place
|
||||
.then(() => app.browserWindow.reload())
|
||||
.pause(1500) // Wait for webview of org to load
|
||||
}
|
||||
|
||||
function switchToWebviewAtIndex(client, index) {
|
||||
return client
|
||||
.windowHandles()
|
||||
.then(function (session) {
|
||||
this.window(session.value[index])
|
||||
})
|
||||
}
|
||||
|
||||
return this.app.client.waitUntilWindowLoaded(5000)
|
||||
.then(() => createOrg(self.app.client, 'Zulip 1', 'chat.zulip.org', 1))
|
||||
.then(switchToWebviewAtIndex.bind(null, self.app.client, 0))
|
||||
.click('#add-action > i').pause(500)
|
||||
.then(switchToWebviewAtIndex.bind(null, self.app.client, 2))
|
||||
.then(() => createOrg(self.app.client, 'Zulip 2', 'chat.zulip.org', 2))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
11
troubleshooting.md
Normal file
11
troubleshooting.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Troubleshooting
|
||||
|
||||
* App icon will only show in the release version. The dev version will use the Electron icon
|
||||
* If you see issue, try deleting `node_modules` and `npm install`
|
||||
* Electron is more or less Chrome, you can get developer tools using `CMD+ALT+I`
|
||||
|
||||
### Error : ChecksumMismatchError
|
||||
- Try deleteing `node_modules` && `app/node_modules` directories. Re-install dependencies using `npm install`.
|
||||
|
||||
### Error : Module version mismatch. Expected 50, got 51
|
||||
- Make sure you have installed [node-gyp](https://github.com/nodejs/node-gyp#installation) dependencies properly.
|
||||
@@ -103,7 +103,20 @@ cleanUp()
|
||||
|
||||
# }}}
|
||||
|
||||
# this function is called when user hits Ctrl-C
|
||||
catchControl_c () {
|
||||
echo -en "\n## Ctrl-C caught; Quitting \n"
|
||||
# exit shell script
|
||||
exit $?;
|
||||
}
|
||||
|
||||
|
||||
|
||||
envSetup $*
|
||||
gitCheckout
|
||||
npmInstallStart
|
||||
cleanUp
|
||||
|
||||
# initialise trap to call catchControl_c function and trap keyboard interrupt (control-c)
|
||||
trap catchControl_c SIGINT
|
||||
sleep 1000
|
||||
Reference in New Issue
Block a user