Compare commits
618 Commits
v0.5.8
...
v1.6.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
dcd2abca6e | ||
|
2fb9efb981 | ||
|
7245b6a110 | ||
|
bcb8ffb55f | ||
|
77094596a5 | ||
|
06ad44bdd7 | ||
|
e719ba139c | ||
|
9853e9226c | ||
|
f2c76b5ca3 | ||
|
e6dbff995b | ||
|
4578d4a5f7 | ||
|
4b895a2312 | ||
|
53c0428a3a | ||
|
0a1866abb5 | ||
|
ce862a4890 | ||
|
1b1ad2cd61 | ||
|
ead7a06308 | ||
|
6659dd5097 | ||
|
ed1f0f6d5b | ||
|
79acf8a6e1 | ||
|
8e0033f03e | ||
|
9144c2630d | ||
|
fae05fc3b1 | ||
|
73603a4fd2 | ||
|
a498ffc7d6 | ||
|
7afcf13401 | ||
|
89a292559d | ||
|
be14517caf | ||
|
3b6c5ae532 | ||
|
40e3ed0f2f | ||
|
5d988858b0 | ||
|
3a974136a3 | ||
|
6ed5a5309c | ||
|
80c37fabb8 | ||
|
79366e19df | ||
|
f409bb0449 | ||
|
45bdde951f | ||
|
6b627780f0 | ||
|
6f67553da5 | ||
|
2e710a9322 | ||
|
91f3afa8fe | ||
|
f784345495 | ||
|
67da435154 | ||
|
c89733610d | ||
|
8f272a67b5 | ||
|
f6c4a76138 | ||
|
b90a4c5254 | ||
|
a06e09e565 | ||
|
ad5bef821e | ||
|
58bbd7bf30 | ||
|
90d080dc96 | ||
|
ad3fcf585e | ||
|
4b8f216bab | ||
|
e620e0c428 | ||
|
50b3151b5d | ||
|
0c32756485 | ||
|
0c0835e364 | ||
|
9e962a5c44 | ||
|
a218f7ea64 | ||
|
13a7f7475a | ||
|
48b17a1549 | ||
|
653598fd9e | ||
|
ddbc282f49 | ||
|
992d92b06d | ||
|
45867ef15e | ||
|
6572c90d49 | ||
|
1ed0011c88 | ||
|
6dd79b205c | ||
|
538c18fa90 | ||
|
29e347c511 | ||
|
ad37a5e0a6 | ||
|
352b775e27 | ||
|
38cec25680 | ||
|
f77ab92202 | ||
|
e843a29316 | ||
|
85837242e7 | ||
|
9f6da5712e | ||
|
158685a869 | ||
|
288b1cb3f2 | ||
|
e24a966d48 | ||
|
306feb2eff | ||
|
f426c932b0 | ||
|
26172d8508 | ||
|
99da0a338f | ||
|
9599249b31 | ||
|
4bdd2564b7 | ||
|
4dcf22a53c | ||
|
0dc97648a0 | ||
|
787f097cf3 | ||
|
5481d55c66 | ||
|
2a052b2c38 | ||
|
3ed253d2e1 | ||
|
5f672fe404 | ||
|
10372787ac | ||
|
bcabb615b4 | ||
|
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 | ||
|
3dade768a7 | ||
|
7b3c7ba5fa | ||
|
616bc0f73b | ||
|
8ecdf1f18a | ||
|
d2daa65059 | ||
|
487ee538e3 | ||
|
6382c6d2b3 | ||
|
cbcff67d28 | ||
|
61a429365b | ||
|
e55f38a962 | ||
|
81798583ae | ||
|
9cab61cebc | ||
|
f290732cb6 | ||
|
2dd44852fa | ||
|
bd2f17deec | ||
|
06faf46bcc | ||
|
468e9d539b | ||
|
449f407236 | ||
|
7efe90e709 | ||
|
184e1a5bc4 | ||
|
85e7b337a7 | ||
|
83759bde1c | ||
|
d9b1d45e0e | ||
|
df91c20f36 | ||
|
c1f6159d69 | ||
|
5a0461211a | ||
|
1394f790c3 | ||
|
4d374ff40c | ||
|
0d15435408 | ||
|
d4448ba086 | ||
|
4bd6fde5b6 | ||
|
e5097ace06 | ||
|
ca078cbbfd | ||
|
6d45105b69 | ||
|
40f81af2dd | ||
|
d5e6184e75 | ||
|
0ec38ba41d | ||
|
460a64710a | ||
|
9ec62a748f | ||
|
a7a80cef99 | ||
|
bcaf54b349 | ||
|
1f6d0762bb | ||
|
1aa1655676 | ||
|
0ad66399d0 | ||
|
3a6bb14224 | ||
|
53eb8051ad | ||
|
ed9174f57c | ||
|
260d6a1906 | ||
|
dc53319c8e | ||
|
77369536b3 | ||
|
531afcb1e5 | ||
|
d6c4eeccf8 | ||
|
496b906fd0 | ||
|
1be29faea6 | ||
|
bc9a7c9890 | ||
|
5745276dbb | ||
|
acf0282aa0 | ||
|
54d942178a | ||
|
d84ada373e | ||
|
d4d36d0582 | ||
|
63e6c634b9 | ||
|
4bc558cdbc | ||
|
34293fd66b | ||
|
5eba4b8200 | ||
|
a949307820 | ||
|
a714977b5a | ||
|
bfcaa51c46 | ||
|
d7d3017bc1 | ||
|
c7ce8fb7c8 | ||
|
65a80de01d | ||
|
f6bf210451 | ||
|
8c23ec3417 | ||
|
97bbd809f7 | ||
|
76381cac08 | ||
|
13c4ceedc2 | ||
|
f0889edf9c | ||
|
ab8367c946 | ||
|
c5887c8f71 | ||
|
7b7ab03d0b | ||
|
394caa7934 | ||
|
db2860b53e | ||
|
6d20df3557 | ||
|
2942cd1244 | ||
|
174049f489 | ||
|
21eae28999 | ||
|
f256cbcd5d | ||
|
ff8c20f0b4 | ||
|
9e0c17a793 | ||
|
b4fb00aa52 | ||
|
13d0b5e51c | ||
|
dc15e4578c | ||
|
673da66ee9 | ||
|
e397e9bfb4 | ||
|
4eca2e9254 | ||
|
a1407826b6 | ||
|
6d8f83798b | ||
|
bfc03c7e95 | ||
|
06737ce629 | ||
|
6b09840347 | ||
|
19b5eecdcd | ||
|
f443918433 | ||
|
bb74e58d63 | ||
|
612e670bb5 | ||
|
34bb55cb9f | ||
|
1457e82649 | ||
|
0bdeaaba18 |
29
.gitignore
vendored
@@ -1,9 +1,28 @@
|
||||
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
|
||||
.python-version
|
||||
|
||||
|
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
6.9.4
|
16
.travis.yml
@@ -5,6 +5,14 @@ os:
|
||||
- osx
|
||||
- linux
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- build-essential
|
||||
- libxext-dev
|
||||
- libxtst-dev
|
||||
- libxkbfile-dev
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- '6'
|
||||
@@ -17,8 +25,12 @@ cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- app/node_modules
|
||||
- $HOME/.electron
|
||||
- $HOME/.cache
|
||||
|
||||
script:
|
||||
- npm run travis
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://zulip.org/zulipbot/travis
|
||||
on_success: always
|
||||
on_failure: always
|
@@ -1,37 +1,47 @@
|
||||
#Contributing Guidelines
|
||||
# Contributing Guidelines
|
||||
|
||||
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 judgment 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 setting up a development environment, setting up with the Zulip webapp project, and testing, can be read [here](https://zulip.readthedocs.io).
|
||||
|
||||
* If you have any questions regarding zulip-electron, open an [issue](https://github.com/zulip/zulip-electron/issues/new/) or ask it on [chat.zulip.org](https://chat.zulip.org/#narrow/stream/electron).
|
||||
|
||||
## 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 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.
|
||||
|
79
README.md
@@ -1,80 +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
|
||||
* npm >= 3.10.3
|
||||
* python (v2.7.x recommended)
|
||||
* If you're on Debian or Ubuntu, you'll need to install following packages:
|
||||
```sh
|
||||
$ sudo apt-get install nodejs-legacy build-essential libxext-dev libxtst-dev libxkbfile-dev
|
||||
```
|
||||
# Download
|
||||
Please see [installation guide](https://zulipchat.com/help/desktop-app-install-guide).
|
||||
|
||||
## 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,46 +1,58 @@
|
||||
'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
|
||||
if (isDev) {
|
||||
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
|
||||
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';
|
||||
// });
|
||||
// }
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
// init for updates
|
||||
// Init for updates
|
||||
autoUpdater.checkForUpdates();
|
||||
}
|
||||
|
||||
exports = module.exports = {
|
||||
module.exports = {
|
||||
appUpdater
|
||||
};
|
||||
|
16
app/main/crash-reporter.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
const { crashReporter } = require('electron');
|
||||
|
||||
const crashHandler = () => {
|
||||
crashReporter.start({
|
||||
productName: 'zulip-electron',
|
||||
companyName: 'Kandra Labs, Inc.',
|
||||
submitURL: 'https://zulip-sentry.herokuapp.com/crashreport',
|
||||
autoSubmit: true
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
crashHandler
|
||||
};
|
@@ -1,36 +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);
|
||||
const data = db.getData('/');
|
||||
|
||||
console.log(data.domain);
|
||||
|
||||
// if (data.domain && window.location.href.indexOf(data.domain) === -1) {
|
||||
// window.location.href = data.domain
|
||||
// }
|
||||
// require('electron-connect').client.create();
|
||||
|
||||
window.addDomain = function () {
|
||||
document.getElementById('main').innerHTML = 'checking...';
|
||||
|
||||
let newDomain = document.getElementById('url').value;
|
||||
newDomain = newDomain.replace(/^https?:\/\//, '');
|
||||
|
||||
const domain = 'https://' + 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 {
|
||||
document.getElementById('main').innerHTML = 'Connect';
|
||||
document.getElementById('server-status').innerHTML = 'Not a valid Zulip Server.';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -1,39 +1,32 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const electron = require('electron');
|
||||
const {app} = require('electron');
|
||||
const ipc = require('electron').ipcMain;
|
||||
const electronLocalshortcut = require('electron-localshortcut');
|
||||
const Configstore = require('configstore');
|
||||
const JsonDB = require('node-json-db');
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
const isDev = require('electron-is-dev');
|
||||
const tray = require('./tray');
|
||||
const appMenu = require('./menu');
|
||||
const {linkIsInternal, skipImages} = require('./link-helper');
|
||||
const {appUpdater} = require('./autoupdater');
|
||||
const { appUpdater } = require('./autoupdater');
|
||||
const { crashHandler } = require('./crash-reporter');
|
||||
|
||||
const db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
|
||||
const data = db.getData('/');
|
||||
const { setAutoLaunch } = require('./startup');
|
||||
|
||||
// adds debug features like hotkeys for triggering dev tools and reload
|
||||
require('electron-debug')();
|
||||
const { app, ipcMain } = electron;
|
||||
|
||||
const conf = new Configstore('Zulip-Desktop');
|
||||
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js');
|
||||
|
||||
// prevent window being garbage collected
|
||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||
// in development mode
|
||||
if (isDev) {
|
||||
require('electron-debug')();
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
||||
const mainURL = 'file://' + path.join(__dirname, '../renderer', 'main.html');
|
||||
|
||||
const isAlreadyRunning = app.makeSingleInstance(() => {
|
||||
if (mainWindow) {
|
||||
@@ -46,18 +39,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');
|
||||
@@ -66,99 +48,86 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1000,
|
||||
defaultHeight: 600
|
||||
});
|
||||
|
||||
// Let's keep the window position global so that we can access it in other process
|
||||
global.mainWindowState = mainWindowState;
|
||||
|
||||
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, '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();
|
||||
});
|
||||
|
||||
win.loadURL(mainURL);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
win.loadURL(targetURL());
|
||||
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('leave-full-screen', () => {
|
||||
win.webContents.send('leave-fullscreen');
|
||||
});
|
||||
|
||||
win.on('resize', function () {
|
||||
const size = this.getSize();
|
||||
conf.set({
|
||||
width: size[0],
|
||||
height: size[1]
|
||||
});
|
||||
// To destroy tray icon when navigate to a new URL
|
||||
win.webContents.on('will-navigate', e => {
|
||||
if (e) {
|
||||
win.webContents.send('destroytray');
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
// 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');
|
||||
|
||||
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();
|
||||
}
|
||||
// eslint-disable-next-line max-params
|
||||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
@@ -168,67 +137,93 @@ app.on('activate', () => {
|
||||
});
|
||||
|
||||
app.on('ready', () => {
|
||||
electron.Menu.setApplicationMenu(appMenu);
|
||||
appMenu.setMenu({
|
||||
tabs: []
|
||||
});
|
||||
mainWindow = createMainWindow();
|
||||
tray.create(mainWindow);
|
||||
|
||||
const page = mainWindow.webContents;
|
||||
|
||||
// TODO - use global shortcut instead
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
|
||||
mainWindow.reload();
|
||||
});
|
||||
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => {
|
||||
if (page.canGoBack()) {
|
||||
page.goBack();
|
||||
}
|
||||
});
|
||||
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+]', () => {
|
||||
if (page.canGoForward()) {
|
||||
page.goForward();
|
||||
}
|
||||
});
|
||||
|
||||
page.on('dom-ready', () => {
|
||||
page.insertCSS(fs.readFileSync(path.join(__dirname, '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', () => {
|
||||
const checkOS = isWindowsOrmacOS();
|
||||
if (checkOS && !isDev) {
|
||||
// Initate auto-updates on macOs and windows
|
||||
appUpdater();
|
||||
// Initate auto-updates on MacOS and Windows
|
||||
appUpdater();
|
||||
crashHandler();
|
||||
});
|
||||
|
||||
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('clear-app-settings', () => {
|
||||
global.mainWindowState.unmanage(mainWindow);
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
});
|
||||
|
||||
ipcMain.on('toggle-app', () => {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
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) => {
|
||||
// Array index == Shown index - 1
|
||||
page.send('switch-server-tab', index - 1);
|
||||
});
|
||||
|
||||
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => {
|
||||
setAutoLaunch(AutoLaunchValue);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
// unregister all the shortcuts so that they don't interfare with other apps
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true;
|
||||
});
|
||||
|
||||
ipc.on('new-domain', (e, domain) => {
|
||||
// mainWindow.loadURL(domain);
|
||||
if (!mainWindow) {
|
||||
mainWindow = createMainWindow();
|
||||
mainWindow.loadURL(domain);
|
||||
} else if (mainWindow.isMinimized()) {
|
||||
mainWindow.loadURL(domain);
|
||||
mainWindow.show();
|
||||
} else {
|
||||
mainWindow.loadURL(domain);
|
||||
}
|
||||
targetLink = domain;
|
||||
// Send crash reports
|
||||
process.on('uncaughtException', err => {
|
||||
console.error(err);
|
||||
console.error(err.stack);
|
||||
});
|
||||
|
@@ -1,15 +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';
|
||||
|
||||
exports = module.exports = {
|
||||
linkIsInternal, skipImages
|
||||
};
|
@@ -1,69 +0,0 @@
|
||||
// Adopted from
|
||||
// https://github.com/wozaki/twitter-js-apps/blob/9bc00eafd575fd180dc7a450e1b1daf425e67b80/redux/src/main/renderer/registries/electron/swipeNavigatorImpl.js
|
||||
|
||||
'use strict'
|
||||
const {remote} = require('electron');
|
||||
|
||||
const THRESHOLD_DELTA_X = 70;
|
||||
const THRESHOLD_LIMIT_DELTA_Y = 50;
|
||||
const THRESHOLD_TIME = 50;
|
||||
|
||||
// TODO avoid module global state
|
||||
let tracking = false;
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
let startTime = 0;
|
||||
let time = 0;
|
||||
|
||||
module.exports.register = function register() {
|
||||
remote.getCurrentWindow()
|
||||
.on('scroll-touch-begin', onScrollBegin)
|
||||
.on('scroll-touch-end', onScrollEnd)
|
||||
.on('swipe', onSwipe)
|
||||
|
||||
window.addEventListener('wheel', onMouseWheel, {passive: true});
|
||||
window.addEventListener('beforeunload', remove);
|
||||
}
|
||||
|
||||
const remove = module.exports.remove = function remove() {
|
||||
remote.getCurrentWindow()
|
||||
.removeListener('scroll-touch-begin', onScrollBegin)
|
||||
.removeListener('scroll-touch-end', onScrollEnd)
|
||||
.removeListener('swipe', onSwipe)
|
||||
|
||||
window.removeEventListener('mousewheel', onMouseWheel);
|
||||
window.removeEventListener('beforeunload', remove);
|
||||
}
|
||||
|
||||
function onSwipe(e, direction) {
|
||||
if (direction === 'left')
|
||||
remote.getCurrentWebContents().goBack()
|
||||
else if (direction === 'right')
|
||||
remote.getCurrentWebContents().goForward()
|
||||
}
|
||||
|
||||
function onMouseWheel(e) {
|
||||
if (tracking) {
|
||||
deltaX = deltaX + e.deltaX
|
||||
deltaY = deltaY + e.deltaY
|
||||
time = (new Date()).getTime() - startTime
|
||||
}
|
||||
}
|
||||
|
||||
function onScrollBegin() {
|
||||
tracking = true
|
||||
startTime = (new Date()).getTime()
|
||||
}
|
||||
|
||||
function onScrollEnd() {
|
||||
if (time > THRESHOLD_TIME && tracking && Math.abs(deltaY) < THRESHOLD_LIMIT_DELTA_Y)
|
||||
if (deltaX > THRESHOLD_DELTA_X)
|
||||
remote.getCurrentWebContents().goForward()
|
||||
else if (deltaX < -THRESHOLD_DELTA_X)
|
||||
remote.getCurrentWebContents().goBack()
|
||||
|
||||
tracking = false
|
||||
deltaX = 0
|
||||
deltaY = 0
|
||||
startTime = 0
|
||||
}
|
598
app/main/menu.js
@@ -1,353 +1,397 @@
|
||||
'use strict';
|
||||
const os = require('os');
|
||||
const electron = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
const {dialog} = require('electron');
|
||||
const { app, shell, BrowserWindow, Menu } = require('electron');
|
||||
|
||||
const fs = require('fs-extra');
|
||||
|
||||
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 {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();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
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 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-current-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: 'About Zulip',
|
||||
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',
|
||||
}, {
|
||||
label: 'Reset App Settings',
|
||||
accelerator: 'Command+Shift+D',
|
||||
click() {
|
||||
clearCache();
|
||||
AppMenu.resetAppSettings();
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
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: 'About Zulip',
|
||||
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',
|
||||
}, {
|
||||
label: 'Reset App Settings',
|
||||
accelerator: 'Ctrl+Shift+D',
|
||||
click() {
|
||||
clearCache();
|
||||
AppMenu.resetAppSettings();
|
||||
}
|
||||
},
|
||||
{
|
||||
}, {
|
||||
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 resetAppSettings() {
|
||||
// We save App's settings/configurations in following files
|
||||
const settingFiles = ['window-state.json', 'domain.json', 'settings.json'];
|
||||
|
||||
settingFiles.forEach(settingFileName => {
|
||||
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
|
||||
fs.access(getSettingFilesPath, error => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
} else {
|
||||
fs.unlink(getSettingFilesPath, () => {
|
||||
AppMenu.sendAction('clear-app-data');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
|
@@ -1 +0,0 @@
|
||||
/* We'll be overriding default styling so that app look more native * /
|
@@ -1,58 +0,0 @@
|
||||
'use strict';
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
const {webFrame} = require('electron');
|
||||
const {spellChecker} = require('./spellchecker');
|
||||
|
||||
// enable swipe back/forward navigation on macOS
|
||||
require('./macos-swipe-navigation.js').register();
|
||||
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
require('./domain');
|
||||
|
||||
// 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', () => {
|
||||
// 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', () => {
|
||||
// create the menu for the below
|
||||
document.querySelector('.dropdown-toggle').click();
|
||||
|
||||
const nodes = document.querySelectorAll('.dropdown-menu li:nth-child(4) a');
|
||||
nodes[nodes.length - 1].click();
|
||||
});
|
||||
|
||||
// To prevent failing this script on linux we need to load it after the document loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// init spellchecker
|
||||
spellChecker();
|
||||
});
|
@@ -1,27 +0,0 @@
|
||||
const {SpellCheckHandler, ContextMenuListener, ContextMenuBuilder} = require('electron-spellchecker');
|
||||
|
||||
function spellChecker() {
|
||||
// Implement spellcheck using electron api
|
||||
window.spellCheckHandler = new SpellCheckHandler();
|
||||
window.spellCheckHandler.attachToInput();
|
||||
|
||||
// Start off as US English
|
||||
window.spellCheckHandler.switchLanguage('en-US');
|
||||
|
||||
const contextMenuBuilder = new ContextMenuBuilder(window.spellCheckHandler);
|
||||
const contextMenuListener = new ContextMenuListener(info => {
|
||||
contextMenuBuilder.showPopupMenu(info);
|
||||
});
|
||||
|
||||
// Clean up events after you navigate away from this page;
|
||||
// otherwise, you may experience errors
|
||||
window.addEventListener('beforeunload', () => {
|
||||
// eslint-disable-next-line no-undef
|
||||
spellCheckHandler.unsubscribe();
|
||||
contextMenuListener.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
exports = module.exports = {
|
||||
spellChecker
|
||||
};
|
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,61 +0,0 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const electron = require('electron');
|
||||
const app = require('electron').app;
|
||||
const {addDomain, about} = require('./windowmanager');
|
||||
|
||||
let tray = null;
|
||||
|
||||
const APP_ICON = path.join(__dirname, '../resources/tray', 'tray');
|
||||
|
||||
const iconPath = () => {
|
||||
if (process.platform === 'linux') {
|
||||
return APP_ICON + 'linux.png';
|
||||
}
|
||||
return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'osx.png');
|
||||
};
|
||||
|
||||
exports.create = () => {
|
||||
const contextMenu = electron.Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'About',
|
||||
click() {
|
||||
about();
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Change Zulip server',
|
||||
click() {
|
||||
addDomain();
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.reload();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
click() {
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
tray = new electron.Tray(iconPath());
|
||||
tray.setToolTip(`${app.getName()}`);
|
||||
tray.setContextMenu(contextMenu);
|
||||
};
|
||||
|
@@ -1,69 +0,0 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const electron = require('electron');
|
||||
|
||||
let domainWindow;
|
||||
let aboutWindow;
|
||||
|
||||
function onClosed() {
|
||||
// dereference the window
|
||||
domainWindow = null;
|
||||
aboutWindow = null;
|
||||
}
|
||||
|
||||
// Change Zulip server Window
|
||||
function createdomainWindow() {
|
||||
const domainwin = new electron.BrowserWindow({
|
||||
frame: false,
|
||||
height: 300,
|
||||
resizable: false,
|
||||
width: 400
|
||||
});
|
||||
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.show();
|
||||
}
|
||||
|
||||
// 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.show();
|
||||
}
|
||||
|
||||
exports = module.exports = {
|
||||
addDomain, about
|
||||
};
|
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "zulip",
|
||||
"productName": "Zulip",
|
||||
"version": "0.5.8",
|
||||
"version": "1.6.0-beta",
|
||||
"description": "Zulip Desktop App",
|
||||
"license": "Apache-2.0",
|
||||
"email":"<svnitakash@gmail.com>",
|
||||
"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,14 @@
|
||||
"InstantMessaging"
|
||||
],
|
||||
"dependencies": {
|
||||
"electron-is-dev": "0.1.2",
|
||||
"electron-updater": "1.6.2",
|
||||
"electron-log": "1.3.0",
|
||||
"configstore": "2.1.0",
|
||||
"dialogs": "1.1.14",
|
||||
"electron-debug": "1.1.0",
|
||||
"electron-localshortcut": "0.6.1",
|
||||
"electron-is-dev": "0.3.0",
|
||||
"electron-log": "2.2.7",
|
||||
"electron-spellchecker": "1.1.2",
|
||||
"electron-updater": "2.16.2",
|
||||
"node-json-db": "0.7.3",
|
||||
"request": "2.79.0",
|
||||
"electron-spellchecker": "1.0.0",
|
||||
"wurl": "2.1.0"
|
||||
"request": "2.81.0",
|
||||
"wurl": "2.5.0",
|
||||
"electron-window-state": "4.1.1",
|
||||
"auto-launch": "5.0.1"
|
||||
}
|
||||
}
|
||||
|
@@ -6,17 +6,19 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="about">
|
||||
<center><img src="../resources/zulip.png"></center>
|
||||
<center><p class="detail" id="version"> Version : ?.?.? </p>
|
||||
<center><p class="detail"> License : Apache </p>
|
||||
<center><p class="detail"> Maintainer : Zulip </p>
|
||||
<p class="left"><a class="bug" onclick="linkInBrowser()" href="#">Found bug?</a></p>
|
||||
<img class="logo" src="../resources/zulip.png" />
|
||||
<p class="detail" id="version">version ?.?.?</p>
|
||||
<div class="maintenance-info">
|
||||
<p class="detail maintainer">Maintained by Zulip</p>
|
||||
<p class="detail license">Available under the Apache License</p>
|
||||
<a class="bug" onclick="linkInBrowser()" href="#">Found bug?</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
const app = require('electron').remote.app;
|
||||
const version_tag = document.getElementById('version');
|
||||
version_tag.innerHTML = ' Version : ' + app.getVersion() + ' ';
|
||||
version_tag.innerHTML = 'version ' + app.getVersion();
|
||||
|
||||
function linkInBrowser(event) {
|
||||
|
||||
|
@@ -1,15 +1,22 @@
|
||||
body {
|
||||
background: #6BB6C7;
|
||||
background: #fafafa;
|
||||
font-family: menu, "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#version {
|
||||
color: #aaa;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.about {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.left {
|
||||
position: absolute;
|
||||
top:89%;
|
||||
left:76%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about p {
|
||||
@@ -18,10 +25,49 @@ body {
|
||||
}
|
||||
|
||||
.about img {
|
||||
width:160px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
text-align: left;
|
||||
margin-left: 35%;
|
||||
}
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail.maintainer {
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail.license {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.maintenance-info {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 20px;
|
||||
left: 0px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.maintenance-info p {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.maintenance-info .bug {
|
||||
display: inline-block;
|
||||
padding: 8px 15px;
|
||||
margin-top: 30px;
|
||||
text-decoration: none;
|
||||
background-color: #52c2af;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.maintenance-info .bug:hover {
|
||||
background-color: #32a692;
|
||||
}
|
||||
|
@@ -1,375 +1,365 @@
|
||||
@charset "UTF-8";
|
||||
header,
|
||||
section {
|
||||
display: block
|
||||
}
|
||||
html {
|
||||
font-size: 100%;
|
||||
overflow-y: scroll;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
html,
|
||||
button,
|
||||
input {
|
||||
font-family: "Helvetica Neue", Arial, sans-serif
|
||||
}
|
||||
/*******************
|
||||
* General rules *
|
||||
*******************/
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0
|
||||
}
|
||||
img {
|
||||
border: 0;
|
||||
-ms-interpolation-mode: bicubic
|
||||
}
|
||||
img {
|
||||
vertical-align: middle
|
||||
}
|
||||
form {
|
||||
margin: 0
|
||||
}
|
||||
fieldset {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
button,
|
||||
input {
|
||||
font-size: 100%;
|
||||
margin: 0;
|
||||
vertical-align: baseline;
|
||||
box-sizing: content-box;
|
||||
-webkit-box-sizing: content-box
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: #eee url(../img/ic_loading.gif) no-repeat;
|
||||
background-size: 60px 60px;
|
||||
background-position: center;
|
||||
}
|
||||
button,
|
||||
input {
|
||||
line-height: normal
|
||||
|
||||
.toggle-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;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
button {
|
||||
cursor: pointer;
|
||||
-webkit-appearance: button;
|
||||
|
||||
.toggle-sidebar div {
|
||||
transition: all 0.5s ease-out;
|
||||
}
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0
|
||||
|
||||
.sidebar-hide {
|
||||
width: 0;
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
hr {
|
||||
display: none
|
||||
|
||||
.sidebar-hide div {
|
||||
transform: translateX(-100%);
|
||||
transition: all 0.6s ease-out;
|
||||
}
|
||||
img {
|
||||
max-width: 100%
|
||||
}
|
||||
h1 {
|
||||
color: #111;
|
||||
line-height: 1em;
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
text-rendering: optimizelegibility;
|
||||
-webkit-text-stroke: none
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 35px
|
||||
src: local('Material Icons'), local('MaterialIcons-Regular'), url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
body.container-layout header #logo {
|
||||
|
||||
/*******************
|
||||
* 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;
|
||||
display: inline-block;
|
||||
vertical-align: top
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
body.container-layout header #logo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-position: -790px 0
|
||||
}
|
||||
button[type=submit] {
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
|
||||
.tab .server-icons {
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
padding: 3px;
|
||||
height: 30px;
|
||||
vertical-align: top;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab .server-tab {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
position: relative;
|
||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
font-size-adjust: auto;
|
||||
vertical-align: bottom;
|
||||
background-color: #e6eaef;
|
||||
border: 2px solid #e6eaef;
|
||||
color: #96a0ac;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
padding: 6px 17px
|
||||
margin: 5px 0 2px 0;
|
||||
z-index: 11;
|
||||
line-height: 31px;
|
||||
color: #eee;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
opacity: 0.6;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.desktop button[type=submit] {
|
||||
-webkit-transition: all .2s ease-in-out;
|
||||
transition: all .2s ease-in-out
|
||||
|
||||
.tab .server-tab:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.desktop button[type=submit]:hover {
|
||||
background-color: #eff2f5;
|
||||
border: 2px solid #eff2f5;
|
||||
color: #96a0ac
|
||||
|
||||
.tab.functional-tab {
|
||||
height: 46px;
|
||||
padding: 0;
|
||||
}
|
||||
button[type=submit]:focus {
|
||||
outline: 0
|
||||
|
||||
.tab.functional-tab.active .server-tab {
|
||||
padding: 2px 0;
|
||||
height: 40px;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
button[type=submit].btn-primary {
|
||||
background-color: #20b36c;
|
||||
border: 2px solid #20b36c;
|
||||
color: #fff
|
||||
|
||||
.tab.functional-tab .server-tab i {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
.desktop button[type=submit].btn-primary:hover {
|
||||
background-color: #39ca83;
|
||||
border: 2px solid #39ca83;
|
||||
color: #fff
|
||||
|
||||
.tab.active .server-tab {
|
||||
opacity: 1;
|
||||
background-color: #648478;
|
||||
}
|
||||
button[type=submit].btn-primary:focus {
|
||||
outline: 0
|
||||
}
|
||||
button[type=submit].btn-large {
|
||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
padding: 12px 30px
|
||||
}
|
||||
input[type=text] {
|
||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
font-weight: 200;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
border: 1px solid #cad0d7;
|
||||
color: #000;
|
||||
font-size: 18px;
|
||||
line-height: 26px;
|
||||
height: 42px;
|
||||
padding: 10px 10px
|
||||
}
|
||||
.desktop input[type=text]:hover {
|
||||
background-color: #fff;
|
||||
border-color: #bbc3cc;
|
||||
color: #111
|
||||
}
|
||||
input[type=text]:focus {
|
||||
background-color: #fff;
|
||||
border-color: #20b36c!important;
|
||||
color: #000;
|
||||
outline: 0
|
||||
}
|
||||
input[type=text]::-webkit-input-placeholder {
|
||||
color: #8e959e
|
||||
}
|
||||
input[type=text]::-moz-placeholder {
|
||||
color: #8e959e
|
||||
}
|
||||
input[type=text]:-ms-input-placeholder {
|
||||
color: #8e959e
|
||||
}
|
||||
.form-large input[type=text] {
|
||||
font-size: 18px;
|
||||
line-height: 26px;
|
||||
height: 54px;
|
||||
padding: 12px 15px
|
||||
}
|
||||
.control-group {
|
||||
margin-top: 40px
|
||||
}
|
||||
h1 {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
-webkit-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
hyphens: auto
|
||||
}
|
||||
.section-main {
|
||||
position: relative
|
||||
}
|
||||
body {
|
||||
color: #111;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||
background: #edf1f3;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box
|
||||
}
|
||||
.desktop body {
|
||||
padding-top: 116px
|
||||
}
|
||||
body.container-layout {
|
||||
padding: 0!important;
|
||||
background-color: #E6EAEF
|
||||
}
|
||||
header {
|
||||
z-index: 800;
|
||||
border-bottom: 1px solid #dae0e7;
|
||||
background: rgba(255, 255, 255, .97);
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%
|
||||
}
|
||||
header .container {
|
||||
position: relative
|
||||
}
|
||||
.desktop header {
|
||||
position: fixed
|
||||
}
|
||||
header .container {
|
||||
position: relative;
|
||||
height: 75px
|
||||
}
|
||||
.container-layout header {
|
||||
position: relative;
|
||||
border: 0;
|
||||
background: 0 0;
|
||||
padding: 30px 0
|
||||
}
|
||||
.container-layout header .container {
|
||||
height: auto
|
||||
}
|
||||
header #logo {
|
||||
|
||||
.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;
|
||||
text-indent: -9999px
|
||||
right: 0;
|
||||
}
|
||||
|
||||
header #logo {
|
||||
position: absolute!important;
|
||||
top: 50%;
|
||||
left: 10px;
|
||||
-webkit-transform: translate(0, -50%);
|
||||
transform: translate(0, -50%)
|
||||
.tab .server-tab-badge {
|
||||
display: none;
|
||||
}
|
||||
body.container-layout header #logo {
|
||||
position: relative!important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-webkit-transform: translate(0, 0);
|
||||
transform: translate(0, 0);
|
||||
margin: 0 auto;
|
||||
display: block
|
||||
|
||||
.tab .server-tab-badge.close-button {
|
||||
width: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
.content {
|
||||
padding-bottom: 40px;
|
||||
overflow: hidden
|
||||
|
||||
.tab .server-tab-badge.close-button i {
|
||||
font-size: 13px;
|
||||
line-height: 17px;
|
||||
}
|
||||
.container-layout .content {
|
||||
max-width: 660px;
|
||||
margin: 0 auto
|
||||
|
||||
#add-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.content .server {
|
||||
position: relative;
|
||||
margin: 0 10px
|
||||
|
||||
.tab .server-tab-shortcut {
|
||||
color: #648478;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-family: sans-serif;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.content .server .container {
|
||||
background: #fff;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 50px 0 0;
|
||||
-webkit-border-radius: 4px;
|
||||
|
||||
|
||||
/*******************
|
||||
* Webview Area *
|
||||
*******************/
|
||||
|
||||
#webviews-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
webview {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
#add-server-tooltip,
|
||||
.server-tooltip {
|
||||
font-family: 'arial';
|
||||
background: #222c31;
|
||||
left: 56px;
|
||||
padding: 10px 20px;
|
||||
position: fixed;
|
||||
margin-top: 8px;
|
||||
z-index: 5000 !important;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: max-content;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#add-server-tooltip:after,
|
||||
.server-tooltip:after {
|
||||
content: " ";
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-right: 8px solid #222c31;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
#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;
|
||||
z-index: 10;
|
||||
max-width: 580px
|
||||
}
|
||||
.content .server h1 {
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Full screen Popup container */
|
||||
|
||||
.popup .popuptext {
|
||||
visibility: hidden;
|
||||
background-color: #555;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 0 10%
|
||||
border-radius: 6px;
|
||||
padding: 9px 0;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
font-family: arial;
|
||||
width: 240px;
|
||||
top: 15px;
|
||||
height: 20px;
|
||||
left: 43%;
|
||||
}
|
||||
.content .server h1 {
|
||||
font-size: 2.4em;
|
||||
line-height: 1.2em;
|
||||
margin-bottom: 10px
|
||||
|
||||
.popup .show {
|
||||
visibility: visible;
|
||||
animation: cssAnimation 0s ease-in 5s forwards;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
.content .server fieldset {
|
||||
padding: 25px 10% 80px 39px;
|
||||
position: relative
|
||||
}
|
||||
.content .server fieldset .control-group .control-field input {
|
||||
width: 100%
|
||||
}
|
||||
.content .server button {
|
||||
width: 100%
|
||||
}
|
||||
@media screen and (min-width: 749px) {
|
||||
input[type=text] {
|
||||
width: 60%
|
||||
|
||||
@keyframes cssAnimation {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1071px) {
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.3em
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
width: 1070px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.container:before,
|
||||
.container:after {
|
||||
content: "";
|
||||
display: table
|
||||
}
|
||||
.container:after {
|
||||
clear: both
|
||||
}
|
||||
.responsive .container {
|
||||
max-width: 1070px;
|
||||
width: auto;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.responsive .container:before,
|
||||
.responsive .container:after {
|
||||
content: "";
|
||||
display: table
|
||||
}
|
||||
.responsive .container:after {
|
||||
clear: both
|
||||
}
|
||||
@media screen and (max-width: 480px) {
|
||||
.responsive h1 {
|
||||
font-size: 1.7em;
|
||||
line-height: 1.3em
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 481px) and (max-width: 640px) {
|
||||
.responsive h1 {
|
||||
font-size: 1.9em;
|
||||
line-height: 1.3em
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 641px) and (max-width: 748px) {
|
||||
.responsive h1 {
|
||||
font-size: 2.2em;
|
||||
line-height: 1.3em
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 748px) {
|
||||
.responsive input[type=text] {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 749px) and (max-width: 1070px) {
|
||||
.responsive h1 {
|
||||
font-size: 2.6em;
|
||||
line-height: 1.3em
|
||||
}
|
||||
}
|
||||
#server-status {
|
||||
text-align: center;
|
||||
color: #c71212;
|
||||
}
|
||||
}
|
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,83 +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;
|
||||
}
|
||||
|
||||
#pic {
|
||||
width: 20px;
|
||||
left: 36%;
|
||||
margin-left: 4px;
|
||||
top: 63%;
|
||||
display: none;
|
||||
position: absolute;
|
||||
}
|
389
app/renderer/css/preference.css
Normal file
@@ -0,0 +1,389 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
font-family: menu, "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.3em 0.8em;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 15px;
|
||||
font-family: Courier New, Courier, monospace;
|
||||
background-color: #383430;
|
||||
color: #ededed;
|
||||
display: inline-block;
|
||||
margin: 0 0.1em;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border-collapse: collapse;
|
||||
color: #383430;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) { background-color: #f7eee6; }
|
||||
|
||||
table tr:nth-child(odd) { background-color: #fff8ef; }
|
||||
|
||||
|
||||
td { padding: 5px; }
|
||||
|
||||
td:nth-child(odd) {
|
||||
text-align: right;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@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');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
src: url(../fonts/Montserrat-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: 'Montserrat';
|
||||
}
|
||||
|
||||
#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: #222c31;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#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: #222c31;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.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;
|
||||
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;
|
||||
width: 70%;
|
||||
border-left: 8px solid #bcbcbc;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
margin: 5px !important;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #ffffff;
|
||||
background: #ef5350;
|
||||
padding: 3px;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: #ffffff;
|
||||
background: #4EBFAC;
|
||||
padding: 3px;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.grey {
|
||||
color: #9E9E9E;
|
||||
background: #FAFAFA;
|
||||
border: 1px solid #9E9E9E;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: Courier New, Courier, monospace;
|
||||
}
|
||||
|
||||
i.open-tab-button {
|
||||
padding: 0 5px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reset-data-button {
|
||||
display: inline-block;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
width: 120px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background-color 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.reset-data-button:hover {
|
||||
background-color: #3c9f8d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#server-info-container {
|
||||
min-height: calc(100% - 235px);
|
||||
}
|
||||
|
||||
#create-organization-container {
|
||||
font-size: 1.15em;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#create-organization-container i {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
#open-create-org-link {
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#open-create-org-link:hover {
|
||||
color: #005580;;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
position: absolute;
|
||||
margin-left: -9999px;
|
||||
visibility: hidden;
|
||||
}
|
||||
.toggle + label {
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input.toggle-round + label {
|
||||
padding: 2px;
|
||||
width: 50px;
|
||||
height: 25px;
|
||||
background-color: #dddddd;
|
||||
border-radius: 25px;
|
||||
}
|
||||
input.toggle-round + label:before,
|
||||
input.toggle-round + label:after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
content: "";
|
||||
}
|
||||
input.toggle-round + label:before {
|
||||
right: 2px;
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 25px;
|
||||
transition: background 0.4s;
|
||||
}
|
||||
input.toggle-round + label:after {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-color: #fff;
|
||||
border-radius: 100%;
|
||||
transition: margin 0.4s;
|
||||
}
|
||||
input.toggle-round:checked + label:before {
|
||||
background-color: #4EBFAC;
|
||||
}
|
||||
input.toggle-round:checked + label:after {
|
||||
margin-left: 25px;
|
||||
}
|
9
app/renderer/css/preload.css
Normal file
@@ -0,0 +1,9 @@
|
||||
/* 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/Montserrat-Regular.ttf
Normal file
Before Width: | Height: | Size: 803 B |
BIN
app/renderer/img/ic_loading.gif
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
app/renderer/img/ic_server_tab_default.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
app/renderer/img/icon.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 20 KiB |
BIN
app/renderer/img/zulip_network.png
Normal file
After Width: | Height: | Size: 27 KiB |
@@ -1,42 +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 class="container-layout">
|
||||
<div class="section-main">
|
||||
<header>
|
||||
<div class="container">
|
||||
<img src="../resources/zulip.png" id="logo"/>
|
||||
</div>
|
||||
</header>
|
||||
<hr>
|
||||
<section class="content">
|
||||
<section class="server">
|
||||
<div class="container">
|
||||
<h1>Enter your Zulip Server URL</h1>
|
||||
<form id="frm-signInForm" class="form-large" onsubmit="addDomain(); return false">
|
||||
<fieldset>
|
||||
<div class="control-group control-required">
|
||||
<div class="control-field">
|
||||
<input type="text" id="url" autofocus="autofocus" spellcheck="false" placeholder="Server URL">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="control-submit">
|
||||
<button type="submit" id="main" class="btn-primary btn-large" value="Submit" onclick="addDomain();">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
<p id="server-status"><p>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
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
@@ -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;
|
59
app/renderer/js/components/server-tab.js
Normal file
@@ -0,0 +1,59 @@
|
||||
'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-tooltip" style="display:none"></div>
|
||||
<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;
|
48
app/renderer/js/components/tab.js
Normal file
@@ -0,0 +1,48 @@
|
||||
'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);
|
||||
this.$el.addEventListener('mouseover', this.props.onHover);
|
||||
this.$el.addEventListener('mouseout', this.props.onHoverOut);
|
||||
}
|
||||
|
||||
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;
|
193
app/renderer/js/components/webview.js
Normal file
@@ -0,0 +1,193 @@
|
||||
'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, app } = 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('page-favicon-updated', event => {
|
||||
const { favicons } = event;
|
||||
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
|
||||
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
|
||||
if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') {
|
||||
// This api is only supported on macOS
|
||||
app.dock.setBadge('●');
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
402
app/renderer/js/main.js
Normal file
@@ -0,0 +1,402 @@
|
||||
'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.$addServerTooltip = document.getElementById('add-server-tooltip');
|
||||
this.$reloadTooltip = $actionsContainer.querySelector('#reload-tooltip');
|
||||
this.$settingsTooltip = $actionsContainer.querySelector('#setting-tooltip');
|
||||
this.$serverIconTooltip = document.getElementsByClassName('server-tooltip');
|
||||
|
||||
this.$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);
|
||||
}
|
||||
// Open last active tab
|
||||
this.activateTab(ConfigUtil.getConfigItem('lastActiveTab'));
|
||||
} else {
|
||||
this.openSettings('Servers');
|
||||
}
|
||||
}
|
||||
|
||||
initServer(server, index) {
|
||||
this.tabs.push(new ServerTab({
|
||||
role: 'server',
|
||||
icon: server.icon,
|
||||
$root: this.$tabsContainer,
|
||||
onClick: this.activateLastTab.bind(this, index),
|
||||
index,
|
||||
onHover: this.onHover.bind(this, index, server.alias),
|
||||
onHoverOut: this.onHoverOut.bind(this, 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.sidebarHoverEvent(this.$addServerButton, this.$addServerTooltip);
|
||||
this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip);
|
||||
this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip);
|
||||
}
|
||||
|
||||
sidebarHoverEvent(SidebarButton, SidebarTooltip) {
|
||||
SidebarButton.addEventListener('mouseover', () => {
|
||||
SidebarTooltip.removeAttribute('style');
|
||||
});
|
||||
SidebarButton.addEventListener('mouseout', () => {
|
||||
SidebarTooltip.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
onHover(index, serverName) {
|
||||
this.$serverIconTooltip[index].innerHTML = serverName;
|
||||
this.$serverIconTooltip[index].removeAttribute('style');
|
||||
}
|
||||
|
||||
onHoverOut(index) {
|
||||
this.$serverIconTooltip[index].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`
|
||||
});
|
||||
}
|
||||
|
||||
activateLastTab(index) {
|
||||
// Open last active tab
|
||||
ConfigUtil.setConfigItem('lastActiveTab', index);
|
||||
// Open all the tabs in background
|
||||
this.activateTab(index);
|
||||
}
|
||||
|
||||
activateTab(index, hideOldTab = true) {
|
||||
if (!this.tabs[index]) {
|
||||
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 = '';
|
||||
}
|
||||
|
||||
reloadView() {
|
||||
// Save and remember the index of last active tab so that we can use it later
|
||||
const lastActiveTab = this.tabs[this.activeTabIndex].props.index;
|
||||
ConfigUtil.setConfigItem('lastActiveTab', lastActiveTab);
|
||||
|
||||
// Destroy the current view and re-initiate it
|
||||
this.destroyView();
|
||||
this.initTabs();
|
||||
}
|
||||
|
||||
// This will trigger when pressed CTRL/CMD + R [WIP]
|
||||
// It won't reload the current view properly when you add/delete a server.
|
||||
reloadCurrentView() {
|
||||
this.$reloadButton.click();
|
||||
}
|
||||
|
||||
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('sidebar-hide');
|
||||
} else {
|
||||
this.$sidebar.classList.add('sidebar-hide');
|
||||
}
|
||||
}
|
||||
|
||||
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, this.tabs[this.activeTabIndex].props.index));
|
||||
|
||||
ipcRenderer.on('reload-current-viewer', this.reloadCurrentView.bind(this));
|
||||
|
||||
ipcRenderer.on('hard-reload', () => {
|
||||
ipcRenderer.send('reload-full-app');
|
||||
});
|
||||
|
||||
ipcRenderer.on('clear-app-data', () => {
|
||||
ipcRenderer.send('clear-app-settings');
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
};
|
30
app/renderer/js/notification.js
Normal file
@@ -0,0 +1,30 @@
|
||||
'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 baseNotification extends NativeNotification {
|
||||
constructor(title, opts) {
|
||||
opts.silent = ConfigUtil.getConfigItem('silent') || false;
|
||||
super(title, opts);
|
||||
}
|
||||
static requestPermission() {
|
||||
return; // eslint-disable-line no-useless-return
|
||||
}
|
||||
// Override default Notification permission
|
||||
static get permission() {
|
||||
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
|
||||
}
|
||||
}
|
||||
|
||||
window.Notification = baseNotification;
|
||||
|
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
@@ -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(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
|
||||
}
|
||||
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();
|
46
app/renderer/js/pages/preference/base-section.js
Normal file
@@ -0,0 +1,46 @@
|
||||
'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">
|
||||
<div class="switch">
|
||||
<input class="toggle toggle-round" type="checkbox" checked>
|
||||
<label></label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="action">
|
||||
<div class="switch">
|
||||
<input class="toggle toggle-round" type="checkbox">
|
||||
<label></label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
reloadApp() {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseSection;
|
37
app/renderer/js/pages/preference/create-new-org.js
Normal file
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const BaseComponent = require(__dirname + '/../../components/base.js');
|
||||
const shell = require('electron').shell;
|
||||
|
||||
class CreateOrganziation extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="setting-row">
|
||||
<div class="setting-description">
|
||||
<span id="open-create-org-link">Create a new organization on zulipchat.com<i class="material-icons open-tab-button">open_in_new</i></span>
|
||||
</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.openCreateNewOrgExternalLink();
|
||||
}
|
||||
|
||||
openCreateNewOrgExternalLink() {
|
||||
const link = 'https://zulipchat.com/beta/';
|
||||
const externalCreateNewOrgEl = document.getElementById('open-create-org-link');
|
||||
externalCreateNewOrgEl.addEventListener('click', () => {
|
||||
shell.openExternal(link);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CreateOrganziation;
|
239
app/renderer/js/pages/preference/general-section.js
Normal file
@@ -0,0 +1,239 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const { app, dialog } = require('electron').remote;
|
||||
|
||||
const fs = require('fs-extra');
|
||||
|
||||
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">Cmd Or Ctrl+S</span>)</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="badge-option">
|
||||
<div class="setting-description">Show app unread badge</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="flash-taskbar-option" style= "display:${process.platform === 'win32' ? '' : 'none'}">
|
||||
<div class="setting-description">Flash taskbar on new message</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">Desktop Notification</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="show-notification-option">
|
||||
<div class="setting-description">Show Desktop Notifications</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="silent-option">
|
||||
<div class="setting-description">Mute all sounds from Zulip</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 class="setting-row" id="enable-spellchecker-option">
|
||||
<div class="setting-description">Enable Spellchecker (requires restart)</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">Reset Application Data</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="resetdata-option">
|
||||
<div class="setting-description">This will delete all application data including all added accounts and preferences
|
||||
</div>
|
||||
<button class="reset-data-button blue">Reset App Data</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.updateTrayOption();
|
||||
this.updateBadgeOption();
|
||||
this.updateSilentOption();
|
||||
this.updateUpdateOption();
|
||||
this.updateSidebarOption();
|
||||
this.updateStartAtLoginOption();
|
||||
this.updateResetDataOption();
|
||||
this.showDesktopNotification();
|
||||
this.enableSpellchecker();
|
||||
|
||||
// Platform specific settings
|
||||
// Flashing taskbar on Windows
|
||||
if (process.platform === 'win32') {
|
||||
this.updateFlashTaskbar();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateFlashTaskbar() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#flash-taskbar-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('flashTaskbarOnMessage', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('flashTaskbarOnMessage');
|
||||
ConfigUtil.setConfigItem('flashTaskbarOnMessage', newValue);
|
||||
this.updateFlashTaskbar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showDesktopNotification() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#show-notification-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('showNotification', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('showNotification', true);
|
||||
ConfigUtil.setConfigItem('showNotification', newValue);
|
||||
this.showDesktopNotification();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableSpellchecker() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#enable-spellchecker-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('enableSpellchecker', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('enableSpellchecker');
|
||||
ConfigUtil.setConfigItem('enableSpellchecker', newValue);
|
||||
this.enableSpellchecker();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearAppDataDialog() {
|
||||
const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.';
|
||||
const getAppPath = path.join(app.getPath('appData'), app.getName());
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['YES', 'NO'],
|
||||
defaultId: 0,
|
||||
message: 'Are you sure',
|
||||
detail: clearAppDataMessage
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
fs.remove(getAppPath);
|
||||
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateResetDataOption() {
|
||||
const resetDataButton = document.querySelector('#resetdata-option .reset-data-button');
|
||||
resetDataButton.addEventListener('click', () => {
|
||||
this.clearAppDataDialog();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = GeneralSection;
|
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', 'Shortcuts'];
|
||||
|
||||
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
@@ -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 blue" 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
@@ -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="Enter the URL of your Zulip organization..."/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<div class="action blue server-save-action">
|
||||
<i class="material-icons">check_box</i>
|
||||
<span>Add</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;
|
109
app/renderer/js/pages/preference/preference.js
Normal file
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const BaseComponent = require(__dirname + '/js/components/base.js');
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/js/utils/config-util.js');
|
||||
|
||||
const Nav = require(__dirname + '/js/pages/preference/nav.js');
|
||||
const ServersSection = require(__dirname + '/js/pages/preference/servers-section.js');
|
||||
const GeneralSection = require(__dirname + '/js/pages/preference/general-section.js');
|
||||
const NetworkSection = require(__dirname + '/js/pages/preference/network-section.js');
|
||||
const ShortcutsSection = require(__dirname + '/js/pages/preference/shortcuts-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();
|
||||
this.setDefaultSettings();
|
||||
}
|
||||
|
||||
setDefaultView() {
|
||||
let nav = 'General';
|
||||
const hasTag = window.location.hash;
|
||||
if (hasTag) {
|
||||
nav = hasTag.substring(1);
|
||||
}
|
||||
this.handleNavigation(nav);
|
||||
}
|
||||
|
||||
// Settings are initialized only when user clicks on General/Server/Network section settings
|
||||
// In case, user doesn't visit these section, those values set to be null automatically
|
||||
// This will make sure the default settings are correctly set to either true or false
|
||||
setDefaultSettings() {
|
||||
// Default settings which should be respected
|
||||
const settingOptions = {
|
||||
trayIcon: true,
|
||||
useProxy: false,
|
||||
showSidebar: true,
|
||||
badgeOption: true,
|
||||
startAtLogin: false,
|
||||
enableSpellchecker: true,
|
||||
showNotification: true,
|
||||
betaUpdate: false,
|
||||
silent: false
|
||||
};
|
||||
|
||||
for (const i in settingOptions) {
|
||||
if (ConfigUtil.getConfigItem(i) === null) {
|
||||
ConfigUtil.setConfigItem(i, settingOptions[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
case 'Shortcuts': {
|
||||
this.section = new ShortcutsSection({
|
||||
$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
@@ -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;
|
73
app/renderer/js/pages/preference/servers-section.js
Normal file
@@ -0,0 +1,73 @@
|
||||
'use strict';
|
||||
|
||||
const BaseSection = require(__dirname + '/base-section.js');
|
||||
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
|
||||
const ServerInfoForm = require(__dirname + '/server-info-form.js');
|
||||
const NewServerForm = require(__dirname + '/new-server-form.js');
|
||||
const CreateOrganziation = require(__dirname + '/create-new-org.js');
|
||||
|
||||
class ServersSection extends BaseSection {
|
||||
constructor(props) {
|
||||
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 id="create-organization-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();
|
||||
|
||||
this.$createOrganizationContainer = document.getElementById('create-organization-container');
|
||||
this.initCreateNewOrganization();
|
||||
|
||||
for (let i = 0; i < servers.length; i++) {
|
||||
new ServerInfoForm({
|
||||
$root: this.$serverInfoContainer,
|
||||
server: servers[i],
|
||||
index: i,
|
||||
onChange: this.reloadApp
|
||||
}).init();
|
||||
}
|
||||
}
|
||||
|
||||
initCreateNewOrganization() {
|
||||
new CreateOrganziation({
|
||||
$root: this.$createOrganizationContainer
|
||||
}).init();
|
||||
}
|
||||
|
||||
initNewServerForm() {
|
||||
new NewServerForm({
|
||||
$root: this.$newServerContainer,
|
||||
onChange: this.reloadApp
|
||||
}).init();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServersSection;
|
311
app/renderer/js/pages/preference/shortcuts-section.js
Normal file
@@ -0,0 +1,311 @@
|
||||
'use strict';
|
||||
|
||||
const BaseSection = require(__dirname + '/base-section.js');
|
||||
|
||||
class ShortcutsSection extends BaseSection {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
templateMac() {
|
||||
const userOSKey = '⌘';
|
||||
|
||||
return `
|
||||
<div class="settings-pane">
|
||||
<div class="title">Application Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>,</kbd></td>
|
||||
<td>Settings</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>K</kbd></td>
|
||||
<td>Keyboard Shortcuts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>D</kbd></td>
|
||||
<td>Reset App Settings</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>L</kbd></td>
|
||||
<td>Log Out</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
|
||||
<td>Hide Zulip</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
|
||||
<td>Hide Others</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>Q</kbd></td>
|
||||
<td>Quit Zulip</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">Edit Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
|
||||
<td>Undo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
|
||||
<td>Redo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>X</kbd></td>
|
||||
<td>Cut</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>C</kbd></td>
|
||||
<td>Copy</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
|
||||
<td>Paste</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
|
||||
<td>Paste and Match Style</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>A</kbd></td>
|
||||
<td>Select All</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
|
||||
<td>Find</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>G</kbd></td>
|
||||
<td>Find Next</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>Space</kbd></td>
|
||||
<td>Emoji & Symbols</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">View Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
|
||||
<td>Reload</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
|
||||
<td>Hard Reload</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
|
||||
<td>Enter Full Screen</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>=</kbd></td>
|
||||
<td>Zoom In</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>-</kbd></td>
|
||||
<td>Zoom Out</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>0</kbd></td>
|
||||
<td>Actual Size</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>S</kbd></td>
|
||||
<td>Toggle Sidebar</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>I</kbd></td>
|
||||
<td>Toggle DevTools for Zulip App</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>U</kbd></td>
|
||||
<td>Toggle DevTools for Active Tab</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">History Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>←</kbd></td>
|
||||
<td>Back</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>→</kbd></td>
|
||||
<td>Forward</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">Window Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>M</kbd></td>
|
||||
<td>Minimize</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>W</kbd></td>
|
||||
<td>Close</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
templateWinLin() {
|
||||
const userOSKey = 'Ctrl';
|
||||
|
||||
return `
|
||||
<div class="settings-pane">
|
||||
<div class="title">Application Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>,</kbd></td>
|
||||
<td>Settings</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>K</kbd></td>
|
||||
<td>Keyboard Shortcuts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>L</kbd></td>
|
||||
<td>Log Out</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Q</kbd></td>
|
||||
<td>Quit Zulip</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">Edit Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Z</kbd></td>
|
||||
<td>Undo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Y</kbd></td>
|
||||
<td>Redo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>X</kbd></td>
|
||||
<td>Cut</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>C</kbd></td>
|
||||
<td>Copy</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>V</kbd></td>
|
||||
<td>Paste</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd></td>
|
||||
<td>Paste and Match Style</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>A</kbd></td>
|
||||
<td>Select All</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">View Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>R</kbd></td>
|
||||
<td>Reload</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd></td>
|
||||
<td>Hard Reload</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>F11</kbd></td>
|
||||
<td>Toggle Full Screen</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>=</kbd></td>
|
||||
<td>Zoom In</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>-</kbd></td>
|
||||
<td>Zoom Out</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>0</kbd></td>
|
||||
<td>Actual Size</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>S</kbd></td>
|
||||
<td>Toggle Sidebar</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd></td>
|
||||
<td>Toggle DevTools for Zulip App</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>U</kbd></td>
|
||||
<td>Toggle DevTools for Active Tab</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">History Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>Alt</kbd> + <kbd>←</kbd></td>
|
||||
<td>Back</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Alt</kbd> + <kbd>→</kbd></td>
|
||||
<td>Forward</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">Window Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>M</kbd></td>
|
||||
<td>Minimize</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>W</kbd></td>
|
||||
<td>Close</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.props.$root.innerHTML = (process.platform === 'darwin') ?
|
||||
this.templateMac() : this.templateWinLin();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ShortcutsSection;
|
@@ -1,48 +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
|
||||
function addDomain() {
|
||||
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);
|
||||
document.getElementById('main').innerHTML = 'checking...';
|
||||
document.getElementById('pic').style.display = 'block';
|
||||
|
||||
let newDomain = document.getElementById('url').value;
|
||||
newDomain = newDomain.replace(/^https?:\/\//, '');
|
||||
|
||||
const domain = 'https://' + newDomain;
|
||||
const checkDomain = domain + '/static/audio/zulip.ogg';
|
||||
|
||||
request(checkDomain, (error, response) => {
|
||||
if (!error && response.statusCode !== 404) {
|
||||
document.getElementById('pic').style.display = 'none';
|
||||
document.getElementById('main').innerHTML = 'Switch';
|
||||
document.getElementById('urladded').innerHTML = 'Switched to ' + newDomain;
|
||||
db.push('/domain', domain);
|
||||
ipcRenderer.send('new-domain', domain);
|
||||
} else {
|
||||
document.getElementById('pic').style.display = 'none';
|
||||
document.getElementById('main').innerHTML = 'Switch';
|
||||
document.getElementById('urladded').innerHTML = 'Not a valid Zulip Server.';
|
||||
}
|
||||
});
|
||||
}
|
62
app/renderer/js/preload.js
Normal file
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const SetupSpellChecker = require('./spellchecker');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/utils/config-util.js');
|
||||
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
require('./notification');
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
const shortcut = () => {
|
||||
// Create the menu for the below
|
||||
const node = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
|
||||
// Additional check
|
||||
if (node.text.trim().toLowerCase() === 'keyboard shortcuts') {
|
||||
node.click();
|
||||
} else {
|
||||
// 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', () => {
|
||||
// Get the default language of the server
|
||||
const serverLanguage = page_params.default_language; // eslint-disable-line no-undef, camelcase
|
||||
|
||||
if (serverLanguage) {
|
||||
// Set spellcheker language
|
||||
ConfigUtil.setConfigItem('spellcheckerLanguage', serverLanguage);
|
||||
// Init spellchecker
|
||||
SetupSpellChecker.init();
|
||||
}
|
||||
|
||||
// redirect users to network troubleshooting page
|
||||
const getRestartButton = document.querySelector('.restart_get_events_button');
|
||||
if (getRestartButton) {
|
||||
getRestartButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up spellchecker events after you navigate away from this page;
|
||||
// otherwise, you may experience errors
|
||||
window.addEventListener('beforeunload', () => {
|
||||
SetupSpellChecker.unsubscribeSpellChecker();
|
||||
});
|
||||
|
56
app/renderer/js/spellchecker.js
Normal file
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
const { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } = require('electron-spellchecker');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/utils/config-util.js');
|
||||
|
||||
class SetupSpellChecker {
|
||||
init() {
|
||||
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
|
||||
this.enableSpellChecker();
|
||||
}
|
||||
this.enableContextMenu();
|
||||
}
|
||||
|
||||
enableSpellChecker() {
|
||||
try {
|
||||
this.SpellCheckHandler = new SpellCheckHandler();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
enableContextMenu() {
|
||||
if (this.SpellCheckHandler) {
|
||||
this.SpellCheckHandler.attachToInput();
|
||||
|
||||
const userLanguage = ConfigUtil.getConfigItem('spellcheckerLanguage');
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
process.platform === 'darwin' ?
|
||||
// On macOS, spellchecker fails to auto-detect the lanugage user is typing in
|
||||
// that's why we need to mention it explicitly
|
||||
this.SpellCheckHandler.switchLanguage(userLanguage) :
|
||||
// On Linux and Windows, spellchecker can automatically detects the language the user is typing in
|
||||
// and silently switches on the fly; thus we can start off as US English
|
||||
this.SpellCheckHandler.switchLanguage('en-US');
|
||||
}
|
||||
|
||||
const contextMenuBuilder = new ContextMenuBuilder(this.SpellCheckHandler);
|
||||
this.contextMenuListener = new ContextMenuListener(info => {
|
||||
contextMenuBuilder.showPopupMenu(info);
|
||||
});
|
||||
}
|
||||
|
||||
unsubscribeSpellChecker() {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (this.SpellCheckHandler) {
|
||||
this.SpellCheckHandler.unsubscribe();
|
||||
}
|
||||
if (this.contextMenuListener) {
|
||||
this.contextMenuListener.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SetupSpellChecker();
|
222
app/renderer/js/tray.js
Normal file
@@ -0,0 +1,222 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
|
||||
const electron = require('electron');
|
||||
|
||||
const {ipcRenderer, remote} = electron;
|
||||
|
||||
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';
|
||||
}
|
||||
return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'osx.png');
|
||||
};
|
||||
|
||||
let unread = 0;
|
||||
|
||||
const trayIconSize = () => {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return 20;
|
||||
case 'win32':
|
||||
return 100;
|
||||
case 'linux':
|
||||
return 100;
|
||||
default: return 80;
|
||||
}
|
||||
};
|
||||
|
||||
// Default config for Icon we might make it OS specific if needed like the size
|
||||
const config = {
|
||||
pixelRatio: window.devicePixelRatio,
|
||||
unreadCount: 0,
|
||||
showUnreadCount: true,
|
||||
unreadColor: '#000000',
|
||||
readColor: '#000000',
|
||||
unreadBackgroundColor: '#B9FEEA',
|
||||
readBackgroundColor: '#B9FEEA',
|
||||
size: trayIconSize(),
|
||||
thick: process.platform === 'win32'
|
||||
};
|
||||
|
||||
const renderCanvas = function (arg) {
|
||||
config.unreadCount = arg;
|
||||
|
||||
return new Promise(resolve => {
|
||||
const SIZE = config.size * config.pixelRatio;
|
||||
const PADDING = SIZE * 0.05;
|
||||
const CENTER = SIZE / 2;
|
||||
const HAS_COUNT = config.showUnreadCount && config.unreadCount;
|
||||
const color = config.unreadCount ? config.unreadColor : config.readColor;
|
||||
const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = SIZE;
|
||||
canvas.height = SIZE;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Circle
|
||||
// If (!config.thick || config.thick && HAS_COUNT) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(CENTER, CENTER, (SIZE / 2) - PADDING, 0, 2 * Math.PI, false);
|
||||
ctx.fillStyle = backgroundColor;
|
||||
ctx.fill();
|
||||
ctx.lineWidth = SIZE / (config.thick ? 10 : 20);
|
||||
ctx.strokeStyle = backgroundColor;
|
||||
ctx.stroke();
|
||||
// Count or Icon
|
||||
if (HAS_COUNT) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.textAlign = 'center';
|
||||
if (config.unreadCount > 99) {
|
||||
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`;
|
||||
ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15));
|
||||
} else if (config.unreadCount < 10) {
|
||||
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
|
||||
ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.20));
|
||||
} else {
|
||||
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
|
||||
ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.15));
|
||||
}
|
||||
|
||||
resolve(canvas);
|
||||
}
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Renders the tray icon as a native image
|
||||
* @param arg: Unread count
|
||||
* @return the native image
|
||||
*/
|
||||
const renderNativeImage = function (arg) {
|
||||
return Promise.resolve()
|
||||
.then(() => renderCanvas(arg))
|
||||
.then(canvas => {
|
||||
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPng();
|
||||
return Promise.resolve(nativeImage.createFromBuffer(pngData, config.pixelRatio));
|
||||
});
|
||||
};
|
||||
|
||||
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() {
|
||||
// We need to focus the main window first
|
||||
ipcRenderer.send('focus-app');
|
||||
sendAction('open-about');
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Focus',
|
||||
click() {
|
||||
ipcRenderer.send('focus-app');
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
click() {
|
||||
ipcRenderer.send('focus-app');
|
||||
sendAction('open-settings');
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
click() {
|
||||
ipcRenderer.send('quit-app');
|
||||
}
|
||||
}
|
||||
]);
|
||||
window.tray.setContextMenu(contextMenu);
|
||||
window.tray.on('click', () => {
|
||||
// Click event only works on Windows
|
||||
if (process.platform === 'win32') {
|
||||
ipcRenderer.send('toggle-app');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ipcRenderer.on('destroytray', event => {
|
||||
if (!window.tray) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.tray.destroy();
|
||||
if (window.tray.isDestroyed()) {
|
||||
window.tray = null;
|
||||
} else {
|
||||
throw new Error('Tray icon not properly destroyed.');
|
||||
}
|
||||
|
||||
return event;
|
||||
});
|
||||
|
||||
ipcRenderer.on('tray', (event, arg) => {
|
||||
if (!window.tray) {
|
||||
return;
|
||||
}
|
||||
// We don't want to create tray from unread messages on macOS since it already has dock badges.
|
||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||
if (arg === 0) {
|
||||
unread = arg;
|
||||
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');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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' || process.platform === 'win32') {
|
||||
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
@@ -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();
|
256
app/renderer/js/utils/domain-util.js
Normal file
@@ -0,0 +1,256 @@
|
||||
'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',
|
||||
'Error: unable to get local issuer 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 {
|
||||
const invalidZulipServerError = `${domain} does not appear to be a valid Zulip server. Make sure that \
|
||||
(1) you can connect to that URL in a web browser and \n (2) if you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings`;
|
||||
reject(invalidZulipServerError);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
|
||||
// Following check handles both the cases
|
||||
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
|
||||
url: data.realm_uri,
|
||||
alias: 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
@@ -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
@@ -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();
|
44
app/renderer/main.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!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" class="toggle-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>
|
||||
<span id="add-server-tooltip" style="display:none">Add organization</span>
|
||||
</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
@@ -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,22 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="css/pref.css">
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="close" id="close-button">Close</div>
|
||||
<div class="form">
|
||||
<form onsubmit="addDomain(); return false">
|
||||
<input id="url" type="text" placeholder="Server URL">
|
||||
<button type="submit" id="main" value="Submit" onclick="addDomain();">
|
||||
Switch</button>
|
||||
<img id="pic" src="img/loader.gif" />
|
||||
</form>
|
||||
<p id="urladded"><p>
|
||||
</div>
|
||||
<script src="js/pref.js"></script>
|
||||
</body>
|
||||
</html>
|
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>
|
BIN
app/resources/tray/trayosx@2x.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
app/resources/tray/trayosx@3x.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
app/resources/tray/trayosx@4x.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
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
@@ -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
@@ -0,0 +1,2 @@
|
||||
# Installation instructions
|
||||
* [[Windows]]
|
58
docs/Windows.md
Normal file
@@ -0,0 +1,58 @@
|
||||
** Windows Set up instructions **
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* [Git](http://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [Node.js](https://nodejs.org) >= v6.9.0
|
||||
* [python](https://www.python.org/downloads/release/python-2713/) (v2.7.x recommended)
|
||||
* [node-gyp](https://github.com/nodejs/node-gyp#installation) (installed via powershell)
|
||||
|
||||
## System specific dependencies
|
||||
|
||||
* use only 32bit or 64bit for all of the installers, do not mix architectures
|
||||
* install using default settings
|
||||
* open Windows Powershell as Admin
|
||||
```powershell
|
||||
C:\Windows\system32> npm install --global --production windows-build-tools
|
||||
```
|
||||
|
||||
## 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 pack
|
||||
npm run dist
|
||||
```
|
||||
It will start the packaging process. The ready for distribution file (e.g. dmg, windows installer, deb package) will be outputted to the `dist` directory.
|
||||
|
||||
# Troubleshooting
|
||||
If you have any problems running the app please see the [most common issues](./troubleshooting.md).
|
||||
|
||||
|
||||
|
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
@@ -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
@@ -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
@@ -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
|
||||
```
|
114
package.json
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "zulip",
|
||||
"productName": "Zulip",
|
||||
"version": "0.5.8",
|
||||
"version": "1.6.0-beta",
|
||||
"main": "./app/main",
|
||||
"description": "Zulip Desktop App",
|
||||
"license": "Apache-2.0",
|
||||
"email":"<svnitakash@gmail.com>",
|
||||
"copyright": "©2016 Kandra Labs, Inc.",
|
||||
"email": "<svnitakash@gmail.com>",
|
||||
"copyright": "©2017 Kandra Labs, Inc.",
|
||||
"author": {
|
||||
"name": "Akash Nimare",
|
||||
"name": "Kandra Labs, Inc.",
|
||||
"email": "svnitakash@gmail.com"
|
||||
},
|
||||
"repository": {
|
||||
@@ -18,37 +19,45 @@
|
||||
"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",
|
||||
"reinstall": "rm -rf node_modules; rm -rf app/node_modules; npm install",
|
||||
"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",
|
||||
"asar": true,
|
||||
"files": [
|
||||
"**/*",
|
||||
"!docs${/*}",
|
||||
"!node_modules/@paulcbetts/cld/deps/cld${/*}"
|
||||
],
|
||||
"copyright": "©2017 Kandra Labs, Inc.",
|
||||
"mac": {
|
||||
"category": "public.app-category.productivity"
|
||||
},
|
||||
"linux" : {
|
||||
"synopsis": "Zulip Desktop App",
|
||||
"category": "",
|
||||
"linux": {
|
||||
"category": "Chat;GNOME;GTK;Network;InstantMessaging",
|
||||
"packageCategory": "GNOME;GTK;Network;InstantMessaging",
|
||||
"description": "Zulip Desktop Client for Linux",
|
||||
"target" : ["deb", "AppImage"],
|
||||
"version" : "0.5.8",
|
||||
"title" : "Zulip",
|
||||
"license": "Apache-2.0",
|
||||
"target": [
|
||||
"deb",
|
||||
"zip",
|
||||
"AppImage"
|
||||
],
|
||||
"maintainer": "Akash Nimare <svnitakash@gmail.com>"
|
||||
},
|
||||
"deb": {
|
||||
"synopsis": "Zulip Desktop App"
|
||||
},
|
||||
"dmg": {
|
||||
"background": "build/appdmg.png",
|
||||
"icon": "build/icon.icns",
|
||||
@@ -68,54 +77,79 @@
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis-web",
|
||||
"arch": [
|
||||
"x64",
|
||||
"ia32"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "build/icon.ico",
|
||||
"iconUrl": "https://raw.githubusercontent.com/zulip/zulip-electron/master/build/icon.ico"
|
||||
"publisherName": "Kandra Labs, Inc."
|
||||
},
|
||||
"nsis": {
|
||||
"perMachine": true,
|
||||
"oneClick": false
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"Zulip",
|
||||
"Group Chat app",
|
||||
"electron-app",
|
||||
"electron",
|
||||
"Desktop app",
|
||||
"InstantMessaging"
|
||||
"Zulip",
|
||||
"Group Chat app",
|
||||
"electron-app",
|
||||
"electron",
|
||||
"Desktop app",
|
||||
"InstantMessaging"
|
||||
],
|
||||
"devDependencies": {
|
||||
"assert": "^1.4.1",
|
||||
"devtron": "^1.1.0",
|
||||
"electron-builder": "13.6.0",
|
||||
"electron": "1.4.7",
|
||||
"electron-connect": "^0.4.6",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-mocha": "^3.0.1",
|
||||
"spectron": "^3.3.0",
|
||||
"xo": "*"
|
||||
"assert": "1.4.1",
|
||||
"devtron": "1.4.0",
|
||||
"electron-builder": "19.45.5",
|
||||
"electron": "1.6.14",
|
||||
"electron-connect": "0.6.2",
|
||||
"gulp": "3.9.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",
|
||||
"electron-debug": "1.4.0"
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignore": [
|
||||
"tests/*.js",
|
||||
"app/main/macos-swipe-navigation.js"
|
||||
"tests/*.js"
|
||||
],
|
||||
"envs": [
|
||||
"node",
|
||||
|
@@ -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/main/index.js']
|
||||
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
@@ -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
|