mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-24 08:33:36 +00:00
Compare commits
760 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67fa9cca8c | ||
|
|
a90bf1af08 | ||
|
|
cb145acc73 | ||
|
|
099e10673c | ||
|
|
4b3608fc1e | ||
|
|
6128c0e12a | ||
|
|
14a1f5d3e1 | ||
|
|
9cf26f4890 | ||
|
|
397a7381b8 | ||
|
|
24b28f9ded | ||
|
|
9ceabe02d5 | ||
|
|
b207ee57de | ||
|
|
cf9d0c8aa2 | ||
|
|
e97ab2e6dd | ||
|
|
6a7f26d7e8 | ||
|
|
b6e11f623a | ||
|
|
1c60c335fd | ||
|
|
c9249b1724 | ||
|
|
9e957ba704 | ||
|
|
6c37e30233 | ||
|
|
addfe2e414 | ||
|
|
bda0dd29df | ||
|
|
01926e1234 | ||
|
|
9138bbfaf2 | ||
|
|
596561b731 | ||
|
|
5943c21814 | ||
|
|
2456bba5ae | ||
|
|
0fff6336c7 | ||
|
|
4261874e29 | ||
|
|
f976270d33 | ||
|
|
4782f1cfd3 | ||
|
|
2f70621255 | ||
|
|
b6f4e2b83c | ||
|
|
25f4c3aba8 | ||
|
|
1b23468375 | ||
|
|
5d775405d9 | ||
|
|
7f13d9162a | ||
|
|
e0013c22ff | ||
|
|
5c41afdccd | ||
|
|
99a3530238 | ||
|
|
a5ceffc856 | ||
|
|
f55570f2f5 | ||
|
|
4f890c0316 | ||
|
|
bf651dece0 | ||
|
|
1babd8da42 | ||
|
|
bfd146f2d9 | ||
|
|
3126510245 | ||
|
|
d661895545 | ||
|
|
ca9ab6168e | ||
|
|
bbdf2c6017 | ||
|
|
109a9bbd10 | ||
|
|
22d7ef5615 | ||
|
|
e03de26137 | ||
|
|
983254c310 | ||
|
|
b6059077d8 | ||
|
|
cafff9a008 | ||
|
|
190204b2e5 | ||
|
|
4c25c99abc | ||
|
|
55be93b906 | ||
|
|
34e2b3a3d0 | ||
|
|
e5ece8db9e | ||
|
|
40b26dbb0e | ||
|
|
ae4f03f4ba | ||
|
|
8ea32a7a96 | ||
|
|
6b7cce0366 | ||
|
|
73fec72e6d | ||
|
|
920adfb169 | ||
|
|
98174fdcaf | ||
|
|
a0c033431e | ||
|
|
82421d843a | ||
|
|
d9afee3330 | ||
|
|
a46f2ed618 | ||
|
|
9f3b4ff408 | ||
|
|
fb800f7862 | ||
|
|
ba191c3699 | ||
|
|
e49a880ed6 | ||
|
|
4bfa7c9265 | ||
|
|
39c6fa4ace | ||
|
|
963c2e5388 | ||
|
|
849df4adaf | ||
|
|
fc6ff83485 | ||
|
|
5ae2a717fa | ||
|
|
cfdc08a038 | ||
|
|
b76467529d | ||
|
|
bb88a7b7a8 | ||
|
|
0225778050 | ||
|
|
2154b191c8 | ||
|
|
4093304b4d | ||
|
|
2e03f779e8 | ||
|
|
9464390070 | ||
|
|
16f0af8853 | ||
|
|
bb6d90671f | ||
|
|
e536a03fab | ||
|
|
ee60702276 | ||
|
|
e721211619 | ||
|
|
ff671b53ef | ||
|
|
340eb8da99 | ||
|
|
ae689ad6bb | ||
|
|
7ffddded5d | ||
|
|
714cd926ae | ||
|
|
b2f4af0f49 | ||
|
|
d7136aef25 | ||
|
|
73f8b21a9f | ||
|
|
4ce08cb5a2 | ||
|
|
814ce071a3 | ||
|
|
92fb176f67 | ||
|
|
a03f569af9 | ||
|
|
af59bb7c99 | ||
|
|
4390966a62 | ||
|
|
a6d942fe6c | ||
|
|
9d4093b3d8 | ||
|
|
20a6c5d128 | ||
|
|
c843e179fc | ||
|
|
438d4fffa7 | ||
|
|
5c164bfa7d | ||
|
|
cbc89a72a2 | ||
|
|
fb1e163130 | ||
|
|
2ebeeedba8 | ||
|
|
82a7f97ca6 | ||
|
|
55eb768064 | ||
|
|
611932c66d | ||
|
|
5deffa5022 | ||
|
|
6f01f1362a | ||
|
|
9d2739f050 | ||
|
|
01f6e77237 | ||
|
|
7ac35cc087 | ||
|
|
32e6b3054f | ||
|
|
40bf2a1f20 | ||
|
|
dee2f05ac0 | ||
|
|
ca5de73155 | ||
|
|
d0f8c040c7 | ||
|
|
7cf40f1e08 | ||
|
|
598c0df60b | ||
|
|
d3bcd7306a | ||
|
|
b3261bcdff | ||
|
|
20c6f487c4 | ||
|
|
340797ca10 | ||
|
|
220aac2d54 | ||
|
|
d9f6cf4cc9 | ||
|
|
dc3e5d4930 | ||
|
|
fc2b80c36a | ||
|
|
ab667d8053 | ||
|
|
8b9a10a23d | ||
|
|
15af3e732f | ||
|
|
534f4c1463 | ||
|
|
063324550e | ||
|
|
268471df6b | ||
|
|
ca6b2312be | ||
|
|
ff026e5763 | ||
|
|
8f810481e3 | ||
|
|
f91e95647a | ||
|
|
e9536f247b | ||
|
|
067cbf32a1 | ||
|
|
6824978114 | ||
|
|
fa86f1ca25 | ||
|
|
a63b3873ae | ||
|
|
5064ea4b47 | ||
|
|
6b0d8520c5 | ||
|
|
4b16164155 | ||
|
|
6036a44fb2 | ||
|
|
598b96b6e8 | ||
|
|
675bc2f06c | ||
|
|
eb2988a5e4 | ||
|
|
39ea18228c | ||
|
|
909e0f07e3 | ||
|
|
31af6596bf | ||
|
|
3c9914542f | ||
|
|
f4b9605742 | ||
|
|
e2fc9241fa | ||
|
|
3b18357c74 | ||
|
|
c4beedf740 | ||
|
|
b34bf7236f | ||
|
|
e32968b2f3 | ||
|
|
747fbb5ab0 | ||
|
|
107e522914 | ||
|
|
c83bc08359 | ||
|
|
ef0b056437 | ||
|
|
9370f783ba | ||
|
|
227e59fee2 | ||
|
|
b7147d0b29 | ||
|
|
f93053eb20 | ||
|
|
49b29bfed6 | ||
|
|
0fb610f858 | ||
|
|
1d9a923245 | ||
|
|
9582d32de8 | ||
|
|
a2a21631f2 | ||
|
|
9490265a03 | ||
|
|
70bd619fa8 | ||
|
|
c5797e4edb | ||
|
|
32321daef2 | ||
|
|
95a9568ece | ||
|
|
e7a885a1fb | ||
|
|
17d4d97e2e | ||
|
|
3b14684058 | ||
|
|
7d592a0a1c | ||
|
|
eb1be7106b | ||
|
|
1da6e5d51d | ||
|
|
dae7089c7e | ||
|
|
30b40e2ff2 | ||
|
|
b76f01349a | ||
|
|
8446deb673 | ||
|
|
d4b9663257 | ||
|
|
77044fd9fa | ||
|
|
177b77f0b5 | ||
|
|
99b154b8ae | ||
|
|
3fd8aedf81 | ||
|
|
b4d2e55c6f | ||
|
|
3c701ff518 | ||
|
|
1f79a97b05 | ||
|
|
90e8e9a806 | ||
|
|
59ef505efd | ||
|
|
8d0a111c91 | ||
|
|
a10fa8f3ad | ||
|
|
39427091f5 | ||
|
|
f8d93cf397 | ||
|
|
ab62b8b5bb | ||
|
|
ce071dcac9 | ||
|
|
04c1109d43 | ||
|
|
26f321e7f9 | ||
|
|
47b729cbc2 | ||
|
|
6d34da5953 | ||
|
|
cfd1ebc643 | ||
|
|
dab30dfb7a | ||
|
|
e2bb28c738 | ||
|
|
ebd4d9a69b | ||
|
|
ab80315846 | ||
|
|
c78ea51ec9 | ||
|
|
d521f533f0 | ||
|
|
67a69f0dc2 | ||
|
|
d99cc0d49c | ||
|
|
77a1fc0bd3 | ||
|
|
cf96e94470 | ||
|
|
4395edc3de | ||
|
|
66479e0d88 | ||
|
|
7c44ef5e41 | ||
|
|
296c83949f | ||
|
|
ac6f14f5b4 | ||
|
|
20ac6bf921 | ||
|
|
5e9f9aa9a2 | ||
|
|
4efa98118b | ||
|
|
a7887211ac | ||
|
|
33fadcd876 | ||
|
|
e7e92ebecc | ||
|
|
6c29af6766 | ||
|
|
eabac3ae81 | ||
|
|
fc0385433e | ||
|
|
400c02f264 | ||
|
|
be841cff34 | ||
|
|
7d426c4791 | ||
|
|
939aa50e9b | ||
|
|
dc9b9a1dc3 | ||
|
|
b0da718bfc | ||
|
|
7ea6255e65 | ||
|
|
7338f802e1 | ||
|
|
6860f6d836 | ||
|
|
088ddf9c62 | ||
|
|
95da6c0d58 | ||
|
|
1a3d3fc5ff | ||
|
|
535039216b | ||
|
|
f449db1dc1 | ||
|
|
73e0e81abd | ||
|
|
dd4f3047c3 | ||
|
|
e4ef0e195a | ||
|
|
00a925e39e | ||
|
|
c34b4f9151 | ||
|
|
8a13f7914c | ||
|
|
b13c86e2db | ||
|
|
4fb4773a2b | ||
|
|
deda98b9d4 | ||
|
|
f98457a0cb | ||
|
|
3436fb3e68 | ||
|
|
448822a5fb | ||
|
|
6e07945d7c | ||
|
|
54a8e33b01 | ||
|
|
93c22ca89d | ||
|
|
a2fca89551 | ||
|
|
1516452a8c | ||
|
|
700e73105e | ||
|
|
c41bbf9dfd | ||
|
|
347de5981a | ||
|
|
d932a2b2c4 | ||
|
|
34e403d0d2 | ||
|
|
77c5b8af30 | ||
|
|
b2bc268c09 | ||
|
|
8c52b563bf | ||
|
|
a8109c5106 | ||
|
|
76818b7def | ||
|
|
a5ea3d475f | ||
|
|
2f9205b0c4 | ||
|
|
3f00beb291 | ||
|
|
72f356de9c | ||
|
|
482264d878 | ||
|
|
d7725c121c | ||
|
|
2b19cdecf2 | ||
|
|
1182af23e4 | ||
|
|
6192801083 | ||
|
|
63a913487c | ||
|
|
77899f9f48 | ||
|
|
2b1e594286 | ||
|
|
312dc1daaf | ||
|
|
744d056e26 | ||
|
|
433f11ebd1 | ||
|
|
506982b4c6 | ||
|
|
4adda8b8ae | ||
|
|
fbef3e4d7b | ||
|
|
f076343068 | ||
|
|
d5c9e638ca | ||
|
|
d5a343d549 | ||
|
|
4412f60435 | ||
|
|
cec2b3136d | ||
|
|
9ba2e0ad1e | ||
|
|
eb30f48a82 | ||
|
|
edc5b92ab4 | ||
|
|
29753b1a46 | ||
|
|
f490efb2df | ||
|
|
bb7426a789 | ||
|
|
e7edfffa89 | ||
|
|
fb700350f9 | ||
|
|
79d0688bcd | ||
|
|
c6c36b698d | ||
|
|
8ef39553b8 | ||
|
|
7374fcbe78 | ||
|
|
aa2c55538f | ||
|
|
4d679acb0c | ||
|
|
795b9fdc17 | ||
|
|
82d9f795c2 | ||
|
|
ec91d46f7b | ||
|
|
728418742d | ||
|
|
8b0e9b55fb | ||
|
|
b40be8d891 | ||
|
|
e12d69720d | ||
|
|
8353c4de16 | ||
|
|
8c3a2a80c7 | ||
|
|
5c4221fc31 | ||
|
|
a57c269935 | ||
|
|
ce81a1b367 | ||
|
|
26e97c8746 | ||
|
|
393a0fc2be | ||
|
|
b0348d6bca | ||
|
|
9c68fcdda8 | ||
|
|
543ead4da3 | ||
|
|
b5f8d54aea | ||
|
|
7b07a20c04 | ||
|
|
6b1a55c7e4 | ||
|
|
d46bdba8ce | ||
|
|
d3bc765e59 | ||
|
|
613e44ef8d | ||
|
|
f0e4a3bc53 | ||
|
|
427c86afaf | ||
|
|
0e89880cfb | ||
|
|
b1f1b49982 | ||
|
|
390bf907e5 | ||
|
|
02c8a27567 | ||
|
|
db4e8e5129 | ||
|
|
88b764dcc9 | ||
|
|
9fe72c0d21 | ||
|
|
31b00ee6a6 | ||
|
|
ecec7f8b89 | ||
|
|
139496b716 | ||
|
|
f6239132c1 | ||
|
|
6451373ff6 | ||
|
|
6f74fea0f3 | ||
|
|
405d938223 | ||
|
|
c6b7e9a9ee | ||
|
|
b4073ba7ac | ||
|
|
b504f31c26 | ||
|
|
f71660b3f0 | ||
|
|
5d0516887d | ||
|
|
0dd86f0814 | ||
|
|
eaecc92055 | ||
|
|
4f65c36a72 | ||
|
|
2380b650c9 | ||
|
|
1d713f1df2 | ||
|
|
7fa9c291cb | ||
|
|
8a40e36a63 | ||
|
|
3456720135 | ||
|
|
43840a5245 | ||
|
|
d99c29301e | ||
|
|
6e760973ff | ||
|
|
88c64e9dd6 | ||
|
|
e9db11c156 | ||
|
|
8e8de212d8 | ||
|
|
0e0d7e7c4a | ||
|
|
6b98a49245 | ||
|
|
8e5c326d74 | ||
|
|
862e9e2c8c | ||
|
|
8a0b047d8f | ||
|
|
4a833ef603 | ||
|
|
fefb7247d8 | ||
|
|
54a6903236 | ||
|
|
92ef1bd532 | ||
|
|
bf2e04b3bb | ||
|
|
3e389ea5db | ||
|
|
8edff28daa | ||
|
|
ef6abbf49e | ||
|
|
0f638dd4b5 | ||
|
|
92f0e46927 | ||
|
|
a0dd3832c2 | ||
|
|
a2e84595c9 | ||
|
|
9e33ebae62 | ||
|
|
9bdc5dd9e4 | ||
|
|
ac89ac1cb4 | ||
|
|
31da1131a4 | ||
|
|
4980c71e5a | ||
|
|
3e73511357 | ||
|
|
5f23c8570b | ||
|
|
12f79e18a1 | ||
|
|
ad7f760444 | ||
|
|
7314c1f1dd | ||
|
|
46c3d352a1 | ||
|
|
cef8eadc8f | ||
|
|
d3f742719c | ||
|
|
171d88755c | ||
|
|
09ac1bd338 | ||
|
|
48dd12b738 | ||
|
|
61bbd1cf22 | ||
|
|
bf722da390 | ||
|
|
c24f5b3e45 | ||
|
|
3d0f4d88af | ||
|
|
a2f412c3de | ||
|
|
08559c2f4a | ||
|
|
1f4509a6d2 | ||
|
|
06c8b2e8a2 | ||
|
|
2aa15ee11a | ||
|
|
3c676672ec | ||
|
|
c937317ecf | ||
|
|
44dceda50e | ||
|
|
e1407cb6f7 | ||
|
|
6899a6bc20 | ||
|
|
925fec71d5 | ||
|
|
f847c565f9 | ||
|
|
c2a380d308 | ||
|
|
ac6b206583 | ||
|
|
988405c665 | ||
|
|
a209804692 | ||
|
|
95c4df1898 | ||
|
|
1048c91d9e | ||
|
|
d723c5cd1c | ||
|
|
dcc7cd118c | ||
|
|
d2649dd5d7 | ||
|
|
25e6f6d482 | ||
|
|
bd805b8324 | ||
|
|
e1d98e0d8e | ||
|
|
89e5d2892b | ||
|
|
ab28b56377 | ||
|
|
71aa7e28ba | ||
|
|
ba2bfba459 | ||
|
|
d181b0c2e5 | ||
|
|
bda5c62928 | ||
|
|
b2b4fd1003 | ||
|
|
19770f5fbf | ||
|
|
4592396c4b | ||
|
|
19d3c0f5b5 | ||
|
|
af87789c7f | ||
|
|
36e710dbfe | ||
|
|
a6e1c93d81 | ||
|
|
684d88dd7f | ||
|
|
45aedbd9db | ||
|
|
6f411a819c | ||
|
|
097738332b | ||
|
|
b777f8a50f | ||
|
|
50f06239bb | ||
|
|
94cbc786d6 | ||
|
|
75abf38515 | ||
|
|
c1af019182 | ||
|
|
1ea8aea22a | ||
|
|
a7e418885a | ||
|
|
0ac823c85b | ||
|
|
1096188ddf | ||
|
|
8d0b68316c | ||
|
|
c45ad7096f | ||
|
|
5ca7937e52 | ||
|
|
481b99a2ab | ||
|
|
7eb4adf722 | ||
|
|
4d21a8f8d8 | ||
|
|
94270af265 | ||
|
|
177bff6113 | ||
|
|
b4fa4cacb6 | ||
|
|
06abf6716a | ||
|
|
0ad4ea4fdf | ||
|
|
2ece432d19 | ||
|
|
10cd7850ca | ||
|
|
86e3d06e52 | ||
|
|
6ebc9acbf4 | ||
|
|
278dc686e1 | ||
|
|
cec98c030e | ||
|
|
f10d6eb115 | ||
|
|
8c5e928e9c | ||
|
|
8d142c8c3f | ||
|
|
a09769c560 | ||
|
|
bb99015fd2 | ||
|
|
24f5c9b226 | ||
|
|
dc6c4be5fe | ||
|
|
50647e330b | ||
|
|
73dc3db436 | ||
|
|
09cf21bf49 | ||
|
|
c30d0cc77b | ||
|
|
872ad4d3e7 | ||
|
|
6fd9e1be8b | ||
|
|
76c7f24161 | ||
|
|
f9c270492c | ||
|
|
371c580934 | ||
|
|
3eec4c2209 | ||
|
|
debbfb6b7d | ||
|
|
8bd1492586 | ||
|
|
1115c6d5c3 | ||
|
|
9ba279213c | ||
|
|
89c35cb1d4 | ||
|
|
21d6eb52c5 | ||
|
|
aa1538837b | ||
|
|
ea103380b6 | ||
|
|
60d10d88d1 | ||
|
|
124a842bbd | ||
|
|
7130103999 | ||
|
|
26a144e1c2 | ||
|
|
a5c1ae8726 | ||
|
|
ed5096840d | ||
|
|
3f6d256910 | ||
|
|
28421992ba | ||
|
|
82199dd1c3 | ||
|
|
952baf1f42 | ||
|
|
3342d7da91 | ||
|
|
c0ec292090 | ||
|
|
8f6fba0b97 | ||
|
|
d3453a3fe1 | ||
|
|
18e14fd3f8 | ||
|
|
c52bfd6a45 | ||
|
|
428087110a | ||
|
|
781883a898 | ||
|
|
7299cd51e6 | ||
|
|
a45254cff0 | ||
|
|
4095c121ba | ||
|
|
6787b8d557 | ||
|
|
306e0f3b5e | ||
|
|
f030d5d56b | ||
|
|
acf7c5a238 | ||
|
|
da22f75300 | ||
|
|
df35ffc3b0 | ||
|
|
eeb0b4a666 | ||
|
|
0a893c97c7 | ||
|
|
99a1711bb0 | ||
|
|
a27cf9e0e7 | ||
|
|
22d6c6a2f3 | ||
|
|
c742591471 | ||
|
|
339a415d60 | ||
|
|
f7696cc04d | ||
|
|
1eabf5c5a2 | ||
|
|
4d020c639b | ||
|
|
85a6cf6890 | ||
|
|
ef1ad820aa | ||
|
|
10f6d80e5f | ||
|
|
50ef02c324 | ||
|
|
deed18d75f | ||
|
|
0e3c6bceeb | ||
|
|
c2e138c16a | ||
|
|
8d1b027b0e | ||
|
|
7bb8d78973 | ||
|
|
a7c7791bf8 | ||
|
|
5d120b4416 | ||
|
|
ceaf13dee2 | ||
|
|
f81381dfec | ||
|
|
dbe89cdd09 | ||
|
|
14c59bdae1 | ||
|
|
0ac3e3f6d3 | ||
|
|
d69c1339e6 | ||
|
|
fe56a20334 | ||
|
|
cfc97c9b73 | ||
|
|
2e70b515da | ||
|
|
51e414a508 | ||
|
|
8e7a9bf230 | ||
|
|
6493ddb8ec | ||
|
|
31edbe0d67 | ||
|
|
9980fee785 | ||
|
|
ff9986ec6b | ||
|
|
f3423d394c | ||
|
|
a1da199627 | ||
|
|
537fbe8f9e | ||
|
|
3fccb33fca | ||
|
|
5638590c8b | ||
|
|
29ed00981d | ||
|
|
d7638c0b95 | ||
|
|
7fadbe877b | ||
|
|
32a21889fb | ||
|
|
c4a961f9da | ||
|
|
ceaa898570 | ||
|
|
73fe17041d | ||
|
|
9f756cad3e | ||
|
|
6db6b7c482 | ||
|
|
09c45e75e8 | ||
|
|
120b80cf65 | ||
|
|
22f705960d | ||
|
|
ca8ce1deaa | ||
|
|
f70432f4e3 | ||
|
|
60d693700e | ||
|
|
6e7333eab6 | ||
|
|
0d8dd1cd90 | ||
|
|
0ee3757774 | ||
|
|
811df9f381 | ||
|
|
8bd02cc7e4 | ||
|
|
9d5d221371 | ||
|
|
6006f1a3f8 | ||
|
|
4f96df4a34 | ||
|
|
a13558fa16 | ||
|
|
a1d19a385c | ||
|
|
c98667236e | ||
|
|
b80c3d007b | ||
|
|
70fb198a0b | ||
|
|
7276bfeaa7 | ||
|
|
abd71330ba | ||
|
|
e776222d6b | ||
|
|
30b05571e7 | ||
|
|
0a155c63e7 | ||
|
|
13c750ac6c | ||
|
|
48799f75d1 | ||
|
|
29f4e702ad | ||
|
|
3eb4cf4f64 | ||
|
|
1a97d8a5b0 | ||
|
|
2f96ec6199 | ||
|
|
52de465457 | ||
|
|
e3039cf5a9 | ||
|
|
6c120269eb | ||
|
|
b31fc6b66d | ||
|
|
435e5f086e | ||
|
|
603ad7dfcd | ||
|
|
a47a0e3e90 | ||
|
|
8e04920f54 | ||
|
|
4a7b84e483 | ||
|
|
ffd2ee36fa | ||
|
|
c11fa93642 | ||
|
|
a90dc0c82f | ||
|
|
028bc02d0a | ||
|
|
d673d5b76c | ||
|
|
2bf88aa912 | ||
|
|
c9f179a2ae | ||
|
|
c11089027c | ||
|
|
5c45ab7b66 | ||
|
|
6205ca0aca | ||
|
|
b83e2dd428 | ||
|
|
fd421a62d2 | ||
|
|
fa85241c79 | ||
|
|
b163c237b6 | ||
|
|
5351ee10fa | ||
|
|
d48b6ae80d | ||
|
|
2d07d40c92 | ||
|
|
55ae71c00c | ||
|
|
0c18bb5226 | ||
|
|
6d213c86a8 | ||
|
|
0c1cd96ed7 | ||
|
|
d77b1b5960 | ||
|
|
2c6c0c8c8d | ||
|
|
84bf59d32f | ||
|
|
256c352fb9 | ||
|
|
da28589c17 | ||
|
|
c452ee2ef6 | ||
|
|
3582aa4694 | ||
|
|
7f7eee2455 | ||
|
|
c716b8f233 | ||
|
|
4a40c75127 | ||
|
|
239631a2b6 | ||
|
|
89d1344e2f | ||
|
|
1948ba2cc3 | ||
|
|
b8da7dd6ee | ||
|
|
4a0efb7301 | ||
|
|
aedd95259d | ||
|
|
c8d7a79877 | ||
|
|
6e6db42b54 | ||
|
|
db79284fbb | ||
|
|
2434f06655 | ||
|
|
1d611d3382 | ||
|
|
a746194e9e | ||
|
|
7cc13f7a26 | ||
|
|
6a9bb152a0 | ||
|
|
8b6dcd355f | ||
|
|
91742a5770 | ||
|
|
fb74251a2c | ||
|
|
a920720f91 | ||
|
|
aa8e99b7a6 | ||
|
|
e23f8aaa58 | ||
|
|
5c3208d44c | ||
|
|
c0b57bbe2b | ||
|
|
afe4e8901b | ||
|
|
231e7fd9c2 | ||
|
|
a0d898a5b7 | ||
|
|
1abf62555c | ||
|
|
6befcbaa8f | ||
|
|
e56a01049b | ||
|
|
72cb8459ff | ||
|
|
0b83b22206 | ||
|
|
267d25e5c4 | ||
|
|
8401f8f5ce | ||
|
|
c4a7264f34 | ||
|
|
9d081ecd5a | ||
|
|
dc6582fa82 | ||
|
|
3b412672c6 | ||
|
|
04083bfa81 | ||
|
|
562e82d2f1 | ||
|
|
3b014e0715 | ||
|
|
13178ebc8f | ||
|
|
08693bf105 | ||
|
|
d7a0b63d62 | ||
|
|
a193ecf229 | ||
|
|
31f04754a4 | ||
|
|
ae7374475f | ||
|
|
7697d5d698 | ||
|
|
d3e1b5de45 | ||
|
|
9efa6191f7 | ||
|
|
d86797d2fc | ||
|
|
23eef7edb0 | ||
|
|
a1d5a35ccf | ||
|
|
62e8dfe180 | ||
|
|
8322054984 | ||
|
|
13ae6f07e9 | ||
|
|
dab92be54c | ||
|
|
fe9c66d8c2 | ||
|
|
43b4d511dc | ||
|
|
aa5a47ad53 | ||
|
|
ce27f92900 | ||
|
|
7be051bb6e | ||
|
|
c2a01adabe | ||
|
|
770926e6eb | ||
|
|
10ef627f59 | ||
|
|
4adba0f4b4 | ||
|
|
ed590c26e3 | ||
|
|
6d10291a87 | ||
|
|
3fe3a3da85 | ||
|
|
ba64438a99 | ||
|
|
bcc27894c4 | ||
|
|
0dd0f593d1 | ||
|
|
f5e9342f78 | ||
|
|
083ccdf229 | ||
|
|
1261786db2 | ||
|
|
7f567f55c3 | ||
|
|
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 |
@@ -2,6 +2,7 @@ root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
15
.gitattributes
vendored
15
.gitattributes
vendored
@@ -1 +1,14 @@
|
||||
* text=auto
|
||||
* text=auto eol=lf
|
||||
|
||||
*.gif binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.eot binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.svg binary
|
||||
*.ttf binary
|
||||
*.png binary
|
||||
*.otf binary
|
||||
*.tif binary
|
||||
*.ogg binary
|
||||
|
||||
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Clear steps to reproduce the issue. -->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- Operating System:
|
||||
<!-- (Platform and Version) e.g. macOS 10.13.6 / Windows 10 (1803) / Ubuntu 18.04 x64 -->
|
||||
- Zulip Desktop Version:
|
||||
<!-- e.g. 5.2.0 -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
6
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
6
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
---
|
||||
|
||||
|
||||
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
---
|
||||
|
||||
**Problem Description**
|
||||
<!-- Please add a clear and concise description of what the problem is. -->
|
||||
|
||||
**Proposed Solution**
|
||||
<!-- Describe the solution you'd like in a clear and concise manner -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
<!--
|
||||
Remove the fields that are not appropriate
|
||||
Please include:
|
||||
-->
|
||||
|
||||
**What's this PR do?**
|
||||
|
||||
**Any background context you want to provide?**
|
||||
|
||||
**Screenshots?**
|
||||
|
||||
**You have tested this PR on:**
|
||||
- [ ] Windows
|
||||
- [ ] Linux/Ubuntu
|
||||
- [ ] macOS
|
||||
24
.gitignore
vendored
24
.gitignore
vendored
@@ -1,12 +1,22 @@
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
# Dependency directory
|
||||
/node_modules/
|
||||
|
||||
# npm cache directory
|
||||
.npm
|
||||
|
||||
# transifexrc - if user prefers it to be in working tree
|
||||
.transifexrc
|
||||
|
||||
# Compiled binary build directory
|
||||
dist/
|
||||
|
||||
#snap generated files
|
||||
snap/parts
|
||||
snap/prime
|
||||
snap/snap
|
||||
snap/stage
|
||||
snap/*.snap
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
@@ -23,4 +33,12 @@ yarn-error.log*
|
||||
|
||||
# miscellaneous
|
||||
.idea
|
||||
config.gypi
|
||||
config.gypi
|
||||
|
||||
# Test generated files
|
||||
# tests/package.json
|
||||
|
||||
.python-version
|
||||
|
||||
# Ignore all the typescript compiled files
|
||||
app/**/*.js
|
||||
|
||||
25
.htmlhintrc
Normal file
25
.htmlhintrc
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"tagname-lowercase": true,
|
||||
"attr-lowercase": true,
|
||||
"attr-value-double-quotes": true,
|
||||
"attr-value-not-empty": false,
|
||||
"attr-no-duplication": true,
|
||||
"doctype-first": true,
|
||||
"tag-pair": true,
|
||||
"empty-tag-not-self-closed": true,
|
||||
"spec-char-escape": true,
|
||||
"id-unique": true,
|
||||
"src-not-empty": true,
|
||||
"title-require": true,
|
||||
"alt-require": false,
|
||||
"doctype-html5": true,
|
||||
"id-class-value": "dash",
|
||||
"style-disabled": false,
|
||||
"inline-style-disabled": false,
|
||||
"inline-script-disabled": false,
|
||||
"space-tab-mixed-disabled": "space4",
|
||||
"id-class-ad-disabled": false,
|
||||
"href-abs-or-rel": false,
|
||||
"attr-unsafe-chars": true,
|
||||
"head-script-disabled": true
|
||||
}
|
||||
5
.mailmap
Normal file
5
.mailmap
Normal file
@@ -0,0 +1,5 @@
|
||||
Anders Kaseorg <anders@zulip.com> <anders@zulipchat.com>
|
||||
Rishi Gupta <rishig@zulip.com> <rishig@zulipchat.com>
|
||||
Rishi Gupta <rishig@zulip.com> <rishig@users.noreply.github.com>
|
||||
Tim Abbott <tabbott@zulip.com> <tabbott@zulipchat.com>
|
||||
Tim Abbott <tabbott@zulip.com> <tabbott@mit.edu>
|
||||
@@ -1 +0,0 @@
|
||||
2.7.9
|
||||
67
.stylelintrc
Normal file
67
.stylelintrc
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"rules": {
|
||||
# Stylistic rules for CSS.
|
||||
"function-comma-space-after": "always",
|
||||
"function-comma-space-before": "never",
|
||||
"function-max-empty-lines": 0,
|
||||
"function-whitespace-after": "always",
|
||||
|
||||
"value-keyword-case": "lower",
|
||||
"value-list-comma-newline-after": "always-multi-line",
|
||||
"value-list-comma-space-after": "always-single-line",
|
||||
"value-list-comma-space-before": "never",
|
||||
"value-list-max-empty-lines": 0,
|
||||
|
||||
"unit-case": "lower",
|
||||
"property-case": "lower",
|
||||
"color-hex-case": "lower",
|
||||
|
||||
"declaration-bang-space-before": "always",
|
||||
"declaration-colon-newline-after": "always-multi-line",
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
"declaration-colon-space-before": "never",
|
||||
"declaration-block-semicolon-newline-after": "always",
|
||||
"declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-trailing-semicolon": "always",
|
||||
|
||||
"block-closing-brace-empty-line-before": "never",
|
||||
"block-closing-brace-newline-after": "always",
|
||||
"block-closing-brace-newline-before": "always",
|
||||
"block-opening-brace-newline-after": "always",
|
||||
"block-opening-brace-space-before": "always",
|
||||
|
||||
"selector-attribute-brackets-space-inside": "never",
|
||||
"selector-attribute-operator-space-after": "never",
|
||||
"selector-attribute-operator-space-before": "never",
|
||||
"selector-combinator-space-after": "always",
|
||||
"selector-combinator-space-before": "always",
|
||||
"selector-descendant-combinator-no-non-space": true,
|
||||
"selector-pseudo-class-parentheses-space-inside": "never",
|
||||
"selector-pseudo-element-case": "lower",
|
||||
"selector-pseudo-element-colon-notation": "double",
|
||||
"selector-type-case": "lower",
|
||||
"selector-list-comma-newline-after": "always",
|
||||
"selector-list-comma-space-before": "never",
|
||||
|
||||
"media-feature-colon-space-after": "always",
|
||||
"media-feature-colon-space-before": "never",
|
||||
"media-feature-name-case": "lower",
|
||||
"media-feature-parentheses-space-inside": "never",
|
||||
"media-feature-range-operator-space-after": "always",
|
||||
"media-feature-range-operator-space-before": "always",
|
||||
"media-query-list-comma-newline-after": "always",
|
||||
"media-query-list-comma-space-before": "never",
|
||||
|
||||
"at-rule-name-case": "lower",
|
||||
"at-rule-name-space-after": "always",
|
||||
"at-rule-semicolon-newline-after": "always",
|
||||
"at-rule-semicolon-space-before": "never",
|
||||
|
||||
"comment-whitespace-inside": "always",
|
||||
"indentation": 4,
|
||||
|
||||
# Limit language features
|
||||
"color-no-hex": true,
|
||||
"color-named": "never",
|
||||
}
|
||||
}
|
||||
19
.travis.yml
19
.travis.yml
@@ -1,6 +1,3 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
os:
|
||||
- osx
|
||||
- linux
|
||||
@@ -15,22 +12,12 @@ addons:
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- '6'
|
||||
|
||||
before_install:
|
||||
- npm install -g gulp
|
||||
- npm install
|
||||
- '10'
|
||||
- '12'
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- app/node_modules
|
||||
|
||||
script:
|
||||
- npm run travis
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://zulip.org/zulipbot/travis
|
||||
on_success: always
|
||||
on_failure: always
|
||||
- npm run test
|
||||
|
||||
9
.tx/config
Normal file
9
.tx/config
Normal file
@@ -0,0 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[zulip.desktopjson]
|
||||
file_filter = app/translations/<lang>.json
|
||||
minimum_perc = 0
|
||||
source_file = app/translations/en.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
Thanks for taking the time to contribute!
|
||||
|
||||
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.
|
||||
The following is a set of guidelines for contributing to Zulip's 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](https://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).
|
||||
* If you have any questions regarding zulip-desktop, open an [issue](https://github.com/zulip/zulip-desktop/issues/new/) or ask it on [chat.zulip.org](https://chat.zulip.org/#narrow/stream/16-desktop).
|
||||
|
||||
## Issue
|
||||
Ensure the bug was not already reported by searching on GitHub under [issues](https://github.com/zulip/zulip-electron/issues). If you're unable to find an open issue addressing the bug, open a [new issue](https://github.com/zulip/zulip-electron/issues/new).
|
||||
Ensure the bug was not already reported by searching on GitHub under [issues](https://github.com/zulip/zulip-desktop/issues). If you're unable to find an open issue addressing the bug, open a [new issue](https://github.com/zulip/zulip-desktop/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).
|
||||
|
||||
@@ -38,7 +38,7 @@ For example:
|
||||
|
||||
|
||||
## Pull Requests
|
||||
Pull Requests are always welcome.
|
||||
Pull Requests are always welcome.
|
||||
|
||||
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:
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
Please include:
|
||||
- `Operating System`
|
||||
- `Clear steps to reproduce the issue`
|
||||
- `Relevant error messages and/or screenshots`
|
||||
|
||||
|
||||
41
README.md
41
README.md
@@ -1,39 +1,36 @@
|
||||
# Zulip Desktop Client
|
||||
[](https://travis-ci.org/zulip/zulip-electron)
|
||||
[](https://ci.appveyor.com/project/akashnimare/zulip-electron/branch/master)
|
||||
[](https://travis-ci.org/zulip/zulip-desktop)
|
||||
[](https://ci.appveyor.com/project/zulip/zulip-desktop/branch/master)
|
||||
[](https://github.com/sindresorhus/xo)
|
||||
[](https://chat.zulip.org)
|
||||
|
||||
Desktop client for Zulip. Available for Mac, Linux and Windows.
|
||||
Desktop client for Zulip. Available for Mac, Linux, and Windows.
|
||||
|
||||
<img src="http://i.imgur.com/ChzTq4F.png"/>
|
||||
|
||||
# Download
|
||||
Please see [installation guide](https://zulipchat.com/help/desktop-app-install-guide).
|
||||
Please see the [installation guide](https://zulip.com/help/desktop-app-install-guide).
|
||||
|
||||
# Features
|
||||
* Sign in to multiple teams
|
||||
* Native desktop Notifications
|
||||
* SpellChecker
|
||||
* OSX/Win/Linux installers
|
||||
* Automatic Updates (macOS/Windows)
|
||||
* Keyboard shortcuts
|
||||
* Sign in to multiple organizations
|
||||
* Desktop notifications with inline reply
|
||||
* Tray/dock integration
|
||||
* Multi-language spell checker
|
||||
* Automatic updates
|
||||
|
||||
Description | Keys
|
||||
-----------------------| -----------------------
|
||||
Default shortcuts | <kbd>Cmd/Ctrl</kbd> <kbd>k</kbd>
|
||||
Manage Zulip Servers | <kbd>Cmd/Ctrl</kbd> <kbd>,</kbd>
|
||||
Back | <kbd>Cmd/Ctrl</kbd> <kbd>[</kbd>
|
||||
Forward | <kbd>Cmd/Ctrl</kbd> <kbd>]</kbd>
|
||||
# Reporting issues
|
||||
|
||||
# Development
|
||||
Please see our [development guide](./development.md) to get started and run app locally.
|
||||
This desktop client shares most of its code with the Zulip webapp.
|
||||
Issues in an individual organization's Zulip window should be reported
|
||||
in the [Zulip server and webapp
|
||||
project](https://github.com/zulip/zulip/issues/new). Other
|
||||
issues in the desktop app and its settings should be reported [in this
|
||||
project](https://github.com/zulip/zulip-desktop/issues/new).
|
||||
|
||||
# Contribute
|
||||
|
||||
If you want to contribute please make sure to read [our documentation about contributing](./CONTRIBUTING.md) first.
|
||||
|
||||
* [Issue Tracker](https://github.com/zulip/zulip-electron/issues)
|
||||
* [Source Code](https://github.com/zulip/zulip-electron/)
|
||||
First, join us on the [Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html)!
|
||||
Also see our [contribution guidelines](./CONTRIBUTING.md) and our [development guide](./development.md).
|
||||
|
||||
# License
|
||||
Released under the [Apache-2.0](./LICENSE) license.
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const { app, dialog } = require('electron');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
|
||||
|
||||
function appUpdater() {
|
||||
// Don't initiate auto-updates in development and on Linux system
|
||||
// since autoUpdater doesn't work on Linux
|
||||
if (isDev || process.platform === 'linux') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Logs directory
|
||||
const LogsDir = `${app.getPath('userData')}/Logs`;
|
||||
|
||||
if (!fs.existsSync(LogsDir)) {
|
||||
fs.mkdirSync(LogsDir);
|
||||
}
|
||||
|
||||
// Log whats happening
|
||||
const log = require('electron-log');
|
||||
|
||||
log.transports.file.file = `${LogsDir}/updates.log`;
|
||||
log.transports.file.level = 'info';
|
||||
autoUpdater.logger = log;
|
||||
|
||||
// Handle auto updates for beta/pre releases
|
||||
autoUpdater.allowPrerelease = ConfigUtil.getConfigItem('betaUpdate') || false;
|
||||
|
||||
// Ask the user if update is available
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
autoUpdater.on('update-downloaded', event => {
|
||||
// Ask user to update the app
|
||||
dialog.showMessageBox({
|
||||
type: 'question',
|
||||
buttons: ['Install and Relaunch', 'Install Later'],
|
||||
defaultId: 0,
|
||||
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();
|
||||
// force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
|
||||
app.quit();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
// Init for updates
|
||||
autoUpdater.checkForUpdates();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
appUpdater
|
||||
};
|
||||
109
app/main/autoupdater.ts
Normal file
109
app/main/autoupdater.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import {app, dialog, session} from 'electron';
|
||||
import {UpdateDownloadedEvent, UpdateInfo, autoUpdater} from 'electron-updater';
|
||||
import util from 'util';
|
||||
import {linuxUpdateNotification} from './linuxupdater'; // Required only in case of linux
|
||||
|
||||
import log from 'electron-log';
|
||||
import isDev from 'electron-is-dev';
|
||||
import * as ConfigUtil from '../renderer/js/utils/config-util';
|
||||
import * as LinkUtil from '../renderer/js/utils/link-util';
|
||||
|
||||
const sleep = util.promisify(setTimeout);
|
||||
|
||||
export async function appUpdater(updateFromMenu = false): Promise<void> {
|
||||
// Don't initiate auto-updates in development
|
||||
if (isDev) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.platform === 'linux' && !process.env.APPIMAGE) {
|
||||
const ses = session.fromPartition('persist:webviewsession');
|
||||
await linuxUpdateNotification(ses);
|
||||
return;
|
||||
}
|
||||
|
||||
let updateAvailable = false;
|
||||
|
||||
// Create Logs directory
|
||||
const LogsDir = `${app.getPath('userData')}/Logs`;
|
||||
|
||||
// Log whats happening
|
||||
log.transports.file.file = `${LogsDir}/updates.log`;
|
||||
log.transports.file.level = 'info';
|
||||
autoUpdater.logger = log;
|
||||
|
||||
// Handle auto updates for beta/pre releases
|
||||
const isBetaUpdate = ConfigUtil.getConfigItem('betaUpdate');
|
||||
|
||||
autoUpdater.allowPrerelease = isBetaUpdate || false;
|
||||
|
||||
const eventsListenerRemove = ['update-available', 'update-not-available'];
|
||||
autoUpdater.on('update-available', async (info: UpdateInfo) => {
|
||||
if (updateFromMenu) {
|
||||
updateAvailable = true;
|
||||
|
||||
// This is to prevent removal of 'update-downloaded' and 'error' event listener.
|
||||
eventsListenerRemove.forEach(event => {
|
||||
autoUpdater.removeAllListeners(event);
|
||||
});
|
||||
|
||||
await dialog.showMessageBox({
|
||||
message: `A new version ${info.version}, of Zulip Desktop is available`,
|
||||
detail: 'The update will be downloaded in the background. You will be notified when it is ready to be installed.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', async () => {
|
||||
if (updateFromMenu) {
|
||||
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
|
||||
// listeners don't trigger multiple times.
|
||||
autoUpdater.removeAllListeners();
|
||||
|
||||
await dialog.showMessageBox({
|
||||
message: 'No updates available',
|
||||
detail: `You are running the latest version of Zulip Desktop.\nVersion: ${app.getVersion()}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('error', async (error: Error) => {
|
||||
if (updateFromMenu) {
|
||||
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
|
||||
// listeners don't trigger multiple times.
|
||||
autoUpdater.removeAllListeners();
|
||||
|
||||
const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates');
|
||||
const {response} = await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
buttons: ['Manual Download', 'Cancel'],
|
||||
message: messageText,
|
||||
detail: `Error: ${error.message}\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulip.com/apps/.\n
|
||||
Current Version: ${app.getVersion()}`
|
||||
});
|
||||
if (response === 0) {
|
||||
await LinkUtil.openBrowser(new URL('https://zulip.com/apps/'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Ask the user if update is available
|
||||
autoUpdater.on('update-downloaded', async (event: UpdateDownloadedEvent) => {
|
||||
// Ask user to update the app
|
||||
const {response} = await dialog.showMessageBox({
|
||||
type: 'question',
|
||||
buttons: ['Install and Relaunch', 'Install Later'],
|
||||
defaultId: 0,
|
||||
message: `A new update ${event.version} has been downloaded`,
|
||||
detail: 'It will be installed the next time you restart the application'
|
||||
});
|
||||
if (response === 0) {
|
||||
await sleep(1000);
|
||||
autoUpdater.quitAndInstall();
|
||||
// Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
// Init for updates
|
||||
await autoUpdater.checkForUpdates();
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { crashReporter } = require('electron');
|
||||
|
||||
const crashHandler = () => {
|
||||
crashReporter.start({
|
||||
productName: 'zulip-electron',
|
||||
companyName: 'Kandra Labs, Inc.',
|
||||
submitURL: 'https://zulip-sentry.herokuapp.com/crashreport',
|
||||
autoSubmit: true
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
crashHandler
|
||||
};
|
||||
@@ -1,267 +0,0 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const electron = require('electron');
|
||||
const electronLocalshortcut = require('electron-localshortcut');
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
const appMenu = require('./menu');
|
||||
const { appUpdater } = require('./autoupdater');
|
||||
const { crashHandler } = require('./crash-reporter');
|
||||
|
||||
const { setAutoLaunch } = require('./startup');
|
||||
|
||||
const { app, ipcMain } = electron;
|
||||
|
||||
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js');
|
||||
|
||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||
require('electron-debug')();
|
||||
|
||||
// Prevent window being garbage collected
|
||||
let mainWindow;
|
||||
let badgeCount;
|
||||
|
||||
let isQuitting = false;
|
||||
|
||||
// Load this url in main window
|
||||
const mainURL = 'file://' + path.join(__dirname, '../renderer', 'main.html');
|
||||
|
||||
const isAlreadyRunning = app.makeSingleInstance(() => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
if (isAlreadyRunning) {
|
||||
return app.quit();
|
||||
}
|
||||
|
||||
const APP_ICON = path.join(__dirname, '../resources', 'Icon');
|
||||
|
||||
const iconPath = () => {
|
||||
return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
|
||||
};
|
||||
|
||||
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',
|
||||
icon: iconPath(),
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 600,
|
||||
minHeight: 500,
|
||||
webPreferences: {
|
||||
plugins: true,
|
||||
allowDisplayingInsecureContent: true,
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister all the shortcuts so that they don't interfare with other apps
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
});
|
||||
|
||||
win.setTitle('Zulip');
|
||||
|
||||
win.on('enter-full-screen', () => {
|
||||
win.webContents.send('enter-fullscreen');
|
||||
});
|
||||
|
||||
win.on('leave-full-screen', () => {
|
||||
win.webContents.send('leave-fullscreen');
|
||||
});
|
||||
|
||||
// To destroy tray icon when navigate to a new URL
|
||||
win.webContents.on('will-navigate', e => {
|
||||
if (e) {
|
||||
win.webContents.send('destroytray');
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
function registerLocalShortcuts(page) {
|
||||
// Somehow, reload action cannot be overwritten by the menu item
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
|
||||
page.send('reload-current-viewer');
|
||||
});
|
||||
|
||||
// Also adding these shortcuts because some users might want to use it instead of CMD/Left-Right
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => {
|
||||
page.send('back');
|
||||
});
|
||||
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+]', () => {
|
||||
page.send('forward');
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// Unregister all the shortcuts so that they don't interfare with other apps
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (!mainWindow) {
|
||||
mainWindow = createMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('ready', () => {
|
||||
appMenu.setMenu({
|
||||
tabs: []
|
||||
});
|
||||
mainWindow = createMainWindow();
|
||||
|
||||
const page = mainWindow.webContents;
|
||||
|
||||
registerLocalShortcuts(page);
|
||||
|
||||
page.on('dom-ready', () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
page.once('did-frame-finish-load', () => {
|
||||
// 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) => {
|
||||
electronLocalshortcut.register(mainWindow, `CommandOrControl+${index}`, () => {
|
||||
// Array index == Shown index - 1
|
||||
page.send('switch-server-tab', index - 1);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('local-shortcuts', (event, enable) => {
|
||||
if (enable) {
|
||||
registerLocalShortcuts(page);
|
||||
} else {
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => {
|
||||
setAutoLaunch(AutoLaunchValue);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
// Unregister all the shortcuts so that they don't interfare with other apps
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true;
|
||||
});
|
||||
|
||||
// Send crash reports
|
||||
process.on('uncaughtException', err => {
|
||||
console.error(err);
|
||||
console.error(err.stack);
|
||||
});
|
||||
471
app/main/index.ts
Normal file
471
app/main/index.ts
Normal file
@@ -0,0 +1,471 @@
|
||||
import {sentryInit} from '../renderer/js/utils/sentry-util';
|
||||
import {appUpdater} from './autoupdater';
|
||||
import {setAutoLaunch} from './startup';
|
||||
|
||||
import windowStateKeeper from 'electron-window-state';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import electron, {app, dialog, ipcMain, session} from 'electron';
|
||||
|
||||
import * as AppMenu from './menu';
|
||||
import * as BadgeSettings from '../renderer/js/pages/preference/badge-settings';
|
||||
import * as CertificateUtil from '../renderer/js/utils/certificate-util';
|
||||
import * as ConfigUtil from '../renderer/js/utils/config-util';
|
||||
import * as ProxyUtil from '../renderer/js/utils/proxy-util';
|
||||
import {_getServerSettings, _saveServerIcon, _isOnline} from './request';
|
||||
|
||||
let mainWindowState: windowStateKeeper.State;
|
||||
|
||||
// Prevent window being garbage collected
|
||||
let mainWindow: Electron.BrowserWindow;
|
||||
let badgeCount: number;
|
||||
|
||||
let isQuitting = false;
|
||||
|
||||
// Load this url in main window
|
||||
const mainURL = 'file://' + path.join(__dirname, '../renderer', 'main.html');
|
||||
|
||||
const singleInstanceLock = app.requestSingleInstanceLock();
|
||||
if (singleInstanceLock) {
|
||||
app.on('second-instance', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
const rendererCallbacks = new Map();
|
||||
let nextRendererCallbackId = 0;
|
||||
|
||||
ipcMain.on('renderer-callback', (event: Event, rendererCallbackId: number, ...args: any[]) => {
|
||||
rendererCallbacks.get(rendererCallbackId)(...args);
|
||||
rendererCallbacks.delete(rendererCallbackId);
|
||||
});
|
||||
|
||||
function makeRendererCallback(callback: (...args: any[]) => void): number {
|
||||
rendererCallbacks.set(nextRendererCallbackId, callback);
|
||||
return nextRendererCallbackId++;
|
||||
}
|
||||
|
||||
const APP_ICON = path.join(__dirname, '../resources', 'Icon');
|
||||
|
||||
const iconPath = (): string => APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
|
||||
|
||||
// Toggle the app window
|
||||
const toggleApp = (): void => {
|
||||
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
|
||||
mainWindow.show();
|
||||
} else {
|
||||
mainWindow.hide();
|
||||
}
|
||||
};
|
||||
|
||||
function createMainWindow(): Electron.BrowserWindow {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState: windowStateKeeper.State = windowStateKeeper({
|
||||
defaultWidth: 1100,
|
||||
defaultHeight: 720,
|
||||
path: `${app.getPath('userData')}/config`
|
||||
});
|
||||
|
||||
const win = new electron.BrowserWindow({
|
||||
// This settings needs to be saved in config
|
||||
title: 'Zulip',
|
||||
icon: iconPath(),
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 500,
|
||||
minHeight: 400,
|
||||
webPreferences: {
|
||||
plugins: true,
|
||||
nodeIntegration: true,
|
||||
partition: 'persist:webviewsession',
|
||||
webviewTag: true
|
||||
},
|
||||
show: false
|
||||
});
|
||||
|
||||
win.on('focus', () => {
|
||||
win.webContents.send('focus');
|
||||
});
|
||||
|
||||
(async () => win.loadURL(mainURL))();
|
||||
|
||||
// Keep the app running in background on close event
|
||||
win.on('close', event => {
|
||||
if (ConfigUtil.getConfigItem('quitOnClose')) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
if (!isQuitting) {
|
||||
event.preventDefault();
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
app.hide();
|
||||
} else {
|
||||
win.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
win.setTitle('Zulip');
|
||||
|
||||
win.on('enter-full-screen', () => {
|
||||
win.webContents.send('enter-fullscreen');
|
||||
});
|
||||
|
||||
win.on('leave-full-screen', () => {
|
||||
win.webContents.send('leave-fullscreen');
|
||||
});
|
||||
|
||||
// To destroy tray icon when navigate to a new URL
|
||||
win.webContents.on('will-navigate', event => {
|
||||
if (event) {
|
||||
win.webContents.send('destroytray');
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Decrease load on GPU (experimental)
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
// Temporary fix for Electron render colors differently
|
||||
// More info here - https://github.com/electron/electron/issues/10732
|
||||
app.commandLine.appendSwitch('force-color-profile', 'srgb');
|
||||
|
||||
// This event is only available on macOS. Triggers when you click on the dock icon.
|
||||
app.on('activate', () => {
|
||||
if (mainWindow) {
|
||||
// If there is already a window show it
|
||||
mainWindow.show();
|
||||
} else {
|
||||
mainWindow = createMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('ready', () => {
|
||||
const ses = session.fromPartition('persist:webviewsession');
|
||||
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);
|
||||
|
||||
ipcMain.on('set-spellcheck-langs', () => {
|
||||
ses.setSpellCheckerLanguages(ConfigUtil.getConfigItem('spellcheckerLanguages'));
|
||||
});
|
||||
AppMenu.setMenu({
|
||||
tabs: []
|
||||
});
|
||||
mainWindow = createMainWindow();
|
||||
|
||||
// Auto-hide menu bar on Windows + Linux
|
||||
if (process.platform !== 'darwin') {
|
||||
const shouldHideMenu = ConfigUtil.getConfigItem('autoHideMenubar') || false;
|
||||
mainWindow.autoHideMenuBar = shouldHideMenu;
|
||||
mainWindow.setMenuBarVisibility(!shouldHideMenu);
|
||||
}
|
||||
|
||||
// Initialize sentry for main process
|
||||
const errorReporting = ConfigUtil.getConfigItem('errorReporting');
|
||||
if (errorReporting) {
|
||||
sentryInit();
|
||||
}
|
||||
|
||||
const isSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
|
||||
|
||||
if (isSystemProxy) {
|
||||
(async () => ProxyUtil.resolveSystemProxy(mainWindow))();
|
||||
}
|
||||
|
||||
const page = mainWindow.webContents;
|
||||
|
||||
page.on('dom-ready', () => {
|
||||
if (ConfigUtil.getConfigItem('startMinimized')) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('fetch-user-agent', event => {
|
||||
event.returnValue = session.fromPartition('persist:webviewsession').getUserAgent();
|
||||
});
|
||||
|
||||
ipcMain.handle('get-server-settings', async (event, domain: string) => _getServerSettings(domain, ses));
|
||||
|
||||
ipcMain.handle('save-server-icon', async (event, url: string) => _saveServerIcon(url, ses));
|
||||
|
||||
ipcMain.handle('is-online', async (event, url: string) => _isOnline(url, ses));
|
||||
|
||||
page.once('did-frame-finish-load', async () => {
|
||||
// Initiate auto-updates on MacOS and Windows
|
||||
if (ConfigUtil.getConfigItem('autoUpdate')) {
|
||||
await appUpdater();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('certificate-error', (
|
||||
event: Event,
|
||||
webContents: Electron.WebContents,
|
||||
urlString: string,
|
||||
error: string,
|
||||
certificate: Electron.Certificate,
|
||||
callback: (isTrusted: boolean) => void
|
||||
) /* eslint-disable-line max-params */ => {
|
||||
// TODO: The entire concept of selectively ignoring certificate errors
|
||||
// is ill-conceived, and this handler needs to be deleted.
|
||||
|
||||
const url = new URL(urlString);
|
||||
if (url.protocol === 'wss:') {
|
||||
url.protocol = 'https:';
|
||||
}
|
||||
|
||||
const filename = CertificateUtil.getCertificate(encodeURIComponent(url.origin));
|
||||
if (filename !== undefined) {
|
||||
try {
|
||||
const savedCertificate = fs.readFileSync(
|
||||
path.join(`${app.getPath('userData')}/certificates`, filename),
|
||||
'utf8'
|
||||
);
|
||||
if (certificate.data.replace(/[\r\n]/g, '') ===
|
||||
savedCertificate.replace(/[\r\n]/g, '')) {
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading certificate file ${filename}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
dialog.showErrorBox(
|
||||
'Certificate error',
|
||||
`The server presented an invalid certificate for ${url.origin}:
|
||||
|
||||
${error}`
|
||||
);
|
||||
});
|
||||
|
||||
page.session.setPermissionRequestHandler((webContents, permission, callback, details) => {
|
||||
const {origin} = new URL(details.requestingUrl);
|
||||
page.send('permission-request', {
|
||||
webContentsId: webContents.id === mainWindow.webContents.id ?
|
||||
null :
|
||||
webContents.id,
|
||||
origin,
|
||||
permission
|
||||
}, makeRendererCallback(callback));
|
||||
});
|
||||
|
||||
// Temporarily remove this event
|
||||
// electron.powerMonitor.on('resume', () => {
|
||||
// mainWindow.reload();
|
||||
// page.send('destroytray');
|
||||
// });
|
||||
|
||||
ipcMain.on('focus-app', () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
ipcMain.on('quit-app', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
|
||||
// ipcMain.on('pdf-view', (event, url) => {
|
||||
// // Paddings for pdfWindow so that it fits into the main browserWindow
|
||||
// const paddingWidth = 55;
|
||||
// const paddingHeight = 22;
|
||||
|
||||
// // Get the config of main browserWindow
|
||||
// const mainWindowState = global.mainWindowState;
|
||||
|
||||
// // Window to view the pdf file
|
||||
// const pdfWindow = new electron.BrowserWindow({
|
||||
// x: mainWindowState.x + paddingWidth,
|
||||
// y: mainWindowState.y + paddingHeight,
|
||||
// width: mainWindowState.width - paddingWidth,
|
||||
// height: mainWindowState.height - paddingHeight,
|
||||
// webPreferences: {
|
||||
// plugins: true,
|
||||
// partition: 'persist:webviewsession'
|
||||
// }
|
||||
// });
|
||||
// pdfWindow.loadURL(url);
|
||||
|
||||
// // We don't want to have the menu bar in pdf window
|
||||
// pdfWindow.setMenu(null);
|
||||
// });
|
||||
|
||||
// Reload full app not just webview, useful in debugging
|
||||
ipcMain.on('reload-full-app', () => {
|
||||
mainWindow.reload();
|
||||
page.send('destroytray');
|
||||
});
|
||||
|
||||
ipcMain.on('clear-app-settings', () => {
|
||||
mainWindowState.unmanage();
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
});
|
||||
|
||||
ipcMain.on('toggle-app', () => {
|
||||
toggleApp();
|
||||
});
|
||||
|
||||
ipcMain.on('toggle-badge-option', () => {
|
||||
BadgeSettings.updateBadge(badgeCount, mainWindow);
|
||||
});
|
||||
|
||||
ipcMain.on('toggle-menubar', (_event: Electron.IpcMainEvent, showMenubar: boolean) => {
|
||||
mainWindow.autoHideMenuBar = showMenubar;
|
||||
mainWindow.setMenuBarVisibility(!showMenubar);
|
||||
page.send('toggle-autohide-menubar', showMenubar, true);
|
||||
});
|
||||
|
||||
ipcMain.on('update-badge', (_event: Electron.IpcMainEvent, messageCount: number) => {
|
||||
badgeCount = messageCount;
|
||||
BadgeSettings.updateBadge(badgeCount, mainWindow);
|
||||
page.send('tray', messageCount);
|
||||
});
|
||||
|
||||
ipcMain.on('update-taskbar-icon', (_event: Electron.IpcMainEvent, data: string, text: string) => {
|
||||
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
|
||||
});
|
||||
|
||||
ipcMain.on('forward-message', (_event: Electron.IpcMainEvent, listener: string, ...parameters: unknown[]) => {
|
||||
page.send(listener, ...parameters);
|
||||
});
|
||||
|
||||
ipcMain.on('update-menu', (_event: Electron.IpcMainEvent, props: AppMenu.MenuProps) => {
|
||||
AppMenu.setMenu(props);
|
||||
const activeTab = props.tabs[props.activeTabIndex];
|
||||
if (activeTab) {
|
||||
mainWindow.setTitle(`Zulip - ${activeTab.webview.props.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('toggleAutoLauncher', async (_event: Electron.IpcMainEvent, AutoLaunchValue: boolean) => {
|
||||
await setAutoLaunch(AutoLaunchValue);
|
||||
});
|
||||
|
||||
ipcMain.on('downloadFile', (_event: Electron.IpcMainEvent, url: string, downloadPath: string) => {
|
||||
page.downloadURL(url);
|
||||
page.session.once('will-download', async (_event: Event, item) => {
|
||||
if (ConfigUtil.getConfigItem('promptDownload', false)) {
|
||||
const showDialogOptions: electron.SaveDialogOptions = {
|
||||
defaultPath: path.join(downloadPath, item.getFilename())
|
||||
};
|
||||
item.setSaveDialogOptions(showDialogOptions);
|
||||
} else {
|
||||
const getTimeStamp = (): number => {
|
||||
const date = new Date();
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
const formatFile = (filePath: string): string => {
|
||||
const fileExtension = path.extname(filePath);
|
||||
const baseName = path.basename(filePath, fileExtension);
|
||||
return `${baseName}-${getTimeStamp()}${fileExtension}`;
|
||||
};
|
||||
|
||||
const filePath = path.join(downloadPath, item.getFilename());
|
||||
|
||||
// Update the name and path of the file if it already exists
|
||||
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
|
||||
const setFilePath: string = fs.existsSync(filePath) ? updatedFilePath : filePath;
|
||||
item.setSavePath(setFilePath);
|
||||
}
|
||||
|
||||
const updatedListener = (_event: Event, state: string): void => {
|
||||
switch (state) {
|
||||
case 'interrupted': {
|
||||
// Can interrupted to due to network error, cancel download then
|
||||
console.log('Download interrupted, cancelling and fallback to dialog download.');
|
||||
item.cancel();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'progressing': {
|
||||
if (item.isPaused()) {
|
||||
item.cancel();
|
||||
}
|
||||
|
||||
// This event can also be used to show progress in percentage in future.
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
console.info('Unknown updated state of download item');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
item.on('updated', updatedListener);
|
||||
item.once('done', (_event: Event, state) => {
|
||||
if (state === 'completed') {
|
||||
page.send('downloadFileCompleted', item.getSavePath(), path.basename(item.getSavePath()));
|
||||
} else {
|
||||
console.log('Download failed state:', state);
|
||||
page.send('downloadFileFailed', state);
|
||||
}
|
||||
|
||||
// To stop item for listening to updated events of this file
|
||||
item.removeListener('updated', updatedListener);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('realm-name-changed', (_event: Electron.IpcMainEvent, serverURL: string, realmName: string) => {
|
||||
page.send('update-realm-name', serverURL, realmName);
|
||||
});
|
||||
|
||||
ipcMain.on('realm-icon-changed', (_event: Electron.IpcMainEvent, serverURL: string, iconURL: string) => {
|
||||
page.send('update-realm-icon', serverURL, iconURL);
|
||||
});
|
||||
|
||||
// Using event.sender.send instead of page.send here to
|
||||
// make sure the value of errorReporting is sent only once on load.
|
||||
ipcMain.on('error-reporting', (event: Electron.IpcMainEvent) => {
|
||||
event.sender.send('error-reporting-val', errorReporting);
|
||||
});
|
||||
|
||||
ipcMain.on('save-last-tab', (_event: Electron.IpcMainEvent, index: number) => {
|
||||
ConfigUtil.setConfigItem('lastActiveTab', index);
|
||||
});
|
||||
|
||||
// Update user idle status for each realm after every 15s
|
||||
const idleCheckInterval = 15 * 1000; // 15 seconds
|
||||
setInterval(() => {
|
||||
// Set user idle if no activity in 1 second (idleThresholdSeconds)
|
||||
const idleThresholdSeconds = 1; // 1 second
|
||||
const idleState = electron.powerMonitor.getSystemIdleState(idleThresholdSeconds);
|
||||
if (idleState === 'active') {
|
||||
page.send('set-active');
|
||||
} else {
|
||||
page.send('set-idle');
|
||||
}
|
||||
}, idleCheckInterval);
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true;
|
||||
});
|
||||
|
||||
// Send crash reports
|
||||
process.on('uncaughtException', err => {
|
||||
console.error(err);
|
||||
console.error(err.stack);
|
||||
});
|
||||
43
app/main/linuxupdater.ts
Normal file
43
app/main/linuxupdater.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {app, Notification, net} from 'electron';
|
||||
|
||||
import getStream from 'get-stream';
|
||||
import semver from 'semver';
|
||||
import * as ConfigUtil from '../renderer/js/utils/config-util';
|
||||
import * as LinuxUpdateUtil from '../renderer/js/utils/linux-update-util';
|
||||
import Logger from '../renderer/js/utils/logger-util';
|
||||
import {fetchResponse} from './request';
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'linux-update-util.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
export async function linuxUpdateNotification(session: Electron.session): Promise<void> {
|
||||
let url = 'https://api.github.com/repos/zulip/zulip-desktop/releases';
|
||||
url = ConfigUtil.getConfigItem('betaUpdate') ? url : url + '/latest';
|
||||
|
||||
try {
|
||||
const response = await fetchResponse(net.request({url, session}));
|
||||
if (response.statusCode !== 200) {
|
||||
logger.log('Linux update response status: ', response.statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(await getStream(response));
|
||||
const latestVersion = ConfigUtil.getConfigItem('betaUpdate') ? data[0].tag_name : data.tag_name;
|
||||
if (typeof latestVersion !== 'string') {
|
||||
throw new TypeError('Expected string for tag_name');
|
||||
}
|
||||
|
||||
if (semver.gt(latestVersion, app.getVersion())) {
|
||||
const notified = LinuxUpdateUtil.getUpdateItem(latestVersion);
|
||||
if (notified === null) {
|
||||
new Notification({title: 'Zulip Update', body: `A new version ${latestVersion} is available. Please update using your package manager.`}).show();
|
||||
LinuxUpdateUtil.setUpdateItem(latestVersion, true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Linux update error.');
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
387
app/main/menu.js
387
app/main/menu.js
@@ -1,387 +0,0 @@
|
||||
'use strict';
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { app, shell, BrowserWindow, Menu } = require('electron');
|
||||
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');
|
||||
|
||||
const appName = app.getName();
|
||||
|
||||
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'
|
||||
});
|
||||
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');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
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) {
|
||||
AppMenu.sendAction('shortcut');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Reset App Settings',
|
||||
accelerator: 'Command+Shift+D',
|
||||
click() {
|
||||
AppMenu.resetAppSettings();
|
||||
}
|
||||
}, {
|
||||
label: 'Log Out',
|
||||
accelerator: 'Cmd+L',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('log-out');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
role: 'services',
|
||||
submenu: []
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
role: 'hide'
|
||||
}, {
|
||||
role: 'hideothers'
|
||||
}, {
|
||||
role: 'unhide'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
role: 'quit'
|
||||
}]
|
||||
}, {
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
role: 'undo'
|
||||
}, {
|
||||
role: 'redo'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
role: 'cut'
|
||||
}, {
|
||||
role: 'copy'
|
||||
}, {
|
||||
role: 'paste'
|
||||
}, {
|
||||
role: 'pasteandmatchstyle'
|
||||
}, {
|
||||
role: 'delete'
|
||||
}, {
|
||||
role: 'selectall'
|
||||
}]
|
||||
}, {
|
||||
label: 'View',
|
||||
submenu: this.getViewSubmenu()
|
||||
}, {
|
||||
label: 'History',
|
||||
submenu: this.getHistorySubmenu()
|
||||
}, {
|
||||
label: 'Window',
|
||||
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
|
||||
}, {
|
||||
role: 'help',
|
||||
submenu: this.getHelpSubmenu()
|
||||
}];
|
||||
}
|
||||
|
||||
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: 'Settings',
|
||||
accelerator: 'Ctrl+,',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('open-settings');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Keyboard Shortcuts',
|
||||
accelerator: 'Ctrl+K',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('shortcut');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Reset App Settings',
|
||||
accelerator: 'Ctrl+Shift+D',
|
||||
click() {
|
||||
AppMenu.resetAppSettings();
|
||||
}
|
||||
}, {
|
||||
label: 'Log Out',
|
||||
accelerator: 'Ctrl+L',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('log-out');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
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: this.getViewSubmenu()
|
||||
}, {
|
||||
label: 'History',
|
||||
submenu: this.getHistorySubmenu()
|
||||
}, {
|
||||
label: 'Window',
|
||||
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
|
||||
}, {
|
||||
role: 'help',
|
||||
submenu: this.getHelpSubmenu()
|
||||
}];
|
||||
}
|
||||
|
||||
static sendAction(action, ...params) {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
win.restore();
|
||||
}
|
||||
|
||||
win.webContents.send(action, ...params);
|
||||
}
|
||||
|
||||
static resetAppSettings() {
|
||||
const getAppPath = path.join(app.getPath('appData'), appName, 'window-state.json');
|
||||
|
||||
fs.unlink(getAppPath, () => {
|
||||
setTimeout(() => AppMenu.sendAction('clear-app-data'), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
570
app/main/menu.ts
Normal file
570
app/main/menu.ts
Normal file
@@ -0,0 +1,570 @@
|
||||
import {app, shell, BrowserWindow, Menu} from 'electron';
|
||||
import {appUpdater} from './autoupdater';
|
||||
|
||||
import AdmZip from 'adm-zip';
|
||||
import * as DNDUtil from '../renderer/js/utils/dnd-util';
|
||||
import * as ConfigUtil from '../renderer/js/utils/config-util';
|
||||
import * as LinkUtil from '../renderer/js/utils/link-util';
|
||||
import * as t from '../renderer/js/utils/translation-util';
|
||||
import type {ServerOrFunctionalTab} from '../renderer/js/main';
|
||||
|
||||
export interface MenuProps {
|
||||
tabs: ServerOrFunctionalTab[];
|
||||
activeTabIndex?: number;
|
||||
enableMenu?: boolean;
|
||||
}
|
||||
|
||||
const appName = app.name;
|
||||
|
||||
function getHistorySubmenu(enableMenu: boolean): Electron.MenuItemConstructorOptions[] {
|
||||
return [{
|
||||
label: t.__('Back'),
|
||||
accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left',
|
||||
enabled: enableMenu,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('back');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Forward'),
|
||||
accelerator: process.platform === 'darwin' ? 'Command+Right' : 'Alt+Right',
|
||||
enabled: enableMenu,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('forward');
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
|
||||
return [{
|
||||
label: t.__('Check for Updates'),
|
||||
async click() {
|
||||
await checkForUpdate();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t.__('Release Notes'),
|
||||
async click() {
|
||||
await LinkUtil.openBrowser(new URL(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`));
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: t.__('Download App Logs'),
|
||||
click() {
|
||||
const zip = new AdmZip();
|
||||
const date = new Date();
|
||||
const dateString = date.toLocaleDateString().replace(/\//g, '-');
|
||||
|
||||
// Create a zip file of all the logs and config data
|
||||
zip.addLocalFolder(`${app.getPath('appData')}/${appName}/Logs`);
|
||||
zip.addLocalFolder(`${app.getPath('appData')}/${appName}/config`);
|
||||
|
||||
// Put the log file in downloads folder
|
||||
const logFilePath = `${app.getPath('downloads')}/Zulip-logs-${dateString}.zip`;
|
||||
zip.writeZip(logFilePath);
|
||||
|
||||
// Open and select the log file
|
||||
shell.showItemInFolder(logFilePath);
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: t.__('Toggle DevTools for Zulip App'),
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.openDevTools({mode: 'undocked'});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t.__('Toggle DevTools for Active Tab'),
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('tab-devtools');
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
|
||||
return [{
|
||||
label: t.__('Reload'),
|
||||
accelerator: 'CommandOrControl+R',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('reload-current-viewer');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Hard Reload'),
|
||||
accelerator: 'CommandOrControl+Shift+R',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('hard-reload');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Toggle Full Screen'),
|
||||
role: 'togglefullscreen'
|
||||
}, {
|
||||
label: t.__('Zoom In'),
|
||||
accelerator: 'CommandOrControl+=',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('zoomIn');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Zoom Out'),
|
||||
accelerator: 'CommandOrControl+-',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('zoomOut');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Actual Size'),
|
||||
accelerator: 'CommandOrControl+0',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('zoomActualSize');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Toggle Tray Icon'),
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.send('toggletray');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Toggle Sidebar'),
|
||||
accelerator: 'CommandOrControl+Shift+S',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
const newValue = !ConfigUtil.getConfigItem('showSidebar');
|
||||
focusedWindow.webContents.send('toggle-sidebar', newValue);
|
||||
ConfigUtil.setConfigItem('showSidebar', newValue);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Auto hide Menu bar'),
|
||||
checked: ConfigUtil.getConfigItem('autoHideMenubar', false),
|
||||
visible: process.platform !== 'darwin',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
const newValue = !ConfigUtil.getConfigItem('autoHideMenubar');
|
||||
focusedWindow.autoHideMenuBar = newValue;
|
||||
focusedWindow.setMenuBarVisibility(!newValue);
|
||||
focusedWindow.webContents.send('toggle-autohide-menubar', newValue);
|
||||
ConfigUtil.setConfigItem('autoHideMenubar', newValue);
|
||||
}
|
||||
},
|
||||
type: 'checkbox'
|
||||
}];
|
||||
}
|
||||
|
||||
function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
|
||||
return [
|
||||
{
|
||||
label: `${appName + ' Desktop'} v${app.getVersion()}`,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: t.__('About Zulip'),
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('open-about');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t.__('Help Center'),
|
||||
click(focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('open-help');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t.__('Report an Issue'),
|
||||
click() {
|
||||
// The goal is to notify the main.html BrowserWindow
|
||||
// which may not be the focused window.
|
||||
BrowserWindow.getAllWindows().forEach(window => {
|
||||
window.webContents.send('open-feedback-modal');
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function getWindowSubmenu(tabs: ServerOrFunctionalTab[], activeTabIndex: number): Electron.MenuItemConstructorOptions[] {
|
||||
const initialSubmenu: Electron.MenuItemConstructorOptions[] = [{
|
||||
label: t.__('Minimize'),
|
||||
role: 'minimize'
|
||||
}, {
|
||||
label: t.__('Close'),
|
||||
role: 'close'
|
||||
}];
|
||||
|
||||
if (tabs.length > 0) {
|
||||
const ShortcutKey = process.platform === 'darwin' ? 'Cmd' : 'Ctrl';
|
||||
initialSubmenu.push({
|
||||
type: 'separator'
|
||||
});
|
||||
tabs.forEach(tab => {
|
||||
// Do not add functional tab settings to list of windows in menu bar
|
||||
if (tab.props.role === 'function' && tab.props.name === 'Settings') {
|
||||
return;
|
||||
}
|
||||
|
||||
initialSubmenu.push({
|
||||
label: tab.props.name,
|
||||
accelerator: tab.props.role === 'function' ? '' : `${ShortcutKey} + ${tab.props.index + 1}`,
|
||||
checked: tab.props.index === activeTabIndex,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('switch-server-tab', tab.props.index);
|
||||
}
|
||||
},
|
||||
type: 'checkbox'
|
||||
});
|
||||
});
|
||||
initialSubmenu.push({
|
||||
type: 'separator'
|
||||
});
|
||||
initialSubmenu.push({
|
||||
label: t.__('Switch to Next Organization'),
|
||||
accelerator: 'Ctrl+Tab',
|
||||
enabled: tabs.length > 1,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('switch-server-tab', getNextServer(tabs, activeTabIndex));
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Switch to Previous Organization'),
|
||||
accelerator: 'Ctrl+Shift+Tab',
|
||||
enabled: tabs.length > 1,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('switch-server-tab', getPreviousServer(tabs, activeTabIndex));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return initialSubmenu;
|
||||
}
|
||||
|
||||
function getDarwinTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
|
||||
const {tabs, activeTabIndex, enableMenu} = props;
|
||||
|
||||
return [{
|
||||
label: app.name,
|
||||
submenu: [{
|
||||
label: t.__('Add Organization'),
|
||||
accelerator: 'Cmd+Shift+N',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('new-server');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Toggle Do Not Disturb'),
|
||||
accelerator: 'Cmd+Shift+M',
|
||||
click() {
|
||||
const dndUtil = DNDUtil.toggle();
|
||||
sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
|
||||
}
|
||||
}, {
|
||||
label: t.__('Desktop Settings'),
|
||||
accelerator: 'Cmd+,',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('open-settings');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Keyboard Shortcuts'),
|
||||
accelerator: 'Cmd+Shift+K',
|
||||
enabled: enableMenu,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('shortcut');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Copy Zulip URL'),
|
||||
accelerator: 'Cmd+Shift+C',
|
||||
enabled: enableMenu,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('copy-zulip-url');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Log Out of Organization'),
|
||||
accelerator: 'Cmd+L',
|
||||
enabled: enableMenu,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('log-out');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Services'),
|
||||
role: 'services',
|
||||
submenu: []
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Hide'),
|
||||
role: 'hide'
|
||||
}, {
|
||||
label: t.__('Hide Others'),
|
||||
role: 'hideOthers'
|
||||
}, {
|
||||
label: t.__('Unhide'),
|
||||
role: 'unhide'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Minimize'),
|
||||
role: 'minimize'
|
||||
}, {
|
||||
label: t.__('Close'),
|
||||
role: 'close'
|
||||
}, {
|
||||
label: t.__('Quit'),
|
||||
role: 'quit'
|
||||
}]
|
||||
}, {
|
||||
label: t.__('Edit'),
|
||||
submenu: [{
|
||||
label: t.__('Undo'),
|
||||
accelerator: 'Cmd+Z',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('undo');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Redo'),
|
||||
accelerator: 'Cmd+Shift+Z',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('redo');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Cut'),
|
||||
role: 'cut'
|
||||
}, {
|
||||
label: t.__('Copy'),
|
||||
role: 'copy'
|
||||
}, {
|
||||
label: t.__('Paste'),
|
||||
role: 'paste'
|
||||
}, {
|
||||
label: t.__('Paste and Match Style'),
|
||||
role: 'pasteAndMatchStyle'
|
||||
}, {
|
||||
label: t.__('Select All'),
|
||||
role: 'selectAll'
|
||||
}]
|
||||
}, {
|
||||
label: t.__('View'),
|
||||
submenu: getViewSubmenu()
|
||||
}, {
|
||||
label: t.__('History'),
|
||||
submenu: getHistorySubmenu(enableMenu)
|
||||
}, {
|
||||
label: t.__('Window'),
|
||||
submenu: getWindowSubmenu(tabs, activeTabIndex)
|
||||
}, {
|
||||
label: t.__('Tools'),
|
||||
submenu: getToolsSubmenu()
|
||||
}, {
|
||||
label: t.__('Help'),
|
||||
role: 'help',
|
||||
submenu: getHelpSubmenu()
|
||||
}];
|
||||
}
|
||||
|
||||
function getOtherTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
|
||||
const {tabs, activeTabIndex, enableMenu} = props;
|
||||
return [{
|
||||
label: t.__('File'),
|
||||
submenu: [{
|
||||
label: t.__('Add Organization'),
|
||||
accelerator: 'Ctrl+Shift+N',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('new-server');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Toggle Do Not Disturb'),
|
||||
accelerator: 'Ctrl+Shift+M',
|
||||
click() {
|
||||
const dndUtil = DNDUtil.toggle();
|
||||
sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
|
||||
}
|
||||
}, {
|
||||
label: t.__('Desktop Settings'),
|
||||
accelerator: 'Ctrl+,',
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('open-settings');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Keyboard Shortcuts'),
|
||||
accelerator: 'Ctrl+Shift+K',
|
||||
enabled: enableMenu,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('shortcut');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Copy Zulip URL'),
|
||||
accelerator: 'Ctrl+Shift+C',
|
||||
enabled: enableMenu,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('copy-zulip-url');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: t.__('Log Out of Organization'),
|
||||
accelerator: 'Ctrl+L',
|
||||
enabled: enableMenu,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction('log-out');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Minimize'),
|
||||
role: 'minimize'
|
||||
}, {
|
||||
label: t.__('Close'),
|
||||
role: 'close'
|
||||
}, {
|
||||
label: t.__('Quit'),
|
||||
role: 'quit',
|
||||
accelerator: 'Ctrl+Q'
|
||||
}]
|
||||
}, {
|
||||
label: t.__('Edit'),
|
||||
submenu: [{
|
||||
label: t.__('Undo'),
|
||||
role: 'undo'
|
||||
}, {
|
||||
label: t.__('Redo'),
|
||||
role: 'redo'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Cut'),
|
||||
role: 'cut'
|
||||
}, {
|
||||
label: t.__('Copy'),
|
||||
role: 'copy'
|
||||
}, {
|
||||
label: t.__('Paste'),
|
||||
role: 'paste'
|
||||
}, {
|
||||
label: t.__('Paste and Match Style'),
|
||||
role: 'pasteAndMatchStyle'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: t.__('Select All'),
|
||||
role: 'selectAll'
|
||||
}]
|
||||
}, {
|
||||
label: t.__('View'),
|
||||
submenu: getViewSubmenu()
|
||||
}, {
|
||||
label: t.__('History'),
|
||||
submenu: getHistorySubmenu(enableMenu)
|
||||
}, {
|
||||
label: t.__('Window'),
|
||||
submenu: getWindowSubmenu(tabs, activeTabIndex)
|
||||
}, {
|
||||
label: t.__('Tools'),
|
||||
submenu: getToolsSubmenu()
|
||||
}, {
|
||||
label: t.__('Help'),
|
||||
role: 'help',
|
||||
submenu: getHelpSubmenu()
|
||||
}];
|
||||
}
|
||||
|
||||
function sendAction(action: string, ...parameters: unknown[]): void {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
win.restore();
|
||||
}
|
||||
|
||||
win.webContents.send(action, ...parameters);
|
||||
}
|
||||
|
||||
async function checkForUpdate(): Promise<void> {
|
||||
await appUpdater(true);
|
||||
}
|
||||
|
||||
function getNextServer(tabs: ServerOrFunctionalTab[], activeTabIndex: number): number {
|
||||
do {
|
||||
activeTabIndex = (activeTabIndex + 1) % tabs.length;
|
||||
}
|
||||
while (tabs[activeTabIndex].props.role !== 'server');
|
||||
|
||||
return activeTabIndex;
|
||||
}
|
||||
|
||||
function getPreviousServer(tabs: ServerOrFunctionalTab[], activeTabIndex: number): number {
|
||||
do {
|
||||
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
|
||||
}
|
||||
while (tabs[activeTabIndex].props.role !== 'server');
|
||||
|
||||
return activeTabIndex;
|
||||
}
|
||||
|
||||
export function setMenu(props: MenuProps): void {
|
||||
const tpl = process.platform === 'darwin' ? getDarwinTpl(props) : getOtherTpl(props);
|
||||
const menu = Menu.buildFromTemplate(tpl);
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
111
app/main/request.ts
Normal file
111
app/main/request.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {ClientRequest, IncomingMessage, app, net} from 'electron';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import stream from 'stream';
|
||||
import util from 'util';
|
||||
import * as Messages from '../resources/messages';
|
||||
import Logger from '../renderer/js/utils/logger-util';
|
||||
import {ServerConf} from '../renderer/js/utils/domain-util';
|
||||
import escape from 'escape-html';
|
||||
import getStream from 'get-stream';
|
||||
|
||||
export async function fetchResponse(request: ClientRequest): Promise<IncomingMessage> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.on('response', resolve);
|
||||
request.on('abort', () => reject(new Error('Request aborted')));
|
||||
request.on('error', reject);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
/* Request: domain-util */
|
||||
|
||||
const defaultIconUrl = '../renderer/img/icon.png';
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'domain-util.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
const generateFilePath = (url: string): string => {
|
||||
const dir = `${app.getPath('userData')}/server-icons`;
|
||||
const extension = path.extname(url).split('?')[0];
|
||||
|
||||
let hash = 5381;
|
||||
let {length} = url;
|
||||
|
||||
while (length) {
|
||||
hash = (hash * 33) ^ url.charCodeAt(--length);
|
||||
}
|
||||
|
||||
// Create 'server-icons' directory if not existed
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir);
|
||||
}
|
||||
|
||||
return `${dir}/${hash >>> 0}${extension}`;
|
||||
};
|
||||
|
||||
export const _getServerSettings = async (domain: string, session: Electron.session): Promise<ServerConf> => {
|
||||
const response = await fetchResponse(net.request({
|
||||
url: domain + '/api/v1/server_settings',
|
||||
session
|
||||
}));
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(Messages.invalidZulipServerError(domain));
|
||||
}
|
||||
|
||||
const {realm_name, realm_uri, realm_icon} = JSON.parse(await getStream(response));
|
||||
if (
|
||||
typeof realm_name !== 'string' ||
|
||||
typeof realm_uri !== 'string' ||
|
||||
typeof realm_icon !== 'string'
|
||||
) {
|
||||
throw new TypeError(Messages.noOrgsError(domain));
|
||||
}
|
||||
|
||||
return {
|
||||
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
|
||||
// Following check handles both the cases
|
||||
icon: realm_icon.startsWith('/') ? realm_uri + realm_icon : realm_icon,
|
||||
url: realm_uri,
|
||||
alias: escape(realm_name)
|
||||
};
|
||||
};
|
||||
|
||||
export const _saveServerIcon = async (url: string, session: Electron.session): Promise<string> => {
|
||||
try {
|
||||
const response = await fetchResponse(net.request({url, session}));
|
||||
if (response.statusCode !== 200) {
|
||||
logger.log('Could not get server icon.');
|
||||
return defaultIconUrl;
|
||||
}
|
||||
|
||||
const filePath = generateFilePath(url);
|
||||
await pipeline(response, fs.createWriteStream(filePath));
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
logger.log('Could not get server icon.');
|
||||
logger.log(error);
|
||||
logger.reportSentry(error);
|
||||
return defaultIconUrl;
|
||||
}
|
||||
};
|
||||
|
||||
/* Request: reconnect-util */
|
||||
|
||||
export const _isOnline = async (url: string, session: Electron.session): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetchResponse(net.request({
|
||||
url: `${url}/static/favicon.ico`,
|
||||
session
|
||||
}));
|
||||
const isValidResponse = response.statusCode >= 200 && response.statusCode < 400;
|
||||
return isValidResponse;
|
||||
} catch (error) {
|
||||
logger.log(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
'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
|
||||
};
|
||||
32
app/main/startup.ts
Normal file
32
app/main/startup.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {app} from 'electron';
|
||||
|
||||
import AutoLaunch from 'auto-launch';
|
||||
import isDev from 'electron-is-dev';
|
||||
import * as ConfigUtil from '../renderer/js/utils/config-util';
|
||||
|
||||
export const setAutoLaunch = async (AutoLaunchValue: boolean): Promise<void> => {
|
||||
// Don't run this in development
|
||||
if (isDev) {
|
||||
return;
|
||||
}
|
||||
|
||||
const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue);
|
||||
|
||||
// `setLoginItemSettings` doesn't support linux
|
||||
if (process.platform === 'linux') {
|
||||
const ZulipAutoLauncher = new AutoLaunch({
|
||||
name: 'Zulip',
|
||||
isHidden: false
|
||||
});
|
||||
if (autoLaunchOption) {
|
||||
await ZulipAutoLauncher.enable();
|
||||
} else {
|
||||
await ZulipAutoLauncher.disable();
|
||||
}
|
||||
} else {
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: autoLaunchOption,
|
||||
openAsHidden: false
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "zulip",
|
||||
"productName": "Zulip",
|
||||
"version": "1.5.0",
|
||||
"description": "Zulip Desktop App",
|
||||
"license": "Apache-2.0",
|
||||
"email": "<svnitakash@gmail.com>",
|
||||
"copyright": "©2017 Kandra Labs, Inc.",
|
||||
"author": {
|
||||
"name": "Kandra Labs, Inc.",
|
||||
"email": "svnitakash@gmail.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zulip/zulip-electron.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/zulip/zulip-electron/issues"
|
||||
},
|
||||
"main": "main/index.js",
|
||||
"keywords": [
|
||||
"Zulip",
|
||||
"Group Chat app",
|
||||
"electron-app",
|
||||
"electron",
|
||||
"Desktop app",
|
||||
"InstantMessaging"
|
||||
],
|
||||
"dependencies": {
|
||||
"electron-debug": "1.4.0",
|
||||
"electron-is-dev": "0.3.0",
|
||||
"electron-localshortcut": "2.0.2",
|
||||
"electron-log": "2.2.7",
|
||||
"electron-spellchecker": "1.2.0",
|
||||
"electron-updater": "2.8.9",
|
||||
"node-json-db": "0.7.3",
|
||||
"request": "2.81.0",
|
||||
"wurl": "2.5.0",
|
||||
"electron-window-state": "4.1.1",
|
||||
"auto-launch": "5.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="css/about.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="about">
|
||||
<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();
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="css/about.css">
|
||||
<title>Zulip - About</title>
|
||||
</head>
|
||||
|
||||
function linkInBrowser(event) {
|
||||
<body>
|
||||
<div class="about">
|
||||
<img class="logo" src="../resources/zulip.png" />
|
||||
<p class="detail" id="version">v?.?.?</p>
|
||||
<div class="maintenance-info">
|
||||
<p class="detail maintainer">
|
||||
Maintained by
|
||||
<a href="https://zulip.com" target="_blank" rel="noopener noreferrer">Zulip</a>
|
||||
</p>
|
||||
<p class="detail license">
|
||||
Available under the
|
||||
<a href="https://github.com/zulip/zulip-desktop/blob/master/LICENSE" target="_blank" rel="noopener noreferrer">Apache 2.0 License</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
const shell = require('electron').shell;
|
||||
|
||||
const url = "https://github.com/zulip/zulip-electron/issues/new?body=Please describe your issue and steps to reproduce it."
|
||||
|
||||
shell.openExternal(url);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
const { app } = require('electron').remote;
|
||||
const version_tag = document.querySelector('#version');
|
||||
version_tag.innerHTML = 'v' + app.getVersion();
|
||||
</script>
|
||||
<script>require('./js/shared/preventdrag.js')</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,73 +1,66 @@
|
||||
body {
|
||||
background: #fafafa;
|
||||
font-family: menu, "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: rgba(250, 250, 250, 1.000);
|
||||
font-family: menu, "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
margin: -40px auto;
|
||||
}
|
||||
|
||||
#version {
|
||||
color: #aaa;
|
||||
font-size: 0.9em;
|
||||
color: rgba(68, 67, 67, 1.000);
|
||||
font-size: 1.3em;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.about {
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
margin: 25vh auto;
|
||||
height: 25vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about p {
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, 0.62);
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, 0.62);
|
||||
}
|
||||
|
||||
.about img {
|
||||
width: 150px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail.maintainer {
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail.license {
|
||||
font-size: 0.8em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.maintenance-info {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 20px;
|
||||
left: 0px;
|
||||
color: #444;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
color: rgba(68, 68, 68, 1.000);
|
||||
}
|
||||
|
||||
.maintenance-info p {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
|
||||
width: 100%;
|
||||
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;
|
||||
p.detail a {
|
||||
color: rgba(53, 95, 76, 1.000);
|
||||
}
|
||||
|
||||
.maintenance-info .bug:hover {
|
||||
background-color: #32a692;
|
||||
p.detail a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
19
app/renderer/css/feedback.css
Normal file
19
app/renderer/css/feedback.css
Normal file
@@ -0,0 +1,19 @@
|
||||
:host {
|
||||
--button-color: rgb(69, 166, 149);
|
||||
}
|
||||
|
||||
button:hover,
|
||||
button:focus {
|
||||
border-color: var(--button-color);
|
||||
color: var(--button-color);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: rgb(241, 241, 241);
|
||||
color: var(--button-color);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--button-color);
|
||||
border-color: var(--button-color);
|
||||
}
|
||||
@@ -13,13 +13,10 @@ body {
|
||||
#content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: #eee url(../img/ic_loading.gif) no-repeat;
|
||||
background-size: 60px 60px;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.toggle-sidebar {
|
||||
background: #222c31;
|
||||
background: rgba(34, 44, 49, 1.000);
|
||||
width: 54px;
|
||||
padding: 27px 0 20px 0;
|
||||
justify-content: space-between;
|
||||
@@ -28,6 +25,7 @@ body {
|
||||
-webkit-app-region: drag;
|
||||
overflow: hidden;
|
||||
transition: all 0.5s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.toggle-sidebar div {
|
||||
@@ -44,6 +42,28 @@ body {
|
||||
transition: all 0.6s ease-out;
|
||||
}
|
||||
|
||||
#view-controls-container {
|
||||
height: calc(100% - 208px);
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#view-controls-container:hover {
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
||||
#view-controls-container::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
#view-controls-container::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#view-controls-container::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(169, 169, 169, 1.000);
|
||||
outline: 1px solid rgba(169, 169, 169, 1.000);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
@@ -81,11 +101,17 @@ body {
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
#actions-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
@@ -93,16 +119,39 @@ body {
|
||||
}
|
||||
|
||||
.action-button i {
|
||||
color: #6c8592;
|
||||
color: rgba(108, 133, 146, 1.000);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.action-button:hover i {
|
||||
color: #98a9b3;
|
||||
color: rgba(152, 169, 179, 1.000);
|
||||
}
|
||||
|
||||
.action-button.disable {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.action-button.disable:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button.disable:hover i {
|
||||
color: rgba(108, 133, 146, 1.000);
|
||||
}
|
||||
|
||||
.action-button.active {
|
||||
/* background-color: rgba(255, 255, 255, 0.25); */
|
||||
background-color: rgba(239, 239, 239, 1.000);
|
||||
opacity: 0.9;
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
.action-button.active i {
|
||||
color: rgba(28, 38, 43, 1.000);
|
||||
}
|
||||
|
||||
.tab:first-child {
|
||||
margin-top: 8px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@@ -116,25 +165,33 @@ body {
|
||||
}
|
||||
|
||||
.tab .server-icons {
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
padding: 3px;
|
||||
height: 30px;
|
||||
width: 35px;
|
||||
vertical-align: top;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab .server-tab {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
position: relative;
|
||||
margin: 5px 0 2px 0;
|
||||
margin-top: 5px;
|
||||
z-index: 11;
|
||||
line-height: 31px;
|
||||
color: #eee;
|
||||
color: rgba(238, 238, 238, 1.000);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
opacity: 0.6;
|
||||
padding: 2px 0;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.server-tab .alt-icon {
|
||||
font-family: Verdana;
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
border: 2px solid rgba(34, 44, 49, 1.000);
|
||||
margin-left: 17%;
|
||||
width: 35px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab .server-tab:hover {
|
||||
@@ -159,7 +216,7 @@ body {
|
||||
|
||||
.tab.active .server-tab {
|
||||
opacity: 1;
|
||||
background-color: #648478;
|
||||
background-color: rgba(100, 132, 120, 1.000);
|
||||
}
|
||||
|
||||
.tab .server-tab-badge.active {
|
||||
@@ -167,7 +224,7 @@ body {
|
||||
min-width: 11px;
|
||||
padding: 0 3px;
|
||||
height: 17px;
|
||||
background-color: #f44336;
|
||||
background-color: rgba(244, 67, 54, 1.000);
|
||||
font-size: 10px;
|
||||
font-family: sans-serif;
|
||||
position: absolute;
|
||||
@@ -175,7 +232,7 @@ body {
|
||||
z-index: 15;
|
||||
top: 6px;
|
||||
float: right;
|
||||
color: #fff;
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
text-align: center;
|
||||
line-height: 17px;
|
||||
display: block;
|
||||
@@ -203,13 +260,25 @@ body {
|
||||
}
|
||||
|
||||
.tab .server-tab-shortcut {
|
||||
color: #648478;
|
||||
color: rgba(100, 132, 120, 1.000);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-family: sans-serif;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.refresh {
|
||||
animation: rotate-loader 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate-loader {
|
||||
from {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************
|
||||
* Webview Area *
|
||||
@@ -221,53 +290,84 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Pseudo element for loading indicator */
|
||||
#webviews-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background: rgba(255, 255, 255, 1.000) url(../img/ic_loading.gif) no-repeat;
|
||||
background-size: 60px 60px;
|
||||
background-position: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* When the active webview is loaded */
|
||||
#webviews-container.loaded::before {
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
webview {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in;
|
||||
/* transition: opacity 0.3s ease-in; */
|
||||
flex-grow: 1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
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.active {
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
webview:focus {
|
||||
webview.disabled {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
webview.focus {
|
||||
outline: 0px solid transparent;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip styling */
|
||||
|
||||
#loading-tooltip,
|
||||
#dnd-tooltip,
|
||||
#back-tooltip,
|
||||
#reload-tooltip,
|
||||
#setting-tooltip {
|
||||
font-family: sans-serif;
|
||||
background: #222c31;
|
||||
margin-left: 68px;
|
||||
background: rgba(34, 44, 49, 1.000);
|
||||
margin-left: 48px;
|
||||
padding: 6px 8px;
|
||||
position: absolute;
|
||||
margin-top: 0px;
|
||||
z-index: 1000;
|
||||
color: white;
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: 55px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#reload-tooltip:after,
|
||||
#setting-tooltip:after {
|
||||
#loading-tooltip::after,
|
||||
#dnd-tooltip::after,
|
||||
#back-tooltip::after,
|
||||
#reload-tooltip::after,
|
||||
#setting-tooltip::after {
|
||||
content: " ";
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-right: 8px solid #222c31;
|
||||
border-right: 8px solid rgba(34, 44, 49, 1.000);
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 68px;
|
||||
@@ -276,25 +376,25 @@ webview:focus {
|
||||
#add-server-tooltip,
|
||||
.server-tooltip {
|
||||
font-family: 'arial';
|
||||
background: #222c31;
|
||||
background: rgba(34, 44, 49, 1.000);
|
||||
left: 56px;
|
||||
padding: 10px 20px;
|
||||
position: fixed;
|
||||
margin-top: 8px;
|
||||
margin-top: 11px;
|
||||
z-index: 5000 !important;
|
||||
color: #fff;
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: max-content;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#add-server-tooltip:after,
|
||||
.server-tooltip:after {
|
||||
#add-server-tooltip::after,
|
||||
.server-tooltip::after {
|
||||
content: " ";
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-right: 8px solid #222c31;
|
||||
border-right: 8px solid rgba(34, 44, 49, 1.000);
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -5px;
|
||||
@@ -306,14 +406,14 @@ webview:focus {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #222c31;
|
||||
background: rgba(34, 44, 49, 1.000);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
box-shadow: #999 1px 1px;
|
||||
box-shadow: rgba(153, 153, 153, 1.000) 1px 1px;
|
||||
}
|
||||
|
||||
#collapse-button i {
|
||||
color: #efefef;
|
||||
color: rgba(239, 239, 239, 1.000);
|
||||
}
|
||||
|
||||
#main-container {
|
||||
@@ -321,6 +421,8 @@ webview:focus {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
@@ -332,8 +434,8 @@ webview:focus {
|
||||
|
||||
.popup .popuptext {
|
||||
visibility: hidden;
|
||||
background-color: #555;
|
||||
color: #fff;
|
||||
background-color: rgba(85, 85, 85, 1.000);
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 9px 0;
|
||||
@@ -362,4 +464,27 @@ webview:focus {
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send-feedback {
|
||||
width: 60%;
|
||||
height: 85%;
|
||||
}
|
||||
|
||||
#feedback-modal {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(68, 67, 67, 0.81);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
transition: all 1s ease-out;
|
||||
}
|
||||
|
||||
#feedback-modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
user-select:none;
|
||||
color: rgba(51, 51, 51, 1.000);
|
||||
background: rgba(255, 255, 255, 1.000);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#content {
|
||||
@@ -16,27 +17,43 @@ html, body {
|
||||
}
|
||||
|
||||
#title {
|
||||
text-align: left;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#subtitle {
|
||||
font-size: 20px;
|
||||
text-align: left;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
#description {
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
#reconnect {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#settings {
|
||||
margin-left: 116px;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 16px;
|
||||
background: #009688;
|
||||
color: #fff;
|
||||
width: 84px;
|
||||
background: rgba(0, 150, 136, 1.000);
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
width: 96px;
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
line-height: 32px;
|
||||
margin: 20px auto 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#reconnect:hover {
|
||||
.button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
@@ -5,79 +5,89 @@ body {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
font-family: menu, "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #efefef;
|
||||
background: rgba(239, 239, 239, 1.000);
|
||||
letter-spacing: -.08px;
|
||||
line-height: 18px;
|
||||
color: rgba(139, 142, 143, 1.000);
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.3em 0.8em;
|
||||
border: 1px solid #ccc;
|
||||
display: inline-block;
|
||||
border: 1px solid rgba(204, 204, 204, 1.000);
|
||||
border-radius: 4px;
|
||||
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;
|
||||
background-color: rgba(247, 247, 247, 1.000);
|
||||
color: rgba(51, 51, 51, 1.000);
|
||||
margin: 0 0.1em;
|
||||
padding: 0.3em 0.8em;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 1.000);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
color: #383430;
|
||||
color: rgba(56, 52, 48, 1.000);
|
||||
}
|
||||
|
||||
table {
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
table tr:nth-child(even) { background-color: #f7eee6; }
|
||||
table tr:nth-child(even) {
|
||||
background-color: rgba(250, 250, 250, 1.000);
|
||||
}
|
||||
|
||||
table tr:nth-child(odd) { background-color: #fff8ef; }
|
||||
table tr:nth-child(odd) {
|
||||
background-color: rgba(255, 255, 255, 1.000);
|
||||
}
|
||||
|
||||
|
||||
td { padding: 5px; }
|
||||
td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
td:nth-child(odd) {
|
||||
text-align: right;
|
||||
width: 50%;
|
||||
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-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');
|
||||
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;
|
||||
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 {
|
||||
@@ -87,11 +97,13 @@ td:nth-child(odd) {
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 80px;
|
||||
padding: 30px;
|
||||
width: 150px;
|
||||
min-width: 100px;
|
||||
padding: 30px 30px 30px 35px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
background: rgba(242, 242, 242, 1.000);
|
||||
}
|
||||
|
||||
#nav-container {
|
||||
@@ -99,29 +111,35 @@ td:nth-child(odd) {
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding: 5px 0;
|
||||
color: #999;
|
||||
padding: 7px 0;
|
||||
color: rgba(153, 153, 153, 1.000);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav.active {
|
||||
color: #464e5a;
|
||||
color: rgba(78, 191, 172, 1.000);
|
||||
cursor: default;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav.active::before {
|
||||
background: #464e5a;
|
||||
background: rgba(70, 78, 90, 1.000);
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
height: 18px;
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
/* We don't want to show this in nav item since we have the + button for adding an Organization */
|
||||
|
||||
#nav-AddServer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#settings-header {
|
||||
font-size: 22px;
|
||||
color: #222c31;
|
||||
color: rgba(34, 44, 49, 1.000);
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -134,41 +152,77 @@ td:nth-child(odd) {
|
||||
}
|
||||
|
||||
#new-server-container {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s;
|
||||
padding-left: 42px;
|
||||
padding-top: 25px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 4px 0 6px 0;
|
||||
font-weight: 500;
|
||||
color: rgba(34, 44, 49, 1.000);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: rgba(34, 44, 49, 1.000);
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #222c31;
|
||||
text-transform: uppercase;
|
||||
padding: 4px 0 6px 0;
|
||||
}
|
||||
|
||||
.add-server-info-row {
|
||||
display: flex;
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.add-server-info-right {
|
||||
flex-grow: 1;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
padding: 4px 0 6px 0;
|
||||
font-weight: bold;
|
||||
color: #616161;
|
||||
color: rgba(97, 97, 97, 1.000);
|
||||
}
|
||||
|
||||
img.server-info-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 4px;
|
||||
padding: 4px 4px 4px 10px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.server-info-left {
|
||||
margin: 10px 20px 0 0;
|
||||
margin: 4px 20px 0 0;
|
||||
min-width: 40%;
|
||||
}
|
||||
|
||||
.server-info-right {
|
||||
margin-top: 4px;
|
||||
width: 55%;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.server-info-row {
|
||||
display: flex;
|
||||
margin: 8px 0 0 0;
|
||||
display: inline-block;
|
||||
margin: 5px 0 0 0;
|
||||
}
|
||||
|
||||
.server-info-left .server-info-row {
|
||||
display: inline-flex;
|
||||
align-items: inherit;
|
||||
vertical-align: -2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.server-url {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.server-info-alias {
|
||||
@@ -176,6 +230,10 @@ img.server-info-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.server-url-info {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.setting-input-key {
|
||||
font-size: 14px;
|
||||
height: 27px;
|
||||
@@ -189,26 +247,30 @@ img.server-info-icon {
|
||||
.setting-input-value {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-bottom: #ededed 1px solid;
|
||||
border-radius: 4px;
|
||||
padding: 13px;
|
||||
border: rgba(237, 237, 237, 1.000) 2px solid;
|
||||
outline-width: 0;
|
||||
background: transparent;
|
||||
max-width: 500px;
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
.setting-input-value:focus {
|
||||
border-bottom: #7cb980 1px solid;
|
||||
border: rgba(78, 191, 172, 1.000) 2px solid;
|
||||
}
|
||||
|
||||
.setting-block {
|
||||
width: 100%;
|
||||
.invalid-input-value:focus {
|
||||
border: rgba(239, 83, 80, 1) 2px solid;
|
||||
}
|
||||
|
||||
.manual-proxy-block {
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: #235d3a;
|
||||
color: rgba(35, 93, 58, 1.000);
|
||||
vertical-align: middle;
|
||||
margin: 10px 0;
|
||||
flex-wrap: wrap;
|
||||
@@ -224,11 +286,12 @@ img.server-info-icon {
|
||||
.action i {
|
||||
margin-right: 5px;
|
||||
font-size: 18px;
|
||||
line-height: 27px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.settings-pane {
|
||||
flex-grow: 1;
|
||||
min-width: 550px;
|
||||
}
|
||||
|
||||
.action:hover {
|
||||
@@ -240,7 +303,7 @@ img.server-info-icon {
|
||||
}
|
||||
|
||||
.action.disabled {
|
||||
color: #999;
|
||||
color: rgba(153, 153, 153, 1.000);
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
@@ -248,9 +311,13 @@ img.server-info-icon {
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 30px;
|
||||
margin: 10px 0 20px 0;
|
||||
background: #fff;
|
||||
width: 70%;
|
||||
border-left: 8px solid #bcbcbc;
|
||||
background: rgba(255, 255, 255, 1.000);
|
||||
width: 80%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.settings-card:hover {
|
||||
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 0px 0px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
@@ -259,25 +326,59 @@ img.server-info-icon {
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #ffffff;
|
||||
background: #ef5350;
|
||||
padding: 3px;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
color: rgb(240, 148, 148);
|
||||
background: rgba(255, 255, 255, 1.000);
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
border: 2px solid rgb(240, 148, 148);
|
||||
padding: 10px;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.2s ease;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: #ffffff;
|
||||
background: #4EBFAC;
|
||||
padding: 3px;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
.red:hover {
|
||||
background-color: rgb(240, 148, 148);
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
}
|
||||
|
||||
.green {
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
background: rgba(78, 191, 172, 1.000);
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.2s ease;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.green:hover {
|
||||
background-color: rgba(60, 159, 141, 1.000);
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
}
|
||||
|
||||
.w-150 {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.w-250 {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.grey {
|
||||
color: #9E9E9E;
|
||||
background: #FAFAFA;
|
||||
border: 1px solid #9E9E9E;
|
||||
color: rgba(158, 158, 158, 1.000);
|
||||
background: rgba(250, 250, 250, 1.000);
|
||||
border: 1px solid rgba(158, 158, 158, 1.000);
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
@@ -288,102 +389,369 @@ img.server-info-icon {
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
#note {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: Courier New, Courier, monospace;
|
||||
}
|
||||
|
||||
i.open-tab-button {
|
||||
padding: 0 5px;
|
||||
font-size: 18px;
|
||||
padding-left: 5px;
|
||||
font-size: 19px;
|
||||
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;
|
||||
#css-delete-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
/*
|
||||
.action class will add extra margin to right which
|
||||
we don't want for a button; the extra margin is intended for radio buttons
|
||||
*/
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.reset-data-button:hover {
|
||||
background-color: #3c9f8d;
|
||||
color: #fff;
|
||||
.selected-css-path,
|
||||
.download-folder-path {
|
||||
background: rgba(238, 238, 238, 1.000);
|
||||
padding: 5px 10px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
width: 90%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#remove-custom-css {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
#server-info-container {
|
||||
min-height: calc(100% - 235px);
|
||||
/* min-height: calc(100% - 260px); */
|
||||
}
|
||||
|
||||
#new-org-button {
|
||||
margin: 30px 0px 30px 0px;
|
||||
}
|
||||
|
||||
#create-organization-container {
|
||||
font-size: 1.15em;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
#create-organization-container i {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
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;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
position: absolute;
|
||||
margin-left: -9999px;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
margin-left: -9999px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.toggle + label {
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.disallowed:hover {
|
||||
background-color: rgba(241, 241, 241, 1.000);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input.toggle-round + label {
|
||||
padding: 2px;
|
||||
width: 50px;
|
||||
height: 25px;
|
||||
background-color: #dddddd;
|
||||
border-radius: 25px;
|
||||
padding: 2px;
|
||||
width: 50px;
|
||||
height: 25px;
|
||||
background-color: rgba(221, 221, 221, 1.000);
|
||||
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,
|
||||
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::before {
|
||||
background-color: rgba(241, 241, 241, 1.000);
|
||||
border-radius: 25px;
|
||||
top: 0;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
input.toggle-round + label:after {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-color: #fff;
|
||||
border-radius: 100%;
|
||||
transition: margin 0.4s;
|
||||
|
||||
input.toggle-round + label::after {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-color: rgba(255, 255, 255, 1.000);
|
||||
border-radius: 100%;
|
||||
}
|
||||
input.toggle-round:checked + label:before {
|
||||
background-color: #4EBFAC;
|
||||
|
||||
input.toggle-round:checked + label::before {
|
||||
background-color: rgba(78, 191, 172, 1.000);
|
||||
top: 0;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
input.toggle-round:checked + label::after {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
/* Add new server modal */
|
||||
|
||||
.add-server-modal {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
padding-top: 15vh;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* background: rgba(61, 64, 67, 15); */
|
||||
background: linear-gradient(35deg, rgba(0, 59, 82, 1.000), rgba(69, 181, 155, 1.000));
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Modal Content */
|
||||
|
||||
.modal-container {
|
||||
background-color: rgba(244, 247, 248, 1.000);
|
||||
margin: auto;
|
||||
padding: 57px;
|
||||
border: rgba(218, 225, 227, 1.000) 1px solid;
|
||||
width: 550px;
|
||||
height: 370px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.add-server-modal .page-title {
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin-bottom: 30px;
|
||||
margin-top: 30px;
|
||||
color: rgba(125, 135, 138, 1.000);
|
||||
}
|
||||
|
||||
.divider hr {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
width: 44%;
|
||||
}
|
||||
|
||||
.left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.server-center {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
padding-top: 13px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.server-center button {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
margin: auto;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
background: rgba(78, 191, 172, 1.000);
|
||||
border-color: none;
|
||||
border: none;
|
||||
width: 98%;
|
||||
height: 46px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.server-center button:hover {
|
||||
background: rgba(50, 149, 136, 1.000);
|
||||
}
|
||||
|
||||
.server-center button:focus {
|
||||
background: rgba(50, 149, 136, 1.000);
|
||||
}
|
||||
|
||||
.certificates-card {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.certificate-input {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.certificate-input div {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.certificate-input .setting-input-value {
|
||||
margin-left: 10px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#add-certificate-button {
|
||||
margin: 10px 10px 10px 37px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tip:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.md-14 {
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
#open-hotkeys-link {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.server-network-option {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
margin-top: 10px;
|
||||
padding-top: 15px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: rgb(78, 191, 172);
|
||||
width: 98%;
|
||||
height: 46px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
i.open-network-button {
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding-left: 5px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* responsive grid */
|
||||
@media (min-width: 500px) and (max-width: 720px) {
|
||||
#new-server-container {
|
||||
padding-left: 0px;
|
||||
width: 60vw;
|
||||
padding-right: 4vh;
|
||||
}
|
||||
.page-title {
|
||||
width: 60vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
#new-server-container {
|
||||
padding-left: 0px;
|
||||
width: 54%;
|
||||
}
|
||||
.page-title {
|
||||
width: 54%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
.selected-css-path,
|
||||
.download-folder-path {
|
||||
margin-right: 15px;
|
||||
}
|
||||
#css-delete-action {
|
||||
margin-left: 10px;
|
||||
}
|
||||
#css-delete-action span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.modal-container {
|
||||
width: 60vw;
|
||||
padding: 40px;
|
||||
min-width: 300px;
|
||||
}
|
||||
.server-center button {
|
||||
margin-right: -12px;
|
||||
width: 100%;
|
||||
}
|
||||
.divider {
|
||||
margin-right: -8px;
|
||||
}
|
||||
.divider hr {
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
width: 43%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.divider {
|
||||
margin-left: 4%;
|
||||
}
|
||||
.divider hr {
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.settings-card {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.server-info-right {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.action {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.lang-menu {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
background: rgba(78, 191, 172, 1.000);
|
||||
width: 100px;
|
||||
height: 38px;
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
border-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
input.toggle-round:checked + label:after {
|
||||
margin-left: 25px;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
/* Override css rules */
|
||||
|
||||
.portico-wrap>.header {
|
||||
.portico-wrap > .header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.portico-container>.footer {
|
||||
.portico-container > .footer {
|
||||
display: none;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 14 KiB |
82
app/renderer/js/clipboard-decrypter.ts
Normal file
82
app/renderer/js/clipboard-decrypter.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import {clipboard} from 'electron';
|
||||
import crypto from 'crypto';
|
||||
|
||||
// This helper is exposed via electron_bridge for use in the social
|
||||
// login flow.
|
||||
//
|
||||
// It consists of a key and a promised token. The in-app page sends
|
||||
// the key to the server, and opens the user’s browser to a page where
|
||||
// they can log in and get a token encrypted to that key. When the
|
||||
// user copies the encrypted token from their browser to the
|
||||
// clipboard, we decrypt it and resolve the promise. The in-app page
|
||||
// then uses the decrypted token to log the user in within the app.
|
||||
//
|
||||
// The encryption is authenticated (AES-GCM) to guarantee that we
|
||||
// don’t leak anything from the user’s clipboard other than the token
|
||||
// intended for us.
|
||||
|
||||
export class ClipboardDecrypterImpl implements ClipboardDecrypter {
|
||||
version: number;
|
||||
key: Uint8Array;
|
||||
pasted: Promise<string>;
|
||||
|
||||
constructor(_: number) {
|
||||
// At this time, the only version is 1.
|
||||
this.version = 1;
|
||||
this.key = crypto.randomBytes(32);
|
||||
this.pasted = new Promise(resolve => {
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
const startPolling = () => {
|
||||
if (interval === null) {
|
||||
interval = setInterval(poll, 1000);
|
||||
}
|
||||
|
||||
poll();
|
||||
};
|
||||
|
||||
const stopPolling = () => {
|
||||
if (interval !== null) {
|
||||
clearInterval(interval);
|
||||
interval = null;
|
||||
}
|
||||
};
|
||||
|
||||
const poll = () => {
|
||||
let plaintext;
|
||||
|
||||
try {
|
||||
const data = Buffer.from(clipboard.readText(), 'hex');
|
||||
const iv = data.slice(0, 12);
|
||||
const ciphertext = data.slice(12, -16);
|
||||
const authTag = data.slice(-16);
|
||||
const decipher = crypto.createDecipheriv(
|
||||
'aes-256-gcm',
|
||||
this.key,
|
||||
iv,
|
||||
{authTagLength: 16}
|
||||
);
|
||||
decipher.setAuthTag(authTag);
|
||||
plaintext =
|
||||
decipher.update(ciphertext, undefined, 'utf8') +
|
||||
decipher.final('utf8');
|
||||
} catch {
|
||||
// If the parsing or decryption failed in any way,
|
||||
// the correct token hasn’t been copied yet; try
|
||||
// again next time.
|
||||
return;
|
||||
}
|
||||
|
||||
window.removeEventListener('focus', startPolling);
|
||||
window.removeEventListener('blur', stopPolling);
|
||||
stopPolling();
|
||||
resolve(plaintext);
|
||||
};
|
||||
|
||||
window.addEventListener('focus', startPolling);
|
||||
window.addEventListener('blur', stopPolling);
|
||||
if (document.hasFocus()) {
|
||||
startPolling();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
class BaseComponent {
|
||||
generateNodeFromTemplate(template) {
|
||||
export default class BaseComponent {
|
||||
generateNodeFromTemplate(template: string): Element | null {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = template;
|
||||
return wrapper.firstElementChild;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseComponent;
|
||||
113
app/renderer/js/components/context-menu.ts
Normal file
113
app/renderer/js/components/context-menu.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import {remote, ContextMenuParams} from 'electron';
|
||||
import * as t from '../utils/translation-util';
|
||||
const {clipboard, Menu} = remote;
|
||||
|
||||
export const contextMenu = (webContents: Electron.WebContents, event: Event, props: ContextMenuParams) => {
|
||||
const isText = props.selectionText !== '';
|
||||
const isLink = props.linkURL !== '';
|
||||
const linkURL = isLink ? new URL(props.linkURL) : undefined;
|
||||
const isEmailAddress = isLink ? linkURL.protocol === 'mailto:' : undefined;
|
||||
|
||||
const makeSuggestion = (suggestion: string) => ({
|
||||
label: suggestion,
|
||||
visible: true,
|
||||
async click() {
|
||||
await webContents.insertText(suggestion);
|
||||
}
|
||||
});
|
||||
|
||||
let menuTemplate: Electron.MenuItemConstructorOptions[] = [{
|
||||
label: t.__('Add to Dictionary'),
|
||||
visible: props.isEditable && isText && props.misspelledWord.length !== 0,
|
||||
click(_item) {
|
||||
webContents.session.addWordToSpellCheckerDictionary(props.misspelledWord);
|
||||
}
|
||||
}, {
|
||||
type: 'separator',
|
||||
visible: props.isEditable && isText && props.misspelledWord.length !== 0
|
||||
}, {
|
||||
label: `${t.__('Look Up')} "${props.selectionText}"`,
|
||||
visible: process.platform === 'darwin' && isText,
|
||||
click(_item) {
|
||||
webContents.showDefinitionForSelection();
|
||||
}
|
||||
}, {
|
||||
type: 'separator',
|
||||
visible: process.platform === 'darwin' && isText
|
||||
}, {
|
||||
label: t.__('Cut'),
|
||||
visible: isText,
|
||||
enabled: props.isEditable,
|
||||
accelerator: 'CommandOrControl+X',
|
||||
click(_item) {
|
||||
webContents.cut();
|
||||
}
|
||||
}, {
|
||||
label: t.__('Copy'),
|
||||
accelerator: 'CommandOrControl+C',
|
||||
enabled: props.editFlags.canCopy,
|
||||
click(_item) {
|
||||
webContents.copy();
|
||||
}
|
||||
}, {
|
||||
label: t.__('Paste'), // Bug: Paste replaces text
|
||||
accelerator: 'CommandOrControl+V',
|
||||
enabled: props.isEditable,
|
||||
click() {
|
||||
webContents.paste();
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: isEmailAddress ? t.__('Copy Email Address') : t.__('Copy Link'),
|
||||
visible: isLink,
|
||||
click(_item) {
|
||||
clipboard.write({
|
||||
bookmark: props.linkText,
|
||||
text: isEmailAddress ? linkURL.pathname : props.linkURL
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: t.__('Copy Image'),
|
||||
visible: props.mediaType === 'image',
|
||||
click(_item) {
|
||||
webContents.copyImageAt(props.x, props.y);
|
||||
}
|
||||
}, {
|
||||
label: t.__('Copy Image URL'),
|
||||
visible: props.mediaType === 'image',
|
||||
click(_item) {
|
||||
clipboard.write({
|
||||
bookmark: props.srcURL,
|
||||
text: props.srcURL
|
||||
});
|
||||
}
|
||||
}, {
|
||||
type: 'separator',
|
||||
visible: isLink || props.mediaType === 'image'
|
||||
}, {
|
||||
label: t.__('Services'),
|
||||
visible: process.platform === 'darwin',
|
||||
role: 'services'
|
||||
}];
|
||||
|
||||
if (props.misspelledWord) {
|
||||
if (props.dictionarySuggestions.length > 0) {
|
||||
const suggestions: Electron.MenuItemConstructorOptions[] = props.dictionarySuggestions.map((suggestion: string) => makeSuggestion(suggestion));
|
||||
menuTemplate = suggestions.concat(menuTemplate);
|
||||
} else {
|
||||
menuTemplate.unshift({
|
||||
label: t.__('No Suggestion Found'),
|
||||
enabled: false
|
||||
});
|
||||
}
|
||||
}
|
||||
// Hide the invisible separators on Linux and Windows
|
||||
// Electron has a bug which ignores visible: false parameter for separator menuitems. So we remove them here.
|
||||
// https://github.com/electron/electron/issues/5869
|
||||
// https://github.com/electron/electron/issues/6906
|
||||
|
||||
const filteredMenuTemplate = menuTemplate.filter(menuItem => menuItem.visible ?? true);
|
||||
const menu = Menu.buildFromTemplate(filteredMenuTemplate);
|
||||
menu.popup();
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
'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;
|
||||
47
app/renderer/js/components/functional-tab.ts
Normal file
47
app/renderer/js/components/functional-tab.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import Tab, {TabProps} from './tab';
|
||||
|
||||
export default class FunctionalTab extends Tab {
|
||||
$closeButton: Element;
|
||||
|
||||
constructor(props: TabProps) {
|
||||
super(props);
|
||||
this.init();
|
||||
}
|
||||
|
||||
template(): string {
|
||||
return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
|
||||
<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(): void {
|
||||
this.$el = this.generateNodeFromTemplate(this.template());
|
||||
if (this.props.name !== 'Settings') {
|
||||
this.props.$root.append(this.$el);
|
||||
this.$closeButton = this.$el.querySelectorAll('.server-tab-badge')[0];
|
||||
this.registerListeners();
|
||||
}
|
||||
}
|
||||
|
||||
registerListeners(): void {
|
||||
super.registerListeners();
|
||||
|
||||
this.$el.addEventListener('mouseover', () => {
|
||||
this.$closeButton.classList.add('active');
|
||||
});
|
||||
|
||||
this.$el.addEventListener('mouseout', () => {
|
||||
this.$closeButton.classList.remove('active');
|
||||
});
|
||||
|
||||
this.$closeButton.addEventListener('click', (event: Event) => {
|
||||
this.props.onDestroy();
|
||||
event.stopPropagation();
|
||||
});
|
||||
}
|
||||
}
|
||||
60
app/renderer/js/components/handle-external-link.ts
Normal file
60
app/renderer/js/components/handle-external-link.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {ipcRenderer, remote} from 'electron';
|
||||
|
||||
import * as LinkUtil from '../utils/link-util';
|
||||
import * as ConfigUtil from '../utils/config-util';
|
||||
import type WebView from './webview';
|
||||
|
||||
const {shell, app} = remote;
|
||||
|
||||
const dingSound = new Audio('../resources/sounds/ding.ogg');
|
||||
|
||||
export default function handleExternalLink(this: WebView, event: Electron.NewWindowEvent): void {
|
||||
event.preventDefault();
|
||||
|
||||
const url = new URL(event.url);
|
||||
const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`);
|
||||
|
||||
if (LinkUtil.isUploadsUrl(this.props.url, url)) {
|
||||
ipcRenderer.send('downloadFile', url.href, downloadPath);
|
||||
ipcRenderer.once('downloadFileCompleted', async (_event: Event, filePath: string, fileName: string) => {
|
||||
const downloadNotification = new Notification('Download Complete', {
|
||||
body: `Click to show ${fileName} in folder`,
|
||||
silent: true // We'll play our own sound - ding.ogg
|
||||
});
|
||||
|
||||
downloadNotification.addEventListener('click', () => {
|
||||
// Reveal file in download folder
|
||||
shell.showItemInFolder(filePath);
|
||||
});
|
||||
ipcRenderer.removeAllListeners('downloadFileFailed');
|
||||
|
||||
// Play sound to indicate download complete
|
||||
if (!ConfigUtil.getConfigItem('silent')) {
|
||||
await dingSound.play();
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.once('downloadFileFailed', (_event: Event, state: string) => {
|
||||
// Automatic download failed, so show save dialog prompt and download
|
||||
// through webview
|
||||
// Only do this if it is the automatic download, otherwise show an error (so we aren't showing two save
|
||||
// prompts right after each other)
|
||||
// Check that the download is not cancelled by user
|
||||
if (state !== 'cancelled') {
|
||||
if (ConfigUtil.getConfigItem('promptDownload', false)) {
|
||||
// We need to create a "new Notification" to display it, but just `Notification(...)` on its own
|
||||
// doesn't work
|
||||
new Notification('Download Complete', { // eslint-disable-line no-new
|
||||
body: 'Download failed'
|
||||
});
|
||||
} else {
|
||||
this.$el.downloadURL(url.href);
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.removeAllListeners('downloadFileCompleted');
|
||||
});
|
||||
} else {
|
||||
(async () => LinkUtil.openBrowser(url))();
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
'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;
|
||||
63
app/renderer/js/components/server-tab.ts
Normal file
63
app/renderer/js/components/server-tab.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
import Tab, {TabProps} from './tab';
|
||||
import * as SystemUtil from '../utils/system-util';
|
||||
|
||||
export default class ServerTab extends Tab {
|
||||
$badge: Element;
|
||||
|
||||
constructor(props: TabProps) {
|
||||
super(props);
|
||||
this.init();
|
||||
}
|
||||
|
||||
template(): string {
|
||||
return `<div class="tab" data-tab-id="${this.props.tabIndex}">
|
||||
<div class="server-tooltip" style="display:none">${this.props.name}</div>
|
||||
<div class="server-tab-badge"></div>
|
||||
<div class="server-tab">
|
||||
<img class="server-icons" src='${this.props.icon}'/>
|
||||
</div>
|
||||
<div class="server-tab-shortcut">${this.generateShortcutText()}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.$el = this.generateNodeFromTemplate(this.template());
|
||||
this.props.$root.append(this.$el);
|
||||
this.registerListeners();
|
||||
this.$badge = this.$el.querySelectorAll('.server-tab-badge')[0];
|
||||
}
|
||||
|
||||
updateBadge(count: number): void {
|
||||
if (count > 0) {
|
||||
const formattedCount = count > 999 ? '1K+' : count.toString();
|
||||
this.$badge.innerHTML = formattedCount;
|
||||
this.$badge.classList.add('active');
|
||||
} else {
|
||||
this.$badge.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
generateShortcutText(): string {
|
||||
// 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}`;
|
||||
}
|
||||
|
||||
// Array index == Shown index - 1
|
||||
ipcRenderer.send('switch-server-tab', shownIndex - 1);
|
||||
|
||||
return shortcutText;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
'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;
|
||||
54
app/renderer/js/components/tab.ts
Normal file
54
app/renderer/js/components/tab.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import WebView from './webview';
|
||||
import BaseComponent from './base';
|
||||
|
||||
export interface TabProps {
|
||||
role: string;
|
||||
icon?: string;
|
||||
name: string;
|
||||
$root: Element;
|
||||
onClick: () => void;
|
||||
index: number;
|
||||
tabIndex: number;
|
||||
onHover?: () => void;
|
||||
onHoverOut?: () => void;
|
||||
webview: WebView;
|
||||
materialIcon?: string;
|
||||
onDestroy?: () => void;
|
||||
}
|
||||
|
||||
export default class Tab extends BaseComponent {
|
||||
props: TabProps;
|
||||
webview: WebView;
|
||||
$el: Element;
|
||||
constructor(props: TabProps) {
|
||||
super();
|
||||
|
||||
this.props = props;
|
||||
this.webview = this.props.webview;
|
||||
}
|
||||
|
||||
registerListeners(): void {
|
||||
this.$el.addEventListener('click', this.props.onClick);
|
||||
this.$el.addEventListener('mouseover', this.props.onHover);
|
||||
this.$el.addEventListener('mouseout', this.props.onHoverOut);
|
||||
}
|
||||
|
||||
showNetworkError(): void {
|
||||
this.webview.forceLoad();
|
||||
}
|
||||
|
||||
activate(): void {
|
||||
this.$el.classList.add('active');
|
||||
this.webview.load();
|
||||
}
|
||||
|
||||
deactivate(): void {
|
||||
this.$el.classList.remove('active');
|
||||
this.webview.hide();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.$el.remove();
|
||||
this.webview.$el.remove();
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
'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;
|
||||
311
app/renderer/js/components/webview.ts
Normal file
311
app/renderer/js/components/webview.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import {ipcRenderer, remote} from 'electron';
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import * as ConfigUtil from '../utils/config-util';
|
||||
import * as SystemUtil from '../utils/system-util';
|
||||
import BaseComponent from './base';
|
||||
import handleExternalLink from './handle-external-link';
|
||||
import {contextMenu} from './context-menu';
|
||||
|
||||
const {app, dialog} = remote;
|
||||
|
||||
const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
|
||||
|
||||
interface WebViewProps {
|
||||
$root: Element;
|
||||
index: number;
|
||||
tabIndex: number;
|
||||
url: string;
|
||||
role: string;
|
||||
name: string;
|
||||
isActive: () => boolean;
|
||||
switchLoading: (loading: boolean, url: string) => void;
|
||||
onNetworkError: (index: number) => void;
|
||||
nodeIntegration: boolean;
|
||||
preload: boolean;
|
||||
onTitleChange: () => void;
|
||||
hasPermission?: (origin: string, permission: string) => boolean;
|
||||
}
|
||||
|
||||
export default class WebView extends BaseComponent {
|
||||
props: WebViewProps;
|
||||
zoomFactor: number;
|
||||
badgeCount: number;
|
||||
loading: boolean;
|
||||
customCSS: string;
|
||||
$webviewsContainer: DOMTokenList;
|
||||
$el: Electron.WebviewTag;
|
||||
domReady?: Promise<void>;
|
||||
|
||||
constructor(props: WebViewProps) {
|
||||
super();
|
||||
|
||||
this.props = props;
|
||||
this.zoomFactor = 1;
|
||||
this.loading = true;
|
||||
this.badgeCount = 0;
|
||||
this.customCSS = ConfigUtil.getConfigItem('customCSS');
|
||||
this.$webviewsContainer = document.querySelector('#webviews-container').classList;
|
||||
}
|
||||
|
||||
template(): string {
|
||||
return `<webview
|
||||
class="disabled"
|
||||
data-tab-id="${this.props.tabIndex}"
|
||||
src="${this.props.url}"
|
||||
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
|
||||
${this.props.preload ? 'preload="js/preload.js"' : ''}
|
||||
partition="persist:webviewsession"
|
||||
name="${this.props.name}"
|
||||
webpreferences="
|
||||
${this.props.nodeIntegration ? '' : 'contextIsolation,'}
|
||||
${ConfigUtil.getConfigItem('enableSpellchecker') ? 'spellcheck,' : ''}
|
||||
javascript
|
||||
">
|
||||
</webview>`;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.$el = this.generateNodeFromTemplate(this.template()) as Electron.WebviewTag;
|
||||
this.domReady = new Promise(resolve => {
|
||||
this.$el.addEventListener('dom-ready', () => resolve(), true);
|
||||
});
|
||||
this.props.$root.append(this.$el);
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
registerListeners(): void {
|
||||
this.$el.addEventListener('new-window', event => {
|
||||
handleExternalLink.call(this, event);
|
||||
});
|
||||
|
||||
if (shouldSilentWebview) {
|
||||
this.$el.addEventListener('dom-ready', () => {
|
||||
this.$el.setAudioMuted(true);
|
||||
});
|
||||
}
|
||||
|
||||
this.$el.addEventListener('page-title-updated', event => {
|
||||
const {title} = event;
|
||||
this.badgeCount = this.getBadgeCount(title);
|
||||
this.props.onTitleChange();
|
||||
});
|
||||
|
||||
this.$el.addEventListener('did-navigate-in-page', event => {
|
||||
const isSettingPage = event.url.includes('renderer/preference.html');
|
||||
if (isSettingPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.canGoBackButton();
|
||||
});
|
||||
|
||||
this.$el.addEventListener('did-navigate', () => {
|
||||
this.canGoBackButton();
|
||||
});
|
||||
|
||||
this.$el.addEventListener('page-favicon-updated', event => {
|
||||
const {favicons} = event;
|
||||
|
||||
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
|
||||
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
|
||||
if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') {
|
||||
// This api is only supported on macOS
|
||||
app.dock.setBadge('●');
|
||||
// Bounce the dock
|
||||
if (ConfigUtil.getConfigItem('dockBouncing')) {
|
||||
app.dock.bounce();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$el.addEventListener('dom-ready', () => {
|
||||
const webContents = remote.webContents.fromId(this.$el.getWebContentsId());
|
||||
webContents.addListener('context-menu', (event, menuParameters) => {
|
||||
contextMenu(webContents, event, menuParameters);
|
||||
});
|
||||
|
||||
if (this.props.role === 'server') {
|
||||
this.$el.classList.add('onload');
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.props.switchLoading(false, this.props.url);
|
||||
this.show();
|
||||
|
||||
// Refocus text boxes after reload
|
||||
// Remove when upstream issue https://github.com/electron/electron/issues/14474 is fixed
|
||||
this.$el.blur();
|
||||
this.$el.focus();
|
||||
});
|
||||
|
||||
this.$el.addEventListener('did-fail-load', event => {
|
||||
const {errorDescription} = event;
|
||||
const hasConnectivityErr = SystemUtil.connectivityERR.includes(errorDescription);
|
||||
if (hasConnectivityErr) {
|
||||
console.error('error', errorDescription);
|
||||
if (!this.props.url.includes('network.html')) {
|
||||
this.props.onNetworkError(this.props.index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$el.addEventListener('did-start-loading', () => {
|
||||
const isSettingPage = this.props.url.includes('renderer/preference.html');
|
||||
if (!isSettingPage) {
|
||||
this.props.switchLoading(true, this.props.url);
|
||||
}
|
||||
});
|
||||
|
||||
this.$el.addEventListener('did-stop-loading', () => {
|
||||
this.props.switchLoading(false, this.props.url);
|
||||
});
|
||||
}
|
||||
|
||||
getBadgeCount(title: string): number {
|
||||
const messageCountInTitle = (/\((\d+)\)/).exec(title);
|
||||
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
|
||||
}
|
||||
|
||||
showNotificationSettings(): void {
|
||||
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'show-notification-settings');
|
||||
}
|
||||
|
||||
show(): void {
|
||||
// Do not show WebView if another tab was selected and this tab should be in background.
|
||||
if (!this.props.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// To show or hide the loading indicator in the the active tab
|
||||
if (this.loading) {
|
||||
this.$webviewsContainer.remove('loaded');
|
||||
} else {
|
||||
this.$webviewsContainer.add('loaded');
|
||||
}
|
||||
|
||||
this.$el.classList.remove('disabled');
|
||||
this.$el.classList.add('active');
|
||||
setTimeout(() => {
|
||||
if (this.props.role === 'server') {
|
||||
this.$el.classList.remove('onload');
|
||||
}
|
||||
}, 1000);
|
||||
this.focus();
|
||||
this.props.onTitleChange();
|
||||
// Injecting preload css in webview to override some css rules
|
||||
(async () => this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8')))();
|
||||
|
||||
// Get customCSS again from config util to avoid warning user again
|
||||
this.customCSS = ConfigUtil.getConfigItem('customCSS');
|
||||
if (this.customCSS) {
|
||||
if (!fs.existsSync(this.customCSS)) {
|
||||
this.customCSS = null;
|
||||
ConfigUtil.setConfigItem('customCSS', null);
|
||||
|
||||
const errorMessage = 'The custom css previously set is deleted!';
|
||||
dialog.showErrorBox('custom css file deleted!', errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => this.$el.insertCSS(fs.readFileSync(path.resolve(__dirname, this.customCSS), 'utf8')))();
|
||||
}
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
// Focus Webview and it's contents when Window regain focus.
|
||||
const webContents = remote.webContents.fromId(this.$el.getWebContentsId());
|
||||
// HACK: webContents.isFocused() seems to be true even without the element
|
||||
// being in focus. So, we check against `document.activeElement`.
|
||||
if (webContents && this.$el !== document.activeElement) {
|
||||
// HACK: Looks like blur needs to be called on the previously focused
|
||||
// element to transfer focus correctly, in Electron v3.0.10
|
||||
// See https://github.com/electron/electron/issues/15718
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
this.$el.focus();
|
||||
webContents.focus();
|
||||
}
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.$el.classList.add('disabled');
|
||||
this.$el.classList.remove('active');
|
||||
}
|
||||
|
||||
load(): void {
|
||||
if (this.$el) {
|
||||
this.show();
|
||||
} else {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn(): void {
|
||||
this.zoomFactor += 0.1;
|
||||
this.$el.setZoomFactor(this.zoomFactor);
|
||||
}
|
||||
|
||||
zoomOut(): void {
|
||||
this.zoomFactor -= 0.1;
|
||||
this.$el.setZoomFactor(this.zoomFactor);
|
||||
}
|
||||
|
||||
zoomActualSize(): void {
|
||||
this.zoomFactor = 1;
|
||||
this.$el.setZoomFactor(this.zoomFactor);
|
||||
}
|
||||
|
||||
logOut(): void {
|
||||
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'logout');
|
||||
}
|
||||
|
||||
showShortcut(): void {
|
||||
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'shortcut');
|
||||
}
|
||||
|
||||
openDevTools(): void {
|
||||
this.$el.openDevTools();
|
||||
}
|
||||
|
||||
back(): void {
|
||||
if (this.$el.canGoBack()) {
|
||||
this.$el.goBack();
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
canGoBackButton(): void {
|
||||
const $backButton = document.querySelector('#actions-container #back-action');
|
||||
if (this.$el.canGoBack()) {
|
||||
$backButton.classList.remove('disable');
|
||||
} else {
|
||||
$backButton.classList.add('disable');
|
||||
}
|
||||
}
|
||||
|
||||
forward(): void {
|
||||
if (this.$el.canGoForward()) {
|
||||
this.$el.goForward();
|
||||
}
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
this.hide();
|
||||
// Shows the loading indicator till the webview is reloaded
|
||||
this.$webviewsContainer.remove('loaded');
|
||||
this.loading = true;
|
||||
this.props.switchLoading(true, this.props.url);
|
||||
this.$el.reload();
|
||||
}
|
||||
|
||||
forceLoad(): void {
|
||||
this.init();
|
||||
}
|
||||
|
||||
async send(channel: string, ...parameters: unknown[]): Promise<void> {
|
||||
await this.domReady;
|
||||
await this.$el.send(channel, ...parameters);
|
||||
}
|
||||
}
|
||||
81
app/renderer/js/electron-bridge.ts
Normal file
81
app/renderer/js/electron-bridge.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
import {ClipboardDecrypterImpl} from './clipboard-decrypter';
|
||||
import {NotificationData, newNotification} from './notification';
|
||||
|
||||
type ListenerType = ((...args: any[]) => void);
|
||||
|
||||
class ElectronBridgeImpl extends EventEmitter implements ElectronBridge {
|
||||
send_notification_reply_message_supported: boolean;
|
||||
idle_on_system: boolean;
|
||||
last_active_on_system: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.send_notification_reply_message_supported = false;
|
||||
// Indicates if the user is idle or not
|
||||
this.idle_on_system = false;
|
||||
|
||||
// Indicates the time at which user was last active
|
||||
this.last_active_on_system = Date.now();
|
||||
}
|
||||
|
||||
send_event = (eventName: string | symbol, ...args: unknown[]): void => {
|
||||
this.emit(eventName, ...args);
|
||||
};
|
||||
|
||||
on_event = (eventName: string, listener: ListenerType): void => {
|
||||
this.on(eventName, listener);
|
||||
};
|
||||
|
||||
new_notification = (
|
||||
title: string,
|
||||
options: NotificationOptions | undefined,
|
||||
dispatch: (type: string, eventInit: EventInit) => boolean
|
||||
): NotificationData =>
|
||||
newNotification(title, options, dispatch);
|
||||
|
||||
get_idle_on_system = (): boolean => this.idle_on_system;
|
||||
|
||||
get_last_active_on_system = (): number => this.last_active_on_system;
|
||||
|
||||
get_send_notification_reply_message_supported = (): boolean =>
|
||||
this.send_notification_reply_message_supported;
|
||||
|
||||
set_send_notification_reply_message_supported = (value: boolean): void => {
|
||||
this.send_notification_reply_message_supported = value;
|
||||
};
|
||||
|
||||
decrypt_clipboard = (version: number): ClipboardDecrypterImpl =>
|
||||
new ClipboardDecrypterImpl(version);
|
||||
}
|
||||
|
||||
const electron_bridge = new ElectronBridgeImpl();
|
||||
|
||||
electron_bridge.on('total_unread_count', (...args) => {
|
||||
ipcRenderer.send('unread-count', ...args);
|
||||
});
|
||||
|
||||
electron_bridge.on('realm_name', realmName => {
|
||||
const serverURL = location.origin;
|
||||
ipcRenderer.send('realm-name-changed', serverURL, realmName);
|
||||
});
|
||||
|
||||
electron_bridge.on('realm_icon_url', (iconURL: unknown) => {
|
||||
if (typeof iconURL !== 'string') {
|
||||
throw new TypeError('Expected string for iconURL');
|
||||
}
|
||||
|
||||
const serverURL = location.origin;
|
||||
iconURL = iconURL.includes('http') ? iconURL : `${serverURL}${iconURL}`;
|
||||
ipcRenderer.send('realm-icon-changed', serverURL, iconURL);
|
||||
});
|
||||
|
||||
// This follows node's idiomatic implementation of event
|
||||
// emitters to make event handling more simpler instead of using
|
||||
// functions zulip side will emit event using ElectronBrigde.send_event
|
||||
// which is alias of .emit and on this side we can handle the data by adding
|
||||
// a listener for the event.
|
||||
export default electron_bridge;
|
||||
50
app/renderer/js/feedback.ts
Normal file
50
app/renderer/js/feedback.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {remote} from 'electron';
|
||||
import SendFeedback from '@electron-elements/send-feedback';
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const {app} = remote;
|
||||
|
||||
customElements.define('send-feedback', SendFeedback);
|
||||
export const sendFeedback: SendFeedback = document.querySelector('send-feedback');
|
||||
export const feedbackHolder = sendFeedback.parentElement;
|
||||
|
||||
// Make the button color match zulip app's theme
|
||||
sendFeedback.customStylesheet = 'css/feedback.css';
|
||||
|
||||
// Customize the fields of custom elements
|
||||
sendFeedback.title = 'Report Issue';
|
||||
sendFeedback.titleLabel = 'Issue title:';
|
||||
sendFeedback.titlePlaceholder = 'Enter issue title';
|
||||
sendFeedback.textareaLabel = 'Describe the issue:';
|
||||
sendFeedback.textareaPlaceholder = 'Succinctly describe your issue and steps to reproduce it...';
|
||||
|
||||
sendFeedback.buttonLabel = 'Report Issue';
|
||||
sendFeedback.loaderSuccessText = '';
|
||||
|
||||
sendFeedback.useReporter('emailReporter', {
|
||||
email: 'support@zulip.com'
|
||||
});
|
||||
|
||||
feedbackHolder.addEventListener('click', (event: Event) => {
|
||||
// Only remove the class if the grey out faded
|
||||
// part is clicked and not the feedback element itself
|
||||
if (event.target === event.currentTarget) {
|
||||
feedbackHolder.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
sendFeedback.addEventListener('feedback-submitted', () => {
|
||||
setTimeout(() => {
|
||||
feedbackHolder.classList.remove('show');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
sendFeedback.addEventListener('feedback-cancelled', () => {
|
||||
feedbackHolder.classList.remove('show');
|
||||
});
|
||||
|
||||
const dataDir = app.getPath('userData');
|
||||
const logsDir = path.join(dataDir, '/Logs');
|
||||
sendFeedback.logs.push(...fs.readdirSync(logsDir).map(file => path.join(logsDir, file)));
|
||||
133
app/renderer/js/injected.ts
Normal file
133
app/renderer/js/injected.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
'use strict';
|
||||
|
||||
interface CompatElectronBridge extends ElectronBridge {
|
||||
readonly idle_on_system: boolean;
|
||||
readonly last_active_on_system: number;
|
||||
send_notification_reply_message_supported: boolean;
|
||||
}
|
||||
|
||||
(() => {
|
||||
const zulipWindow = window as typeof window & {
|
||||
electron_bridge: CompatElectronBridge;
|
||||
narrow: {
|
||||
by_subject?: (target_id: number, opts: {trigger?: string}) => void;
|
||||
by_topic?: (target_id: number, opts: {trigger?: string}) => void;
|
||||
};
|
||||
page_params?: {
|
||||
default_language?: string;
|
||||
};
|
||||
raw_electron_bridge: ElectronBridge;
|
||||
};
|
||||
|
||||
const electron_bridge: CompatElectronBridge = {
|
||||
...zulipWindow.raw_electron_bridge,
|
||||
|
||||
get idle_on_system(): boolean {
|
||||
return this.get_idle_on_system();
|
||||
},
|
||||
|
||||
get last_active_on_system(): number {
|
||||
return this.get_last_active_on_system();
|
||||
},
|
||||
|
||||
get send_notification_reply_message_supported(): boolean {
|
||||
return this.get_send_notification_reply_message_supported();
|
||||
},
|
||||
|
||||
set send_notification_reply_message_supported(value: boolean) {
|
||||
this.set_send_notification_reply_message_supported(value);
|
||||
}
|
||||
};
|
||||
|
||||
zulipWindow.electron_bridge = electron_bridge;
|
||||
|
||||
(async () => {
|
||||
if (document.readyState === 'loading') {
|
||||
await new Promise(resolve => {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const {page_params} = zulipWindow;
|
||||
if (page_params) {
|
||||
electron_bridge.send_event('zulip-loaded');
|
||||
}
|
||||
})();
|
||||
|
||||
electron_bridge.on_event('narrow-by-topic', (id: number) => {
|
||||
const {narrow} = zulipWindow;
|
||||
const narrowByTopic = narrow.by_topic || narrow.by_subject;
|
||||
narrowByTopic(id, {trigger: 'notification'});
|
||||
});
|
||||
|
||||
function attributeListener<T extends EventTarget>(type: string): PropertyDescriptor {
|
||||
const symbol = Symbol('on' + type);
|
||||
|
||||
function listener(this: T, ev: Event): void {
|
||||
if ((this as any)[symbol].call(this, ev) === false) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get(this: T) {
|
||||
return (this as any)[symbol];
|
||||
},
|
||||
set(this: T, value: unknown) {
|
||||
if (typeof value === 'function') {
|
||||
if (!(symbol in this)) {
|
||||
this.addEventListener(type, listener);
|
||||
}
|
||||
|
||||
(this as any)[symbol] = value;
|
||||
} else if (symbol in this) {
|
||||
this.removeEventListener(type, listener);
|
||||
delete (this as any)[symbol];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const NativeNotification = Notification;
|
||||
|
||||
class InjectedNotification extends EventTarget {
|
||||
constructor(title: string, options?: NotificationOptions) {
|
||||
super();
|
||||
Object.assign(
|
||||
this,
|
||||
electron_bridge.new_notification(title, options, (type: string, eventInit: EventInit) =>
|
||||
this.dispatchEvent(new Event(type, eventInit))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
static get maxActions(): number {
|
||||
return NativeNotification.maxActions;
|
||||
}
|
||||
|
||||
static get permission(): NotificationPermission {
|
||||
return NativeNotification.permission;
|
||||
}
|
||||
|
||||
static async requestPermission(callback?: NotificationPermissionCallback): Promise<NotificationPermission> {
|
||||
if (callback) {
|
||||
callback(await Promise.resolve(NativeNotification.permission));
|
||||
}
|
||||
|
||||
return NativeNotification.permission;
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(InjectedNotification.prototype, {
|
||||
onclick: attributeListener('click'),
|
||||
onclose: attributeListener('close'),
|
||||
onerror: attributeListener('error'),
|
||||
onshow: attributeListener('show')
|
||||
});
|
||||
|
||||
window.Notification = InjectedNotification as any;
|
||||
})();
|
||||
@@ -1,406 +0,0 @@
|
||||
'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');
|
||||
}
|
||||
ipcRenderer.send('local-shortcuts', true);
|
||||
}
|
||||
|
||||
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 = '';
|
||||
|
||||
// Destroy shortcuts
|
||||
ipcRenderer.send('local-shortcuts', false);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
};
|
||||
1057
app/renderer/js/main.ts
Normal file
1057
app/renderer/js/main.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
||||
'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;
|
||||
|
||||
111
app/renderer/js/notification/darwin-notifications.ts
Normal file
111
app/renderer/js/notification/darwin-notifications.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {ipcRenderer} from 'electron';
|
||||
import {
|
||||
appId, customReply, focusCurrentServer, parseReply
|
||||
} from './helpers';
|
||||
|
||||
import MacNotifier from 'node-mac-notifier';
|
||||
import * as ConfigUtil from '../utils/config-util';
|
||||
import electron_bridge from '../electron-bridge';
|
||||
|
||||
type ReplyHandler = (response: string) => void;
|
||||
type ClickHandler = () => void;
|
||||
let replyHandler: ReplyHandler;
|
||||
let clickHandler: ClickHandler;
|
||||
|
||||
interface NotificationHandlerArgs {
|
||||
response: string;
|
||||
}
|
||||
|
||||
class DarwinNotification {
|
||||
tag: number;
|
||||
|
||||
constructor(title: string, options: NotificationOptions) {
|
||||
const silent: boolean = ConfigUtil.getConfigItem('silent') || false;
|
||||
const {icon} = options;
|
||||
const profilePic = new URL(icon, location.href).href;
|
||||
|
||||
this.tag = Number.parseInt(options.tag, 10);
|
||||
const notification = new MacNotifier(title, Object.assign(options, {
|
||||
bundleId: appId,
|
||||
canReply: true,
|
||||
silent,
|
||||
icon: profilePic
|
||||
}));
|
||||
|
||||
notification.addEventListener('click', () => {
|
||||
// Focus to the server who sent the
|
||||
// notification if not focused already
|
||||
if (clickHandler) {
|
||||
clickHandler();
|
||||
}
|
||||
|
||||
focusCurrentServer();
|
||||
ipcRenderer.send('focus-app');
|
||||
});
|
||||
|
||||
notification.addEventListener('reply', this.notificationHandler);
|
||||
}
|
||||
|
||||
static requestPermission(): void {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// Override default Notification permission
|
||||
static get permission(): NotificationPermission {
|
||||
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
|
||||
}
|
||||
|
||||
get onreply(): ReplyHandler {
|
||||
return replyHandler;
|
||||
}
|
||||
|
||||
set onreply(handler: ReplyHandler) {
|
||||
replyHandler = handler;
|
||||
}
|
||||
|
||||
get onclick(): ClickHandler {
|
||||
return clickHandler;
|
||||
}
|
||||
|
||||
set onclick(handler: ClickHandler) {
|
||||
clickHandler = handler;
|
||||
}
|
||||
|
||||
// Not something that is common or
|
||||
// used by zulip server but added to be
|
||||
// future proff.
|
||||
addEventListener(event: string, handler: ClickHandler | ReplyHandler): void {
|
||||
if (event === 'click') {
|
||||
clickHandler = handler as ClickHandler;
|
||||
}
|
||||
|
||||
if (event === 'reply') {
|
||||
replyHandler = handler as ReplyHandler;
|
||||
}
|
||||
}
|
||||
|
||||
async notificationHandler({response}: NotificationHandlerArgs): Promise<void> {
|
||||
response = await parseReply(response);
|
||||
focusCurrentServer();
|
||||
if (electron_bridge.send_notification_reply_message_supported) {
|
||||
electron_bridge.send_event('send_notification_reply_message', this.tag, response);
|
||||
return;
|
||||
}
|
||||
|
||||
electron_bridge.emit('narrow-by-topic', this.tag);
|
||||
if (replyHandler) {
|
||||
replyHandler(response);
|
||||
return;
|
||||
}
|
||||
|
||||
customReply(response);
|
||||
}
|
||||
|
||||
// Method specific to notification api
|
||||
// used by zulip
|
||||
close(): void {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DarwinNotification;
|
||||
28
app/renderer/js/notification/default-notification.ts
Normal file
28
app/renderer/js/notification/default-notification.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {ipcRenderer} from 'electron';
|
||||
import {focusCurrentServer} from './helpers';
|
||||
|
||||
import * as ConfigUtil from '../utils/config-util';
|
||||
|
||||
const NativeNotification = window.Notification;
|
||||
export default class BaseNotification extends NativeNotification {
|
||||
constructor(title: string, options: NotificationOptions) {
|
||||
options.silent = true;
|
||||
super(title, options);
|
||||
|
||||
this.addEventListener('click', () => {
|
||||
// Focus to the server who sent the
|
||||
// notification if not focused already
|
||||
focusCurrentServer();
|
||||
ipcRenderer.send('focus-app');
|
||||
});
|
||||
}
|
||||
|
||||
static async requestPermission(): Promise<NotificationPermission> {
|
||||
return this.permission;
|
||||
}
|
||||
|
||||
// Override default Notification permission
|
||||
static get permission(): NotificationPermission {
|
||||
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
|
||||
}
|
||||
}
|
||||
137
app/renderer/js/notification/helpers.ts
Normal file
137
app/renderer/js/notification/helpers.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import {remote} from 'electron';
|
||||
|
||||
import Logger from '../utils/logger-util';
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'errors.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
// Do not change this
|
||||
export const appId = 'org.zulip.zulip-electron';
|
||||
|
||||
export type BotListItem = [string, string];
|
||||
const botsList: BotListItem[] = [];
|
||||
let botsListLoaded = false;
|
||||
|
||||
// This function load list of bots from the server
|
||||
// in case botsList isn't already completely loaded when required in parseRely
|
||||
export async function loadBots(): Promise<void> {
|
||||
botsList.length = 0;
|
||||
const response = await fetch('/json/users');
|
||||
if (response.ok) {
|
||||
const {members} = await response.json();
|
||||
members.forEach(({is_bot, full_name}: {[key: string]: unknown}) => {
|
||||
if (is_bot && typeof full_name === 'string') {
|
||||
const bot = `@${full_name}`;
|
||||
const mention = `@**${bot.replace(/^@/, '')}**`;
|
||||
botsList.push([bot, mention]);
|
||||
}
|
||||
});
|
||||
botsListLoaded = true;
|
||||
} else {
|
||||
logger.log('Load bots request failed: ', await response.text());
|
||||
logger.log('Load bots request status: ', response.status);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkElements(...elements: unknown[]): boolean {
|
||||
let status = true;
|
||||
elements.forEach(element => {
|
||||
if (element === null || element === undefined) {
|
||||
status = false;
|
||||
}
|
||||
});
|
||||
return status;
|
||||
}
|
||||
|
||||
export function customReply(reply: string): void {
|
||||
// Server does not support notification reply yet.
|
||||
const buttonSelector = '.messagebox #send_controls button[type=submit]';
|
||||
const messageboxSelector = '.selected_message .messagebox .messagebox-border .messagebox-content';
|
||||
const textarea: HTMLInputElement = document.querySelector('#compose-textarea');
|
||||
const messagebox: HTMLButtonElement = document.querySelector(messageboxSelector);
|
||||
const sendButton: HTMLButtonElement = document.querySelector(buttonSelector);
|
||||
|
||||
// Sanity check for old server versions
|
||||
const elementsExists = checkElements(textarea, messagebox, sendButton);
|
||||
if (!elementsExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
textarea.value = reply;
|
||||
messagebox.click();
|
||||
sendButton.click();
|
||||
}
|
||||
|
||||
const currentWindow = remote.getCurrentWindow();
|
||||
const webContents = remote.getCurrentWebContents();
|
||||
const webContentsId = webContents.id;
|
||||
|
||||
// This function will focus the server that sent
|
||||
// the notification. Main function implemented in main.js
|
||||
export function focusCurrentServer(): void {
|
||||
currentWindow.webContents.send('focus-webview-with-id', webContentsId);
|
||||
}
|
||||
|
||||
// This function parses the reply from to notification
|
||||
// making it easier to reply from notification eg
|
||||
// @username in reply will be converted to @**username**
|
||||
// #stream in reply will be converted to #**stream**
|
||||
// bot mentions are not yet supported
|
||||
export async function parseReply(reply: string): Promise<string> {
|
||||
const usersDiv = document.querySelectorAll('#user_presences li');
|
||||
const streamHolder = document.querySelectorAll('#stream_filters li');
|
||||
|
||||
type UsersItem = BotListItem;
|
||||
type StreamsItem = BotListItem;
|
||||
const users: UsersItem[] = [];
|
||||
const streams: StreamsItem[] = [];
|
||||
|
||||
usersDiv.forEach(userRow => {
|
||||
const anchor = userRow.querySelector('span a');
|
||||
if (anchor !== null) {
|
||||
const user = `@${anchor.textContent.trim()}`;
|
||||
const mention = `@**${user.replace(/^@/, '')}**`;
|
||||
users.push([user, mention]);
|
||||
}
|
||||
});
|
||||
|
||||
streamHolder.forEach(stream => {
|
||||
const streamAnchor = stream.querySelector('div a');
|
||||
if (streamAnchor !== null) {
|
||||
const streamName = `#${streamAnchor.textContent.trim()}`;
|
||||
const streamMention = `#**${streamName.replace(/^#/, '')}**`;
|
||||
streams.push([streamName, streamMention]);
|
||||
}
|
||||
});
|
||||
|
||||
users.forEach(([user, mention]) => {
|
||||
if (reply.includes(user)) {
|
||||
const regex = new RegExp(user, 'g');
|
||||
reply = reply.replace(regex, mention);
|
||||
}
|
||||
});
|
||||
|
||||
streams.forEach(([stream, streamMention]) => {
|
||||
const regex = new RegExp(stream, 'g');
|
||||
reply = reply.replace(regex, streamMention);
|
||||
});
|
||||
|
||||
// If botsList isn't completely loaded yet, make a request for list
|
||||
if (!botsListLoaded) {
|
||||
await loadBots();
|
||||
}
|
||||
|
||||
// Iterate for every bot name and replace in reply
|
||||
// @botname with @**botname**
|
||||
botsList.forEach(([bot, mention]) => {
|
||||
if (reply.includes(bot)) {
|
||||
const regex = new RegExp(bot, 'g');
|
||||
reply = reply.replace(regex, mention);
|
||||
}
|
||||
});
|
||||
|
||||
reply = reply.replace(/\\n/, '\n');
|
||||
return reply;
|
||||
}
|
||||
73
app/renderer/js/notification/index.ts
Normal file
73
app/renderer/js/notification/index.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import {remote} from 'electron';
|
||||
import electron_bridge from '../electron-bridge';
|
||||
import {appId, loadBots} from './helpers';
|
||||
|
||||
import DefaultNotification from './default-notification';
|
||||
const {app} = remote;
|
||||
|
||||
// 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(appId);
|
||||
|
||||
let Notification = DefaultNotification;
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
Notification = require('./darwin-notifications');
|
||||
}
|
||||
|
||||
export interface NotificationData {
|
||||
close: () => void;
|
||||
title: string;
|
||||
dir: NotificationDirection;
|
||||
lang: string;
|
||||
body: string;
|
||||
tag: string;
|
||||
image: string;
|
||||
icon: string;
|
||||
badge: string;
|
||||
vibrate: readonly number[];
|
||||
timestamp: number;
|
||||
renotify: boolean;
|
||||
silent: boolean;
|
||||
requireInteraction: boolean;
|
||||
data: unknown;
|
||||
actions: readonly NotificationAction[];
|
||||
}
|
||||
|
||||
export function newNotification(
|
||||
title: string,
|
||||
options: NotificationOptions | undefined,
|
||||
dispatch: (type: string, eventInit: EventInit) => boolean
|
||||
): NotificationData {
|
||||
const notification = new Notification(title, options);
|
||||
for (const type of ['click', 'close', 'error', 'show']) {
|
||||
notification.addEventListener(type, (ev: Event) => {
|
||||
if (!dispatch(type, ev)) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
close: () => notification.close(),
|
||||
title: notification.title,
|
||||
dir: notification.dir,
|
||||
lang: notification.lang,
|
||||
body: notification.body,
|
||||
tag: notification.tag,
|
||||
image: notification.image,
|
||||
icon: notification.icon,
|
||||
badge: notification.badge,
|
||||
vibrate: notification.vibrate,
|
||||
timestamp: notification.timestamp,
|
||||
renotify: notification.renotify,
|
||||
silent: notification.silent,
|
||||
requireInteraction: notification.requireInteraction,
|
||||
data: notification.data,
|
||||
actions: notification.actions
|
||||
};
|
||||
}
|
||||
|
||||
electron_bridge.once('zulip-loaded', async () => {
|
||||
await loadBots();
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
'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();
|
||||
};
|
||||
10
app/renderer/js/pages/network.ts
Normal file
10
app/renderer/js/pages/network.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
export function init($reconnectButton: Element, $settingsButton: Element): void {
|
||||
$reconnectButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
});
|
||||
$settingsButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'open-settings');
|
||||
});
|
||||
}
|
||||
99
app/renderer/js/pages/preference/add-certificate.ts
Normal file
99
app/renderer/js/pages/preference/add-certificate.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
'use-strict';
|
||||
|
||||
import {remote, OpenDialogOptions} from 'electron';
|
||||
|
||||
import path from 'path';
|
||||
import BaseComponent from '../../components/base';
|
||||
import * as CertificateUtil from '../../utils/certificate-util';
|
||||
import * as DomainUtil from '../../utils/domain-util';
|
||||
import * as t from '../../utils/translation-util';
|
||||
|
||||
interface AddCertificateProps {
|
||||
$root: Element;
|
||||
}
|
||||
|
||||
const {dialog} = remote;
|
||||
|
||||
export default class AddCertificate extends BaseComponent {
|
||||
props: AddCertificateProps;
|
||||
_certFile: string;
|
||||
$addCertificate: Element | null;
|
||||
addCertificateButton: Element | null;
|
||||
serverUrl: HTMLInputElement | null;
|
||||
constructor(props: AddCertificateProps) {
|
||||
super();
|
||||
this.props = props;
|
||||
this._certFile = '';
|
||||
}
|
||||
|
||||
template(): string {
|
||||
return `
|
||||
<div class="settings-card certificates-card">
|
||||
<div class="certificate-input">
|
||||
<div>${t.__('Organization URL')}</div>
|
||||
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
|
||||
</div>
|
||||
<div class="certificate-input">
|
||||
<div>${t.__('Certificate file')}</div>
|
||||
<button class="green" id="add-certificate-button">${t.__('Upload')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.$addCertificate = this.generateNodeFromTemplate(this.template());
|
||||
this.props.$root.append(this.$addCertificate);
|
||||
this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button');
|
||||
this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
|
||||
this.initListeners();
|
||||
}
|
||||
|
||||
async validateAndAdd(): Promise<void> {
|
||||
const certificate = this._certFile;
|
||||
const serverUrl = this.serverUrl.value;
|
||||
if (certificate !== '' && serverUrl !== '') {
|
||||
const server = encodeURIComponent(DomainUtil.formatUrl(serverUrl));
|
||||
const fileName = path.basename(certificate);
|
||||
const copy = CertificateUtil.copyCertificate(server, certificate, fileName);
|
||||
if (!copy) {
|
||||
return;
|
||||
}
|
||||
|
||||
CertificateUtil.setCertificate(server, fileName);
|
||||
this.serverUrl.value = '';
|
||||
await dialog.showMessageBox({
|
||||
title: 'Success',
|
||||
message: 'Certificate saved!'
|
||||
});
|
||||
} else {
|
||||
dialog.showErrorBox('Error', `Please, ${serverUrl === '' ?
|
||||
'Enter an Organization URL' : 'Choose certificate file'}`);
|
||||
}
|
||||
}
|
||||
|
||||
async addHandler(): Promise<void> {
|
||||
const showDialogOptions: OpenDialogOptions = {
|
||||
title: 'Select file',
|
||||
properties: ['openFile'],
|
||||
filters: [{name: 'crt, pem', extensions: ['crt', 'pem']}]
|
||||
};
|
||||
const {filePaths, canceled} = await dialog.showOpenDialog(showDialogOptions);
|
||||
if (!canceled) {
|
||||
this._certFile = filePaths[0] || '';
|
||||
await this.validateAndAdd();
|
||||
}
|
||||
}
|
||||
|
||||
initListeners(): void {
|
||||
this.addCertificateButton.addEventListener('click', async () => {
|
||||
await this.addHandler();
|
||||
});
|
||||
|
||||
this.serverUrl.addEventListener('keypress', async event => {
|
||||
if (event.key === 'Enter') {
|
||||
await this.addHandler();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
'use strict';
|
||||
const electron = require('electron');
|
||||
const { app } = require('electron');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
|
||||
|
||||
let instance = null;
|
||||
|
||||
class BadgeSettings {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
showBadgeCount(messageCount, mainWindow) {
|
||||
if (process.platform === 'darwin') {
|
||||
app.setBadgeCount(messageCount);
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
this.updateOverlayIcon(messageCount, mainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
hideBadgeCount(mainWindow) {
|
||||
if (process.platform === 'darwin') {
|
||||
app.setBadgeCount(0);
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
}
|
||||
}
|
||||
|
||||
updateBadge(badgeCount, mainWindow) {
|
||||
if (ConfigUtil.getConfigItem('badgeOption', true)) {
|
||||
this.showBadgeCount(badgeCount, mainWindow);
|
||||
} else {
|
||||
this.hideBadgeCount(mainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
updateOverlayIcon(messageCount, mainWindow) {
|
||||
if (!mainWindow.isFocused()) {
|
||||
mainWindow.flashFrame(true);
|
||||
}
|
||||
if (messageCount === 0) {
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
} else {
|
||||
mainWindow.webContents.send('render-taskbar-icon', messageCount);
|
||||
}
|
||||
}
|
||||
|
||||
updateTaskbarIcon(data, text, mainWindow) {
|
||||
const img = electron.nativeImage.createFromDataURL(data);
|
||||
mainWindow.setOverlayIcon(img, text);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new BadgeSettings();
|
||||
44
app/renderer/js/pages/preference/badge-settings.ts
Normal file
44
app/renderer/js/pages/preference/badge-settings.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import electron, {app} from 'electron';
|
||||
|
||||
import * as ConfigUtil from '../../utils/config-util';
|
||||
|
||||
function showBadgeCount(messageCount: number, mainWindow: electron.BrowserWindow): void {
|
||||
if (process.platform === 'win32') {
|
||||
updateOverlayIcon(messageCount, mainWindow);
|
||||
} else {
|
||||
app.badgeCount = messageCount;
|
||||
}
|
||||
}
|
||||
|
||||
function hideBadgeCount(mainWindow: electron.BrowserWindow): void {
|
||||
if (process.platform === 'win32') {
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
} else {
|
||||
app.badgeCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateBadge(badgeCount: number, mainWindow: electron.BrowserWindow): void {
|
||||
if (ConfigUtil.getConfigItem('badgeOption', true)) {
|
||||
showBadgeCount(badgeCount, mainWindow);
|
||||
} else {
|
||||
hideBadgeCount(mainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
function updateOverlayIcon(messageCount: number, mainWindow: electron.BrowserWindow): void {
|
||||
if (!mainWindow.isFocused()) {
|
||||
mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
|
||||
}
|
||||
|
||||
if (messageCount === 0) {
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
} else {
|
||||
mainWindow.webContents.send('render-taskbar-icon', messageCount);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTaskbarIcon(data: string, text: string, mainWindow: electron.BrowserWindow): void {
|
||||
const img = electron.nativeImage.createFromDataURL(data);
|
||||
mainWindow.setOverlayIcon(img, text);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
'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;
|
||||
65
app/renderer/js/pages/preference/base-section.ts
Normal file
65
app/renderer/js/pages/preference/base-section.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {ipcRenderer} from 'electron';
|
||||
import escape from 'escape-html';
|
||||
|
||||
import BaseComponent from '../../components/base';
|
||||
|
||||
interface BaseSectionProps {
|
||||
$element: HTMLElement;
|
||||
disabled?: boolean;
|
||||
value: boolean;
|
||||
clickHandler: () => void;
|
||||
}
|
||||
|
||||
export default class BaseSection extends BaseComponent {
|
||||
generateSettingOption(props: BaseSectionProps): void {
|
||||
const {$element, disabled, value, clickHandler} = props;
|
||||
|
||||
$element.innerHTML = '';
|
||||
|
||||
const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value, disabled));
|
||||
$element.append($optionControl);
|
||||
|
||||
if (!disabled) {
|
||||
$optionControl.addEventListener('click', clickHandler);
|
||||
}
|
||||
}
|
||||
|
||||
generateOptionTemplate(settingOption: boolean, disabled?: boolean): string {
|
||||
const label = disabled ? '<label class="disallowed" title="Setting locked by system administrator."/>' : '<label/>';
|
||||
if (settingOption) {
|
||||
return `
|
||||
<div class="action">
|
||||
<div class="switch">
|
||||
<input class="toggle toggle-round" type="checkbox" checked disabled>
|
||||
${label}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="action">
|
||||
<div class="switch">
|
||||
<input class="toggle toggle-round" type="checkbox">
|
||||
${label}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/* A method that in future can be used to create dropdown menus using <select> <option> tags.
|
||||
it needs an object which has ``key: value`` pairs and will return a string that can be appended to HTML
|
||||
*/
|
||||
generateSelectTemplate(options: {[key: string]: string}, className?: string, idName?: string): string {
|
||||
let select = `<select class="${escape(className)}" id="${escape(idName)}">\n`;
|
||||
Object.keys(options).forEach(key => {
|
||||
select += `<option name="${escape(key)}" value="${escape(key)}">${escape(options[key])}</option>\n`;
|
||||
});
|
||||
select += '</select>';
|
||||
return select;
|
||||
}
|
||||
|
||||
reloadApp(): void {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
}
|
||||
}
|
||||
89
app/renderer/js/pages/preference/connected-org-section.ts
Normal file
89
app/renderer/js/pages/preference/connected-org-section.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
import BaseSection from './base-section';
|
||||
import * as DomainUtil from '../../utils/domain-util';
|
||||
import ServerInfoForm from './server-info-form';
|
||||
import AddCertificate from './add-certificate';
|
||||
import FindAccounts from './find-accounts';
|
||||
import * as t from '../../utils/translation-util';
|
||||
|
||||
interface ConnectedOrgSectionProps {
|
||||
$root: Element;
|
||||
}
|
||||
|
||||
export default class ConnectedOrgSection extends BaseSection {
|
||||
props: ConnectedOrgSectionProps;
|
||||
$serverInfoContainer: Element | null;
|
||||
$existingServers: Element | null;
|
||||
$newOrgButton: HTMLButtonElement | null;
|
||||
$addCertificateContainer: Element | null;
|
||||
$findAccountsContainer: Element | null;
|
||||
constructor(props: ConnectedOrgSectionProps) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template(): string {
|
||||
return `
|
||||
<div class="settings-pane" id="server-settings-pane">
|
||||
<div class="page-title">${t.__('Connected organizations')}</div>
|
||||
<div class="title" id="existing-servers">${t.__('All the connected orgnizations will appear here.')}</div>
|
||||
<div id="server-info-container"></div>
|
||||
<div id="new-org-button"><button class="green sea w-250">${t.__('Connect to another organization')}</button></div>
|
||||
<div class="page-title">${t.__('Add Custom Certificates')}</div>
|
||||
<div id="add-certificate-container"></div>
|
||||
<div class="page-title">${t.__('Find accounts by email')}</div>
|
||||
<div id="find-accounts-container"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.initServers();
|
||||
}
|
||||
|
||||
initServers(): void {
|
||||
this.props.$root.innerHTML = '';
|
||||
|
||||
const servers = DomainUtil.getDomains();
|
||||
this.props.$root.innerHTML = this.template();
|
||||
|
||||
this.$serverInfoContainer = document.querySelector('#server-info-container');
|
||||
this.$existingServers = document.querySelector('#existing-servers');
|
||||
this.$newOrgButton = document.querySelector('#new-org-button');
|
||||
this.$addCertificateContainer = document.querySelector('#add-certificate-container');
|
||||
this.$findAccountsContainer = document.querySelector('#find-accounts-container');
|
||||
|
||||
const noServerText = t.__('All the connected orgnizations will appear here');
|
||||
// Show noServerText if no servers are there otherwise hide it
|
||||
this.$existingServers.innerHTML = servers.length === 0 ? noServerText : '';
|
||||
|
||||
for (const [i, server] of servers.entries()) {
|
||||
new ServerInfoForm({
|
||||
$root: this.$serverInfoContainer,
|
||||
server,
|
||||
index: i,
|
||||
onChange: this.reloadApp
|
||||
}).init();
|
||||
}
|
||||
|
||||
this.$newOrgButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'open-org-tab');
|
||||
});
|
||||
|
||||
this.initAddCertificate();
|
||||
this.initFindAccounts();
|
||||
}
|
||||
|
||||
initAddCertificate(): void {
|
||||
new AddCertificate({
|
||||
$root: this.$addCertificateContainer
|
||||
}).init();
|
||||
}
|
||||
|
||||
initFindAccounts(): void {
|
||||
new FindAccounts({
|
||||
$root: this.$findAccountsContainer
|
||||
}).init();
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
'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;
|
||||
80
app/renderer/js/pages/preference/find-accounts.ts
Normal file
80
app/renderer/js/pages/preference/find-accounts.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
'use-strict';
|
||||
|
||||
import BaseComponent from '../../components/base';
|
||||
import * as LinkUtil from '../../utils/link-util';
|
||||
import * as t from '../../utils/translation-util';
|
||||
|
||||
interface FindAccountsProps {
|
||||
$root: Element;
|
||||
}
|
||||
|
||||
export default class FindAccounts extends BaseComponent {
|
||||
props: FindAccountsProps;
|
||||
$findAccounts: Element | null;
|
||||
$findAccountsButton: Element | null;
|
||||
$serverUrlField: HTMLInputElement | null;
|
||||
constructor(props: FindAccountsProps) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template(): string {
|
||||
return `
|
||||
<div class="settings-card certificate-card">
|
||||
<div class="certificate-input">
|
||||
<div>${t.__('Organization URL')}</div>
|
||||
<input class="setting-input-value" value="zulipchat.com"/>
|
||||
</div>
|
||||
<div class="certificate-input">
|
||||
<button class="green w-150" id="find-accounts-button">${t.__('Find accounts')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.$findAccounts = this.generateNodeFromTemplate(this.template());
|
||||
this.props.$root.append(this.$findAccounts);
|
||||
this.$findAccountsButton = this.$findAccounts.querySelector('#find-accounts-button');
|
||||
this.$serverUrlField = this.$findAccounts.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
|
||||
this.initListeners();
|
||||
}
|
||||
|
||||
async findAccounts(url: string): Promise<void> {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url.startsWith('http')) {
|
||||
url = 'https://' + url;
|
||||
}
|
||||
|
||||
await LinkUtil.openBrowser(new URL('/accounts/find', url));
|
||||
}
|
||||
|
||||
initListeners(): void {
|
||||
this.$findAccountsButton.addEventListener('click', async () => {
|
||||
await this.findAccounts(this.$serverUrlField.value);
|
||||
});
|
||||
|
||||
this.$serverUrlField.addEventListener('click', () => {
|
||||
if (this.$serverUrlField.value === 'zulipchat.com') {
|
||||
this.$serverUrlField.setSelectionRange(0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
this.$serverUrlField.addEventListener('keypress', async event => {
|
||||
if (event.key === 'Enter') {
|
||||
await this.findAccounts(this.$serverUrlField.value);
|
||||
}
|
||||
});
|
||||
|
||||
this.$serverUrlField.addEventListener('input', () => {
|
||||
if (this.$serverUrlField.value) {
|
||||
this.$serverUrlField.classList.remove('invalid-input-value');
|
||||
} else {
|
||||
this.$serverUrlField.classList.add('invalid-input-value');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
'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>
|
||||
<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>
|
||||
<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.updateUpdateOption();
|
||||
this.updateSilentOption();
|
||||
this.updateSidebarOption();
|
||||
this.updateStartAtLoginOption();
|
||||
this.updateResetDataOption();
|
||||
this.showDesktopNotification();
|
||||
}
|
||||
|
||||
updateTrayOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#tray-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('trayIcon', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('trayIcon');
|
||||
ConfigUtil.setConfigItem('trayIcon', newValue);
|
||||
ipcRenderer.send('forward-message', 'toggletray');
|
||||
this.updateTrayOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateBadgeOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#badge-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('badgeOption', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('badgeOption');
|
||||
ConfigUtil.setConfigItem('badgeOption', newValue);
|
||||
ipcRenderer.send('toggle-badge-option', newValue);
|
||||
this.updateBadgeOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateUpdateOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#betaupdate-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('betaUpdate', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
|
||||
ConfigUtil.setConfigItem('betaUpdate', newValue);
|
||||
this.updateUpdateOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSilentOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#silent-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('silent', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('silent', true);
|
||||
ConfigUtil.setConfigItem('silent', newValue);
|
||||
this.updateSilentOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
581
app/renderer/js/pages/preference/general-section.ts
Normal file
581
app/renderer/js/pages/preference/general-section.ts
Normal file
@@ -0,0 +1,581 @@
|
||||
import {ipcRenderer, remote, OpenDialogOptions} from 'electron';
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
const {app, dialog, session} = remote;
|
||||
const currentBrowserWindow = remote.getCurrentWindow();
|
||||
|
||||
import BaseSection from './base-section';
|
||||
import * as ConfigUtil from '../../utils/config-util';
|
||||
import * as EnterpriseUtil from '../../utils/enterprise-util';
|
||||
import * as t from '../../utils/translation-util';
|
||||
import supportedLocales from '../../../../translations/supported-locales.json';
|
||||
import Tagify from '@yaireo/tagify';
|
||||
import ISO6391 from 'iso-639-1';
|
||||
|
||||
interface GeneralSectionProps {
|
||||
$root: Element;
|
||||
}
|
||||
|
||||
export default class GeneralSection extends BaseSection {
|
||||
props: GeneralSectionProps;
|
||||
constructor(props: GeneralSectionProps) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template(): string {
|
||||
return `
|
||||
<div class="settings-pane">
|
||||
<div class="title">${t.__('Appearance')}</div>
|
||||
<div id="appearance-option-settings" class="settings-card">
|
||||
<div class="setting-row" id="tray-option">
|
||||
<div class="setting-description">${t.__('Show app icon in system tray')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="menubar-option" style= "display:${process.platform === 'darwin' ? 'none' : ''}">
|
||||
<div class="setting-description">${t.__('Auto hide menu bar (Press Alt key to display)')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="sidebar-option">
|
||||
<div class="setting-description">${t.__('Show sidebar')} (<span class="code">${process.platform === 'darwin' ? 'Cmd+Shift+S' : 'Ctrl+Shift+S'}</span>)</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="badge-option">
|
||||
<div class="setting-description">${t.__('Show app unread badge')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="dock-bounce-option" style= "display:${process.platform === 'darwin' ? '' : 'none'}">
|
||||
<div class="setting-description">${t.__('Bounce dock on new private message')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="flash-taskbar-option" style= "display:${process.platform === 'win32' ? '' : 'none'}">
|
||||
<div class="setting-description">${t.__('Flash taskbar on new message')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">${t.__('Desktop Notifications')}</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="show-notification-option">
|
||||
<div class="setting-description">${t.__('Show desktop notifications')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="silent-option">
|
||||
<div class="setting-description">${t.__('Mute all sounds from Zulip')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">${t.__('App Updates')}</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="autoupdate-option">
|
||||
<div class="setting-description">${t.__('Enable auto updates')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="betaupdate-option">
|
||||
<div class="setting-description">${t.__('Get beta updates')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">${t.__('Functionality')}</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="startAtLogin-option">
|
||||
<div class="setting-description">${t.__('Start app at login')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="start-minimize-option">
|
||||
<div class="setting-description">${t.__('Always start minimized')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="quitOnClose-option">
|
||||
<div class="setting-description">${t.__('Quit when the window is closed')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="enable-spellchecker-option">
|
||||
<div class="setting-description">${t.__('Enable spellchecker (requires restart)')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="spellcheck-langs" style= "display:${process.platform === 'darwin' ? 'none' : ''}"></div>
|
||||
<div class="setting-row" id="note"></div>
|
||||
</div>
|
||||
|
||||
<div class="title">${t.__('Advanced')}</div>
|
||||
<div class="settings-card">
|
||||
|
||||
<div class="setting-row" id="enable-error-reporting">
|
||||
<div class="setting-description">${t.__('Enable error reporting (requires restart)')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row" id="app-language">
|
||||
<div class="setting-description">${t.__('App language (requires restart)')}</div>
|
||||
<div id="lang-div" class="lang-div"></div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row" id="add-custom-css">
|
||||
<div class="setting-description">
|
||||
${t.__('Add custom CSS')}
|
||||
</div>
|
||||
<button class="custom-css-button green">${t.__('Upload')}</button>
|
||||
</div>
|
||||
<div class="setting-row" id="remove-custom-css">
|
||||
<div class="setting-description">
|
||||
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigString('customCSS', '')}</div>
|
||||
</div>
|
||||
<div class="action red" id="css-delete-action">
|
||||
<i class="material-icons">indeterminate_check_box</i>
|
||||
<span>${t.__('Delete')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row" id="download-folder">
|
||||
<div class="setting-description">
|
||||
${t.__('Default download location')}
|
||||
</div>
|
||||
<button class="download-folder-button green">${t.__('Change')}</button>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-description">
|
||||
<div class="download-folder-path">${ConfigUtil.getConfigString('downloadsPath', app.getPath('downloads'))}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row" id="prompt-download">
|
||||
<div class="setting-description">${t.__('Ask where to save files before downloading')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">${t.__('Factory Reset Data')}</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="factory-reset-option">
|
||||
<div class="setting-description">${t.__('Reset the application, thus deleting all the connected organizations, accounts, and certificates.')}
|
||||
</div>
|
||||
<button class="factory-reset-button red w-150">${t.__('Factory Reset')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.updateTrayOption();
|
||||
this.updateBadgeOption();
|
||||
this.updateSilentOption();
|
||||
this.autoUpdateOption();
|
||||
this.betaUpdateOption();
|
||||
this.updateSidebarOption();
|
||||
this.updateStartAtLoginOption();
|
||||
this.factoryReset();
|
||||
this.showDesktopNotification();
|
||||
this.enableSpellchecker();
|
||||
this.minimizeOnStart();
|
||||
this.addCustomCSS();
|
||||
this.showCustomCSSPath();
|
||||
this.removeCustomCSS();
|
||||
this.downloadFolder();
|
||||
this.updateQuitOnCloseOption();
|
||||
this.updatePromptDownloadOption();
|
||||
this.enableErrorReporting();
|
||||
this.setLocale();
|
||||
this.initSpellChecker();
|
||||
|
||||
// Platform specific settings
|
||||
|
||||
// Flashing taskbar on Windows
|
||||
if (process.platform === 'win32') {
|
||||
this.updateFlashTaskbar();
|
||||
}
|
||||
|
||||
// Dock bounce on macOS
|
||||
if (process.platform === 'darwin') {
|
||||
this.updateDockBouncing();
|
||||
}
|
||||
|
||||
// Auto hide menubar on Windows and Linux
|
||||
if (process.platform !== 'darwin') {
|
||||
this.updateMenubarOption();
|
||||
}
|
||||
}
|
||||
|
||||
updateTrayOption(): void {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateMenubarOption(): void {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#menubar-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('autoHideMenubar', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('autoHideMenubar');
|
||||
ConfigUtil.setConfigItem('autoHideMenubar', newValue);
|
||||
ipcRenderer.send('toggle-menubar', newValue);
|
||||
this.updateMenubarOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateBadgeOption(): void {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateDockBouncing(): void {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#dock-bounce-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('dockBouncing', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('dockBouncing');
|
||||
ConfigUtil.setConfigItem('dockBouncing', newValue);
|
||||
this.updateDockBouncing();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateFlashTaskbar(): void {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
autoUpdateOption(): void {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#autoupdate-option .setting-control'),
|
||||
disabled: EnterpriseUtil.configItemExists('autoUpdate'),
|
||||
value: ConfigUtil.getConfigItem('autoUpdate', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('autoUpdate');
|
||||
ConfigUtil.setConfigItem('autoUpdate', newValue);
|
||||
if (!newValue) {
|
||||
ConfigUtil.setConfigItem('betaUpdate', false);
|
||||
this.betaUpdateOption();
|
||||
}
|
||||
|
||||
this.autoUpdateOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
betaUpdateOption(): void {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#betaupdate-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('betaUpdate', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
|
||||
if (ConfigUtil.getConfigItem('autoUpdate')) {
|
||||
ConfigUtil.setConfigItem('betaUpdate', newValue);
|
||||
this.betaUpdateOption();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSilentOption(): void {
|
||||
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();
|
||||
currentBrowserWindow.webContents.send('toggle-silent', newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showDesktopNotification(): void {
|
||||
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(): void {
|
||||
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(): void {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateQuitOnCloseOption(): void {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#quitOnClose-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('quitOnClose', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('quitOnClose');
|
||||
ConfigUtil.setConfigItem('quitOnClose', newValue);
|
||||
this.updateQuitOnCloseOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableSpellchecker(): void {
|
||||
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();
|
||||
const spellcheckerLanguageInput: HTMLElement = document.querySelector('#spellcheck-langs');
|
||||
const spellcheckerNote: HTMLElement = document.querySelector('#note');
|
||||
spellcheckerLanguageInput.style.display = spellcheckerLanguageInput.style.display === 'none' ? '' : 'none';
|
||||
spellcheckerNote.style.display = spellcheckerNote.style.display === 'none' ? '' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableErrorReporting(): void {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#enable-error-reporting .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('errorReporting', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('errorReporting');
|
||||
ConfigUtil.setConfigItem('errorReporting', newValue);
|
||||
this.enableErrorReporting();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async customCssDialog(): Promise<void> {
|
||||
const showDialogOptions: OpenDialogOptions = {
|
||||
title: 'Select file',
|
||||
properties: ['openFile'],
|
||||
filters: [{name: 'CSS file', extensions: ['css']}]
|
||||
};
|
||||
|
||||
const {filePaths, canceled} = await dialog.showOpenDialog(showDialogOptions);
|
||||
if (!canceled) {
|
||||
ConfigUtil.setConfigItem('customCSS', filePaths[0]);
|
||||
ipcRenderer.send('forward-message', 'hard-reload');
|
||||
}
|
||||
}
|
||||
|
||||
setLocale(): void {
|
||||
const langDiv: HTMLSelectElement = document.querySelector('.lang-div');
|
||||
const langList = this.generateSelectTemplate(supportedLocales, 'lang-menu');
|
||||
langDiv.innerHTML += langList;
|
||||
// `langMenu` is the select-option dropdown menu formed after executing the previous command
|
||||
const langMenu: HTMLSelectElement = document.querySelector('.lang-menu');
|
||||
|
||||
// The next three lines set the selected language visible on the dropdown button
|
||||
let language = ConfigUtil.getConfigItem('appLanguage');
|
||||
language = language && langMenu.options.namedItem(language) ? language : 'en';
|
||||
langMenu.options.namedItem(language).selected = true;
|
||||
|
||||
langMenu.addEventListener('change', (event: Event) => {
|
||||
ConfigUtil.setConfigItem('appLanguage', (event.target as HTMLSelectElement).value);
|
||||
});
|
||||
}
|
||||
|
||||
minimizeOnStart(): void {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#start-minimize-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('startMinimized', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('startMinimized');
|
||||
ConfigUtil.setConfigItem('startMinimized', newValue);
|
||||
this.minimizeOnStart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addCustomCSS(): void {
|
||||
const customCSSButton = document.querySelector('#add-custom-css .custom-css-button');
|
||||
customCSSButton.addEventListener('click', async () => {
|
||||
await this.customCssDialog();
|
||||
});
|
||||
}
|
||||
|
||||
showCustomCSSPath(): void {
|
||||
if (!ConfigUtil.getConfigItem('customCSS')) {
|
||||
const cssPATH: HTMLElement = document.querySelector('#remove-custom-css');
|
||||
cssPATH.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
removeCustomCSS(): void {
|
||||
const removeCSSButton = document.querySelector('#css-delete-action');
|
||||
removeCSSButton.addEventListener('click', () => {
|
||||
ConfigUtil.setConfigItem('customCSS', '');
|
||||
ipcRenderer.send('forward-message', 'hard-reload');
|
||||
});
|
||||
}
|
||||
|
||||
async downloadFolderDialog(): Promise<void> {
|
||||
const showDialogOptions: OpenDialogOptions = {
|
||||
title: 'Select Download Location',
|
||||
properties: ['openDirectory']
|
||||
};
|
||||
|
||||
const {filePaths, canceled} = await dialog.showOpenDialog(showDialogOptions);
|
||||
if (!canceled) {
|
||||
ConfigUtil.setConfigItem('downloadsPath', filePaths[0]);
|
||||
const downloadFolderPath: HTMLElement = document.querySelector('.download-folder-path');
|
||||
downloadFolderPath.textContent = filePaths[0];
|
||||
}
|
||||
}
|
||||
|
||||
downloadFolder(): void {
|
||||
const downloadFolder = document.querySelector('#download-folder .download-folder-button');
|
||||
downloadFolder.addEventListener('click', async () => {
|
||||
await this.downloadFolderDialog();
|
||||
});
|
||||
}
|
||||
|
||||
updatePromptDownloadOption(): void {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#prompt-download .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('promptDownload', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('promptDownload');
|
||||
ConfigUtil.setConfigItem('promptDownload', newValue);
|
||||
this.updatePromptDownloadOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async factoryResetSettings(): Promise<void> {
|
||||
const clearAppDataMessage = 'When the application restarts, it will be as if you have just downloaded Zulip app.';
|
||||
const getAppPath = path.join(app.getPath('appData'), app.name);
|
||||
|
||||
const {response} = await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['YES', 'NO'],
|
||||
defaultId: 0,
|
||||
message: 'Are you sure?',
|
||||
detail: clearAppDataMessage
|
||||
});
|
||||
if (response === 0) {
|
||||
await fs.remove(getAppPath);
|
||||
setTimeout(() => ipcRenderer.send('clear-app-settings'), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
factoryReset(): void {
|
||||
const factoryResetButton = document.querySelector('#factory-reset-option .factory-reset-button');
|
||||
factoryResetButton.addEventListener('click', async () => {
|
||||
await this.factoryResetSettings();
|
||||
});
|
||||
}
|
||||
|
||||
initSpellChecker(): void {
|
||||
// The elctron API is a no-op on macOS and macOS default spellchecker is used.
|
||||
if (process.platform === 'darwin') {
|
||||
const note: HTMLElement = document.querySelector('#note');
|
||||
note.append(t.__('On macOS, the OS spellchecker is used.'));
|
||||
note.append(document.createElement('br'));
|
||||
note.append(t.__('Change the language from System Preferences → Keyboard → Text → Spelling.'));
|
||||
} else {
|
||||
const note: HTMLElement = document.querySelector('#note');
|
||||
note.append(t.__('You can select a maximum of 3 languages for spellchecking.'));
|
||||
const spellDiv: HTMLElement = document.querySelector('#spellcheck-langs');
|
||||
spellDiv.innerHTML += `
|
||||
<div class="setting-description">${t.__('Spellchecker Languages')}</div>
|
||||
<input name='spellcheck' placeholder='Enter Languages'>`;
|
||||
|
||||
const availableLanguages = session.fromPartition('persist:webviewsession').availableSpellCheckerLanguages;
|
||||
let languagePairs: Map<string, string> = new Map();
|
||||
availableLanguages.forEach((l: string) => {
|
||||
if (ISO6391.validate(l)) {
|
||||
languagePairs.set(ISO6391.getName(l), l);
|
||||
}
|
||||
});
|
||||
|
||||
// Manually set names for languages not available in ISO6391
|
||||
languagePairs.set('English (AU)', 'en-AU');
|
||||
languagePairs.set('English (CA)', 'en-CA');
|
||||
languagePairs.set('English (GB)', 'en-GB');
|
||||
languagePairs.set('English (US)', 'en-US');
|
||||
languagePairs.set('Spanish (Latin America)', 'es-419');
|
||||
languagePairs.set('Spanish (Argentina)', 'es-AR');
|
||||
languagePairs.set('Spanish (Mexico)', 'es-MX');
|
||||
languagePairs.set('Spanish (US)', 'es-US');
|
||||
languagePairs.set('Portuguese (Brazil)', 'pt-BR');
|
||||
languagePairs.set('Portuguese (Portugal)', 'pt-PT');
|
||||
languagePairs.set('Serbo-Croatian', 'sh');
|
||||
|
||||
languagePairs = new Map([...languagePairs].sort((a, b) => ((a[0] < b[0]) ? -1 : 1)));
|
||||
|
||||
const tagField: HTMLElement = document.querySelector('input[name=spellcheck]');
|
||||
const tagify = new Tagify(tagField, {
|
||||
whitelist: [...languagePairs.keys()],
|
||||
enforceWhitelist: true,
|
||||
maxTags: 3,
|
||||
dropdown: {
|
||||
enabled: 0,
|
||||
maxItems: Infinity,
|
||||
closeOnSelect: false,
|
||||
highlightFirst: true
|
||||
}
|
||||
});
|
||||
|
||||
const configuredLanguages: string[] = ConfigUtil.getConfigItem('spellcheckerLanguages').map((code: string) => [...languagePairs].filter(pair => (pair[1] === code))[0][0]);
|
||||
tagify.addTags(configuredLanguages);
|
||||
|
||||
tagField.addEventListener('change', event => {
|
||||
if ((event.target as HTMLInputElement).value.length === 0) {
|
||||
ConfigUtil.setConfigItem('spellcheckerLanguages', []);
|
||||
ipcRenderer.send('set-spellcheck-langs');
|
||||
} else {
|
||||
const spellLangs: string[] = [...JSON.parse((event.target as HTMLInputElement).value).values()].map(elt => languagePairs.get(elt.value));
|
||||
ConfigUtil.setConfigItem('spellcheckerLanguages', spellLangs);
|
||||
ipcRenderer.send('set-spellcheck-langs');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Do not display the spellchecker input and note if it is disabled
|
||||
if (!ConfigUtil.getConfigItem('enableSpellchecker')) {
|
||||
const spellcheckerLanguageInput: HTMLElement = document.querySelector('#spellcheck-langs');
|
||||
const spellcheckerNote: HTMLElement = document.querySelector('#note');
|
||||
spellcheckerLanguageInput.style.display = 'none';
|
||||
spellcheckerNote.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
'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;
|
||||
68
app/renderer/js/pages/preference/nav.ts
Normal file
68
app/renderer/js/pages/preference/nav.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import BaseComponent from '../../components/base';
|
||||
import * as t from '../../utils/translation-util';
|
||||
|
||||
interface PreferenceNavProps {
|
||||
$root: Element;
|
||||
onItemSelected: (navItem: string) => void;
|
||||
}
|
||||
|
||||
export default class PreferenceNav extends BaseComponent {
|
||||
props: PreferenceNavProps;
|
||||
navItems: string[];
|
||||
$el: Element;
|
||||
constructor(props: PreferenceNavProps) {
|
||||
super();
|
||||
this.props = props;
|
||||
this.navItems = ['General', 'Network', 'AddServer', 'Organizations', 'Shortcuts'];
|
||||
this.init();
|
||||
}
|
||||
|
||||
template(): string {
|
||||
let navItemsTemplate = '';
|
||||
for (const navItem of this.navItems) {
|
||||
navItemsTemplate += `<div class="nav" id="nav-${navItem}">${t.__(navItem)}</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div>
|
||||
<div id="settings-header">${t.__('Settings')}</div>
|
||||
<div id="nav-container">${navItemsTemplate}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.$el = this.generateNodeFromTemplate(this.template());
|
||||
this.props.$root.append(this.$el);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
registerListeners(): void {
|
||||
for (const navItem of this.navItems) {
|
||||
const $item = document.querySelector(`#nav-${navItem}`);
|
||||
$item.addEventListener('click', () => {
|
||||
this.props.onItemSelected(navItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
select(navItemToSelect: string): void {
|
||||
for (const navItem of this.navItems) {
|
||||
if (navItem === navItemToSelect) {
|
||||
this.activate(navItem);
|
||||
} else {
|
||||
this.deactivate(navItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activate(navItem: string): void {
|
||||
const $item = document.querySelector(`#nav-${navItem}`);
|
||||
$item.classList.add('active');
|
||||
}
|
||||
|
||||
deactivate(navItem: string): void {
|
||||
const $item = document.querySelector(`#nav-${navItem}`);
|
||||
$item.classList.remove('active');
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
'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;
|
||||
138
app/renderer/js/pages/preference/network-section.ts
Normal file
138
app/renderer/js/pages/preference/network-section.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
import BaseSection from './base-section';
|
||||
import * as ConfigUtil from '../../utils/config-util';
|
||||
import * as t from '../../utils/translation-util';
|
||||
|
||||
interface NetworkSectionProps {
|
||||
$root: Element;
|
||||
}
|
||||
|
||||
export default class NetworkSection extends BaseSection {
|
||||
props: NetworkSectionProps;
|
||||
$proxyPAC: HTMLInputElement;
|
||||
$proxyRules: HTMLInputElement;
|
||||
$proxyBypass: HTMLInputElement;
|
||||
$proxySaveAction: Element;
|
||||
$manualProxyBlock: Element;
|
||||
constructor(props: NetworkSectionProps) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template(): string {
|
||||
return `
|
||||
<div class="settings-pane">
|
||||
<div class="title">${t.__('Proxy')}</div>
|
||||
<div id="appearance-option-settings" class="settings-card">
|
||||
<div class="setting-row" id="use-system-settings">
|
||||
<div class="setting-description">${t.__('Use system proxy settings (requires restart)')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="use-manual-settings">
|
||||
<div class="setting-description">${t.__('Manual proxy configuration')}</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="manual-proxy-block">
|
||||
<div class="setting-row" id="proxy-pac-option">
|
||||
<span class="setting-input-key">PAC ${t.__('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">${t.__('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">${t.__('Proxy bypass rules')}</span>
|
||||
<input class="setting-input-value" placeholder="e.g. foobar.com"/>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="action green" id="proxy-save-action">
|
||||
<span>${t.__('Save')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
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.querySelector('#proxy-save-action');
|
||||
this.$manualProxyBlock = this.props.$root.querySelector('.manual-proxy-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(): void {
|
||||
const manualProxyEnabled = ConfigUtil.getConfigItem('useManualProxy', false);
|
||||
this.toggleManualProxySettings(manualProxyEnabled);
|
||||
|
||||
this.updateProxyOption();
|
||||
}
|
||||
|
||||
toggleManualProxySettings(option: boolean): void {
|
||||
if (option) {
|
||||
this.$manualProxyBlock.classList.remove('hidden');
|
||||
} else {
|
||||
this.$manualProxyBlock.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
updateProxyOption(): void {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#use-system-settings .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('useSystemProxy', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('useSystemProxy');
|
||||
const manualProxyValue = ConfigUtil.getConfigItem('useManualProxy');
|
||||
if (manualProxyValue && newValue) {
|
||||
ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue);
|
||||
this.toggleManualProxySettings(!manualProxyValue);
|
||||
}
|
||||
|
||||
if (!newValue) {
|
||||
// Remove proxy system proxy settings
|
||||
ConfigUtil.setConfigItem('proxyRules', '');
|
||||
ipcRenderer.send('forward-message', 'reload-proxy', false);
|
||||
}
|
||||
|
||||
ConfigUtil.setConfigItem('useSystemProxy', newValue);
|
||||
this.updateProxyOption();
|
||||
}
|
||||
});
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#use-manual-settings .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('useManualProxy', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('useManualProxy');
|
||||
const systemProxyValue = ConfigUtil.getConfigItem('useSystemProxy');
|
||||
this.toggleManualProxySettings(newValue);
|
||||
if (systemProxyValue && newValue) {
|
||||
ConfigUtil.setConfigItem('useSystemProxy', !systemProxyValue);
|
||||
}
|
||||
|
||||
ConfigUtil.setConfigItem('proxyRules', '');
|
||||
ConfigUtil.setConfigItem('useManualProxy', newValue);
|
||||
// Reload app only when turning manual proxy off, hence !newValue
|
||||
ipcRenderer.send('forward-message', 'reload-proxy', !newValue);
|
||||
this.updateProxyOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
'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;
|
||||
111
app/renderer/js/pages/preference/new-server-form.ts
Normal file
111
app/renderer/js/pages/preference/new-server-form.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {ipcRenderer, remote} from 'electron';
|
||||
|
||||
import BaseComponent from '../../components/base';
|
||||
import * as DomainUtil from '../../utils/domain-util';
|
||||
import * as LinkUtil from '../../utils/link-util';
|
||||
import * as t from '../../utils/translation-util';
|
||||
|
||||
const {dialog} = remote;
|
||||
|
||||
interface NewServerFormProps {
|
||||
$root: Element;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export default class NewServerForm extends BaseComponent {
|
||||
props: NewServerFormProps;
|
||||
$newServerForm: Element;
|
||||
$saveServerButton: HTMLButtonElement;
|
||||
$newServerUrl: HTMLInputElement;
|
||||
constructor(props: NewServerFormProps) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template(): string {
|
||||
return `
|
||||
<div class="server-input-container">
|
||||
<div class="title">${t.__('Organization URL')}</div>
|
||||
<div class="add-server-info-row">
|
||||
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
|
||||
</div>
|
||||
<div class="server-center">
|
||||
<button id="connect">${t.__('Connect')}</button>
|
||||
</div>
|
||||
<div class="server-center">
|
||||
<div class="divider">
|
||||
<hr class="left"/>${t.__('OR')}<hr class="right" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-center">
|
||||
<button id="open-create-org-link">${t.__('Create a new organization')}</button>
|
||||
</div>
|
||||
<div class="server-center">
|
||||
<div class="server-network-option">
|
||||
<span id="open-network-settings">${t.__('Network and Proxy Settings')}</span>
|
||||
<i class="material-icons open-network-button">open_in_new</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.initForm();
|
||||
this.initActions();
|
||||
}
|
||||
|
||||
initForm(): void {
|
||||
this.$newServerForm = this.generateNodeFromTemplate(this.template());
|
||||
this.$saveServerButton = this.$newServerForm.querySelector('#connect');
|
||||
this.props.$root.innerHTML = '';
|
||||
this.props.$root.append(this.$newServerForm);
|
||||
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
|
||||
}
|
||||
|
||||
async submitFormHandler(): Promise<void> {
|
||||
this.$saveServerButton.innerHTML = 'Connecting...';
|
||||
let serverConf;
|
||||
try {
|
||||
serverConf = await DomainUtil.checkDomain(this.$newServerUrl.value);
|
||||
} catch (error) {
|
||||
this.$saveServerButton.innerHTML = 'Connect';
|
||||
await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
message: error.toString(),
|
||||
buttons: ['OK']
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await DomainUtil.addDomain(serverConf);
|
||||
this.props.onChange();
|
||||
}
|
||||
|
||||
openCreateNewOrgExternalLink(): void {
|
||||
const link = 'https://zulip.com/new/';
|
||||
const externalCreateNewOrgElement = document.querySelector('#open-create-org-link');
|
||||
externalCreateNewOrgElement.addEventListener('click', async () => {
|
||||
await LinkUtil.openBrowser(new URL(link));
|
||||
});
|
||||
}
|
||||
|
||||
networkSettingsLink(): void {
|
||||
const networkSettingsId = document.querySelectorAll('.server-network-option')[0];
|
||||
networkSettingsId.addEventListener('click', () => ipcRenderer.send('forward-message', 'open-network-settings'));
|
||||
}
|
||||
|
||||
initActions(): void {
|
||||
this.$saveServerButton.addEventListener('click', async () => {
|
||||
await this.submitFormHandler();
|
||||
});
|
||||
this.$newServerUrl.addEventListener('keypress', async event => {
|
||||
if (event.key === 'Enter') {
|
||||
await this.submitFormHandler();
|
||||
}
|
||||
});
|
||||
// Open create new org link in default browser
|
||||
this.openCreateNewOrgExternalLink();
|
||||
this.networkSettingsLink();
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const BaseComponent = require(__dirname + '/js/components/base.js');
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
const Nav = require(__dirname + '/js/pages/preference/nav.js');
|
||||
const ServersSection = require(__dirname + '/js/pages/preference/servers-section.js');
|
||||
const GeneralSection = require(__dirname + '/js/pages/preference/general-section.js');
|
||||
const NetworkSection = require(__dirname + '/js/pages/preference/network-section.js');
|
||||
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();
|
||||
}
|
||||
|
||||
setDefaultView() {
|
||||
let nav = 'General';
|
||||
const hasTag = window.location.hash;
|
||||
if (hasTag) {
|
||||
nav = hasTag.substring(1);
|
||||
}
|
||||
this.handleNavigation(nav);
|
||||
}
|
||||
|
||||
handleNavigation(navItem) {
|
||||
this.nav.select(navItem);
|
||||
switch (navItem) {
|
||||
case 'Servers': {
|
||||
this.section = new ServersSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'General': {
|
||||
this.section = new GeneralSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'Network': {
|
||||
this.section = new NetworkSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
break;
|
||||
}
|
||||
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();
|
||||
};
|
||||
130
app/renderer/js/pages/preference/preference.ts
Normal file
130
app/renderer/js/pages/preference/preference.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
import BaseComponent from '../../components/base';
|
||||
import Nav from './nav';
|
||||
import ServersSection from './servers-section';
|
||||
import GeneralSection from './general-section';
|
||||
import NetworkSection from './network-section';
|
||||
import ConnectedOrgSection from './connected-org-section';
|
||||
import ShortcutsSection from './shortcuts-section';
|
||||
import type {DNDSettings} from '../../utils/dnd-util';
|
||||
|
||||
type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection;
|
||||
|
||||
export default class PreferenceView extends BaseComponent {
|
||||
$sidebarContainer: Element;
|
||||
$settingsContainer: Element;
|
||||
nav: Nav;
|
||||
section: Section;
|
||||
constructor() {
|
||||
super();
|
||||
this.$sidebarContainer = document.querySelector('#sidebar');
|
||||
this.$settingsContainer = document.querySelector('#settings-container');
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.nav = new Nav({
|
||||
$root: this.$sidebarContainer,
|
||||
onItemSelected: this.handleNavigation.bind(this)
|
||||
});
|
||||
|
||||
this.setDefaultView();
|
||||
this.registerIpcs();
|
||||
}
|
||||
|
||||
setDefaultView(): void {
|
||||
let nav = 'General';
|
||||
const hasTag = window.location.hash;
|
||||
if (hasTag) {
|
||||
nav = hasTag.slice(1);
|
||||
}
|
||||
|
||||
this.handleNavigation(nav);
|
||||
}
|
||||
|
||||
handleNavigation(navItem: string): void {
|
||||
this.nav.select(navItem);
|
||||
switch (navItem) {
|
||||
case 'AddServer': {
|
||||
this.section = new ServersSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'General': {
|
||||
this.section = new GeneralSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Organizations': {
|
||||
this.section = new ConnectedOrgSection({
|
||||
$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}`;
|
||||
}
|
||||
|
||||
// Handle toggling and reflect changes in preference page
|
||||
handleToggle(elementName: string, state: boolean): void {
|
||||
const inputSelector = `#${elementName} .action .switch input`;
|
||||
const input: HTMLInputElement = document.querySelector(inputSelector);
|
||||
if (input) {
|
||||
input.checked = state;
|
||||
}
|
||||
}
|
||||
|
||||
registerIpcs(): void {
|
||||
ipcRenderer.on('switch-settings-nav', (_event: Event, navItem: string) => {
|
||||
this.handleNavigation(navItem);
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggle-sidebar-setting', (_event: Event, state: boolean) => {
|
||||
this.handleToggle('sidebar-option', state);
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggle-menubar-setting', (_event: Event, state: boolean) => {
|
||||
this.handleToggle('menubar-option', state);
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggletray', (_event: Event, state: boolean) => {
|
||||
this.handleToggle('tray-option', state);
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggle-dnd', (_event: Event, _state: boolean, newSettings: DNDSettings) => {
|
||||
this.handleToggle('show-notification-option', newSettings.showNotification);
|
||||
this.handleToggle('silent-option', newSettings.silent);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
this.handleToggle('flash-taskbar-option', newSettings.flashTaskbarOnMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const preferenceView = new PreferenceView();
|
||||
preferenceView.init();
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
'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;
|
||||
97
app/renderer/js/pages/preference/server-info-form.ts
Normal file
97
app/renderer/js/pages/preference/server-info-form.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {remote, ipcRenderer} from 'electron';
|
||||
|
||||
import BaseComponent from '../../components/base';
|
||||
import * as DomainUtil from '../../utils/domain-util';
|
||||
import * as Messages from '../../../../resources/messages';
|
||||
import * as t from '../../utils/translation-util';
|
||||
|
||||
const {dialog} = remote;
|
||||
|
||||
interface ServerInfoFormProps {
|
||||
$root: Element;
|
||||
server: DomainUtil.ServerConf;
|
||||
index: number;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export default class ServerInfoForm extends BaseComponent {
|
||||
props: ServerInfoFormProps;
|
||||
$serverInfoForm: Element;
|
||||
$serverInfoAlias: Element;
|
||||
$serverIcon: Element;
|
||||
$deleteServerButton: Element;
|
||||
$openServerButton: Element;
|
||||
constructor(props: ServerInfoFormProps) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template(): string {
|
||||
return `
|
||||
<div class="settings-card">
|
||||
<div class="server-info-left">
|
||||
<img class="server-info-icon" src="${this.props.server.icon}"/>
|
||||
<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>
|
||||
<div class="server-info-right">
|
||||
<div class="server-info-row server-url">
|
||||
<span class="server-url-info" title="${this.props.server.url}">${this.props.server.url}</span>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<div class="action red server-delete-action">
|
||||
<span>${t.__('Disconnect')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.initForm();
|
||||
this.initActions();
|
||||
}
|
||||
|
||||
initForm(): void {
|
||||
this.$serverInfoForm = this.generateNodeFromTemplate(this.template());
|
||||
this.$serverInfoAlias = this.$serverInfoForm.querySelectorAll('.server-info-alias')[0];
|
||||
this.$serverIcon = this.$serverInfoForm.querySelectorAll('.server-info-icon')[0];
|
||||
this.$deleteServerButton = this.$serverInfoForm.querySelectorAll('.server-delete-action')[0];
|
||||
this.$openServerButton = this.$serverInfoForm.querySelectorAll('.open-tab-button')[0];
|
||||
this.props.$root.append(this.$serverInfoForm);
|
||||
}
|
||||
|
||||
initActions(): void {
|
||||
this.$deleteServerButton.addEventListener('click', async () => {
|
||||
const {response} = await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: [t.__('YES'), t.__('NO')],
|
||||
defaultId: 0,
|
||||
message: t.__('Are you sure you want to disconnect this organization?')
|
||||
});
|
||||
if (response === 0) {
|
||||
if (DomainUtil.removeDomain(this.props.index)) {
|
||||
ipcRenderer.send('reload-full-app');
|
||||
} else {
|
||||
const {title, content} = Messages.orgRemovalError(DomainUtil.getDomain(this.props.index).url);
|
||||
dialog.showErrorBox(title, content);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$openServerButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
|
||||
});
|
||||
|
||||
this.$serverInfoAlias.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
|
||||
});
|
||||
|
||||
this.$serverIcon.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
'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;
|
||||
49
app/renderer/js/pages/preference/servers-section.ts
Normal file
49
app/renderer/js/pages/preference/servers-section.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import BaseSection from './base-section';
|
||||
import NewServerForm from './new-server-form';
|
||||
import * as t from '../../utils/translation-util';
|
||||
|
||||
interface ServersSectionProps {
|
||||
$root: Element;
|
||||
}
|
||||
|
||||
export default class ServersSection extends BaseSection {
|
||||
props: ServersSectionProps;
|
||||
$newServerContainer: Element;
|
||||
constructor(props: ServersSectionProps) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template(): string {
|
||||
return `
|
||||
<div class="add-server-modal">
|
||||
<div class="modal-container">
|
||||
<div class="settings-pane" id="server-settings-pane">
|
||||
<div class="page-title">${t.__('Add a Zulip organization')}</div>
|
||||
<div id="new-server-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.initServers();
|
||||
}
|
||||
|
||||
initServers(): void {
|
||||
this.props.$root.innerHTML = '';
|
||||
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.$newServerContainer = document.querySelector('#new-server-container');
|
||||
|
||||
this.initNewServerForm();
|
||||
}
|
||||
|
||||
initNewServerForm(): void {
|
||||
new NewServerForm({
|
||||
$root: this.$newServerContainer,
|
||||
onChange: this.reloadApp
|
||||
}).init();
|
||||
}
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
'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;
|
||||
345
app/renderer/js/pages/preference/shortcuts-section.ts
Normal file
345
app/renderer/js/pages/preference/shortcuts-section.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
import BaseSection from './base-section';
|
||||
import * as LinkUtil from '../../utils/link-util';
|
||||
import * as t from '../../utils/translation-util';
|
||||
|
||||
interface ShortcutsSectionProps {
|
||||
$root: Element;
|
||||
}
|
||||
|
||||
export default class ShortcutsSection extends BaseSection {
|
||||
props: ShortcutsSectionProps;
|
||||
constructor(props: ShortcutsSectionProps) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
// TODO - Deduplicate templateMac and templateWinLin functions. In theory
|
||||
// they both should be the same the only thing different should be the userOSKey
|
||||
// variable but there seems to be inconsistences between both function, one has more
|
||||
// lines though one may just be using more new lines and other thing is the use of +.
|
||||
templateMac(): string {
|
||||
const userOSKey = '⌘';
|
||||
|
||||
return `
|
||||
<div class="settings-pane">
|
||||
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>${t.__('Tip')}: </b>${t.__('These desktop app shortcuts extend the Zulip webapp\'s')} <span id="open-hotkeys-link"> ${t.__('keyboard shortcuts')}</span>.</p></div>
|
||||
<div class="title">${t.__('Application Shortcuts')}</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>,</kbd></td>
|
||||
<td>${t.__('Settings')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>K</kbd></td>
|
||||
<td>${t.__('Keyboard Shortcuts')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
|
||||
<td>${t.__('Toggle Do Not Disturb')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>D</kbd></td>
|
||||
<td>${t.__('Reset App Settings')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>L</kbd></td>
|
||||
<td>${t.__('Log Out')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
|
||||
<td>${t.__('Hide Zulip')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
|
||||
<td>${t.__('Hide Others')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>Q</kbd></td>
|
||||
<td>${t.__('Quit Zulip')}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">${t.__('Edit Shortcuts')}</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
|
||||
<td>${t.__('Undo')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
|
||||
<td>${t.__('Redo')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>X</kbd></td>
|
||||
<td>${t.__('Cut')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>C</kbd></td>
|
||||
<td>${t.__('Copy')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
|
||||
<td>${t.__('Paste')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
|
||||
<td>${t.__('Paste and Match Style')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>A</kbd></td>
|
||||
<td>${t.__('Select All')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>Space</kbd></td>
|
||||
<td>${t.__('Emoji & Symbols')}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">${t.__('View Shortcuts')}</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
|
||||
<td>${t.__('Reload')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
|
||||
<td>${t.__('Hard Reload')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
|
||||
<td>${t.__('Enter Full Screen')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>+</kbd></td>
|
||||
<td>${t.__('Zoom In')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>-</kbd></td>
|
||||
<td>${t.__('Zoom Out')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>0</kbd></td>
|
||||
<td>${t.__('Actual Size')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
|
||||
<td>${t.__('Toggle Sidebar')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>I</kbd></td>
|
||||
<td>${t.__('Toggle DevTools for Zulip App')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>U</kbd></td>
|
||||
<td>${t.__('Toggle DevTools for Active Tab')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Ctrl</kbd> + <kbd>Tab</kbd></td>
|
||||
<td>${t.__('Switch to Next Organization')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
|
||||
<td>${t.__('Switch to Previous Organization')}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">${t.__('History Shortcuts')}</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>←</kbd></td>
|
||||
<td>${t.__('Back')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>→</kbd></td>
|
||||
<td>${t.__('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>${t.__('Minimize')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>W</kbd></td>
|
||||
<td>${t.__('Close')}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
templateWinLin(): string {
|
||||
const userOSKey = 'Ctrl';
|
||||
|
||||
return `
|
||||
<div class="settings-pane">
|
||||
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>${t.__('Tip')}: </b>${t.__('These desktop app shortcuts extend the Zulip webapp\'s')} <span id="open-hotkeys-link"> ${t.__('keyboard shortcuts')}</span>.</p></div>
|
||||
<div class="title">${t.__('Application Shortcuts')}</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>,</kbd></td>
|
||||
<td>${t.__('Settings')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>K</kbd></td>
|
||||
<td>${t.__('Keyboard Shortcuts')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
|
||||
<td>${t.__('Toggle Do Not Disturb')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>L</kbd></td>
|
||||
<td>${t.__('Log Out')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Q</kbd></td>
|
||||
<td>${t.__('Quit Zulip')}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">${t.__('Edit Shortcuts')}</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Z</kbd></td>
|
||||
<td>${t.__('Undo')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Y</kbd></td>
|
||||
<td>${t.__('Redo')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>X</kbd></td>
|
||||
<td>${t.__('Cut')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>C</kbd></td>
|
||||
<td>${t.__('Copy')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>V</kbd></td>
|
||||
<td>${t.__('Paste')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd></td>
|
||||
<td>${t.__('Paste and Match Style')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>A</kbd></td>
|
||||
<td>${t.__('Select All')}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">${t.__('View Shortcuts')}</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>R</kbd></td>
|
||||
<td>${t.__('Reload')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd></td>
|
||||
<td>${t.__('Hard Reload')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>F11</kbd></td>
|
||||
<td>${t.__('Toggle Full Screen')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>=</kbd></td>
|
||||
<td>${t.__('Zoom In')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>-</kbd></td>
|
||||
<td>${t.__('Zoom Out')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>0</kbd></td>
|
||||
<td>${t.__('Actual Size')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
|
||||
<td>${t.__('Toggle Sidebar')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd></td>
|
||||
<td>${t.__('Toggle DevTools for Zulip App')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>U</kbd></td>
|
||||
<td>${t.__('Toggle DevTools for Active Tab')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Tab</kbd></td>
|
||||
<td>${t.__('Switch to Next Organization')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
|
||||
<td>${t.__('Switch to Previous Organization')}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">${t.__('History Shortcuts')}</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>Alt</kbd> + <kbd>←</kbd></td>
|
||||
<td>${t.__('Back')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Alt</kbd> + <kbd>→</kbd></td>
|
||||
<td>${t.__('Forward')}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="title">${t.__('Window Shortcuts')}</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>M</kbd></td>
|
||||
<td>${t.__('Minimize')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>W</kbd></td>
|
||||
<td>${t.__('Close')}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
openHotkeysExternalLink(): void {
|
||||
const link = 'https://zulip.com/help/keyboard-shortcuts';
|
||||
const externalCreateNewOrgElement = document.querySelector('#open-hotkeys-link');
|
||||
externalCreateNewOrgElement.addEventListener('click', async () => {
|
||||
await LinkUtil.openBrowser(new URL(link));
|
||||
});
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.props.$root.innerHTML = (process.platform === 'darwin') ?
|
||||
this.templateMac() : this.templateWinLin();
|
||||
this.openHotkeysExternalLink();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const { spellChecker } = require('./spellchecker');
|
||||
// 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', () => {
|
||||
// Init spellchecker
|
||||
spellChecker();
|
||||
|
||||
// redirect users to network troubleshooting page
|
||||
const getRestartButton = document.querySelector('.restart_get_events_button');
|
||||
if (getRestartButton) {
|
||||
getRestartButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
});
|
||||
}
|
||||
});
|
||||
113
app/renderer/js/preload.ts
Normal file
113
app/renderer/js/preload.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import {contextBridge, ipcRenderer, webFrame} from 'electron';
|
||||
import fs from 'fs';
|
||||
|
||||
import isDev from 'electron-is-dev';
|
||||
|
||||
import * as NetworkError from './pages/network';
|
||||
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
import './notification';
|
||||
|
||||
// Prevent drag and drop event in main process which prevents remote code executaion
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
import './shared/preventdrag';
|
||||
|
||||
import electron_bridge from './electron-bridge';
|
||||
contextBridge.exposeInMainWorld('raw_electron_bridge', electron_bridge);
|
||||
|
||||
ipcRenderer.on('logout', () => {
|
||||
// Create the menu for the below
|
||||
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
|
||||
dropdown.click();
|
||||
|
||||
const nodes: NodeListOf<HTMLElement> = document.querySelectorAll('.dropdown-menu li:last-child a');
|
||||
nodes[nodes.length - 1].click();
|
||||
});
|
||||
|
||||
ipcRenderer.on('shortcut', () => {
|
||||
// Create the menu for the below
|
||||
const node: HTMLElement = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
|
||||
// Additional check
|
||||
if (node.textContent.trim().toLowerCase() === 'keyboard shortcuts (?)') {
|
||||
node.click();
|
||||
} else {
|
||||
// Atleast click the dropdown
|
||||
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
|
||||
dropdown.click();
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('show-notification-settings', () => {
|
||||
// Create the menu for the below
|
||||
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
|
||||
dropdown.click();
|
||||
|
||||
const nodes: NodeListOf<HTMLElement> = document.querySelectorAll('.dropdown-menu li a');
|
||||
nodes[2].click();
|
||||
|
||||
const notificationItem: NodeListOf<HTMLElement> = document.querySelectorAll('.normal-settings-list li div');
|
||||
|
||||
// Wait until the notification dom element shows up
|
||||
setTimeout(() => {
|
||||
notificationItem[2].click();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
electron_bridge.once('zulip-loaded', () => {
|
||||
// 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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('load', (event: any): void => {
|
||||
if (!event.target.URL.includes('app/renderer/network.html')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $reconnectButton = document.querySelector('#reconnect');
|
||||
const $settingsButton = document.querySelector('#settings');
|
||||
NetworkError.init($reconnectButton, $settingsButton);
|
||||
});
|
||||
|
||||
// Electron's globalShortcut can cause unexpected results
|
||||
// so adding the reload shortcut in the old-school way
|
||||
// Zoom from numpad keys is not supported by electron, so adding it through listeners.
|
||||
document.addEventListener('keydown', event => {
|
||||
const cmdOrCtrl = event.ctrlKey || event.metaKey;
|
||||
if (event.code === 'F5') {
|
||||
ipcRenderer.send('forward-message', 'hard-reload');
|
||||
} else if (cmdOrCtrl && (event.code === 'NumpadAdd' || event.code === 'Equal')) {
|
||||
ipcRenderer.send('forward-message', 'zoomIn');
|
||||
} else if (cmdOrCtrl && event.code === 'NumpadSubtract') {
|
||||
ipcRenderer.send('forward-message', 'zoomOut');
|
||||
} else if (cmdOrCtrl && event.code === 'Numpad0') {
|
||||
ipcRenderer.send('forward-message', 'zoomActualSize');
|
||||
}
|
||||
});
|
||||
|
||||
// Set user as active and update the time of last activity
|
||||
ipcRenderer.on('set-active', () => {
|
||||
if (isDev) {
|
||||
console.log('active');
|
||||
}
|
||||
|
||||
electron_bridge.idle_on_system = false;
|
||||
electron_bridge.last_active_on_system = Date.now();
|
||||
});
|
||||
|
||||
// Set user as idle and time of last activity is left unchanged
|
||||
ipcRenderer.on('set-idle', () => {
|
||||
if (isDev) {
|
||||
console.log('idle');
|
||||
}
|
||||
|
||||
electron_bridge.idle_on_system = true;
|
||||
});
|
||||
|
||||
(async () => webFrame.executeJavaScript(
|
||||
fs.readFileSync(require.resolve('./injected'), 'utf8')
|
||||
))();
|
||||
17
app/renderer/js/shared/preventdrag.ts
Normal file
17
app/renderer/js/shared/preventdrag.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// This is a security fix. Following function prevents drag and drop event in the app
|
||||
// so that attackers can't execute any remote code within the app
|
||||
// It doesn't affect the compose box so that users can still
|
||||
// use drag and drop event to share files etc
|
||||
|
||||
const preventDragAndDrop = (): void => {
|
||||
const preventEvents = ['dragover', 'drop'];
|
||||
preventEvents.forEach(dragEvents => {
|
||||
document.addEventListener(dragEvents, event => {
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
preventDragAndDrop();
|
||||
|
||||
export {};
|
||||
@@ -1,29 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
spellChecker
|
||||
};
|
||||
@@ -1,222 +0,0 @@
|
||||
'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();
|
||||
}
|
||||
220
app/renderer/js/tray.ts
Normal file
220
app/renderer/js/tray.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import {ipcRenderer, remote, WebviewTag, NativeImage} from 'electron';
|
||||
|
||||
import path from 'path';
|
||||
import * as ConfigUtil from './utils/config-util';
|
||||
|
||||
const {Tray, Menu, nativeImage, BrowserWindow} = remote;
|
||||
|
||||
let tray: Electron.Tray;
|
||||
|
||||
const ICON_DIR = '../../resources/tray';
|
||||
|
||||
const TRAY_SUFFIX = 'tray';
|
||||
|
||||
const APP_ICON = path.join(__dirname, ICON_DIR, TRAY_SUFFIX);
|
||||
|
||||
const iconPath = (): string => {
|
||||
if (process.platform === 'linux') {
|
||||
return APP_ICON + 'linux.png';
|
||||
}
|
||||
|
||||
return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'macOSTemplate.png');
|
||||
};
|
||||
|
||||
let unread = 0;
|
||||
|
||||
const trayIconSize = (): number => {
|
||||
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: number): HTMLCanvasElement {
|
||||
config.unreadCount = arg;
|
||||
|
||||
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(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.2));
|
||||
} else {
|
||||
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
|
||||
ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.15));
|
||||
}
|
||||
}
|
||||
|
||||
return canvas;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the tray icon as a native image
|
||||
* @param arg: Unread count
|
||||
* @return the native image
|
||||
*/
|
||||
const renderNativeImage = function (arg: number): NativeImage {
|
||||
const canvas = renderCanvas(arg);
|
||||
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
|
||||
return nativeImage.createFromBuffer(pngData, {
|
||||
scaleFactor: config.pixelRatio
|
||||
});
|
||||
};
|
||||
|
||||
function sendAction(action: string): void {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
win.restore();
|
||||
}
|
||||
|
||||
win.webContents.send(action);
|
||||
}
|
||||
|
||||
const createTray = function (): void {
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Zulip',
|
||||
click() {
|
||||
ipcRenderer.send('focus-app');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
click() {
|
||||
ipcRenderer.send('focus-app');
|
||||
sendAction('open-settings');
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
click() {
|
||||
ipcRenderer.send('quit-app');
|
||||
}
|
||||
}
|
||||
]);
|
||||
tray = new Tray(iconPath());
|
||||
tray.setContextMenu(contextMenu);
|
||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||
tray.on('click', () => {
|
||||
ipcRenderer.send('toggle-app');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ipcRenderer.on('destroytray', (event: Event): Event => {
|
||||
if (!tray) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
tray.destroy();
|
||||
if (tray.isDestroyed()) {
|
||||
tray = null;
|
||||
} else {
|
||||
throw new Error('Tray icon not properly destroyed.');
|
||||
}
|
||||
|
||||
return event;
|
||||
});
|
||||
|
||||
ipcRenderer.on('tray', (_event: Event, arg: number): void => {
|
||||
if (!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;
|
||||
tray.setImage(iconPath());
|
||||
tray.setToolTip('No unread messages');
|
||||
} else {
|
||||
unread = arg;
|
||||
const image = renderNativeImage(arg);
|
||||
tray.setImage(image);
|
||||
tray.setToolTip(`${arg} unread messages`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function toggleTray(): void {
|
||||
let state;
|
||||
if (tray) {
|
||||
state = false;
|
||||
tray.destroy();
|
||||
if (tray.isDestroyed()) {
|
||||
tray = null;
|
||||
}
|
||||
|
||||
ConfigUtil.setConfigItem('trayIcon', false);
|
||||
} else {
|
||||
state = true;
|
||||
createTray();
|
||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||
const image = renderNativeImage(unread);
|
||||
tray.setImage(image);
|
||||
tray.setToolTip(`${unread} unread messages`);
|
||||
}
|
||||
|
||||
ConfigUtil.setConfigItem('trayIcon', true);
|
||||
}
|
||||
|
||||
const selector = 'webview:not([class*=disabled])';
|
||||
const webview: WebviewTag = document.querySelector(selector);
|
||||
const webContents = remote.webContents.fromId(webview.getWebContentsId());
|
||||
webContents.send('toggletray', state);
|
||||
}
|
||||
|
||||
ipcRenderer.on('toggletray', toggleTray);
|
||||
|
||||
if (ConfigUtil.getConfigItem('trayIcon', true)) {
|
||||
createTray();
|
||||
}
|
||||
|
||||
export {};
|
||||
78
app/renderer/js/utils/certificate-util.ts
Normal file
78
app/renderer/js/utils/certificate-util.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import electron from 'electron';
|
||||
import {JsonDB} from 'node-json-db';
|
||||
import {initSetUp} from './default-util';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import Logger from './logger-util';
|
||||
|
||||
const {app, dialog} =
|
||||
process.type === 'renderer' ? electron.remote : electron;
|
||||
|
||||
initSetUp();
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'certificate-util.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
const certificatesDir = `${app.getPath('userData')}/certificates`;
|
||||
|
||||
let db: JsonDB;
|
||||
|
||||
reloadDB();
|
||||
|
||||
export function getCertificate(server: string): string | undefined {
|
||||
reloadDB();
|
||||
return db.getData('/')[server];
|
||||
}
|
||||
|
||||
// Function to copy the certificate to userData folder
|
||||
export function copyCertificate(_server: string, location: string, fileName: string): boolean {
|
||||
let copied = false;
|
||||
const filePath = `${certificatesDir}/${fileName}`;
|
||||
try {
|
||||
fs.copyFileSync(location, filePath);
|
||||
copied = true;
|
||||
} catch (error) {
|
||||
dialog.showErrorBox(
|
||||
'Error saving certificate',
|
||||
'We encountered error while saving the certificate.'
|
||||
);
|
||||
logger.error('Error while copying the certificate to certificates folder.');
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
return copied;
|
||||
}
|
||||
|
||||
export function setCertificate(server: string, fileName: string): void {
|
||||
const filePath = `${fileName}`;
|
||||
db.push(`/${server}`, filePath, true);
|
||||
reloadDB();
|
||||
}
|
||||
|
||||
export function removeCertificate(server: string): void {
|
||||
db.delete(`/${server}`);
|
||||
reloadDB();
|
||||
}
|
||||
|
||||
function reloadDB(): void {
|
||||
const settingsJsonPath = path.join(app.getPath('userData'), '/config/certificates.json');
|
||||
try {
|
||||
const file = fs.readFileSync(settingsJsonPath, 'utf8');
|
||||
JSON.parse(file);
|
||||
} catch (error) {
|
||||
if (fs.existsSync(settingsJsonPath)) {
|
||||
fs.unlinkSync(settingsJsonPath);
|
||||
dialog.showErrorBox(
|
||||
'Error saving settings',
|
||||
'We encountered error while saving the certificate.'
|
||||
);
|
||||
logger.error('Error while JSON parsing certificates.json: ');
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
db = new JsonDB(settingsJsonPath, true, true);
|
||||
}
|
||||
8
app/renderer/js/utils/common-util.ts
Normal file
8
app/renderer/js/utils/common-util.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// Unescape already encoded/escaped strings
|
||||
export function decodeString(stringInput: string): string {
|
||||
const parser = new DOMParser();
|
||||
const dom = parser.parseFromString(
|
||||
'<!doctype html><body>' + stringInput,
|
||||
'text/html');
|
||||
return dom.body.textContent;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
'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();
|
||||
105
app/renderer/js/utils/config-util.ts
Normal file
105
app/renderer/js/utils/config-util.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {JsonDB} from 'node-json-db';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import electron from 'electron';
|
||||
import Logger from './logger-util';
|
||||
import * as EnterpriseUtil from './enterprise-util';
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'config-util.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
let dialog: Electron.Dialog = null;
|
||||
let app: Electron.App = null;
|
||||
|
||||
/* To make the util runnable in both main and renderer process */
|
||||
if (process.type === 'renderer') {
|
||||
const {remote} = electron;
|
||||
dialog = remote.dialog;
|
||||
app = remote.app;
|
||||
} else {
|
||||
dialog = electron.dialog;
|
||||
app = electron.app;
|
||||
}
|
||||
|
||||
let db: JsonDB;
|
||||
|
||||
reloadDB();
|
||||
|
||||
export function getConfigItem(key: string, defaultValue: unknown = null): any {
|
||||
try {
|
||||
db.reload();
|
||||
} catch (error) {
|
||||
logger.error('Error while reloading settings.json: ');
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
const value = db.getData('/')[key];
|
||||
if (value === undefined) {
|
||||
setConfigItem(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getConfigString(key: string, defaultValue: string): string {
|
||||
const value = getConfigItem(key, defaultValue);
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
setConfigItem(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// This function returns whether a key exists in the configuration file (settings.json)
|
||||
export function isConfigItemExists(key: string): boolean {
|
||||
try {
|
||||
db.reload();
|
||||
} catch (error) {
|
||||
logger.error('Error while reloading settings.json: ');
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
const value = db.getData('/')[key];
|
||||
return (value !== undefined);
|
||||
}
|
||||
|
||||
export function setConfigItem(key: string, value: unknown, override? : boolean): void {
|
||||
if (EnterpriseUtil.configItemExists(key) && !override) {
|
||||
// If item is in global config and we're not trying to override
|
||||
return;
|
||||
}
|
||||
|
||||
db.push(`/${key}`, value, true);
|
||||
db.save();
|
||||
}
|
||||
|
||||
export function removeConfigItem(key: string): void {
|
||||
db.delete(`/${key}`);
|
||||
db.save();
|
||||
}
|
||||
|
||||
function reloadDB(): void {
|
||||
const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.json');
|
||||
try {
|
||||
const file = fs.readFileSync(settingsJsonPath, 'utf8');
|
||||
JSON.parse(file);
|
||||
} catch (error) {
|
||||
if (fs.existsSync(settingsJsonPath)) {
|
||||
fs.unlinkSync(settingsJsonPath);
|
||||
dialog.showErrorBox(
|
||||
'Error saving settings',
|
||||
'We encountered an error while saving the settings.'
|
||||
);
|
||||
logger.error('Error while JSON parsing settings.json: ');
|
||||
logger.error(error);
|
||||
logger.reportSentry(error);
|
||||
}
|
||||
}
|
||||
|
||||
db = new JsonDB(settingsJsonPath, true, true);
|
||||
}
|
||||
75
app/renderer/js/utils/default-util.ts
Normal file
75
app/renderer/js/utils/default-util.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import electron from 'electron';
|
||||
import fs from 'fs';
|
||||
|
||||
let app: Electron.App = null;
|
||||
let setupCompleted = false;
|
||||
if (process.type === 'renderer') {
|
||||
app = electron.remote.app;
|
||||
} else {
|
||||
app = electron.app;
|
||||
}
|
||||
|
||||
const zulipDir = app.getPath('userData');
|
||||
const logDir = `${zulipDir}/Logs/`;
|
||||
const certificatesDir = `${zulipDir}/certificates/`;
|
||||
const configDir = `${zulipDir}/config/`;
|
||||
export const initSetUp = (): void => {
|
||||
// If it is the first time the app is running
|
||||
// create zulip dir in userData folder to
|
||||
// avoid errors
|
||||
if (!setupCompleted) {
|
||||
if (!fs.existsSync(zulipDir)) {
|
||||
fs.mkdirSync(zulipDir);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(certificatesDir)) {
|
||||
fs.mkdirSync(certificatesDir);
|
||||
}
|
||||
|
||||
// Migrate config files from app data folder to config folder inside app
|
||||
// data folder. This will be done once when a user updates to the new version.
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir);
|
||||
const domainJson = `${zulipDir}/domain.json`;
|
||||
const certificatesJson = `${zulipDir}/certificates.json`;
|
||||
const settingsJson = `${zulipDir}/settings.json`;
|
||||
const updatesJson = `${zulipDir}/updates.json`;
|
||||
const windowStateJson = `${zulipDir}/window-state.json`;
|
||||
const configData = [
|
||||
{
|
||||
path: domainJson,
|
||||
fileName: 'domain.json'
|
||||
},
|
||||
{
|
||||
path: certificatesJson,
|
||||
fileName: 'certificates.json'
|
||||
},
|
||||
{
|
||||
path: settingsJson,
|
||||
fileName: 'settings.json'
|
||||
},
|
||||
{
|
||||
path: updatesJson,
|
||||
fileName: 'updates.json'
|
||||
}
|
||||
];
|
||||
configData.forEach(data => {
|
||||
if (fs.existsSync(data.path)) {
|
||||
fs.copyFileSync(data.path, configDir + data.fileName);
|
||||
fs.unlinkSync(data.path);
|
||||
}
|
||||
});
|
||||
// `window-state.json` is only deleted not moved, as the electron-window-state
|
||||
// package will recreate the file in the config folder.
|
||||
if (fs.existsSync(windowStateJson)) {
|
||||
fs.unlinkSync(windowStateJson);
|
||||
}
|
||||
}
|
||||
|
||||
setupCompleted = true;
|
||||
}
|
||||
};
|
||||
48
app/renderer/js/utils/dnd-util.ts
Normal file
48
app/renderer/js/utils/dnd-util.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as ConfigUtil from './config-util';
|
||||
|
||||
type SettingName = 'showNotification' | 'silent' | 'flashTaskbarOnMessage';
|
||||
|
||||
export interface DNDSettings {
|
||||
showNotification?: boolean;
|
||||
silent?: boolean;
|
||||
flashTaskbarOnMessage?: boolean;
|
||||
}
|
||||
|
||||
interface Toggle {
|
||||
dnd: boolean;
|
||||
newSettings: DNDSettings;
|
||||
}
|
||||
|
||||
export function toggle(): Toggle {
|
||||
const dnd = !ConfigUtil.getConfigItem('dnd', false);
|
||||
const dndSettingList: SettingName[] = ['showNotification', 'silent'];
|
||||
if (process.platform === 'win32') {
|
||||
dndSettingList.push('flashTaskbarOnMessage');
|
||||
}
|
||||
|
||||
let newSettings: DNDSettings;
|
||||
if (dnd) {
|
||||
const oldSettings: DNDSettings = {};
|
||||
newSettings = {};
|
||||
|
||||
// Iterate through the dndSettingList.
|
||||
for (const settingName of dndSettingList) {
|
||||
// Store the current value of setting.
|
||||
oldSettings[settingName] = ConfigUtil.getConfigItem(settingName);
|
||||
// New value of setting.
|
||||
newSettings[settingName] = (settingName === 'silent');
|
||||
}
|
||||
|
||||
// Store old value in oldSettings.
|
||||
ConfigUtil.setConfigItem('dndPreviousSettings', oldSettings);
|
||||
} else {
|
||||
newSettings = ConfigUtil.getConfigItem('dndPreviousSettings');
|
||||
}
|
||||
|
||||
for (const settingName of dndSettingList) {
|
||||
ConfigUtil.setConfigItem(settingName, newSettings[settingName]);
|
||||
}
|
||||
|
||||
ConfigUtil.setConfigItem('dnd', dnd);
|
||||
return {dnd, newSettings};
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { app, dialog } = require('electron').remote;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const JsonDB = require('node-json-db');
|
||||
const request = require('request');
|
||||
|
||||
let instance = null;
|
||||
|
||||
const defaultIconUrl = '../renderer/img/icon.png';
|
||||
|
||||
class DomainUtil {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
this.reloadDB();
|
||||
// Migrate from old schema
|
||||
if (this.db.getData('/').domain) {
|
||||
this.addDomain({
|
||||
alias: 'Zulip',
|
||||
url: this.db.getData('/domain')
|
||||
});
|
||||
this.db.delete('/domain');
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
getDomains() {
|
||||
this.reloadDB();
|
||||
if (this.db.getData('/').domains === undefined) {
|
||||
return [];
|
||||
} else {
|
||||
return this.db.getData('/domains');
|
||||
}
|
||||
}
|
||||
|
||||
getDomain(index) {
|
||||
this.reloadDB();
|
||||
return this.db.getData(`/domains[${index}]`);
|
||||
}
|
||||
|
||||
updateDomain(index, server) {
|
||||
this.reloadDB();
|
||||
this.db.push(`/domains[${index}]`, server, true);
|
||||
}
|
||||
|
||||
addDomain(server) {
|
||||
return new Promise(resolve => {
|
||||
if (server.icon) {
|
||||
this.saveServerIcon(server.icon).then(localIconUrl => {
|
||||
server.icon = localIconUrl;
|
||||
this.db.push('/domains[]', server, true);
|
||||
this.reloadDB();
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
server.icon = defaultIconUrl;
|
||||
this.db.push('/domains[]', server, true);
|
||||
this.reloadDB();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeDomains() {
|
||||
this.db.delete('/domains');
|
||||
this.reloadDB();
|
||||
}
|
||||
|
||||
removeDomain(index) {
|
||||
this.db.delete(`/domains[${index}]`);
|
||||
this.reloadDB();
|
||||
}
|
||||
|
||||
// Check if domain is already added
|
||||
duplicateDomain(domain) {
|
||||
domain = this.formatUrl(domain);
|
||||
const servers = this.getDomains();
|
||||
for (const i in servers) {
|
||||
if (servers[i].url === domain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
checkDomain(domain, silent = false) {
|
||||
if (!silent && this.duplicateDomain(domain)) {
|
||||
// Do not check duplicate in silent mode
|
||||
alert('This server has been added.');
|
||||
return;
|
||||
}
|
||||
|
||||
domain = this.formatUrl(domain);
|
||||
|
||||
const checkDomain = domain + '/static/audio/zulip.ogg';
|
||||
|
||||
const serverConf = {
|
||||
icon: defaultIconUrl,
|
||||
url: domain,
|
||||
alias: domain
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(checkDomain, (error, response) => {
|
||||
const certsError =
|
||||
['Error: self signed certificate',
|
||||
'Error: unable to verify the first certificate'
|
||||
];
|
||||
if (!error && response.statusCode !== 404) {
|
||||
// Correct
|
||||
this.getServerSettings(domain).then(serverSettings => {
|
||||
resolve(serverSettings);
|
||||
}, () => {
|
||||
resolve(serverConf);
|
||||
});
|
||||
} else if (certsError.indexOf(error.toString()) >= 0) {
|
||||
if (silent) {
|
||||
this.getServerSettings(domain).then(serverSettings => {
|
||||
resolve(serverSettings);
|
||||
}, () => {
|
||||
resolve(serverConf);
|
||||
});
|
||||
} else {
|
||||
const certErrorMessage = `Do you trust certificate from ${domain}? \n ${error}`;
|
||||
const certErrorDetail = `The server you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
|
||||
\n Unless you have a good reason to believe otherwise, you should not proceed.
|
||||
\n You can click here if you'd like to proceed with the connection.`;
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['Yes', 'No'],
|
||||
defaultId: 0,
|
||||
message: certErrorMessage,
|
||||
detail: certErrorDetail
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
this.getServerSettings(domain).then(serverSettings => {
|
||||
resolve(serverSettings);
|
||||
}, () => {
|
||||
resolve(serverConf);
|
||||
});
|
||||
} else {
|
||||
reject('Untrusted Certificate.');
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject('Not a valid Zulip server');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getServerSettings(domain) {
|
||||
const serverSettingsUrl = domain + '/api/v1/server_settings';
|
||||
return new Promise((resolve, reject) => {
|
||||
request(serverSettingsUrl, (error, response) => {
|
||||
if (!error && response.statusCode === 200) {
|
||||
const data = JSON.parse(response.body);
|
||||
if (data.hasOwnProperty('realm_icon') && data.realm_icon) {
|
||||
resolve({
|
||||
// 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();
|
||||
203
app/renderer/js/utils/domain-util.ts
Normal file
203
app/renderer/js/utils/domain-util.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import {JsonDB} from 'node-json-db';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import Logger from './logger-util';
|
||||
import {remote, ipcRenderer} from 'electron';
|
||||
|
||||
import * as EnterpriseUtil from './enterprise-util';
|
||||
import * as Messages from '../../../resources/messages';
|
||||
|
||||
const {app, dialog} = remote;
|
||||
|
||||
export interface ServerConf {
|
||||
url: string;
|
||||
alias?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'domain-util.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
const defaultIconUrl = '../renderer/img/icon.png';
|
||||
|
||||
let db: JsonDB;
|
||||
|
||||
reloadDB();
|
||||
// Migrate from old schema
|
||||
if (db.getData('/').domain) {
|
||||
(async () => {
|
||||
await addDomain({
|
||||
alias: 'Zulip',
|
||||
url: db.getData('/domain')
|
||||
});
|
||||
db.delete('/domain');
|
||||
})();
|
||||
}
|
||||
|
||||
export function getDomains(): ServerConf[] {
|
||||
reloadDB();
|
||||
if (db.getData('/').domains === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return db.getData('/domains');
|
||||
}
|
||||
|
||||
export function getDomain(index: number): ServerConf {
|
||||
reloadDB();
|
||||
return db.getData(`/domains[${index}]`);
|
||||
}
|
||||
|
||||
export function updateDomain(index: number, server: ServerConf): void {
|
||||
reloadDB();
|
||||
db.push(`/domains[${index}]`, server, true);
|
||||
}
|
||||
|
||||
export async function addDomain(server: ServerConf): Promise<void> {
|
||||
if (server.icon) {
|
||||
const localIconUrl = await saveServerIcon(server);
|
||||
server.icon = localIconUrl;
|
||||
db.push('/domains[]', server, true);
|
||||
reloadDB();
|
||||
} else {
|
||||
server.icon = defaultIconUrl;
|
||||
db.push('/domains[]', server, true);
|
||||
reloadDB();
|
||||
}
|
||||
}
|
||||
|
||||
export function removeDomains(): void {
|
||||
db.delete('/domains');
|
||||
reloadDB();
|
||||
}
|
||||
|
||||
export function removeDomain(index: number): boolean {
|
||||
if (EnterpriseUtil.isPresetOrg(getDomain(index).url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
db.delete(`/domains[${index}]`);
|
||||
reloadDB();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if domain is already added
|
||||
export function duplicateDomain(domain: string): boolean {
|
||||
domain = formatUrl(domain);
|
||||
return getDomains().some(server => server.url === domain);
|
||||
}
|
||||
|
||||
async function checkCertError(domain: string, serverConf: ServerConf, error: any, silent: boolean): Promise<ServerConf> {
|
||||
if (silent) {
|
||||
// Since getting server settings has already failed
|
||||
return serverConf;
|
||||
}
|
||||
|
||||
// Report error to sentry to get idea of possible certificate errors
|
||||
// users get when adding the servers
|
||||
logger.reportSentry(error);
|
||||
const certErrorMessage = Messages.certErrorMessage(domain, error);
|
||||
const certErrorDetail = Messages.certErrorDetail();
|
||||
|
||||
await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
buttons: ['OK'],
|
||||
message: certErrorMessage,
|
||||
detail: certErrorDetail
|
||||
});
|
||||
throw new Error('Untrusted certificate.');
|
||||
}
|
||||
|
||||
export async function checkDomain(domain: string, silent = false): Promise<ServerConf> {
|
||||
if (!silent && duplicateDomain(domain)) {
|
||||
// Do not check duplicate in silent mode
|
||||
throw new Error('This server has been added.');
|
||||
}
|
||||
|
||||
domain = formatUrl(domain);
|
||||
|
||||
const serverConf = {
|
||||
icon: defaultIconUrl,
|
||||
url: domain,
|
||||
alias: domain
|
||||
};
|
||||
|
||||
try {
|
||||
return await getServerSettings(domain);
|
||||
} catch (error_) {
|
||||
// Make sure that error is an error or string not undefined
|
||||
// so validation does not throw error.
|
||||
const error = error_ || '';
|
||||
|
||||
const certsError = error.toString().includes('certificate');
|
||||
if (certsError) {
|
||||
const result = await checkCertError(domain, serverConf, error, silent);
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new Error(Messages.invalidZulipServerError(domain));
|
||||
}
|
||||
}
|
||||
|
||||
async function getServerSettings(domain: string): Promise<ServerConf> {
|
||||
return ipcRenderer.invoke('get-server-settings', domain);
|
||||
}
|
||||
|
||||
export async function saveServerIcon(server: ServerConf): Promise<string> {
|
||||
return ipcRenderer.invoke('save-server-icon', server.icon);
|
||||
}
|
||||
|
||||
export async function updateSavedServer(url: string, index: number): Promise<void> {
|
||||
// Does not promise successful update
|
||||
const oldIcon = getDomain(index).icon;
|
||||
try {
|
||||
const newServerConf = await checkDomain(url, true);
|
||||
const localIconUrl = await saveServerIcon(newServerConf);
|
||||
if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') {
|
||||
newServerConf.icon = localIconUrl;
|
||||
updateDomain(index, newServerConf);
|
||||
reloadDB();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('Could not update server icon.');
|
||||
logger.log(error);
|
||||
logger.reportSentry(error);
|
||||
}
|
||||
}
|
||||
|
||||
function reloadDB(): void {
|
||||
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
|
||||
try {
|
||||
const file = fs.readFileSync(domainJsonPath, 'utf8');
|
||||
JSON.parse(file);
|
||||
} catch (error) {
|
||||
if (fs.existsSync(domainJsonPath)) {
|
||||
fs.unlinkSync(domainJsonPath);
|
||||
dialog.showErrorBox(
|
||||
'Error saving new organization',
|
||||
'There seems to be error while saving new organization, ' +
|
||||
'you may have to re-add your previous organizations back.'
|
||||
);
|
||||
logger.error('Error while JSON parsing domain.json: ');
|
||||
logger.error(error);
|
||||
logger.reportSentry(error);
|
||||
}
|
||||
}
|
||||
|
||||
db = new JsonDB(domainJsonPath, true, true);
|
||||
}
|
||||
|
||||
export function formatUrl(domain: string): string {
|
||||
if (domain.startsWith('http://') || domain.startsWith('https://')) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
if (domain.startsWith('localhost:')) {
|
||||
return `http://${domain}`;
|
||||
}
|
||||
|
||||
return `https://${domain}`;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user