mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-23 03:31:56 +00:00
Compare commits
971 Commits
test-new-d
...
v5.10.0
Author | SHA1 | Date | |
---|---|---|---|
|
7fba8cfae9 | ||
|
32301656cc | ||
|
0e16283a37 | ||
|
d86482a804 | ||
|
3af350e4dc | ||
|
39fc2053c5 | ||
|
044f1fd0f9 | ||
|
10fb0a82f9 | ||
|
123bd5b2c0 | ||
|
ad771c3da8 | ||
|
4c58bc3aa3 | ||
|
9a8680d209 | ||
|
1569890f4d | ||
|
2ed400c23c | ||
|
70621431dc | ||
|
55b7e09796 | ||
|
de2829a968 | ||
|
296de41779 | ||
|
8b9ebeee25 | ||
|
76e81ca337 | ||
|
2e7a9bb4ed | ||
|
77638f6287 | ||
|
6e8fe36876 | ||
|
2eea4a32a5 | ||
|
677dfe425c | ||
|
1da3ec545a | ||
|
3cb6ea4694 | ||
|
0cb7297017 | ||
|
b8d7003446 | ||
|
6d27cf8c7d | ||
|
1ac2483cc4 | ||
|
4d3420dcd0 | ||
|
38450a9aed | ||
|
24de7ebb97 | ||
|
5a571d66d0 | ||
|
0ae998a51e | ||
|
447dd18b8b | ||
|
9a200dc40c | ||
|
d42b752ac1 | ||
|
2f4103248d | ||
|
985d731d2b | ||
|
032f95150c | ||
|
d1aa5778c3 | ||
|
13ce24b75e | ||
|
c89ec2faf1 | ||
|
56ab0833b8 | ||
|
c62b393c52 | ||
|
991de77cad | ||
|
94780c44c8 | ||
|
82542a6390 | ||
|
53ff8443dc | ||
|
3855ecab58 | ||
|
a57cbb4aa8 | ||
|
56a4461c2a | ||
|
cd023ec5ab | ||
|
1aa4ade3c0 | ||
|
dcb46eef4f | ||
|
e3e8ef6e3e | ||
|
6808b1971a | ||
|
1dd5269549 | ||
|
d33adca1e8 | ||
|
8ea7f7864f | ||
|
493ae06e52 | ||
|
2b8f3536d3 | ||
|
544d23ec09 | ||
|
588d32fd22 | ||
|
1c471fe624 | ||
|
52486d687d | ||
|
73441d791c | ||
|
1bb6423721 | ||
|
d6775d64a3 | ||
|
e1326eae91 | ||
|
b93955b28f | ||
|
e3452bda22 | ||
|
0aab691b44 | ||
|
1bfb2dd975 | ||
|
fb7937314b | ||
|
e39d2a9b95 | ||
|
3b04b61662 | ||
|
829b2a0f2a | ||
|
5edffbdf21 | ||
|
27576c95e6 | ||
|
5acc45cba4 | ||
|
343e0ed848 | ||
|
0c784b12fa | ||
|
2b50b21752 | ||
|
ad604f020d | ||
|
4151e020f6 | ||
|
bc59714192 | ||
|
b43a7b6809 | ||
|
fba8aa0ab0 | ||
|
5623ab3866 | ||
|
a4fbf9bd28 | ||
|
db730da45c | ||
|
b5a938d3b0 | ||
|
863d1e25ba | ||
|
a90aaeb86c | ||
|
8b6af78f2a | ||
|
6c2dcb450b | ||
|
f57962d02f | ||
|
2983c381ae | ||
|
1ea7fa813a | ||
|
e434c5b5d0 | ||
|
9c1f47badd | ||
|
4ed4328bf8 | ||
|
c6022e94bb | ||
|
06eb169c65 | ||
|
2f7529cd71 | ||
|
3a8541f601 | ||
|
0eb910b2e8 | ||
|
76a879e4fd | ||
|
7026e43575 | ||
|
869361bac3 | ||
|
832ea3c04e | ||
|
68232f966e | ||
|
86b7da45ef | ||
|
b853856317 | ||
|
6676f1c6ac | ||
|
e0243bc460 | ||
|
fd6cb548f8 | ||
|
743b2d6054 | ||
|
fb5c6b365e | ||
|
f092e99f42 | ||
|
751eb6ef98 | ||
|
980de649e3 | ||
|
84849d2c84 | ||
|
b263997bed | ||
|
12c773bc71 | ||
|
d937539618 | ||
|
0a5d07f839 | ||
|
5dcd3956ac | ||
|
3ffc7251f4 | ||
|
7fb0cfd176 | ||
|
5c83952ba1 | ||
|
a7a051bb2a | ||
|
2b2c5dbe5c | ||
|
ffe87a9729 | ||
|
b366195415 | ||
|
f9f2b20e90 | ||
|
e16811065d | ||
|
f66a1127de | ||
|
06ef60c4c2 | ||
|
4b93298b58 | ||
|
a41a771923 | ||
|
a43f7d9bcf | ||
|
c9453f877b | ||
|
525fa94b18 | ||
|
460b9e5e55 | ||
|
8fc41a7ca8 | ||
|
4c7b9cf4e3 | ||
|
f4479dfda4 | ||
|
377f08ad5d | ||
|
add43bafda | ||
|
b35d45955b | ||
|
2ecb970da0 | ||
|
edb2933dad | ||
|
8141927974 | ||
|
4db89ac3a7 | ||
|
feb67e6c2d | ||
|
014e97b563 | ||
|
a3f4e19aa2 | ||
|
90a65ab6cc | ||
|
c00e1618e7 | ||
|
ceb6417979 | ||
|
1d40ebb65f | ||
|
6301427ef4 | ||
|
64d1d6c88d | ||
|
adcacd7d45 | ||
|
b6729b0d0a | ||
|
ec7d5b4046 | ||
|
380ea3a891 | ||
|
320e152897 | ||
|
c00d0abe0d | ||
|
aaa83da0f8 | ||
|
494e716dfe | ||
|
50c266295e | ||
|
55a6122a6c | ||
|
2a648b79c9 | ||
|
0bc49bf723 | ||
|
cb7d1faa52 | ||
|
fa3c744e76 | ||
|
54be4dccce | ||
|
6a407d0e42 | ||
|
47171fffd5 | ||
|
e48c9067a3 | ||
|
1d30c83f7a | ||
|
9f76fb295e | ||
|
07e2ebe340 | ||
|
884d5e0e16 | ||
|
a3a79534ab | ||
|
6acf1d3411 | ||
|
e32480abfb | ||
|
07e7251d7d | ||
|
753b244630 | ||
|
892f7c8e47 | ||
|
6a3f50d606 | ||
|
a49cb77840 | ||
|
79f9362736 | ||
|
164038ec3c | ||
|
eacd52fb6c | ||
|
20295ddc50 | ||
|
994c412bd2 | ||
|
3b3fa88c89 | ||
|
afec96025b | ||
|
57b6144e7f | ||
|
9bff18ece3 | ||
|
e89f44c87f | ||
|
99fe94fbab | ||
|
e0c2f43b2b | ||
|
0c81eb93b9 | ||
|
391c515779 | ||
|
077f3e6e78 | ||
|
3b2256bcec | ||
|
87f5fa049f | ||
|
5774b8a67b | ||
|
80fe51702b | ||
|
303ec73fa8 | ||
|
815d9d4e28 | ||
|
6044b6328d | ||
|
9a81ade1c8 | ||
|
33c21d0153 | ||
|
1735ce6a8a | ||
|
385ec00640 | ||
|
4f58a2a357 | ||
|
1e10a3c406 | ||
|
ff3ea429d2 | ||
|
bdf7d1b813 | ||
|
ae4b3a4778 | ||
|
66bc43674b | ||
|
8f0f82f98e | ||
|
43f0b4c902 | ||
|
867bb61e0d | ||
|
10912fe270 | ||
|
76d9c36426 | ||
|
6ca4d77b8f | ||
|
7b3d40ca1e | ||
|
ea2d84e810 | ||
|
e477aed8ff | ||
|
07ffb09391 | ||
|
54f02c9616 | ||
|
eeade47eb6 | ||
|
8ec9a98c86 | ||
|
277d7ef824 | ||
|
cc844e6905 | ||
|
9b550d6e4f | ||
|
9c25807b99 | ||
|
b07995c3ed | ||
|
67228d295d | ||
|
4521041619 | ||
|
ce9a680333 | ||
|
2c40843306 | ||
|
8d3dad234e | ||
|
6da7cf6b8e | ||
|
25d0aefe37 | ||
|
afb25d5b3d | ||
|
78ae1b34ab | ||
|
d3401cc87e | ||
|
9cbe4fdb4a | ||
|
851bb7904f | ||
|
d7598d3091 | ||
|
76d321fa79 | ||
|
c42aafe0b9 | ||
|
28db945b91 | ||
|
a805e260c4 | ||
|
6e95e5439b | ||
|
a9f479d60d | ||
|
7b095a683c | ||
|
78931bbb22 | ||
|
12ae84b757 | ||
|
75da0a16c1 | ||
|
2a0f9b30e6 | ||
|
109795ca3e | ||
|
e6e5e8a311 | ||
|
bd0869ec07 | ||
|
07ae127cc8 | ||
|
baa76c3244 | ||
|
7ac31f80ed | ||
|
a95ee64f7d | ||
|
7d6c6bc10a | ||
|
18b41938de | ||
|
9fe382b27f | ||
|
f022b338e6 | ||
|
855d99dfa0 | ||
|
cc2424e0bf | ||
|
fa6d72268f | ||
|
762dd92ec3 | ||
|
2e90e24552 | ||
|
d7adce0ebf | ||
|
a1bb6da4fb | ||
|
873fecf548 | ||
|
682511bb68 | ||
|
02fbe1a6a1 | ||
|
0cb82a6f5e | ||
|
79808e8ee9 | ||
|
2c38df10c8 | ||
|
1ca15d44a0 | ||
|
82450a91a9 | ||
|
62edfa6f8b | ||
|
fe86315ece | ||
|
df3f719e89 | ||
|
0632d8199f | ||
|
047bf0ca45 | ||
|
356c879668 | ||
|
ba432d32b3 | ||
|
c8ada3f47d | ||
|
cd77fc6448 | ||
|
a2f926c611 | ||
|
6c5eb85a16 | ||
|
cadb1c6eaa | ||
|
73710319e6 | ||
|
da91dc5595 | ||
|
31d5e5a092 | ||
|
13ee1d0990 | ||
|
d5a9063378 | ||
|
918064f35d | ||
|
193b8326bc | ||
|
9abb7f376e | ||
|
ac338fa438 | ||
|
f5b78ee845 | ||
|
126bb26a6e | ||
|
23e86abb5b | ||
|
3a3714787f | ||
|
bc57aabc97 | ||
|
08df02a1ea | ||
|
35ad6fbad0 | ||
|
97f8fe71af | ||
|
a9d59b3dcd | ||
|
b7240e1c40 | ||
|
62aa849657 | ||
|
c302ebe282 | ||
|
6404bed519 | ||
|
8d4d168988 | ||
|
d4d3805be8 | ||
|
e853af40c4 | ||
|
941200cf3b | ||
|
cf1f659ebf | ||
|
eb381a87bc | ||
|
68bc0ae4a0 | ||
|
178bc7f401 | ||
|
0f1245b975 | ||
|
960312a932 | ||
|
0e00f3bbce | ||
|
ec205f68a6 | ||
|
5fe5989710 | ||
|
69141b5395 | ||
|
8d66f05924 | ||
|
e7330dbff8 | ||
|
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 |
@@ -1,15 +1,11 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[{package.json,*.yml}]
|
||||
[{*.css,*.html,*.js,*.json,*.ts}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
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
|
||||
|
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
github: zulip
|
||||
patreon: zulip
|
||||
open_collective: zulip
|
8
.github/ISSUE_TEMPLATE.md
vendored
8
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,8 +0,0 @@
|
||||
---
|
||||
<!-- Please Include: -->
|
||||
- **Operating System**:
|
||||
- [ ] Windows
|
||||
- [ ] Linux/Ubutnu
|
||||
- [ ] macOS
|
||||
- **Clear steps to reproduce the issue**:
|
||||
- **Relevant error messages and/or screenshots**:
|
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
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. -->
|
4
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
4
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
---
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
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. -->
|
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,4 +1,5 @@
|
||||
---
|
||||
|
||||
<!--
|
||||
Remove the fields that are not appropriate
|
||||
Please include:
|
||||
@@ -11,6 +12,7 @@ Please include:
|
||||
**Screenshots?**
|
||||
|
||||
**You have tested this PR on:**
|
||||
- [ ] Windows
|
||||
- [ ] Linux/Ubuntu
|
||||
- [ ] macOS
|
||||
|
||||
- [ ] Windows
|
||||
- [ ] Linux/Ubuntu
|
||||
- [ ] macOS
|
||||
|
15
.github/workflows/node.js.yml
vendored
Normal file
15
.github/workflows/node.js.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: npm ci
|
||||
- run: npm test
|
19
.gitignore
vendored
19
.gitignore
vendored
@@ -1,11 +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/
|
||||
/dist/
|
||||
/dist-electron/
|
||||
|
||||
#snap generated files
|
||||
snap/parts
|
||||
snap/prime
|
||||
snap/snap
|
||||
snap/stage
|
||||
snap/*.snap
|
||||
|
||||
# Logs
|
||||
logs
|
||||
@@ -26,6 +37,6 @@ yarn-error.log*
|
||||
config.gypi
|
||||
|
||||
# Test generated files
|
||||
tests/package.json
|
||||
# tests/package.json
|
||||
|
||||
.python-version
|
||||
|
19
.htmlhintrc
Normal file
19
.htmlhintrc
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"attr-value-not-empty": false,
|
||||
"attr-no-duplication": true,
|
||||
"doctype-first": 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,
|
||||
"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 @@
|
||||
6.9.4
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
/dist
|
||||
/dist-electron
|
||||
/public/translations/*.json
|
12
.stylelintrc
Normal file
12
.stylelintrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"rules": {
|
||||
"color-named": "never",
|
||||
"color-no-hex": true,
|
||||
"font-family-no-missing-generic-family-keyword": [
|
||||
true,
|
||||
{"ignoreFontFamilies": ["Material Icons"]}
|
||||
],
|
||||
"selector-type-no-unknown": [true, {"ignoreTypes": ["webview"]}]
|
||||
}
|
||||
}
|
37
.travis.yml
37
.travis.yml
@@ -1,37 +0,0 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
os:
|
||||
- osx
|
||||
- linux
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- build-essential
|
||||
- libxext-dev
|
||||
- libxtst-dev
|
||||
- libxkbfile-dev
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- '6'
|
||||
|
||||
before_install:
|
||||
- ./scripts/travis-xvfb.sh
|
||||
- npm install -g gulp
|
||||
- npm install
|
||||
|
||||
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
|
9
.tx/config
Normal file
9
.tx/config
Normal file
@@ -0,0 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:zulip:p:zulip:r:desktopjson]
|
||||
file_filter = public/translations/<lang>.json
|
||||
minimum_perc = 0
|
||||
source_file = public/translations/en.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
@@ -2,46 +2,50 @@
|
||||
|
||||
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).
|
||||
- The whole Zulip documentation, such as setting up a development environment, setting up with the Zulip web app 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).
|
||||
|
||||
Please pay attention to the following points while opening an issue.
|
||||
|
||||
### Does it happen on web browsers? (especially Chrome)
|
||||
|
||||
Zulip's desktop client is based on Electron, which integrates the Chrome engine within a standalone application.
|
||||
If the problem you encounter can be reproduced on web browsers, it may be an issue with [Zulip web app](https://github.com/zulip/zulip).
|
||||
|
||||
### Write detailed information
|
||||
|
||||
Detailed information is very helpful to understand an issue.
|
||||
|
||||
For example:
|
||||
* How to reproduce the issue, step-by-step.
|
||||
* The expected behavior (or what is wrong).
|
||||
* Screenshots for GUI issues.
|
||||
* The application version.
|
||||
* The operating system.
|
||||
* The Zulip-Desktop version.
|
||||
|
||||
- How to reproduce the issue, step-by-step.
|
||||
- The expected behavior (or what is wrong).
|
||||
- Screenshots for GUI issues.
|
||||
- The application version.
|
||||
- The operating system.
|
||||
- The Zulip-Desktop version.
|
||||
|
||||
## Pull Requests
|
||||
Pull Requests are 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:
|
||||
* The operating system on which you tested.
|
||||
* The Zulip-Desktop version on which you tested.
|
||||
* The relevant issue number, if applicable.
|
||||
- The operating system on which you tested.
|
||||
- The Zulip-Desktop version on which you tested.
|
||||
- The relevant issue number, if applicable.
|
||||
|
43
README.md
43
README.md
@@ -1,32 +1,41 @@
|
||||
# Zulip Desktop Client
|
||||
[](https://travis-ci.org/zulip/zulip-electron)
|
||||
[](https://ci.appveyor.com/project/akashnimare/zulip-electron/branch/master)
|
||||
|
||||
[](https://travis-ci.com/github/zulip/zulip-desktop)
|
||||
[](https://ci.appveyor.com/project/zulip/zulip-desktop/branch/main)
|
||||
[](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
|
||||
* Desktop Notifications with inline reply support
|
||||
* Multilanguage SpellChecker
|
||||
* OSX/Win/Linux installers
|
||||
* Automatic Updates (macOS/Windows/Linux)
|
||||
* Keyboard shortcuts
|
||||
|
||||
# Development
|
||||
Please see our [development guide](./development.md) to get started and run app locally.
|
||||
- Sign in to multiple organizations
|
||||
- Desktop notifications with inline reply
|
||||
- Tray/dock integration
|
||||
- Multi-language spell checker
|
||||
- Automatic updates
|
||||
|
||||
# Reporting issues
|
||||
|
||||
This desktop client shares most of its code with the Zulip web app.
|
||||
Issues in an individual organization's Zulip window should be reported
|
||||
in the [Zulip server and web app
|
||||
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.
|
||||
|
44
app/common/config-schemata.ts
Normal file
44
app/common/config-schemata.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const dndSettingsSchemata = {
|
||||
showNotification: z.boolean(),
|
||||
silent: z.boolean(),
|
||||
flashTaskbarOnMessage: z.boolean(),
|
||||
};
|
||||
|
||||
export const configSchemata = {
|
||||
...dndSettingsSchemata,
|
||||
appLanguage: z.string().nullable(),
|
||||
autoHideMenubar: z.boolean(),
|
||||
autoUpdate: z.boolean(),
|
||||
badgeOption: z.boolean(),
|
||||
betaUpdate: z.boolean(),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
customCSS: z.string().or(z.literal(false)).nullable(),
|
||||
dnd: z.boolean(),
|
||||
dndPreviousSettings: z.object(dndSettingsSchemata).partial(),
|
||||
dockBouncing: z.boolean(),
|
||||
downloadsPath: z.string(),
|
||||
enableSpellchecker: z.boolean(),
|
||||
errorReporting: z.boolean(),
|
||||
lastActiveTab: z.number(),
|
||||
promptDownload: z.boolean(),
|
||||
proxyBypass: z.string(),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
proxyPAC: z.string(),
|
||||
proxyRules: z.string(),
|
||||
quitOnClose: z.boolean(),
|
||||
showSidebar: z.boolean(),
|
||||
spellcheckerLanguages: z.string().array().nullable(),
|
||||
startAtLogin: z.boolean(),
|
||||
startMinimized: z.boolean(),
|
||||
trayIcon: z.boolean(),
|
||||
useManualProxy: z.boolean(),
|
||||
useProxy: z.boolean(),
|
||||
useSystemProxy: z.boolean(),
|
||||
};
|
||||
|
||||
export const enterpriseConfigSchemata = {
|
||||
...configSchemata,
|
||||
presetOrganizations: z.string().array(),
|
||||
};
|
100
app/common/config-util.ts
Normal file
100
app/common/config-util.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import * as Sentry from "@sentry/electron";
|
||||
import {JsonDB} from "node-json-db";
|
||||
import {DataError} from "node-json-db/dist/lib/Errors";
|
||||
import type {z} from "zod";
|
||||
import {app, dialog} from "zulip:remote";
|
||||
|
||||
import {configSchemata} from "./config-schemata.js";
|
||||
import * as EnterpriseUtil from "./enterprise-util.js";
|
||||
import Logger from "./logger-util.js";
|
||||
|
||||
export type Config = {
|
||||
[Key in keyof typeof configSchemata]: z.output<(typeof configSchemata)[Key]>;
|
||||
};
|
||||
|
||||
const logger = new Logger({
|
||||
file: "config-util.log",
|
||||
});
|
||||
|
||||
let db: JsonDB;
|
||||
|
||||
reloadDb();
|
||||
|
||||
export function getConfigItem<Key extends keyof Config>(
|
||||
key: Key,
|
||||
defaultValue: Config[Key],
|
||||
): z.output<(typeof configSchemata)[Key]> {
|
||||
try {
|
||||
db.reload();
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error while reloading settings.json: ");
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
try {
|
||||
return configSchemata[key].parse(db.getObject<unknown>(`/${key}`));
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof DataError)) throw error;
|
||||
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: unknown) {
|
||||
logger.error("Error while reloading settings.json: ");
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
return db.exists(`/${key}`);
|
||||
}
|
||||
|
||||
export function setConfigItem<Key extends keyof Config>(
|
||||
key: Key,
|
||||
value: Config[Key],
|
||||
override?: boolean,
|
||||
): void {
|
||||
if (EnterpriseUtil.configItemExists(key) && !override) {
|
||||
// If item is in global config and we're not trying to override
|
||||
return;
|
||||
}
|
||||
|
||||
configSchemata[key].parse(value);
|
||||
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: unknown) {
|
||||
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);
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
}
|
||||
|
||||
db = new JsonDB(settingsJsonPath, true, true);
|
||||
}
|
61
app/common/default-util.ts
Normal file
61
app/common/default-util.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
import {app} from "zulip:remote";
|
||||
|
||||
let setupCompleted = false;
|
||||
|
||||
const zulipDir = app.getPath("userData");
|
||||
const logDir = `${zulipDir}/Logs/`;
|
||||
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);
|
||||
}
|
||||
|
||||
// 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 settingsJson = `${zulipDir}/settings.json`;
|
||||
const updatesJson = `${zulipDir}/updates.json`;
|
||||
const windowStateJson = `${zulipDir}/window-state.json`;
|
||||
const configData = [
|
||||
{
|
||||
path: domainJson,
|
||||
fileName: "domain.json",
|
||||
},
|
||||
{
|
||||
path: settingsJson,
|
||||
fileName: "settings.json",
|
||||
},
|
||||
{
|
||||
path: updatesJson,
|
||||
fileName: "updates.json",
|
||||
},
|
||||
];
|
||||
for (const data of configData) {
|
||||
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;
|
||||
}
|
||||
};
|
60
app/common/dnd-util.ts
Normal file
60
app/common/dnd-util.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import process from "node:process";
|
||||
|
||||
import type {z} from "zod";
|
||||
|
||||
import type {dndSettingsSchemata} from "./config-schemata.js";
|
||||
import * as ConfigUtil from "./config-util.js";
|
||||
|
||||
export type DndSettings = {
|
||||
[Key in keyof typeof dndSettingsSchemata]: z.output<
|
||||
(typeof dndSettingsSchemata)[Key]
|
||||
>;
|
||||
};
|
||||
|
||||
type SettingName = keyof DndSettings;
|
||||
|
||||
type Toggle = {
|
||||
dnd: boolean;
|
||||
newSettings: Partial<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: Partial<DndSettings>;
|
||||
if (dnd) {
|
||||
const oldSettings: Partial<DndSettings> = {};
|
||||
newSettings = {};
|
||||
|
||||
// Iterate through the dndSettingList.
|
||||
for (const settingName of dndSettingList) {
|
||||
// Store the current value of setting.
|
||||
oldSettings[settingName] = ConfigUtil.getConfigItem(
|
||||
settingName,
|
||||
settingName !== "silent",
|
||||
);
|
||||
// New value of setting.
|
||||
newSettings[settingName] = settingName === "silent";
|
||||
}
|
||||
|
||||
// Store old value in oldSettings.
|
||||
ConfigUtil.setConfigItem("dndPreviousSettings", oldSettings);
|
||||
} else {
|
||||
newSettings = ConfigUtil.getConfigItem("dndPreviousSettings", {
|
||||
showNotification: true,
|
||||
silent: false,
|
||||
...(process.platform === "win32" && {flashTaskbarOnMessage: true}),
|
||||
});
|
||||
}
|
||||
|
||||
for (const settingName of dndSettingList) {
|
||||
ConfigUtil.setConfigItem(settingName, newSettings[settingName]!);
|
||||
}
|
||||
|
||||
ConfigUtil.setConfigItem("dnd", dnd);
|
||||
return {dnd, newSettings};
|
||||
}
|
94
app/common/enterprise-util.ts
Normal file
94
app/common/enterprise-util.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
import {z} from "zod";
|
||||
|
||||
import {enterpriseConfigSchemata} from "./config-schemata.js";
|
||||
import Logger from "./logger-util.js";
|
||||
|
||||
type EnterpriseConfig = {
|
||||
[Key in keyof typeof enterpriseConfigSchemata]: z.output<
|
||||
(typeof enterpriseConfigSchemata)[Key]
|
||||
>;
|
||||
};
|
||||
|
||||
const logger = new Logger({
|
||||
file: "enterprise-util.log",
|
||||
});
|
||||
|
||||
let enterpriseSettings: Partial<EnterpriseConfig>;
|
||||
let configFile: boolean;
|
||||
|
||||
reloadDb();
|
||||
|
||||
function reloadDb(): void {
|
||||
let enterpriseFile = "/etc/zulip-desktop-config/global_config.json";
|
||||
if (process.platform === "win32") {
|
||||
enterpriseFile =
|
||||
"C:\\Program Files\\Zulip-Desktop-Config\\global_config.json";
|
||||
}
|
||||
|
||||
enterpriseFile = path.resolve(enterpriseFile);
|
||||
if (fs.existsSync(enterpriseFile)) {
|
||||
configFile = true;
|
||||
try {
|
||||
const file = fs.readFileSync(enterpriseFile, "utf8");
|
||||
const data: unknown = JSON.parse(file);
|
||||
enterpriseSettings = z
|
||||
.object(enterpriseConfigSchemata)
|
||||
.partial()
|
||||
.parse(data);
|
||||
} catch (error: unknown) {
|
||||
logger.log("Error while JSON parsing global_config.json: ");
|
||||
logger.log(error);
|
||||
}
|
||||
} else {
|
||||
configFile = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasConfigFile(): boolean {
|
||||
return configFile;
|
||||
}
|
||||
|
||||
export function getConfigItem<Key extends keyof EnterpriseConfig>(
|
||||
key: Key,
|
||||
defaultValue: EnterpriseConfig[Key],
|
||||
): EnterpriseConfig[Key] {
|
||||
reloadDb();
|
||||
if (!configFile) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const value = enterpriseSettings[key];
|
||||
return value === undefined ? defaultValue : (value as EnterpriseConfig[Key]);
|
||||
}
|
||||
|
||||
export function configItemExists(key: keyof EnterpriseConfig): boolean {
|
||||
reloadDb();
|
||||
if (!configFile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return enterpriseSettings[key] !== undefined;
|
||||
}
|
||||
|
||||
export function isPresetOrg(url: string): boolean {
|
||||
if (!configFile || !configItemExists("presetOrganizations")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const presetOrgs = enterpriseSettings.presetOrganizations;
|
||||
if (!Array.isArray(presetOrgs)) {
|
||||
throw new TypeError("Expected array for presetOrgs");
|
||||
}
|
||||
|
||||
for (const org of presetOrgs) {
|
||||
if (url.includes(org)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
26
app/common/html.ts
Normal file
26
app/common/html.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {htmlEscape} from "escape-goat";
|
||||
|
||||
export class Html {
|
||||
html: string;
|
||||
|
||||
constructor({html}: {html: string}) {
|
||||
this.html = html;
|
||||
}
|
||||
|
||||
join(htmls: readonly Html[]): Html {
|
||||
return new Html({html: htmls.map((html) => html.html).join(this.html)});
|
||||
}
|
||||
}
|
||||
|
||||
export function html(
|
||||
template: TemplateStringsArray,
|
||||
...values: unknown[]
|
||||
): Html {
|
||||
let html = template[0];
|
||||
for (const [index, value] of values.entries()) {
|
||||
html += value instanceof Html ? value.html : htmlEscape(String(value));
|
||||
html += template[index + 1];
|
||||
}
|
||||
|
||||
return new Html({html});
|
||||
}
|
43
app/common/link-util.ts
Normal file
43
app/common/link-util.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {shell} from "electron/common";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import {html} from "./html.js";
|
||||
|
||||
export async function openBrowser(url: URL): Promise<void> {
|
||||
if (["http:", "https:", "mailto:"].includes(url.protocol)) {
|
||||
await shell.openExternal(url.href);
|
||||
} else {
|
||||
// For security, indirect links to non-whitelisted protocols
|
||||
// through a real web browser via a local HTML file.
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "zulip-redirect-"));
|
||||
const file = path.join(dir, "redirect.html");
|
||||
fs.writeFileSync(
|
||||
file,
|
||||
html`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Refresh" content="0; url=${url.href}" />
|
||||
<title>Redirecting</title>
|
||||
<style>
|
||||
html {
|
||||
font-family: menu, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>Opening <a href="${url.href}">${url.href}</a>…</p>
|
||||
</body>
|
||||
</html>
|
||||
`.html,
|
||||
);
|
||||
await shell.openPath(file);
|
||||
setTimeout(() => {
|
||||
fs.unlinkSync(file);
|
||||
fs.rmdirSync(dir);
|
||||
}, 15_000);
|
||||
}
|
||||
}
|
90
app/common/logger-util.ts
Normal file
90
app/common/logger-util.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import {Console} from "node:console"; // eslint-disable-line n/prefer-global/console
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import process from "node:process";
|
||||
|
||||
import {app} from "zulip:remote";
|
||||
|
||||
import {initSetUp} from "./default-util.js";
|
||||
|
||||
type LoggerOptions = {
|
||||
file?: string;
|
||||
};
|
||||
|
||||
initSetUp();
|
||||
|
||||
const logDir = `${app.getPath("userData")}/Logs`;
|
||||
|
||||
type Level = "log" | "debug" | "info" | "warn" | "error";
|
||||
|
||||
export default class Logger {
|
||||
nodeConsole: Console;
|
||||
|
||||
constructor(options: LoggerOptions = {}) {
|
||||
let {file = "console.log"} = options;
|
||||
|
||||
file = `${logDir}/${file}`;
|
||||
|
||||
// Trim log according to type of process
|
||||
if (process.type === "renderer") {
|
||||
requestIdleCallback(async () => this.trimLog(file));
|
||||
} else {
|
||||
process.nextTick(async () => this.trimLog(file));
|
||||
}
|
||||
|
||||
const fileStream = fs.createWriteStream(file, {flags: "a"});
|
||||
const nodeConsole = new Console(fileStream);
|
||||
|
||||
this.nodeConsole = nodeConsole;
|
||||
}
|
||||
|
||||
_log(type: Level, ...args: unknown[]): void {
|
||||
args.unshift(this.getTimestamp() + " |\t");
|
||||
args.unshift(type.toUpperCase() + " |");
|
||||
this.nodeConsole[type](...args);
|
||||
console[type](...args);
|
||||
}
|
||||
|
||||
log(...args: unknown[]): void {
|
||||
this._log("log", ...args);
|
||||
}
|
||||
|
||||
debug(...args: unknown[]): void {
|
||||
this._log("debug", ...args);
|
||||
}
|
||||
|
||||
info(...args: unknown[]): void {
|
||||
this._log("info", ...args);
|
||||
}
|
||||
|
||||
warn(...args: unknown[]): void {
|
||||
this._log("warn", ...args);
|
||||
}
|
||||
|
||||
error(...args: unknown[]): void {
|
||||
this._log("error", ...args);
|
||||
}
|
||||
|
||||
getTimestamp(): string {
|
||||
const date = new Date();
|
||||
const timestamp =
|
||||
`${date.getMonth()}/${date.getDate()} ` +
|
||||
`${date.getMinutes()}:${date.getSeconds()}`;
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
async trimLog(file: string): Promise<void> {
|
||||
const data = await fs.promises.readFile(file, "utf8");
|
||||
|
||||
const maxLogFileLines = 500;
|
||||
const logs = data.split(os.EOL);
|
||||
const logLength = logs.length - 1;
|
||||
|
||||
// Keep bottom maxLogFileLines of each log instance
|
||||
if (logLength > maxLogFileLines) {
|
||||
const trimmedLogs = logs.slice(logLength - maxLogFileLines);
|
||||
const toWrite = trimmedLogs.join(os.EOL);
|
||||
await fs.promises.writeFile(file, toWrite);
|
||||
}
|
||||
}
|
||||
}
|
38
app/common/messages.ts
Normal file
38
app/common/messages.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
type DialogBoxError = {
|
||||
title: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export function invalidZulipServerError(domain: string): string {
|
||||
return `${domain} does not appear to be a valid Zulip server. Make sure that
|
||||
• You can connect to that URL in a web browser.
|
||||
• If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.
|
||||
• It's a Zulip server. (The oldest supported version is 1.6).
|
||||
• The server has a valid certificate.
|
||||
• The SSL is correctly configured for the certificate. Check out the SSL troubleshooting guide -
|
||||
https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`;
|
||||
}
|
||||
|
||||
export function enterpriseOrgError(
|
||||
length: number,
|
||||
domains: string[],
|
||||
): DialogBoxError {
|
||||
let domainList = "";
|
||||
for (const domain of domains) {
|
||||
domainList += `• ${domain}\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: `Could not add the following ${
|
||||
length === 1 ? "organization" : "organizations"
|
||||
}`,
|
||||
content: `${domainList}\nPlease contact your system administrator.`,
|
||||
};
|
||||
}
|
||||
|
||||
export function orgRemovalError(url: string): DialogBoxError {
|
||||
return {
|
||||
title: `Removing ${url} is a restricted operation.`,
|
||||
content: "Please contact your system administrator.",
|
||||
};
|
||||
}
|
15
app/common/paths.ts
Normal file
15
app/common/paths.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import url from "node:url";
|
||||
|
||||
export const bundlePath = __dirname;
|
||||
|
||||
export const publicPath = import.meta.env.DEV
|
||||
? path.join(bundlePath, "../public")
|
||||
: bundlePath;
|
||||
|
||||
export const bundleUrl = import.meta.env.DEV
|
||||
? process.env.VITE_DEV_SERVER_URL
|
||||
: url.pathToFileURL(__dirname).href + "/";
|
||||
|
||||
export const publicUrl = bundleUrl;
|
16
app/common/translation-util.ts
Normal file
16
app/common/translation-util.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import path from "node:path";
|
||||
|
||||
import i18n from "i18n";
|
||||
|
||||
import * as ConfigUtil from "./config-util.js";
|
||||
import {publicPath} from "./paths.js";
|
||||
|
||||
i18n.configure({
|
||||
directory: path.join(publicPath, "translations/"),
|
||||
updateFiles: false,
|
||||
});
|
||||
|
||||
/* Fetches the current appLocale from settings.json */
|
||||
i18n.setLocale(ConfigUtil.getConfigItem("appLanguage", "en") ?? "en");
|
||||
|
||||
export {__} from "i18n";
|
84
app/common/typed-ipc.ts
Normal file
84
app/common/typed-ipc.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type {DndSettings} from "./dnd-util.js";
|
||||
import type {MenuProps, ServerConf} from "./types.js";
|
||||
|
||||
export type MainMessage = {
|
||||
"clear-app-settings": () => void;
|
||||
"configure-spell-checker": () => void;
|
||||
"fetch-user-agent": () => string;
|
||||
"focus-app": () => void;
|
||||
"focus-this-webview": () => void;
|
||||
"new-clipboard-key": () => {key: Uint8Array; sig: Uint8Array};
|
||||
"permission-callback": (permissionCallbackId: number, grant: boolean) => void;
|
||||
"quit-app": () => void;
|
||||
"realm-icon-changed": (serverURL: string, iconURL: string) => void;
|
||||
"realm-name-changed": (serverURL: string, realmName: string) => void;
|
||||
"reload-full-app": () => void;
|
||||
"save-last-tab": (index: number) => void;
|
||||
"switch-server-tab": (index: number) => void;
|
||||
"toggle-app": () => void;
|
||||
"toggle-badge-option": (newValue: boolean) => void;
|
||||
"toggle-menubar": (showMenubar: boolean) => void;
|
||||
toggleAutoLauncher: (AutoLaunchValue: boolean) => void;
|
||||
"unread-count": (unreadCount: number) => void;
|
||||
"update-badge": (messageCount: number) => void;
|
||||
"update-menu": (props: MenuProps) => void;
|
||||
"update-taskbar-icon": (data: string, text: string) => void;
|
||||
};
|
||||
|
||||
export type MainCall = {
|
||||
"get-server-settings": (domain: string) => ServerConf;
|
||||
"is-online": (url: string) => boolean;
|
||||
"poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined;
|
||||
"save-server-icon": (iconURL: string) => string | null;
|
||||
};
|
||||
|
||||
export type RendererMessage = {
|
||||
back: () => void;
|
||||
"copy-zulip-url": () => void;
|
||||
destroytray: () => void;
|
||||
"enter-fullscreen": () => void;
|
||||
focus: () => void;
|
||||
"focus-webview-with-id": (webviewId: number) => void;
|
||||
forward: () => void;
|
||||
"hard-reload": () => void;
|
||||
"leave-fullscreen": () => void;
|
||||
"log-out": () => void;
|
||||
logout: () => void;
|
||||
"new-server": () => void;
|
||||
"open-about": () => void;
|
||||
"open-help": () => void;
|
||||
"open-network-settings": () => void;
|
||||
"open-org-tab": () => void;
|
||||
"open-settings": () => void;
|
||||
"permission-request": (
|
||||
options: {webContentsId: number | null; origin: string; permission: string},
|
||||
rendererCallbackId: number,
|
||||
) => void;
|
||||
"play-ding-sound": () => void;
|
||||
"reload-current-viewer": () => void;
|
||||
"reload-proxy": (showAlert: boolean) => void;
|
||||
"reload-viewer": () => void;
|
||||
"render-taskbar-icon": (messageCount: number) => void;
|
||||
"set-active": () => void;
|
||||
"set-idle": () => void;
|
||||
"show-keyboard-shortcuts": () => void;
|
||||
"show-notification-settings": () => void;
|
||||
"switch-server-tab": (index: number) => void;
|
||||
"tab-devtools": () => void;
|
||||
"toggle-autohide-menubar": (
|
||||
autoHideMenubar: boolean,
|
||||
updateMenu: boolean,
|
||||
) => void;
|
||||
"toggle-dnd": (state: boolean, newSettings: Partial<DndSettings>) => void;
|
||||
"toggle-sidebar": (show: boolean) => void;
|
||||
"toggle-silent": (state: boolean) => void;
|
||||
"toggle-tray": (state: boolean) => void;
|
||||
toggletray: () => void;
|
||||
tray: (arg: number) => void;
|
||||
"update-realm-icon": (serverURL: string, iconURL: string) => void;
|
||||
"update-realm-name": (serverURL: string, realmName: string) => void;
|
||||
"webview-reload": () => void;
|
||||
zoomActualSize: () => void;
|
||||
zoomIn: () => void;
|
||||
zoomOut: () => void;
|
||||
};
|
28
app/common/types.ts
Normal file
28
app/common/types.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export type MenuProps = {
|
||||
tabs: TabData[];
|
||||
activeTabIndex?: number;
|
||||
enableMenu?: boolean;
|
||||
};
|
||||
|
||||
export type NavItem =
|
||||
| "General"
|
||||
| "Network"
|
||||
| "AddServer"
|
||||
| "Organizations"
|
||||
| "Shortcuts";
|
||||
|
||||
export type ServerConf = {
|
||||
url: string;
|
||||
alias: string;
|
||||
icon: string;
|
||||
zulipVersion: string;
|
||||
zulipFeatureLevel: number;
|
||||
};
|
||||
|
||||
export type TabRole = "server" | "function";
|
||||
|
||||
export type TabData = {
|
||||
role: TabRole;
|
||||
name: string;
|
||||
index: number;
|
||||
};
|
@@ -1,59 +0,0 @@
|
||||
'use strict';
|
||||
const { app, dialog } = require('electron');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
|
||||
|
||||
function appUpdater() {
|
||||
// Don't initiate auto-updates in development
|
||||
if (isDev) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.platform === 'linux' && !process.env.APPIMAGE) {
|
||||
const { linuxUpdateNotification } = require('./linuxupdater');
|
||||
linuxUpdateNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Logs directory
|
||||
const LogsDir = `${app.getPath('userData')}/Logs`;
|
||||
|
||||
// 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
|
||||
};
|
119
app/main/autoupdater.ts
Normal file
119
app/main/autoupdater.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import {shell} from "electron/common";
|
||||
import {app, dialog, session} from "electron/main";
|
||||
import process from "node:process";
|
||||
|
||||
import log from "electron-log";
|
||||
import type {UpdateDownloadedEvent, UpdateInfo} from "electron-updater";
|
||||
import {autoUpdater} from "electron-updater";
|
||||
|
||||
import * as ConfigUtil from "../common/config-util.js";
|
||||
|
||||
import {linuxUpdateNotification} from "./linuxupdater.js"; // Required only in case of linux
|
||||
|
||||
let quitting = false;
|
||||
|
||||
export function shouldQuitForUpdate(): boolean {
|
||||
return quitting;
|
||||
}
|
||||
|
||||
export async function appUpdater(updateFromMenu = false): Promise<void> {
|
||||
// Don't initiate auto-updates in development
|
||||
if (!app.isPackaged) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.platform === "linux" && !process.env.APPIMAGE) {
|
||||
const ses = session.fromPartition("persist:webviewsession");
|
||||
await linuxUpdateNotification(ses);
|
||||
return;
|
||||
}
|
||||
|
||||
let updateAvailable = false;
|
||||
|
||||
// Log what's happening
|
||||
log.transports.file.fileName = "updates.log";
|
||||
log.transports.file.level = "info";
|
||||
autoUpdater.logger = log;
|
||||
|
||||
// Handle auto updates for beta/pre releases
|
||||
const isBetaUpdate = ConfigUtil.getConfigItem("betaUpdate", false);
|
||||
|
||||
autoUpdater.allowPrerelease = isBetaUpdate;
|
||||
|
||||
const eventsListenerRemove = [
|
||||
"update-available",
|
||||
"update-not-available",
|
||||
] as const;
|
||||
autoUpdater.on("update-available", async (info: UpdateInfo) => {
|
||||
if (updateFromMenu) {
|
||||
updateAvailable = true;
|
||||
|
||||
// This is to prevent removal of 'update-downloaded' and 'error' event listener.
|
||||
for (const event of eventsListenerRemove) {
|
||||
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}
|
||||
|
||||
The latest version of Zulip Desktop is available at -
|
||||
https://zulip.com/apps/.
|
||||
Current Version: ${app.getVersion()}`,
|
||||
});
|
||||
if (response === 0) {
|
||||
await shell.openExternal("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) {
|
||||
quitting = true;
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
});
|
||||
// Init for updates
|
||||
await autoUpdater.checkForUpdates();
|
||||
}
|
61
app/main/badge-settings.ts
Normal file
61
app/main/badge-settings.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {nativeImage} from "electron/common";
|
||||
import type {BrowserWindow} from "electron/main";
|
||||
import {app} from "electron/main";
|
||||
import process from "node:process";
|
||||
|
||||
import * as ConfigUtil from "../common/config-util.js";
|
||||
|
||||
import {send} from "./typed-ipc-main.js";
|
||||
|
||||
function showBadgeCount(messageCount: number, mainWindow: BrowserWindow): void {
|
||||
if (process.platform === "win32") {
|
||||
updateOverlayIcon(messageCount, mainWindow);
|
||||
} else {
|
||||
app.badgeCount = messageCount;
|
||||
}
|
||||
}
|
||||
|
||||
function hideBadgeCount(mainWindow: BrowserWindow): void {
|
||||
if (process.platform === "win32") {
|
||||
mainWindow.setOverlayIcon(null, "");
|
||||
} else {
|
||||
app.badgeCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateBadge(
|
||||
badgeCount: number,
|
||||
mainWindow: BrowserWindow,
|
||||
): void {
|
||||
if (ConfigUtil.getConfigItem("badgeOption", true)) {
|
||||
showBadgeCount(badgeCount, mainWindow);
|
||||
} else {
|
||||
hideBadgeCount(mainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
function updateOverlayIcon(
|
||||
messageCount: number,
|
||||
mainWindow: BrowserWindow,
|
||||
): void {
|
||||
if (!mainWindow.isFocused()) {
|
||||
mainWindow.flashFrame(
|
||||
ConfigUtil.getConfigItem("flashTaskbarOnMessage", true),
|
||||
);
|
||||
}
|
||||
|
||||
if (messageCount === 0) {
|
||||
mainWindow.setOverlayIcon(null, "");
|
||||
} else {
|
||||
send(mainWindow.webContents, "render-taskbar-icon", messageCount);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTaskbarIcon(
|
||||
data: string,
|
||||
text: string,
|
||||
mainWindow: BrowserWindow,
|
||||
): void {
|
||||
const img = nativeImage.createFromDataURL(data);
|
||||
mainWindow.setOverlayIcon(img, text);
|
||||
}
|
@@ -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
|
||||
};
|
163
app/main/handle-external-link.ts
Normal file
163
app/main/handle-external-link.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import {shell} from "electron/common";
|
||||
import type {
|
||||
HandlerDetails,
|
||||
SaveDialogOptions,
|
||||
WebContents,
|
||||
} from "electron/main";
|
||||
import {Notification, app} from "electron/main";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import * as ConfigUtil from "../common/config-util.js";
|
||||
import * as LinkUtil from "../common/link-util.js";
|
||||
|
||||
import {send} from "./typed-ipc-main.js";
|
||||
|
||||
function isUploadsUrl(server: string, url: URL): boolean {
|
||||
return url.origin === server && url.pathname.startsWith("/user_uploads/");
|
||||
}
|
||||
|
||||
function downloadFile({
|
||||
contents,
|
||||
url,
|
||||
downloadPath,
|
||||
completed,
|
||||
failed,
|
||||
}: {
|
||||
contents: WebContents;
|
||||
url: string;
|
||||
downloadPath: string;
|
||||
completed(filePath: string, fileName: string): Promise<void>;
|
||||
failed(state: string): void;
|
||||
}) {
|
||||
contents.downloadURL(url);
|
||||
contents.session.once("will-download", async (_event: Event, item) => {
|
||||
if (ConfigUtil.getConfigItem("promptDownload", false)) {
|
||||
const showDialogOptions: 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", async (_event: Event, state) => {
|
||||
if (state === "completed") {
|
||||
await completed(item.getSavePath(), path.basename(item.getSavePath()));
|
||||
} else {
|
||||
console.log("Download failed state:", state);
|
||||
failed(state);
|
||||
}
|
||||
|
||||
// To stop item for listening to updated events of this file
|
||||
item.removeListener("updated", updatedListener);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default function handleExternalLink(
|
||||
contents: WebContents,
|
||||
details: HandlerDetails,
|
||||
mainContents: WebContents,
|
||||
): void {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(details.url);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadPath = ConfigUtil.getConfigItem(
|
||||
"downloadsPath",
|
||||
`${app.getPath("downloads")}`,
|
||||
);
|
||||
|
||||
if (isUploadsUrl(new URL(contents.getURL()).origin, url)) {
|
||||
downloadFile({
|
||||
contents,
|
||||
url: url.href,
|
||||
downloadPath,
|
||||
async completed(filePath: string, fileName: string) {
|
||||
const downloadNotification = new Notification({
|
||||
title: "Download Complete",
|
||||
body: `Click to show ${fileName} in folder`,
|
||||
silent: true, // We'll play our own sound - ding.ogg
|
||||
});
|
||||
downloadNotification.on("click", () => {
|
||||
// Reveal file in download folder
|
||||
shell.showItemInFolder(filePath);
|
||||
});
|
||||
downloadNotification.show();
|
||||
|
||||
// Play sound to indicate download complete
|
||||
if (!ConfigUtil.getConfigItem("silent", false)) {
|
||||
send(mainContents, "play-ding-sound");
|
||||
}
|
||||
},
|
||||
failed(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)) {
|
||||
new Notification({
|
||||
title: "Download Complete",
|
||||
body: "Download failed",
|
||||
}).show();
|
||||
} else {
|
||||
contents.downloadURL(url.href);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
(async () => LinkUtil.openBrowser(url))();
|
||||
}
|
||||
}
|
@@ -1,238 +0,0 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const electron = require('electron');
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
const isDev = require('electron-is-dev');
|
||||
const appMenu = require('./menu');
|
||||
const { appUpdater } = require('./autoupdater');
|
||||
const { crashHandler } = require('./crash-reporter');
|
||||
|
||||
const { setAutoLaunch } = require('./startup');
|
||||
|
||||
const { app, ipcMain } = electron;
|
||||
|
||||
const BadgeSettings = require('./../renderer/js/pages/preference/badge-settings.js');
|
||||
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
|
||||
|
||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||
// in development mode
|
||||
if (isDev) {
|
||||
require('electron-debug')();
|
||||
}
|
||||
|
||||
// Prevent window being garbage collected
|
||||
let mainWindow;
|
||||
let 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: 300,
|
||||
minHeight: 400,
|
||||
webPreferences: {
|
||||
plugins: true,
|
||||
allowDisplayingInsecureContent: true,
|
||||
nodeIntegration: true
|
||||
},
|
||||
show: false
|
||||
});
|
||||
|
||||
win.on('focus', () => {
|
||||
win.webContents.send('focus');
|
||||
});
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
if (ConfigUtil.getConfigItem('startMinimized')) {
|
||||
win.minimize();
|
||||
} else {
|
||||
win.show();
|
||||
}
|
||||
});
|
||||
|
||||
win.loadURL(mainURL);
|
||||
|
||||
// Keep the app running in background on close event
|
||||
win.on('close', e => {
|
||||
if (!isQuitting) {
|
||||
e.preventDefault();
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
app.hide();
|
||||
} else {
|
||||
win.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
win.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;
|
||||
}
|
||||
|
||||
// Decrease load on GPU (experimental)
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (!mainWindow) {
|
||||
mainWindow = createMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('ready', () => {
|
||||
appMenu.setMenu({
|
||||
tabs: []
|
||||
});
|
||||
mainWindow = createMainWindow();
|
||||
|
||||
const page = mainWindow.webContents;
|
||||
|
||||
page.on('dom-ready', () => {
|
||||
if (ConfigUtil.getConfigItem('startMinimized')) {
|
||||
mainWindow.minimize();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
page.once('did-frame-finish-load', () => {
|
||||
// Initate auto-updates on MacOS and Windows
|
||||
appUpdater();
|
||||
crashHandler();
|
||||
});
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
// 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('toggleAutoLauncher', (event, AutoLaunchValue) => {
|
||||
setAutoLaunch(AutoLaunchValue);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
isQuitting = true;
|
||||
});
|
||||
|
||||
// Send crash reports
|
||||
process.on('uncaughtException', err => {
|
||||
console.error(err);
|
||||
console.error(err.stack);
|
||||
});
|
454
app/main/index.ts
Normal file
454
app/main/index.ts
Normal file
@@ -0,0 +1,454 @@
|
||||
import {clipboard} from "electron/common";
|
||||
import type {IpcMainEvent, WebContents} from "electron/main";
|
||||
import {BrowserWindow, app, dialog, powerMonitor, session} from "electron/main";
|
||||
import {Buffer} from "node:buffer";
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
import * as remoteMain from "@electron/remote/main";
|
||||
import windowStateKeeper from "electron-window-state";
|
||||
|
||||
import * as ConfigUtil from "../common/config-util.js";
|
||||
import {bundlePath, bundleUrl, publicPath} from "../common/paths.js";
|
||||
import type {RendererMessage} from "../common/typed-ipc.js";
|
||||
import type {MenuProps} from "../common/types.js";
|
||||
|
||||
import {appUpdater, shouldQuitForUpdate} from "./autoupdater.js";
|
||||
import * as BadgeSettings from "./badge-settings.js";
|
||||
import handleExternalLink from "./handle-external-link.js";
|
||||
import * as AppMenu from "./menu.js";
|
||||
import {_getServerSettings, _isOnline, _saveServerIcon} from "./request.js";
|
||||
import {sentryInit} from "./sentry.js";
|
||||
import {setAutoLaunch} from "./startup.js";
|
||||
import {ipcMain, send} from "./typed-ipc-main.js";
|
||||
|
||||
import "gatemaker/electron-setup"; // eslint-disable-line import/no-unassigned-import
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const {GDK_BACKEND} = process.env;
|
||||
|
||||
// Initialize sentry for main process
|
||||
sentryInit();
|
||||
|
||||
let mainWindowState: windowStateKeeper.State;
|
||||
|
||||
// Prevent window being garbage collected
|
||||
let mainWindow: BrowserWindow;
|
||||
let badgeCount: number;
|
||||
|
||||
let isQuitting = false;
|
||||
|
||||
// Load this file in main window
|
||||
const mainUrl = new URL("app/renderer/main.html", bundleUrl).href;
|
||||
|
||||
const permissionCallbacks = new Map<number, (grant: boolean) => void>();
|
||||
let nextPermissionCallbackId = 0;
|
||||
|
||||
const appIcon = path.join(publicPath, "resources/Icon");
|
||||
|
||||
const iconPath = (): string =>
|
||||
appIcon + (process.platform === "win32" ? ".ico" : ".png");
|
||||
|
||||
// Toggle the app window
|
||||
const toggleApp = (): void => {
|
||||
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
|
||||
mainWindow.show();
|
||||
} else {
|
||||
mainWindow.hide();
|
||||
}
|
||||
};
|
||||
|
||||
function createMainWindow(): BrowserWindow {
|
||||
// Load the previous state with fallback to defaults
|
||||
mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1100,
|
||||
defaultHeight: 720,
|
||||
path: `${app.getPath("userData")}/config`,
|
||||
});
|
||||
|
||||
const win = new 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: {
|
||||
preload: path.join(bundlePath, "renderer.js"),
|
||||
sandbox: false,
|
||||
webviewTag: true,
|
||||
},
|
||||
show: false,
|
||||
});
|
||||
remoteMain.enable(win.webContents);
|
||||
|
||||
win.on("focus", () => {
|
||||
send(win.webContents, "focus");
|
||||
});
|
||||
|
||||
(async () => win.loadURL(mainUrl))();
|
||||
|
||||
// Keep the app running in background on close event
|
||||
win.on("close", (event) => {
|
||||
if (ConfigUtil.getConfigItem("quitOnClose", false)) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
if (!isQuitting && !shouldQuitForUpdate()) {
|
||||
event.preventDefault();
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
app.hide();
|
||||
} else {
|
||||
win.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
win.setTitle("Zulip");
|
||||
|
||||
win.on("enter-full-screen", () => {
|
||||
send(win.webContents, "enter-fullscreen");
|
||||
});
|
||||
|
||||
win.on("leave-full-screen", () => {
|
||||
send(win.webContents, "leave-fullscreen");
|
||||
});
|
||||
|
||||
// To destroy tray icon when navigate to a new URL
|
||||
win.webContents.on("will-navigate", (event) => {
|
||||
if (event) {
|
||||
send(win.webContents, "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;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
await app.whenReady();
|
||||
|
||||
if (process.env.GDK_BACKEND !== GDK_BACKEND) {
|
||||
console.warn(
|
||||
"Reverting GDK_BACKEND to work around https://github.com/electron/electron/issues/28436",
|
||||
);
|
||||
if (GDK_BACKEND === undefined) {
|
||||
delete process.env.GDK_BACKEND;
|
||||
} else {
|
||||
process.env.GDK_BACKEND = GDK_BACKEND;
|
||||
}
|
||||
}
|
||||
|
||||
// Used for notifications on Windows
|
||||
app.setAppUserModelId("org.zulip.zulip-electron");
|
||||
|
||||
remoteMain.initialize();
|
||||
|
||||
app.on("second-instance", () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(
|
||||
"permission-callback",
|
||||
(event: Event, permissionCallbackId: number, grant: boolean) => {
|
||||
permissionCallbacks.get(permissionCallbackId)?.(grant);
|
||||
permissionCallbacks.delete(permissionCallbackId);
|
||||
},
|
||||
);
|
||||
|
||||
// This event is only available on macOS. Triggers when you click on the dock icon.
|
||||
app.on("activate", () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
app.on("web-contents-created", (_event: Event, contents: WebContents) => {
|
||||
contents.setWindowOpenHandler((details) => {
|
||||
handleExternalLink(contents, details, page);
|
||||
return {action: "deny"};
|
||||
});
|
||||
});
|
||||
|
||||
const ses = session.fromPartition("persist:webviewsession");
|
||||
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);
|
||||
|
||||
function configureSpellChecker() {
|
||||
const enable = ConfigUtil.getConfigItem("enableSpellchecker", true);
|
||||
if (enable && process.platform !== "darwin") {
|
||||
ses.setSpellCheckerLanguages(
|
||||
ConfigUtil.getConfigItem("spellcheckerLanguages", null) ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
ses.setSpellCheckerEnabled(enable);
|
||||
}
|
||||
|
||||
configureSpellChecker();
|
||||
ipcMain.on("configure-spell-checker", configureSpellChecker);
|
||||
|
||||
const clipboardSigKey = crypto.randomBytes(32);
|
||||
|
||||
ipcMain.on("new-clipboard-key", (event) => {
|
||||
const key = crypto.randomBytes(32);
|
||||
const hmac = crypto.createHmac("sha256", clipboardSigKey);
|
||||
hmac.update(key);
|
||||
event.returnValue = {key, sig: hmac.digest()};
|
||||
});
|
||||
|
||||
ipcMain.handle("poll-clipboard", (event, key, sig) => {
|
||||
// Check that the key was generated here.
|
||||
const hmac = crypto.createHmac("sha256", clipboardSigKey);
|
||||
hmac.update(key);
|
||||
if (!crypto.timingSafeEqual(sig, hmac.digest())) return;
|
||||
|
||||
try {
|
||||
// Check that the data on the clipboard was encrypted to the key.
|
||||
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", key, iv, {
|
||||
authTagLength: 16,
|
||||
});
|
||||
decipher.setAuthTag(authTag);
|
||||
return (
|
||||
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 undefined;
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const page = mainWindow.webContents;
|
||||
|
||||
page.on("dom-ready", () => {
|
||||
if (ConfigUtil.getConfigItem("startMinimized", false)) {
|
||||
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", true)) {
|
||||
await appUpdater();
|
||||
}
|
||||
});
|
||||
|
||||
app.on(
|
||||
"certificate-error",
|
||||
(
|
||||
event: Event,
|
||||
webContents: WebContents,
|
||||
urlString: string,
|
||||
error: string,
|
||||
) => {
|
||||
const url = new URL(urlString);
|
||||
dialog.showErrorBox(
|
||||
"Certificate error",
|
||||
`The server presented an invalid certificate for ${url.origin}:
|
||||
|
||||
${error}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ses.setPermissionRequestHandler(
|
||||
(webContents, permission, callback, details) => {
|
||||
const {origin} = new URL(details.requestingUrl);
|
||||
const permissionCallbackId = nextPermissionCallbackId++;
|
||||
permissionCallbacks.set(permissionCallbackId, callback);
|
||||
send(
|
||||
page,
|
||||
"permission-request",
|
||||
{
|
||||
webContentsId:
|
||||
webContents.id === mainWindow.webContents.id
|
||||
? null
|
||||
: webContents.id,
|
||||
origin,
|
||||
permission,
|
||||
},
|
||||
permissionCallbackId,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Temporarily remove this event
|
||||
// powerMonitor.on('resume', () => {
|
||||
// mainWindow.reload();
|
||||
// send(page, 'destroytray');
|
||||
// });
|
||||
|
||||
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();
|
||||
send(page, "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: IpcMainEvent, showMenubar: boolean) => {
|
||||
mainWindow.autoHideMenuBar = showMenubar;
|
||||
mainWindow.setMenuBarVisibility(!showMenubar);
|
||||
send(page, "toggle-autohide-menubar", showMenubar, true);
|
||||
});
|
||||
|
||||
ipcMain.on("update-badge", (_event: IpcMainEvent, messageCount: number) => {
|
||||
badgeCount = messageCount;
|
||||
BadgeSettings.updateBadge(badgeCount, mainWindow);
|
||||
send(page, "tray", messageCount);
|
||||
});
|
||||
|
||||
ipcMain.on(
|
||||
"update-taskbar-icon",
|
||||
(_event: IpcMainEvent, data: string, text: string) => {
|
||||
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
"forward-message",
|
||||
<Channel extends keyof RendererMessage>(
|
||||
_event: IpcMainEvent,
|
||||
listener: Channel,
|
||||
...parameters: Parameters<RendererMessage[Channel]>
|
||||
) => {
|
||||
send(page, listener, ...parameters);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on("update-menu", (_event: IpcMainEvent, props: MenuProps) => {
|
||||
AppMenu.setMenu(props);
|
||||
if (props.activeTabIndex !== undefined) {
|
||||
const activeTab = props.tabs[props.activeTabIndex];
|
||||
mainWindow.setTitle(`Zulip - ${activeTab.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(
|
||||
"toggleAutoLauncher",
|
||||
async (_event: IpcMainEvent, AutoLaunchValue: boolean) => {
|
||||
await setAutoLaunch(AutoLaunchValue);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
"realm-name-changed",
|
||||
(_event: IpcMainEvent, serverURL: string, realmName: string) => {
|
||||
send(page, "update-realm-name", serverURL, realmName);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
"realm-icon-changed",
|
||||
(_event: IpcMainEvent, serverURL: string, iconURL: string) => {
|
||||
send(page, "update-realm-icon", serverURL, iconURL);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on("save-last-tab", (_event: IpcMainEvent, index: number) => {
|
||||
ConfigUtil.setConfigItem("lastActiveTab", index);
|
||||
});
|
||||
|
||||
ipcMain.on("focus-this-webview", (event: IpcMainEvent) => {
|
||||
send(page, "focus-webview-with-id", event.sender.id);
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
// 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 = powerMonitor.getSystemIdleState(idleThresholdSeconds);
|
||||
if (idleState === "active") {
|
||||
send(page, "set-active");
|
||||
} else {
|
||||
send(page, "set-idle");
|
||||
}
|
||||
}, idleCheckInterval);
|
||||
})();
|
||||
|
||||
app.on("before-quit", () => {
|
||||
isQuitting = true;
|
||||
});
|
||||
|
||||
// Send crash reports
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error(error);
|
||||
console.error(error.stack);
|
||||
});
|
69
app/main/linux-update-util.ts
Normal file
69
app/main/linux-update-util.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {app, dialog} from "electron/main";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import {JsonDB} from "node-json-db";
|
||||
import {DataError} from "node-json-db/dist/lib/Errors";
|
||||
|
||||
import Logger from "../common/logger-util.js";
|
||||
|
||||
const logger = new Logger({
|
||||
file: "linux-update-util.log",
|
||||
});
|
||||
|
||||
let db: JsonDB;
|
||||
|
||||
reloadDb();
|
||||
|
||||
export function getUpdateItem(
|
||||
key: string,
|
||||
defaultValue: true | null = null,
|
||||
): true | null {
|
||||
reloadDb();
|
||||
let value: unknown;
|
||||
try {
|
||||
value = db.getObject<unknown>(`/${key}`);
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof DataError)) throw error;
|
||||
}
|
||||
|
||||
if (value !== true && value !== null) {
|
||||
setUpdateItem(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function setUpdateItem(key: string, value: true | null): void {
|
||||
db.push(`/${key}`, value, true);
|
||||
reloadDb();
|
||||
}
|
||||
|
||||
export function removeUpdateItem(key: string): void {
|
||||
db.delete(`/${key}`);
|
||||
reloadDb();
|
||||
}
|
||||
|
||||
function reloadDb(): void {
|
||||
const linuxUpdateJsonPath = path.join(
|
||||
app.getPath("userData"),
|
||||
"/config/updates.json",
|
||||
);
|
||||
try {
|
||||
const file = fs.readFileSync(linuxUpdateJsonPath, "utf8");
|
||||
JSON.parse(file);
|
||||
} catch (error: unknown) {
|
||||
if (fs.existsSync(linuxUpdateJsonPath)) {
|
||||
fs.unlinkSync(linuxUpdateJsonPath);
|
||||
dialog.showErrorBox(
|
||||
"Error saving update notifications.",
|
||||
"We encountered an error while saving the update notifications.",
|
||||
);
|
||||
logger.error("Error while JSON parsing updates.json: ");
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
db = new JsonDB(linuxUpdateJsonPath, true, true);
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
const { app } = require('electron');
|
||||
const { Notification } = require('electron');
|
||||
|
||||
const request = require('request');
|
||||
const semver = require('semver');
|
||||
const ConfigUtil = require('../renderer/js/utils/config-util');
|
||||
const LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util');
|
||||
|
||||
function linuxUpdateNotification() {
|
||||
let url = 'https://api.github.com/repos/zulip/zulip-electron/releases';
|
||||
url = ConfigUtil.getConfigItem('betaUpdate') ? url : url + '/latest';
|
||||
|
||||
const options = {
|
||||
url,
|
||||
headers: {'User-Agent': 'request'}
|
||||
};
|
||||
|
||||
request(options, (error, response, body) => {
|
||||
if (error) {
|
||||
console.log('Error:', error);
|
||||
return;
|
||||
}
|
||||
if (response.statusCode < 400) {
|
||||
const data = JSON.parse(body);
|
||||
const latestVersion = ConfigUtil.getConfigItem('betaUpdate') ? data[0].tag_name : data.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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('Status:', response.statusCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
linuxUpdateNotification
|
||||
};
|
50
app/main/linuxupdater.ts
Normal file
50
app/main/linuxupdater.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type {Session} from "electron/main";
|
||||
import {Notification, app, net} from "electron/main";
|
||||
|
||||
import getStream from "get-stream";
|
||||
import * as semver from "semver";
|
||||
import {z} from "zod";
|
||||
|
||||
import * as ConfigUtil from "../common/config-util.js";
|
||||
import Logger from "../common/logger-util.js";
|
||||
|
||||
import * as LinuxUpdateUtil from "./linux-update-util.js";
|
||||
import {fetchResponse} from "./request.js";
|
||||
|
||||
const logger = new Logger({
|
||||
file: "linux-update-util.log",
|
||||
});
|
||||
|
||||
export async function linuxUpdateNotification(session: Session): Promise<void> {
|
||||
let url = "https://api.github.com/repos/zulip/zulip-desktop/releases";
|
||||
url = ConfigUtil.getConfigItem("betaUpdate", false) ? 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: unknown = JSON.parse(await getStream(response));
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const latestVersion = ConfigUtil.getConfigItem("betaUpdate", false)
|
||||
? z.array(z.object({tag_name: z.string()})).parse(data)[0].tag_name
|
||||
: z.object({tag_name: z.string()}).parse(data).tag_name;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
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: unknown) {
|
||||
logger.error("Linux update error.");
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
415
app/main/menu.js
415
app/main/menu.js
@@ -1,415 +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: process.platform === 'darwin' ? 'Command+Plus' : 'Control+=',
|
||||
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 + ' Desktop-'} v${app.getVersion()}`,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: `What's New...`,
|
||||
click() {
|
||||
shell.openExternal(`https://github.com/zulip/zulip-electron/releases/tag/v${app.getVersion()}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: `${appName} Help`,
|
||||
click() {
|
||||
shell.openExternal('https://zulipchat.com/help/');
|
||||
}
|
||||
}, {
|
||||
label: 'Show App Logs',
|
||||
click() {
|
||||
shell.openItem(app.getPath('userData'));
|
||||
}
|
||||
}, {
|
||||
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++) {
|
||||
// Do not add functional tab settings to list of windows in menu bar
|
||||
if (tabs[i].props.role === 'function' && tabs[i].webview.props.name === 'Settings') {
|
||||
continue;
|
||||
}
|
||||
|
||||
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: 'checkbox'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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: 'Desktop App Settings',
|
||||
accelerator: 'Cmd+,',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('open-settings');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Keyboard Shortcuts',
|
||||
accelerator: 'Cmd+Shift+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: 'Desktop App Settings',
|
||||
accelerator: 'Ctrl+,',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('open-settings');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Keyboard Shortcuts',
|
||||
accelerator: 'Ctrl+Shift+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() {
|
||||
// We save App's settings/configurations in following files
|
||||
const settingFiles = ['window-state.json', 'domain.json', 'settings.json'];
|
||||
|
||||
settingFiles.forEach(settingFileName => {
|
||||
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
|
||||
fs.access(getSettingFilesPath, error => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
} else {
|
||||
fs.unlink(getSettingFilesPath, () => {
|
||||
AppMenu.sendAction('clear-app-data');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setMenu(props) {
|
||||
const tpl = process.platform === 'darwin' ? this.getDarwinTpl(props) : this.getOtherTpl(props);
|
||||
const menu = Menu.buildFromTemplate(tpl);
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AppMenu();
|
722
app/main/menu.ts
Normal file
722
app/main/menu.ts
Normal file
@@ -0,0 +1,722 @@
|
||||
import {shell} from "electron/common";
|
||||
import type {MenuItemConstructorOptions} from "electron/main";
|
||||
import {BrowserWindow, Menu, app} from "electron/main";
|
||||
import process from "node:process";
|
||||
|
||||
import AdmZip from "adm-zip";
|
||||
|
||||
import * as ConfigUtil from "../common/config-util.js";
|
||||
import * as DNDUtil from "../common/dnd-util.js";
|
||||
import * as t from "../common/translation-util.js";
|
||||
import type {RendererMessage} from "../common/typed-ipc.js";
|
||||
import type {MenuProps, TabData} from "../common/types.js";
|
||||
|
||||
import {appUpdater} from "./autoupdater.js";
|
||||
import {send} from "./typed-ipc-main.js";
|
||||
|
||||
const appName = app.name;
|
||||
|
||||
function getHistorySubmenu(enableMenu: boolean): 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(): MenuItemConstructorOptions[] {
|
||||
return [
|
||||
{
|
||||
label: t.__("Check for Updates"),
|
||||
async click() {
|
||||
await checkForUpdate();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t.__("Release Notes"),
|
||||
async click() {
|
||||
await shell.openExternal(
|
||||
`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(): 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");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t.__("Hard Reload"),
|
||||
visible: false,
|
||||
accelerator: "F5",
|
||||
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 In"),
|
||||
visible: false,
|
||||
accelerator: "CommandOrControl+Plus",
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction("zoomIn");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t.__("Zoom In"),
|
||||
visible: false,
|
||||
accelerator: "CommandOrControl+numadd",
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction("zoomIn");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t.__("Zoom Out"),
|
||||
accelerator: "CommandOrControl+-",
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction("zoomOut");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t.__("Zoom Out"),
|
||||
visible: false,
|
||||
accelerator: "CommandOrControl+numsub",
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction("zoomOut");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t.__("Actual Size"),
|
||||
accelerator: "CommandOrControl+0",
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction("zoomActualSize");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t.__("Actual Size"),
|
||||
visible: false,
|
||||
accelerator: "CommandOrControl+num0",
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction("zoomActualSize");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: t.__("Toggle Tray Icon"),
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
send(focusedWindow.webContents, "toggletray");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t.__("Toggle Sidebar"),
|
||||
accelerator: "CommandOrControl+Shift+S",
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
const newValue = !ConfigUtil.getConfigItem("showSidebar", true);
|
||||
send(focusedWindow.webContents, "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", false);
|
||||
focusedWindow.autoHideMenuBar = newValue;
|
||||
focusedWindow.setMenuBarVisibility(!newValue);
|
||||
send(
|
||||
focusedWindow.webContents,
|
||||
"toggle-autohide-menubar",
|
||||
newValue,
|
||||
false,
|
||||
);
|
||||
ConfigUtil.setConfigItem("autoHideMenubar", newValue);
|
||||
}
|
||||
},
|
||||
type: "checkbox",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getHelpSubmenu(): 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"),
|
||||
async click() {
|
||||
await shell.openExternal("https://zulip.com/help/contact-support");
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getWindowSubmenu(
|
||||
tabs: TabData[],
|
||||
activeTabIndex?: number,
|
||||
): MenuItemConstructorOptions[] {
|
||||
const initialSubmenu: 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",
|
||||
});
|
||||
for (const tab of tabs) {
|
||||
// Skip missing elements left by `delete this.tabs[index]` in
|
||||
// ServerManagerView.
|
||||
if (tab === undefined) continue;
|
||||
|
||||
// Do not add functional tab settings to list of windows in menu bar
|
||||
if (tab.role === "function" && tab.name === "Settings") {
|
||||
continue;
|
||||
}
|
||||
|
||||
initialSubmenu.push({
|
||||
label: tab.name,
|
||||
accelerator:
|
||||
tab.role === "function" ? "" : `${shortcutKey} + ${tab.index + 1}`,
|
||||
checked: tab.index === activeTabIndex,
|
||||
click(_item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
sendAction("switch-server-tab", tab.index);
|
||||
}
|
||||
},
|
||||
type: "checkbox",
|
||||
});
|
||||
}
|
||||
|
||||
initialSubmenu.push(
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
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): MenuItemConstructorOptions[] {
|
||||
const {tabs, activeTabIndex, enableMenu = false} = 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("show-keyboard-shortcuts");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
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"),
|
||||
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"),
|
||||
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",
|
||||
},
|
||||
{
|
||||
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): MenuItemConstructorOptions[] {
|
||||
const {tabs, activeTabIndex, enableMenu = false} = 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("show-keyboard-shortcuts");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
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"),
|
||||
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<Channel extends keyof RendererMessage>(
|
||||
channel: Channel,
|
||||
...args: Parameters<RendererMessage[Channel]>
|
||||
): void {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
win.restore();
|
||||
}
|
||||
|
||||
send(win.webContents, channel, ...args);
|
||||
}
|
||||
|
||||
async function checkForUpdate(): Promise<void> {
|
||||
await appUpdater(true);
|
||||
}
|
||||
|
||||
function getNextServer(tabs: TabData[], activeTabIndex: number): number {
|
||||
do {
|
||||
activeTabIndex = (activeTabIndex + 1) % tabs.length;
|
||||
} while (tabs[activeTabIndex]?.role !== "server");
|
||||
|
||||
return activeTabIndex;
|
||||
}
|
||||
|
||||
function getPreviousServer(tabs: TabData[], activeTabIndex: number): number {
|
||||
do {
|
||||
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
|
||||
} while (tabs[activeTabIndex]?.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);
|
||||
}
|
145
app/main/request.ts
Normal file
145
app/main/request.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type {ClientRequest, IncomingMessage, Session} from "electron/main";
|
||||
import {app, net} from "electron/main";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import stream from "node:stream";
|
||||
import util from "node:util";
|
||||
|
||||
import * as Sentry from "@sentry/electron";
|
||||
import getStream from "get-stream";
|
||||
import {z} from "zod";
|
||||
|
||||
import Logger from "../common/logger-util.js";
|
||||
import * as Messages from "../common/messages.js";
|
||||
import type {ServerConf} from "../common/types.js";
|
||||
|
||||
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 logger = new Logger({
|
||||
file: "domain-util.log",
|
||||
});
|
||||
|
||||
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) {
|
||||
// eslint-disable-next-line no-bitwise, unicorn/prefer-code-point
|
||||
hash = (hash * 33) ^ url.charCodeAt(--length);
|
||||
}
|
||||
|
||||
// Create 'server-icons' directory if not existed
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return `${dir}/${hash >>> 0}${extension}`;
|
||||
};
|
||||
|
||||
export const _getServerSettings = async (
|
||||
domain: string,
|
||||
session: 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 data: unknown = JSON.parse(await getStream(response));
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const {
|
||||
realm_name,
|
||||
realm_uri,
|
||||
realm_icon,
|
||||
zulip_version,
|
||||
zulip_feature_level,
|
||||
} = z
|
||||
.object({
|
||||
realm_name: z.string(),
|
||||
realm_uri: z.string().url(),
|
||||
realm_icon: z.string(),
|
||||
zulip_version: z.string().default("unknown"),
|
||||
zulip_feature_level: z.number().default(0),
|
||||
})
|
||||
.parse(data);
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
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: realm_name,
|
||||
zulipVersion: zulip_version,
|
||||
zulipFeatureLevel: zulip_feature_level,
|
||||
};
|
||||
};
|
||||
|
||||
export const _saveServerIcon = async (
|
||||
url: string,
|
||||
session: Session,
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const response = await fetchResponse(net.request({url, session}));
|
||||
if (response.statusCode !== 200) {
|
||||
logger.log("Could not get server icon.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const filePath = generateFilePath(url);
|
||||
await pipeline(response, fs.createWriteStream(filePath));
|
||||
return filePath;
|
||||
} catch (error: unknown) {
|
||||
logger.log("Could not get server icon.");
|
||||
logger.log(error);
|
||||
Sentry.captureException(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/* Request: reconnect-util */
|
||||
|
||||
export const _isOnline = async (
|
||||
url: string,
|
||||
session: Session,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetchResponse(
|
||||
net.request({
|
||||
method: "HEAD",
|
||||
url: `${url}/api/v1/server_settings`,
|
||||
session,
|
||||
}),
|
||||
);
|
||||
const isValidResponse =
|
||||
response.statusCode >= 200 && response.statusCode < 400;
|
||||
return isValidResponse;
|
||||
} catch (error: unknown) {
|
||||
logger.log(error);
|
||||
return false;
|
||||
}
|
||||
};
|
22
app/main/sentry.ts
Normal file
22
app/main/sentry.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {app} from "electron/main";
|
||||
|
||||
import * as Sentry from "@sentry/electron/main"; // eslint-disable-line n/file-extension-in-import
|
||||
|
||||
import {getConfigItem} from "../common/config-util.js";
|
||||
|
||||
export const sentryInit = (): void => {
|
||||
Sentry.init({
|
||||
dsn: "https://628dc2f2864243a08ead72e63f94c7b1@o48127.ingest.sentry.io/204668",
|
||||
|
||||
// Don't report errors in development or if disabled by the user.
|
||||
beforeSend: (event) =>
|
||||
app.isPackaged && getConfigItem("errorReporting", true) ? event : null,
|
||||
|
||||
// We should ignore this error since it's harmless and we know the reason behind this
|
||||
// This error mainly comes from the console logs.
|
||||
// This is a temp solution until Sentry supports disabling the console logs
|
||||
ignoreErrors: ["does not appear to be a valid Zulip server"],
|
||||
|
||||
/// sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second
|
||||
});
|
||||
};
|
@@ -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
|
||||
};
|
36
app/main/startup.ts
Normal file
36
app/main/startup.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {app} from "electron/main";
|
||||
import process from "node:process";
|
||||
|
||||
import AutoLaunch from "auto-launch";
|
||||
|
||||
import * as ConfigUtil from "../common/config-util.js";
|
||||
|
||||
export const setAutoLaunch = async (
|
||||
AutoLaunchValue: boolean,
|
||||
): Promise<void> => {
|
||||
// Don't run this in development
|
||||
if (!app.isPackaged) {
|
||||
return;
|
||||
}
|
||||
|
||||
const autoLaunchOption = ConfigUtil.getConfigItem(
|
||||
"startAtLogin",
|
||||
AutoLaunchValue,
|
||||
);
|
||||
|
||||
// `setLoginItemSettings` doesn't support linux
|
||||
if (process.platform === "linux") {
|
||||
const zulipAutoLauncher = new AutoLaunch({
|
||||
name: "Zulip",
|
||||
isHidden: false,
|
||||
});
|
||||
await (autoLaunchOption
|
||||
? zulipAutoLauncher.enable()
|
||||
: zulipAutoLauncher.disable());
|
||||
} else {
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: autoLaunchOption,
|
||||
openAsHidden: false,
|
||||
});
|
||||
}
|
||||
};
|
75
app/main/typed-ipc-main.ts
Normal file
75
app/main/typed-ipc-main.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type {
|
||||
IpcMainEvent,
|
||||
IpcMainInvokeEvent,
|
||||
WebContents,
|
||||
} from "electron/main";
|
||||
import {
|
||||
ipcMain as untypedIpcMain, // eslint-disable-line no-restricted-imports
|
||||
} from "electron/main";
|
||||
|
||||
import type {
|
||||
MainCall,
|
||||
MainMessage,
|
||||
RendererMessage,
|
||||
} from "../common/typed-ipc.js";
|
||||
|
||||
type MainListener<Channel extends keyof MainMessage> =
|
||||
MainMessage[Channel] extends (...args: infer Args) => infer Return
|
||||
? (event: IpcMainEvent & {returnValue: Return}, ...args: Args) => void
|
||||
: never;
|
||||
|
||||
type MainHandler<Channel extends keyof MainCall> = MainCall[Channel] extends (
|
||||
...args: infer Args
|
||||
) => infer Return
|
||||
? (event: IpcMainInvokeEvent, ...args: Args) => Return | Promise<Return>
|
||||
: never;
|
||||
|
||||
export const ipcMain: {
|
||||
on(
|
||||
channel: "forward-message",
|
||||
listener: <Channel extends keyof RendererMessage>(
|
||||
event: IpcMainEvent,
|
||||
channel: Channel,
|
||||
...args: Parameters<RendererMessage[Channel]>
|
||||
) => void,
|
||||
): void;
|
||||
on<Channel extends keyof MainMessage>(
|
||||
channel: Channel,
|
||||
listener: MainListener<Channel>,
|
||||
): void;
|
||||
once<Channel extends keyof MainMessage>(
|
||||
channel: Channel,
|
||||
listener: MainListener<Channel>,
|
||||
): void;
|
||||
removeListener<Channel extends keyof MainMessage>(
|
||||
channel: Channel,
|
||||
listener: MainListener<Channel>,
|
||||
): void;
|
||||
removeAllListeners(channel?: keyof MainMessage): void;
|
||||
handle<Channel extends keyof MainCall>(
|
||||
channel: Channel,
|
||||
handler: MainHandler<Channel>,
|
||||
): void;
|
||||
handleOnce<Channel extends keyof MainCall>(
|
||||
channel: Channel,
|
||||
handler: MainHandler<Channel>,
|
||||
): void;
|
||||
removeHandler(channel: keyof MainCall): void;
|
||||
} = untypedIpcMain;
|
||||
|
||||
export function send<Channel extends keyof RendererMessage>(
|
||||
contents: WebContents,
|
||||
channel: Channel,
|
||||
...args: Parameters<RendererMessage[Channel]>
|
||||
): void {
|
||||
contents.send(channel, ...args);
|
||||
}
|
||||
|
||||
export function sendToFrame<Channel extends keyof RendererMessage>(
|
||||
contents: WebContents,
|
||||
frameId: number | [number, number],
|
||||
channel: Channel,
|
||||
...args: Parameters<RendererMessage[Channel]>
|
||||
): void {
|
||||
contents.sendToFrame(frameId, channel, ...args);
|
||||
}
|
1380
app/package-lock.json
generated
1380
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "zulip",
|
||||
"productName": "Zulip",
|
||||
"version": "1.8.2",
|
||||
"description": "Zulip Desktop App",
|
||||
"license": "Apache-2.0",
|
||||
"copyright": "Kandra Labs, Inc.",
|
||||
"author": {
|
||||
"name": "Kandra Labs, Inc.",
|
||||
"email": "support@zulipchat.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zulip/zulip-electron.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/zulip/zulip-electron/issues"
|
||||
},
|
||||
"main": "main/index.js",
|
||||
"keywords": [
|
||||
"Zulip",
|
||||
"Group Chat app",
|
||||
"electron-app",
|
||||
"electron",
|
||||
"Desktop app",
|
||||
"InstantMessaging"
|
||||
],
|
||||
"dependencies": {
|
||||
"auto-launch": "5.0.1",
|
||||
"electron-is-dev": "0.3.0",
|
||||
"electron-log": "2.2.7",
|
||||
"electron-spellchecker": "1.1.2",
|
||||
"electron-updater": "2.18.2",
|
||||
"electron-window-state": "4.1.1",
|
||||
"is-online": "7.0.0",
|
||||
"node-json-db": "0.7.3",
|
||||
"request": "2.81.0",
|
||||
"semver": "5.4.1",
|
||||
"wurl": "2.5.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-mac-notifier": "0.0.13"
|
||||
}
|
||||
}
|
@@ -1,45 +1,26 @@
|
||||
<!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">v?.?.?</p>
|
||||
<div class="maintenance-info">
|
||||
<p class="detail maintainer">
|
||||
Maintained by <a onclick="linkInBrowser('website')">Zulip</a>
|
||||
</p>
|
||||
<p class="detail license">
|
||||
Available under the <a onclick="linkInBrowser('license')">Apache 2.0 License</a>
|
||||
</p>
|
||||
<a class="bug" onclick="linkInBrowser('bug')" href="#">Found bug?</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="css/about.css" />
|
||||
|
||||
const { app } = require('electron').remote;
|
||||
const { shell } = require('electron');
|
||||
const version_tag = document.querySelector('#version');
|
||||
version_tag.innerHTML = 'v' + app.getVersion();
|
||||
|
||||
function linkInBrowser(type) {
|
||||
let url;
|
||||
switch (type) {
|
||||
case 'website':
|
||||
url = "https://zulipchat.com";
|
||||
break;
|
||||
case 'license':
|
||||
url = "https://github.com/zulip/zulip-electron/blob/master/LICENSE";
|
||||
break;
|
||||
default:
|
||||
url = 'https://github.com/zulip/zulip-electron/issues/new?body=' +
|
||||
'%3C!--Please%20describe%20your%20issue%20and%20steps%20to%20reproduce%20it.--%3E';
|
||||
}
|
||||
shell.openExternal(url);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!-- Initially hidden to prevent FOUC -->
|
||||
<div class="about" hidden>
|
||||
<img class="logo" src="../resources/zulip.png" />
|
||||
<p class="detail" id="version"></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/main/LICENSE"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Apache 2.0 License</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,82 +1,69 @@
|
||||
body {
|
||||
background: #fafafa;
|
||||
font-family: menu, "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
:host {
|
||||
contain: strict;
|
||||
display: flow-root;
|
||||
background: rgb(250 250 250 / 100%);
|
||||
font-family: menu, "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: -40px auto;
|
||||
display: block;
|
||||
margin: -40px auto;
|
||||
}
|
||||
|
||||
#version {
|
||||
color: #444343;
|
||||
font-size: 1.3em;
|
||||
padding-top: 40px;
|
||||
color: rgb(68 67 67 / 100%);
|
||||
font-size: 1.3em;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.about {
|
||||
margin: 25vh auto;
|
||||
height: 25vh;
|
||||
text-align: center;
|
||||
display: block !important;
|
||||
margin: 25vh auto;
|
||||
height: 25vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about p {
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, 0.62);
|
||||
font-size: 20px;
|
||||
color: rgb(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 {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
color: #444;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
color: rgb(68 68 68 / 100%);
|
||||
}
|
||||
|
||||
.maintenance-info p {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.maintenance-info .bug {
|
||||
display: inline-block;
|
||||
padding: 8px 15px;
|
||||
margin-top: 30px;
|
||||
text-decoration: none;
|
||||
background-color: #52c2af;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.maintenance-info .bug:hover {
|
||||
background-color: #32a692;
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
p.detail a {
|
||||
color: #355f4c;
|
||||
color: rgb(53 95 76 / 100%);
|
||||
}
|
||||
|
||||
p.detail a:hover {
|
||||
text-decoration: underline;
|
||||
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 {
|
||||
background-color: var(--button-color);
|
||||
border-color: var(--button-color);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
12
app/renderer/css/fonts.css
Normal file
12
app/renderer/css/fonts.css
Normal file
@@ -0,0 +1,12 @@
|
||||
@font-face {
|
||||
font-family: "Material Icons";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local("Material Icons"), local("MaterialIcons-Regular"),
|
||||
url("../fonts/MaterialIcons-Regular.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Montserrat;
|
||||
src: url("../fonts/Montserrat-Regular.ttf") format("truetype");
|
||||
}
|
@@ -4,391 +4,481 @@
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: #fff url(../img/ic_loading.gif) no-repeat;
|
||||
background-size: 60px 60px;
|
||||
background-position: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toggle-sidebar {
|
||||
background: #222c31;
|
||||
width: 54px;
|
||||
padding: 27px 0 20px 0;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-webkit-app-region: drag;
|
||||
overflow: hidden;
|
||||
transition: all 0.5s ease;
|
||||
z-index: 1;
|
||||
background: rgb(34 44 49 / 100%);
|
||||
width: 54px;
|
||||
padding: 27px 0 20px;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-webkit-app-region: drag;
|
||||
overflow: hidden;
|
||||
transition: all 0.5s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.toggle-sidebar div {
|
||||
transition: all 0.5s ease-out;
|
||||
transition: all 0.5s ease-out;
|
||||
}
|
||||
|
||||
.sidebar-hide {
|
||||
width: 0;
|
||||
transition: all 0.8s ease;
|
||||
width: 0;
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.sidebar-hide div {
|
||||
transform: translateX(-100%);
|
||||
transition: all 0.6s ease-out;
|
||||
transform: translateX(-100%);
|
||||
transition: all 0.6s ease-out;
|
||||
}
|
||||
|
||||
@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');
|
||||
#view-controls-container {
|
||||
height: calc(100% - 208px);
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#view-controls-container::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
#view-controls-container::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 6px rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
#view-controls-container::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(169 169 169 / 100%);
|
||||
outline: 1px solid rgb(169 169 169 / 100%);
|
||||
}
|
||||
|
||||
#view-controls-container:hover {
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
||||
/*******************
|
||||
* Left Sidebar *
|
||||
*******************/
|
||||
|
||||
#tabs-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
/* Preferred icon size */
|
||||
font-size: 24px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
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;
|
||||
}
|
||||
|
||||
#actions-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-button i {
|
||||
color: #6c8592;
|
||||
font-size: 28px;
|
||||
color: rgb(108 133 146 / 100%);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.action-button:hover i {
|
||||
color: #98a9b3;
|
||||
}
|
||||
|
||||
.action-button.disable {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.action-button.disable:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button.disable:hover i {
|
||||
color: #6c8592;
|
||||
color: rgb(152 169 179 / 100%);
|
||||
}
|
||||
|
||||
.action-button.active {
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
/* background-color: rgba(255, 255, 255, 0.25); */
|
||||
background-color: rgb(239 239 239 / 100%);
|
||||
opacity: 0.9;
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
.action-button.active i {
|
||||
color: #eee;
|
||||
color: rgb(28 38 43 / 100%);
|
||||
}
|
||||
|
||||
.tab:first-child {
|
||||
margin-top: 8px;
|
||||
.action-button.disable {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.action-button.disable:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button.disable:hover i {
|
||||
color: rgb(108 133 146 / 100%);
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab:first-child {
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.tab .server-icons {
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
padding: 3px;
|
||||
height: 30px;
|
||||
vertical-align: top;
|
||||
width: 35px;
|
||||
vertical-align: top;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab .server-tab {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
position: relative;
|
||||
margin: 5px 0 2px 0;
|
||||
z-index: 11;
|
||||
line-height: 31px;
|
||||
color: #eee;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
opacity: 0.6;
|
||||
padding: 2px 0;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
z-index: 11;
|
||||
line-height: 31px;
|
||||
color: rgb(238 238 238 / 100%);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
opacity: 0.6;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.server-tab .alt-icon {
|
||||
font-family: Verdana, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
border: 2px solid rgb(34 44 49 / 100%);
|
||||
margin-left: 17%;
|
||||
width: 35px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab .server-tab:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tab.functional-tab {
|
||||
height: 46px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tab.functional-tab.active .server-tab {
|
||||
padding: 2px 0;
|
||||
height: 40px;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.tab.functional-tab .server-tab i {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tab.active .server-tab {
|
||||
opacity: 1;
|
||||
background-color: #648478;
|
||||
opacity: 1;
|
||||
background-color: rgb(100 132 120 / 100%);
|
||||
}
|
||||
|
||||
.tab.functional-tab {
|
||||
height: 46px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tab.functional-tab.active .server-tab {
|
||||
padding: 2px 0;
|
||||
height: 40px;
|
||||
background-color: rgb(255 255 255 / 25%);
|
||||
}
|
||||
|
||||
.tab.functional-tab .server-tab i {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.tab .server-tab-badge.active {
|
||||
border-radius: 9px;
|
||||
min-width: 11px;
|
||||
padding: 0 3px;
|
||||
height: 17px;
|
||||
background-color: #f44336;
|
||||
font-size: 10px;
|
||||
font-family: sans-serif;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
z-index: 15;
|
||||
top: 6px;
|
||||
float: right;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 17px;
|
||||
display: block;
|
||||
right: 0;
|
||||
border-radius: 9px;
|
||||
min-width: 11px;
|
||||
padding: 0 3px;
|
||||
height: 17px;
|
||||
background-color: rgb(244 67 54 / 100%);
|
||||
font-size: 10px;
|
||||
font-family: sans-serif;
|
||||
position: absolute;
|
||||
z-index: 15;
|
||||
top: 6px;
|
||||
float: right;
|
||||
color: rgb(255 255 255 / 100%);
|
||||
text-align: center;
|
||||
line-height: 17px;
|
||||
display: block;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tab .server-tab-badge {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab .server-tab-badge.close-button {
|
||||
width: 16px;
|
||||
padding: 0;
|
||||
width: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tab .server-tab-badge.close-button i {
|
||||
font-size: 13px;
|
||||
line-height: 17px;
|
||||
font-size: 13px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
#add-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab .server-tab-shortcut {
|
||||
color: #648478;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-family: sans-serif;
|
||||
margin-bottom: 5px;
|
||||
color: rgb(100 132 120 / 100%);
|
||||
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 *
|
||||
*******************/
|
||||
|
||||
#webviews-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
webview {
|
||||
opacity: 1;
|
||||
/* transition: opacity 0.3s ease-in; */
|
||||
flex-grow: 1;
|
||||
/* Pseudo element for loading indicator */
|
||||
#webviews-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background: rgb(255 255 255 / 100%) url("../img/ic_loading.gif") no-repeat;
|
||||
background-size: 60px 60px;
|
||||
background-position: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
webview.onload {
|
||||
transition: opacity 1s cubic-bezier(0.95, 0.05, 0.795, 0.035);
|
||||
/* When the active webview is loaded */
|
||||
#webviews-container.loaded::before {
|
||||
z-index: -1;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
webview.disabled {
|
||||
flex: 0 1;
|
||||
height: 0;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-out;
|
||||
.webview-pane,
|
||||
.functional-view {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
webview:focus {
|
||||
outline: 0px solid transparent;
|
||||
.webview-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.webview-pane > webview {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.webview-pane.active,
|
||||
.functional-view.active {
|
||||
z-index: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
webview.focus {
|
||||
outline: 0 solid transparent;
|
||||
}
|
||||
|
||||
.webview-unsupported {
|
||||
background: rgb(254 243 199);
|
||||
border: 1px solid rgb(253 230 138);
|
||||
color: rgb(69 26 3);
|
||||
font-family: system-ui;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.webview-unsupported[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.webview-unsupported-message {
|
||||
padding: 0.3em;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.webview-unsupported-dismiss {
|
||||
padding: 0.3em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Tooltip styling */
|
||||
|
||||
#loading-tooltip,
|
||||
#dnd-tooltip,
|
||||
#back-tooltip,
|
||||
#reload-tooltip,
|
||||
#setting-tooltip {
|
||||
font-family: sans-serif;
|
||||
background: #222c31;
|
||||
margin-left: 45px;
|
||||
padding: 6px 8px;
|
||||
position: absolute;
|
||||
margin-top: 0px;
|
||||
z-index: 1000;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: 55px;
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
background: rgb(34 44 49 / 100%);
|
||||
margin-left: 48px;
|
||||
padding: 6px 8px;
|
||||
position: absolute;
|
||||
margin-top: 0;
|
||||
z-index: 1000;
|
||||
color: rgb(255 255 255 / 100%);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: 55px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#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;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 68px;
|
||||
#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 rgb(34 44 49 / 100%);
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 68px;
|
||||
}
|
||||
|
||||
#add-server-tooltip,
|
||||
.server-tooltip {
|
||||
font-family: 'arial';
|
||||
background: #222c31;
|
||||
left: 56px;
|
||||
padding: 10px 20px;
|
||||
position: fixed;
|
||||
margin-top: 8px;
|
||||
z-index: 5000 !important;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: max-content;
|
||||
font-size: 14px;
|
||||
font-family: arial, sans-serif;
|
||||
background: rgb(34 44 49 / 100%);
|
||||
left: 56px;
|
||||
padding: 10px 20px;
|
||||
position: fixed;
|
||||
margin-top: 11px;
|
||||
z-index: 5000 !important;
|
||||
color: rgb(255 255 255 / 100%);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: max-content;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#add-server-tooltip:after,
|
||||
.server-tooltip:after {
|
||||
content: " ";
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-right: 8px solid #222c31;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -5px;
|
||||
#add-server-tooltip::after,
|
||||
.server-tooltip::after {
|
||||
content: " ";
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-right: 8px solid rgb(34 44 49 / 100%);
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
#collapse-button {
|
||||
bottom: 30px;
|
||||
left: 20px;
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #222c31;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
box-shadow: #999 1px 1px;
|
||||
bottom: 30px;
|
||||
left: 20px;
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgb(34 44 49 / 100%);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
box-shadow: rgb(153 153 153 / 100%) 1px 1px;
|
||||
}
|
||||
|
||||
#collapse-button i {
|
||||
color: #efefef;
|
||||
color: rgb(239 239 239 / 100%);
|
||||
}
|
||||
|
||||
#main-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Full screen Popup container */
|
||||
|
||||
.popup .popuptext {
|
||||
visibility: hidden;
|
||||
background-color: #555;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 9px 0;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
font-family: arial;
|
||||
width: 240px;
|
||||
top: 15px;
|
||||
height: 20px;
|
||||
left: 43%;
|
||||
visibility: hidden;
|
||||
background-color: rgb(85 85 85 / 100%);
|
||||
color: rgb(255 255 255 / 100%);
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 9px 0;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
font-family: arial, sans-serif;
|
||||
width: 240px;
|
||||
top: 15px;
|
||||
height: 20px;
|
||||
left: 43%;
|
||||
}
|
||||
|
||||
.popup .show {
|
||||
visibility: visible;
|
||||
animation: cssAnimation 0s ease-in 5s forwards;
|
||||
animation-fill-mode: forwards;
|
||||
visibility: visible;
|
||||
animation: full-screen-popup 0s ease-in 1s forwards;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes cssAnimation {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
}
|
||||
@keyframes full-screen-popup {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
@@ -1,42 +1,59 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
user-select:none;
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
color: rgb(51 51 51 / 100%);
|
||||
background: rgb(255 255 255 / 100%);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: "Trebuchet MS", Helvetica, sans-serif;
|
||||
margin: 100px 200px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: "Trebuchet MS", Helvetica, sans-serif;
|
||||
margin: 100px 200px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#subtitle {
|
||||
font-size: 20px;
|
||||
text-align: left;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
#description {
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
#reconnect {
|
||||
font-size: 16px;
|
||||
background: #009688;
|
||||
color: #fff;
|
||||
width: 84px;
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
line-height: 32px;
|
||||
margin: 20px auto 0;
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#reconnect:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
#settings {
|
||||
margin-left: 116px;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 16px;
|
||||
background: rgb(0 150 136 / 100%);
|
||||
color: rgb(255 255 255 / 100%);
|
||||
width: 96px;
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
line-height: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
/* Override css rules */
|
||||
|
||||
.portico-wrap>.header {
|
||||
display: none;
|
||||
.portico-wrap > .header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.portico-container>.footer {
|
||||
display: none;
|
||||
}
|
||||
.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 |
67
app/renderer/js/clipboard-decrypter.ts
Normal file
67
app/renderer/js/clipboard-decrypter.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {ipcRenderer} from "./typed-ipc-renderer.js";
|
||||
|
||||
// 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 type ClipboardDecrypter = {
|
||||
version: number;
|
||||
key: Uint8Array;
|
||||
pasted: Promise<string>;
|
||||
};
|
||||
|
||||
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;
|
||||
const {key, sig} = ipcRenderer.sendSync("new-clipboard-key");
|
||||
this.key = key;
|
||||
this.pasted = new Promise((resolve) => {
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
const startPolling = () => {
|
||||
if (interval === null) {
|
||||
interval = setInterval(poll, 1000);
|
||||
}
|
||||
|
||||
void poll();
|
||||
};
|
||||
|
||||
const stopPolling = () => {
|
||||
if (interval !== null) {
|
||||
clearInterval(interval);
|
||||
interval = null;
|
||||
}
|
||||
};
|
||||
|
||||
const poll = async () => {
|
||||
const plaintext = await ipcRenderer.invoke("poll-clipboard", key, sig);
|
||||
if (plaintext === undefined) 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 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
class BaseComponent {
|
||||
generateNodeFromTemplate(template) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = template;
|
||||
return wrapper.firstElementChild;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseComponent;
|
12
app/renderer/js/components/base.ts
Normal file
12
app/renderer/js/components/base.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type {Html} from "../../../common/html.js";
|
||||
|
||||
export function generateNodeFromHtml(html: Html): Element {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.innerHTML = html.html;
|
||||
|
||||
if (wrapper.firstElementChild === null) {
|
||||
throw new Error("No element found in HTML");
|
||||
}
|
||||
|
||||
return wrapper.firstElementChild;
|
||||
}
|
149
app/renderer/js/components/context-menu.ts
Normal file
149
app/renderer/js/components/context-menu.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import {clipboard} from "electron/common";
|
||||
import type {WebContents} from "electron/main";
|
||||
import type {
|
||||
ContextMenuParams,
|
||||
MenuItemConstructorOptions,
|
||||
} from "electron/renderer";
|
||||
import process from "node:process";
|
||||
|
||||
import {Menu} from "@electron/remote";
|
||||
|
||||
import * as t from "../../../common/translation-util.js";
|
||||
|
||||
export const contextMenu = (
|
||||
webContents: WebContents,
|
||||
event: Event,
|
||||
props: ContextMenuParams,
|
||||
) => {
|
||||
const isText = props.selectionText !== "";
|
||||
const isLink = props.linkURL !== "";
|
||||
const linkUrl = isLink ? new URL(props.linkURL) : undefined;
|
||||
|
||||
const makeSuggestion = (suggestion: string) => ({
|
||||
label: suggestion,
|
||||
visible: true,
|
||||
async click() {
|
||||
await webContents.insertText(suggestion);
|
||||
},
|
||||
});
|
||||
|
||||
let menuTemplate: 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:
|
||||
linkUrl?.protocol === "mailto:"
|
||||
? t.__("Copy Email Address")
|
||||
: t.__("Copy Link"),
|
||||
visible: isLink,
|
||||
click(_item) {
|
||||
clipboard.write({
|
||||
bookmark: props.linkText,
|
||||
text:
|
||||
linkUrl?.protocol === "mailto:" ? 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: MenuItemConstructorOptions[] =
|
||||
props.dictionarySuggestions.map((suggestion: string) =>
|
||||
makeSuggestion(suggestion),
|
||||
);
|
||||
menuTemplate = [...suggestions, ...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 menu items. 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,44 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const Tab = require(__dirname + '/../components/tab.js');
|
||||
|
||||
class FunctionalTab extends Tab {
|
||||
template() {
|
||||
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() {
|
||||
this.$el = this.generateNodeFromTemplate(this.template());
|
||||
if (this.props.name !== 'Settings') {
|
||||
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;
|
73
app/renderer/js/components/functional-tab.ts
Normal file
73
app/renderer/js/components/functional-tab.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type {Html} from "../../../common/html.js";
|
||||
import {html} from "../../../common/html.js";
|
||||
|
||||
import {generateNodeFromHtml} from "./base.js";
|
||||
import type {TabProps} from "./tab.js";
|
||||
import Tab from "./tab.js";
|
||||
|
||||
export type FunctionalTabProps = {
|
||||
$view: Element;
|
||||
} & TabProps;
|
||||
|
||||
export default class FunctionalTab extends Tab {
|
||||
$view: Element;
|
||||
$el: Element;
|
||||
$closeButton?: Element;
|
||||
|
||||
constructor({$view, ...props}: FunctionalTabProps) {
|
||||
super(props);
|
||||
|
||||
this.$view = $view;
|
||||
this.$el = generateNodeFromHtml(this.templateHtml());
|
||||
if (this.props.name !== "Settings") {
|
||||
this.props.$root.append(this.$el);
|
||||
this.$closeButton = this.$el.querySelector(".server-tab-badge")!;
|
||||
this.registerListeners();
|
||||
}
|
||||
}
|
||||
|
||||
override async activate(): Promise<void> {
|
||||
await super.activate();
|
||||
this.$view.classList.add("active");
|
||||
}
|
||||
|
||||
override async deactivate(): Promise<void> {
|
||||
await super.deactivate();
|
||||
this.$view.classList.remove("active");
|
||||
}
|
||||
|
||||
override async destroy(): Promise<void> {
|
||||
await super.destroy();
|
||||
this.$view.remove();
|
||||
}
|
||||
|
||||
templateHtml(): Html {
|
||||
return html`
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
override 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();
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,60 +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" data-tab-id="${this.props.tabIndex}">
|
||||
<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}`;
|
||||
}
|
||||
|
||||
// Array index == Shown index - 1
|
||||
ipcRenderer.send('switch-server-tab', shownIndex - 1);
|
||||
|
||||
return shortcutText;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServerTab;
|
95
app/renderer/js/components/server-tab.ts
Normal file
95
app/renderer/js/components/server-tab.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import process from "node:process";
|
||||
|
||||
import type {Html} from "../../../common/html.js";
|
||||
import {html} from "../../../common/html.js";
|
||||
import {ipcRenderer} from "../typed-ipc-renderer.js";
|
||||
|
||||
import {generateNodeFromHtml} from "./base.js";
|
||||
import type {TabProps} from "./tab.js";
|
||||
import Tab from "./tab.js";
|
||||
import type WebView from "./webview.js";
|
||||
|
||||
export type ServerTabProps = {
|
||||
webview: Promise<WebView>;
|
||||
} & TabProps;
|
||||
|
||||
export default class ServerTab extends Tab {
|
||||
webview: Promise<WebView>;
|
||||
$el: Element;
|
||||
$name: Element;
|
||||
$icon: HTMLImageElement;
|
||||
$badge: Element;
|
||||
|
||||
constructor({webview, ...props}: ServerTabProps) {
|
||||
super(props);
|
||||
|
||||
this.webview = webview;
|
||||
this.$el = generateNodeFromHtml(this.templateHtml());
|
||||
this.props.$root.append(this.$el);
|
||||
this.registerListeners();
|
||||
this.$name = this.$el.querySelector(".server-tooltip")!;
|
||||
this.$icon = this.$el.querySelector(".server-icons")!;
|
||||
this.$badge = this.$el.querySelector(".server-tab-badge")!;
|
||||
}
|
||||
|
||||
override async activate(): Promise<void> {
|
||||
await super.activate();
|
||||
(await this.webview).load();
|
||||
}
|
||||
|
||||
override async deactivate(): Promise<void> {
|
||||
await super.deactivate();
|
||||
(await this.webview).hide();
|
||||
}
|
||||
|
||||
override async destroy(): Promise<void> {
|
||||
await super.destroy();
|
||||
(await this.webview).destroy();
|
||||
}
|
||||
|
||||
templateHtml(): Html {
|
||||
return html`
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
setName(name: string): void {
|
||||
this.props.name = name;
|
||||
this.$name.textContent = name;
|
||||
}
|
||||
|
||||
setIcon(icon: string): void {
|
||||
this.props.icon = icon;
|
||||
this.$icon.src = icon;
|
||||
}
|
||||
|
||||
updateBadge(count: number): void {
|
||||
this.$badge.textContent = count > 999 ? "1K+" : count.toString();
|
||||
this.$badge.classList.toggle("active", count > 0);
|
||||
}
|
||||
|
||||
generateShortcutText(): string {
|
||||
// Only provide shortcuts for server [0..9]
|
||||
if (this.props.index >= 9) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const shownIndex = this.props.index + 1;
|
||||
|
||||
// Array index == Shown index - 1
|
||||
ipcRenderer.send("switch-server-tab", shownIndex - 1);
|
||||
|
||||
return process.platform === "darwin"
|
||||
? `⌘${shownIndex}`
|
||||
: `Ctrl+${shownIndex}`;
|
||||
}
|
||||
}
|
@@ -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;
|
45
app/renderer/js/components/tab.ts
Normal file
45
app/renderer/js/components/tab.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type {TabRole} from "../../../common/types.js";
|
||||
|
||||
export type TabProps = {
|
||||
role: TabRole;
|
||||
icon?: string;
|
||||
name: string;
|
||||
$root: Element;
|
||||
onClick: () => void;
|
||||
index: number;
|
||||
tabIndex: number;
|
||||
onHover?: () => void;
|
||||
onHoverOut?: () => void;
|
||||
materialIcon?: string;
|
||||
onDestroy?: () => void;
|
||||
};
|
||||
|
||||
export default abstract class Tab {
|
||||
abstract $el: Element;
|
||||
|
||||
constructor(readonly props: TabProps) {}
|
||||
|
||||
registerListeners(): void {
|
||||
this.$el.addEventListener("click", this.props.onClick);
|
||||
|
||||
if (this.props.onHover !== undefined) {
|
||||
this.$el.addEventListener("mouseover", this.props.onHover);
|
||||
}
|
||||
|
||||
if (this.props.onHoverOut !== undefined) {
|
||||
this.$el.addEventListener("mouseout", this.props.onHoverOut);
|
||||
}
|
||||
}
|
||||
|
||||
async activate(): Promise<void> {
|
||||
this.$el.classList.add("active");
|
||||
}
|
||||
|
||||
async deactivate(): Promise<void> {
|
||||
this.$el.classList.remove("active");
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
this.$el.remove();
|
||||
}
|
||||
}
|
@@ -1,239 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const DomainUtil = require(__dirname + '/../utils/domain-util.js');
|
||||
const ConfigUtil = require(__dirname + '/../utils/config-util.js');
|
||||
const SystemUtil = require(__dirname + '/../utils/system-util.js');
|
||||
const LinkUtil = require(__dirname + '/../utils/link-util.js');
|
||||
const { shell, app, dialog } = require('electron').remote;
|
||||
|
||||
const BaseComponent = require(__dirname + '/../components/base.js');
|
||||
|
||||
const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
|
||||
class WebView extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
|
||||
this.props = props;
|
||||
|
||||
this.zoomFactor = 1.0;
|
||||
this.loading = false;
|
||||
this.badgeCount = 0;
|
||||
this.customCSS = ConfigUtil.getConfigItem('customCSS');
|
||||
}
|
||||
|
||||
template() {
|
||||
return `<webview
|
||||
class="disabled"
|
||||
data-tab-id="${this.props.tabIndex}"
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
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('●');
|
||||
}
|
||||
});
|
||||
|
||||
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'));
|
||||
|
||||
// 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 errMsg = 'The custom css previously set is deleted!';
|
||||
dialog.showErrorBox('custom css file deleted!', errMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$el.insertCSS(fs.readFileSync(path.resolve(__dirname, this.customCSS), '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();
|
||||
}
|
||||
}
|
||||
|
||||
canGoBackButton() {
|
||||
const $backButton = document.querySelector('#actions-container #back-action');
|
||||
if (this.$el.canGoBack()) {
|
||||
$backButton.classList.remove('disable');
|
||||
} else {
|
||||
$backButton.classList.add('disable');
|
||||
}
|
||||
}
|
||||
|
||||
forward() {
|
||||
if (this.$el.canGoForward()) {
|
||||
this.$el.goForward();
|
||||
}
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.hide();
|
||||
this.$el.reload();
|
||||
}
|
||||
|
||||
send(...param) {
|
||||
this.$el.send(...param);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebView;
|
340
app/renderer/js/components/webview.ts
Normal file
340
app/renderer/js/components/webview.ts
Normal file
@@ -0,0 +1,340 @@
|
||||
import type {WebContents} from "electron/main";
|
||||
import fs from "node:fs";
|
||||
import process from "node:process";
|
||||
|
||||
import * as remote from "@electron/remote";
|
||||
import {app, dialog} from "@electron/remote";
|
||||
|
||||
import * as ConfigUtil from "../../../common/config-util.js";
|
||||
import type {Html} from "../../../common/html.js";
|
||||
import {html} from "../../../common/html.js";
|
||||
import type {RendererMessage} from "../../../common/typed-ipc.js";
|
||||
import type {TabRole} from "../../../common/types.js";
|
||||
import preloadCss from "../../css/preload.css?raw"; // eslint-disable-line n/file-extension-in-import
|
||||
import {ipcRenderer} from "../typed-ipc-renderer.js";
|
||||
import * as SystemUtil from "../utils/system-util.js";
|
||||
|
||||
import {generateNodeFromHtml} from "./base.js";
|
||||
import {contextMenu} from "./context-menu.js";
|
||||
|
||||
const shouldSilentWebview = ConfigUtil.getConfigItem("silent", false);
|
||||
|
||||
type WebViewProps = {
|
||||
$root: Element;
|
||||
rootWebContents: WebContents;
|
||||
index: number;
|
||||
tabIndex: number;
|
||||
url: string;
|
||||
role: TabRole;
|
||||
isActive: () => boolean;
|
||||
switchLoading: (loading: boolean, url: string) => void;
|
||||
onNetworkError: (index: number) => void;
|
||||
preload?: string;
|
||||
onTitleChange: () => void;
|
||||
hasPermission?: (origin: string, permission: string) => boolean;
|
||||
unsupportedMessage?: string;
|
||||
};
|
||||
|
||||
export default class WebView {
|
||||
static templateHtml(props: WebViewProps): Html {
|
||||
return html`
|
||||
<div class="webview-pane">
|
||||
<div
|
||||
class="webview-unsupported"
|
||||
${props.unsupportedMessage === undefined ? html`hidden` : html``}
|
||||
>
|
||||
<span class="webview-unsupported-message"
|
||||
>${props.unsupportedMessage ?? ""}</span
|
||||
>
|
||||
<span class="webview-unsupported-dismiss">×</span>
|
||||
</div>
|
||||
<webview
|
||||
data-tab-id="${props.tabIndex}"
|
||||
src="${props.url}"
|
||||
${props.preload === undefined
|
||||
? html``
|
||||
: html`preload="${props.preload}"`}
|
||||
partition="persist:webviewsession"
|
||||
allowpopups
|
||||
>
|
||||
</webview>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static async create(props: WebViewProps): Promise<WebView> {
|
||||
const $pane = generateNodeFromHtml(
|
||||
WebView.templateHtml(props),
|
||||
) as HTMLElement;
|
||||
props.$root.append($pane);
|
||||
|
||||
const $webview: HTMLElement = $pane.querySelector(":scope > webview")!;
|
||||
await new Promise<void>((resolve) => {
|
||||
$webview.addEventListener(
|
||||
"did-attach",
|
||||
() => {
|
||||
resolve();
|
||||
},
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// Work around https://github.com/electron/electron/issues/26904
|
||||
function getWebContentsIdFunction(
|
||||
this: undefined,
|
||||
selector: string,
|
||||
): number {
|
||||
return document
|
||||
.querySelector<Electron.WebviewTag>(selector)!
|
||||
.getWebContentsId();
|
||||
}
|
||||
|
||||
const selector = `webview[data-tab-id="${CSS.escape(
|
||||
`${props.tabIndex}`,
|
||||
)}"]`;
|
||||
const webContentsId: unknown =
|
||||
await props.rootWebContents.executeJavaScript(
|
||||
`(${getWebContentsIdFunction.toString()})(${JSON.stringify(selector)})`,
|
||||
);
|
||||
if (typeof webContentsId !== "number") {
|
||||
throw new TypeError("Failed to get WebContents ID");
|
||||
}
|
||||
|
||||
return new WebView(props, $pane, $webview, webContentsId);
|
||||
}
|
||||
|
||||
badgeCount = 0;
|
||||
loading = true;
|
||||
private zoomFactor = 1;
|
||||
private customCss: string | false | null;
|
||||
private readonly $webviewsContainer: DOMTokenList;
|
||||
private readonly $unsupported: HTMLElement;
|
||||
private readonly $unsupportedMessage: HTMLElement;
|
||||
private readonly $unsupportedDismiss: HTMLElement;
|
||||
private unsupportedDismissed = false;
|
||||
|
||||
private constructor(
|
||||
readonly props: WebViewProps,
|
||||
private readonly $pane: HTMLElement,
|
||||
private readonly $webview: HTMLElement,
|
||||
readonly webContentsId: number,
|
||||
) {
|
||||
this.customCss = ConfigUtil.getConfigItem("customCSS", null);
|
||||
this.$webviewsContainer = document.querySelector(
|
||||
"#webviews-container",
|
||||
)!.classList;
|
||||
this.$unsupported = $pane.querySelector(".webview-unsupported")!;
|
||||
this.$unsupportedMessage = $pane.querySelector(
|
||||
".webview-unsupported-message",
|
||||
)!;
|
||||
this.$unsupportedDismiss = $pane.querySelector(
|
||||
".webview-unsupported-dismiss",
|
||||
)!;
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.$pane.remove();
|
||||
}
|
||||
|
||||
getWebContents(): WebContents {
|
||||
return remote.webContents.fromId(this.webContentsId)!;
|
||||
}
|
||||
|
||||
showNotificationSettings(): void {
|
||||
this.send("show-notification-settings");
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.$webview.focus();
|
||||
// Work around https://github.com/electron/electron/issues/31918
|
||||
this.$webview.shadowRoot?.querySelector("iframe")?.focus();
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.$pane.classList.remove("active");
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.show();
|
||||
}
|
||||
|
||||
zoomIn(): void {
|
||||
this.zoomFactor += 0.1;
|
||||
this.getWebContents().setZoomFactor(this.zoomFactor);
|
||||
}
|
||||
|
||||
zoomOut(): void {
|
||||
this.zoomFactor -= 0.1;
|
||||
this.getWebContents().setZoomFactor(this.zoomFactor);
|
||||
}
|
||||
|
||||
zoomActualSize(): void {
|
||||
this.zoomFactor = 1;
|
||||
this.getWebContents().setZoomFactor(this.zoomFactor);
|
||||
}
|
||||
|
||||
logOut(): void {
|
||||
this.send("logout");
|
||||
}
|
||||
|
||||
showKeyboardShortcuts(): void {
|
||||
this.send("show-keyboard-shortcuts");
|
||||
}
|
||||
|
||||
openDevTools(): void {
|
||||
this.getWebContents().openDevTools();
|
||||
}
|
||||
|
||||
back(): void {
|
||||
if (this.getWebContents().canGoBack()) {
|
||||
this.getWebContents().goBack();
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
canGoBackButton(): void {
|
||||
const $backButton = document.querySelector(
|
||||
"#actions-container #back-action",
|
||||
)!;
|
||||
$backButton.classList.toggle("disable", !this.getWebContents().canGoBack());
|
||||
}
|
||||
|
||||
forward(): void {
|
||||
if (this.getWebContents().canGoForward()) {
|
||||
this.getWebContents().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.getWebContents().reload();
|
||||
}
|
||||
|
||||
setUnsupportedMessage(unsupportedMessage: string | undefined) {
|
||||
this.$unsupported.hidden =
|
||||
unsupportedMessage === undefined || this.unsupportedDismissed;
|
||||
this.$unsupportedMessage.textContent = unsupportedMessage ?? "";
|
||||
}
|
||||
|
||||
send<Channel extends keyof RendererMessage>(
|
||||
channel: Channel,
|
||||
...args: Parameters<RendererMessage[Channel]>
|
||||
): void {
|
||||
ipcRenderer.sendTo(this.webContentsId, channel, ...args);
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
const webContents = this.getWebContents();
|
||||
|
||||
if (shouldSilentWebview) {
|
||||
webContents.setAudioMuted(true);
|
||||
}
|
||||
|
||||
webContents.on("page-title-updated", (_event, title) => {
|
||||
this.badgeCount = this.getBadgeCount(title);
|
||||
this.props.onTitleChange();
|
||||
});
|
||||
|
||||
this.$webview.addEventListener("did-navigate-in-page", () => {
|
||||
this.canGoBackButton();
|
||||
});
|
||||
|
||||
this.$webview.addEventListener("did-navigate", () => {
|
||||
this.canGoBackButton();
|
||||
});
|
||||
|
||||
webContents.on("page-favicon-updated", (_event, favicons) => {
|
||||
// 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", true)) {
|
||||
app.dock.bounce();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
webContents.addListener("context-menu", (event, menuParameters) => {
|
||||
contextMenu(webContents, event, menuParameters);
|
||||
});
|
||||
|
||||
this.$webview.addEventListener("dom-ready", () => {
|
||||
this.loading = false;
|
||||
this.props.switchLoading(false, this.props.url);
|
||||
this.show();
|
||||
});
|
||||
|
||||
webContents.on("did-fail-load", (_event, _errorCode, errorDescription) => {
|
||||
const hasConnectivityError =
|
||||
SystemUtil.connectivityError.includes(errorDescription);
|
||||
if (hasConnectivityError) {
|
||||
console.error("error", errorDescription);
|
||||
if (!this.props.url.includes("network.html")) {
|
||||
this.props.onNetworkError(this.props.index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$webview.addEventListener("did-start-loading", () => {
|
||||
this.props.switchLoading(true, this.props.url);
|
||||
});
|
||||
|
||||
this.$webview.addEventListener("did-stop-loading", () => {
|
||||
this.props.switchLoading(false, this.props.url);
|
||||
});
|
||||
|
||||
this.$unsupportedDismiss.addEventListener("click", () => {
|
||||
this.unsupportedDismissed = true;
|
||||
this.$unsupported.hidden = true;
|
||||
});
|
||||
}
|
||||
|
||||
private getBadgeCount(title: string): number {
|
||||
const messageCountInTitle = /^\((\d+)\)/.exec(title);
|
||||
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
|
||||
}
|
||||
|
||||
private 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 active tab
|
||||
this.$webviewsContainer.toggle("loaded", !this.loading);
|
||||
|
||||
this.$pane.classList.add("active");
|
||||
this.focus();
|
||||
this.props.onTitleChange();
|
||||
// Injecting preload css in webview to override some css rules
|
||||
(async () => this.getWebContents().insertCSS(preloadCss))();
|
||||
|
||||
// Get customCSS again from config util to avoid warning user again
|
||||
const customCss = ConfigUtil.getConfigItem("customCSS", null);
|
||||
this.customCss = customCss;
|
||||
if (customCss) {
|
||||
if (!fs.existsSync(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.getWebContents().insertCSS(fs.readFileSync(customCss, "utf8")))();
|
||||
}
|
||||
}
|
||||
}
|
113
app/renderer/js/electron-bridge.ts
Normal file
113
app/renderer/js/electron-bridge.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import {EventEmitter} from "events"; // eslint-disable-line unicorn/prefer-node-protocol
|
||||
|
||||
import type {ClipboardDecrypter} from "./clipboard-decrypter.js";
|
||||
import {ClipboardDecrypterImpl} from "./clipboard-decrypter.js";
|
||||
import type {NotificationData} from "./notification/index.js";
|
||||
import {newNotification} from "./notification/index.js";
|
||||
import {ipcRenderer} from "./typed-ipc-renderer.js";
|
||||
|
||||
type ListenerType = (...args: any[]) => void;
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
export type ElectronBridge = {
|
||||
send_event: (eventName: string | symbol, ...args: unknown[]) => boolean;
|
||||
on_event: (eventName: string, listener: ListenerType) => void;
|
||||
new_notification: (
|
||||
title: string,
|
||||
options: NotificationOptions,
|
||||
dispatch: (type: string, eventInit: EventInit) => boolean,
|
||||
) => NotificationData;
|
||||
get_idle_on_system: () => boolean;
|
||||
get_last_active_on_system: () => number;
|
||||
get_send_notification_reply_message_supported: () => boolean;
|
||||
set_send_notification_reply_message_supported: (value: boolean) => void;
|
||||
decrypt_clipboard: (version: number) => ClipboardDecrypter;
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
let notificationReplySupported = false;
|
||||
// Indicates if the user is idle or not
|
||||
let idle = false;
|
||||
// Indicates the time at which user was last active
|
||||
let lastActive = Date.now();
|
||||
|
||||
export const bridgeEvents = new EventEmitter();
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const electron_bridge: ElectronBridge = {
|
||||
send_event: (eventName: string | symbol, ...args: unknown[]): boolean =>
|
||||
bridgeEvents.emit(eventName, ...args),
|
||||
|
||||
on_event(eventName: string, listener: ListenerType): void {
|
||||
bridgeEvents.on(eventName, listener);
|
||||
},
|
||||
|
||||
new_notification: (
|
||||
title: string,
|
||||
options: NotificationOptions,
|
||||
dispatch: (type: string, eventInit: EventInit) => boolean,
|
||||
): NotificationData => newNotification(title, options, dispatch),
|
||||
|
||||
get_idle_on_system: (): boolean => idle,
|
||||
|
||||
get_last_active_on_system: (): number => lastActive,
|
||||
|
||||
get_send_notification_reply_message_supported: (): boolean =>
|
||||
notificationReplySupported,
|
||||
|
||||
set_send_notification_reply_message_supported(value: boolean): void {
|
||||
notificationReplySupported = value;
|
||||
},
|
||||
|
||||
decrypt_clipboard: (version: number): ClipboardDecrypter =>
|
||||
new ClipboardDecrypterImpl(version),
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
bridgeEvents.on("total_unread_count", (unreadCount: unknown) => {
|
||||
if (typeof unreadCount !== "number") {
|
||||
throw new TypeError("Expected string for unreadCount");
|
||||
}
|
||||
|
||||
ipcRenderer.send("unread-count", unreadCount);
|
||||
});
|
||||
|
||||
bridgeEvents.on("realm_name", (realmName: unknown) => {
|
||||
if (typeof realmName !== "string") {
|
||||
throw new TypeError("Expected string for realmName");
|
||||
}
|
||||
|
||||
const serverUrl = location.origin;
|
||||
ipcRenderer.send("realm-name-changed", serverUrl, realmName);
|
||||
});
|
||||
|
||||
bridgeEvents.on("realm_icon_url", (iconUrl: unknown) => {
|
||||
if (typeof iconUrl !== "string") {
|
||||
throw new TypeError("Expected string for iconUrl");
|
||||
}
|
||||
|
||||
const serverUrl = location.origin;
|
||||
ipcRenderer.send(
|
||||
"realm-icon-changed",
|
||||
serverUrl,
|
||||
iconUrl.includes("http") ? iconUrl : `${serverUrl}${iconUrl}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Set user as active and update the time of last activity
|
||||
ipcRenderer.on("set-active", () => {
|
||||
idle = false;
|
||||
lastActive = Date.now();
|
||||
});
|
||||
|
||||
// Set user as idle and time of last activity is left unchanged
|
||||
ipcRenderer.on("set-idle", () => {
|
||||
idle = true;
|
||||
});
|
||||
|
||||
// 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 ElectronBridge.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;
|
@@ -1,524 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer, remote } = require('electron');
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
const { session } = remote;
|
||||
|
||||
require(__dirname + '/js/tray.js');
|
||||
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');
|
||||
const ReconnectUtil = require(__dirname + '/js/utils/reconnect-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.$backButton = $actionsContainer.querySelector('#back-action');
|
||||
|
||||
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.$backTooltip = $actionsContainer.querySelector('#back-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 = {};
|
||||
this.tabIndex = 0;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadProxy().then(() => {
|
||||
this.initSidebar();
|
||||
this.initTabs();
|
||||
this.initActions();
|
||||
this.registerIpcs();
|
||||
this.initDefaultSettings();
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Settings are initialized only when user clicks on General/Server/Network section settings
|
||||
// In case, user doesn't visit these section, those values set to be null automatically
|
||||
// This will make sure the default settings are correctly set to either true or false
|
||||
initDefaultSettings() {
|
||||
// Default settings which should be respected
|
||||
const settingOptions = {
|
||||
trayIcon: true,
|
||||
useProxy: false,
|
||||
showSidebar: true,
|
||||
badgeOption: true,
|
||||
startAtLogin: false,
|
||||
startMinimized: false,
|
||||
enableSpellchecker: true,
|
||||
showNotification: true,
|
||||
betaUpdate: false,
|
||||
silent: false,
|
||||
lastActiveTab: 0
|
||||
};
|
||||
|
||||
// Platform specific settings
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Only available on Windows
|
||||
settingOptions.flashTaskbarOnMessage = true;
|
||||
}
|
||||
|
||||
for (const i in settingOptions) {
|
||||
if (ConfigUtil.getConfigItem(i) === null) {
|
||||
ConfigUtil.setConfigItem(i, settingOptions[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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'));
|
||||
// Remove focus from the settings icon at sidebar bottom
|
||||
this.$settingsButton.classList.remove('active');
|
||||
} else {
|
||||
this.openSettings('Servers');
|
||||
}
|
||||
}
|
||||
|
||||
initServer(server, index) {
|
||||
const tabIndex = this.getTabIndex();
|
||||
this.tabs.push(new ServerTab({
|
||||
role: 'server',
|
||||
icon: server.icon,
|
||||
$root: this.$tabsContainer,
|
||||
onClick: this.activateLastTab.bind(this, index),
|
||||
index,
|
||||
tabIndex,
|
||||
onHover: this.onHover.bind(this, index, server.alias),
|
||||
onHoverOut: this.onHoverOut.bind(this, index),
|
||||
webview: new WebView({
|
||||
$root: this.$webviewsContainer,
|
||||
index,
|
||||
tabIndex,
|
||||
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.$backButton.addEventListener('click', () => {
|
||||
this.tabs[this.activeTabIndex].webview.back();
|
||||
});
|
||||
|
||||
const $serverImgs = document.querySelectorAll('.server-icons');
|
||||
$serverImgs.forEach($serverImg => {
|
||||
$serverImg.addEventListener('error', () => {
|
||||
$serverImg.src = 'img/icon.png';
|
||||
});
|
||||
});
|
||||
|
||||
this.sidebarHoverEvent(this.$addServerButton, this.$addServerTooltip);
|
||||
this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip);
|
||||
this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip);
|
||||
this.sidebarHoverEvent(this.$backButton, this.$backTooltip);
|
||||
}
|
||||
|
||||
getTabIndex() {
|
||||
const currentIndex = this.tabIndex;
|
||||
this.tabIndex++;
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const tabIndex = this.getTabIndex();
|
||||
this.tabs.push(new FunctionalTab({
|
||||
role: 'function',
|
||||
materialIcon: tabProps.materialIcon,
|
||||
name: tabProps.name,
|
||||
$root: this.$tabsContainer,
|
||||
index: this.functionalTabs[tabProps.name],
|
||||
tabIndex,
|
||||
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],
|
||||
tabIndex,
|
||||
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.$settingsButton.classList.add('active');
|
||||
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) {
|
||||
// If old tab is functional tab Settings, remove focus from the settings icon at sidebar bottom
|
||||
if (this.tabs[this.activeTabIndex].props.role === 'function' && this.tabs[this.activeTabIndex].props.name === 'Settings') {
|
||||
this.$settingsButton.classList.remove('active');
|
||||
}
|
||||
this.tabs[this.activeTabIndex].deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.tabs[index].webview.canGoBackButton();
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
this.activeTabIndex = index;
|
||||
this.tabs[index].activate();
|
||||
|
||||
ipcRenderer.send('update-menu', {
|
||||
tabs: this.tabs,
|
||||
activeTabIndex: this.activeTabIndex
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggle-sidebar', (event, state) => {
|
||||
const selector = 'webview:not([class*=disabled])';
|
||||
const webview = document.querySelector(selector);
|
||||
const webContents = webview.getWebContents();
|
||||
webContents.send('toggle-sidebar', state);
|
||||
});
|
||||
|
||||
ipcRenderer.on('toogle-silent', (event, state) => {
|
||||
const webviews = document.querySelectorAll('webview');
|
||||
webviews.forEach(webview => {
|
||||
try {
|
||||
webview.setAudioMuted(state);
|
||||
} catch (err) {
|
||||
// webview is not ready yet
|
||||
webview.addEventListener('dom-ready', () => {
|
||||
webview.isAudioMuted();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
destroyTab(name, index) {
|
||||
if (this.tabs[index].webview.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabs[index].destroy();
|
||||
|
||||
delete this.tabs[index];
|
||||
delete this.functionalTabs[name];
|
||||
|
||||
// Issue #188: If the functional tab was not focused, do not activate another tab.
|
||||
if (this.activeTabIndex === index) {
|
||||
this.activateTab(0, false);
|
||||
}
|
||||
}
|
||||
|
||||
destroyView() {
|
||||
// Clear global variables
|
||||
this.activeTabIndex = -1;
|
||||
this.tabs = [];
|
||||
this.functionalTabs = {};
|
||||
|
||||
// Clear DOM elements
|
||||
this.$tabsContainer.innerHTML = '';
|
||||
this.$webviewsContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
reloadView() {
|
||||
// Save and remember the index of last active tab so that we can use it later
|
||||
const lastActiveTab = this.tabs[this.activeTabIndex].props.index;
|
||||
ConfigUtil.setConfigItem('lastActiveTab', lastActiveTab);
|
||||
|
||||
// Destroy the current view and re-initiate it
|
||||
this.destroyView();
|
||||
this.initTabs();
|
||||
}
|
||||
|
||||
// This will trigger when pressed CTRL/CMD + R [WIP]
|
||||
// It won't reload the current view properly when you add/delete a server.
|
||||
reloadCurrentView() {
|
||||
this.$reloadButton.click();
|
||||
}
|
||||
|
||||
updateBadge() {
|
||||
let messageCountAll = 0;
|
||||
for (let i = 0; i < this.tabs.length; i++) {
|
||||
if (this.tabs[i] && this.tabs[i].updateBadge) {
|
||||
const count = this.tabs[i].webview.badgeCount;
|
||||
messageCountAll += count;
|
||||
this.tabs[i].updateBadge(count);
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.send('update-badge', messageCountAll);
|
||||
}
|
||||
|
||||
toggleSidebar(show) {
|
||||
if (show) {
|
||||
this.$sidebar.classList.remove('sidebar-hide');
|
||||
} else {
|
||||
this.$sidebar.classList.add('sidebar-hide');
|
||||
}
|
||||
}
|
||||
|
||||
registerIpcs() {
|
||||
const webviewListeners = {
|
||||
'webview-reload': 'reload',
|
||||
back: 'back',
|
||||
focus: 'focus',
|
||||
forward: 'forward',
|
||||
zoomIn: 'zoomIn',
|
||||
zoomOut: 'zoomOut',
|
||||
zoomActualSize: 'zoomActualSize',
|
||||
'log-out': 'logOut',
|
||||
shortcut: 'showShortcut',
|
||||
'tab-devtools': 'openDevTools'
|
||||
};
|
||||
|
||||
for (const key in webviewListeners) {
|
||||
ipcRenderer.on(key, () => {
|
||||
const activeWebview = this.tabs[this.activeTabIndex].webview;
|
||||
if (activeWebview) {
|
||||
activeWebview[webviewListeners[key]]();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ipcRenderer.on('open-settings', (event, settingNav) => {
|
||||
this.openSettings(settingNav);
|
||||
});
|
||||
|
||||
ipcRenderer.on('open-about', this.openAbout.bind(this));
|
||||
|
||||
ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index));
|
||||
|
||||
ipcRenderer.on('reload-current-viewer', this.reloadCurrentView.bind(this));
|
||||
|
||||
ipcRenderer.on('hard-reload', () => {
|
||||
ipcRenderer.send('reload-full-app');
|
||||
});
|
||||
|
||||
ipcRenderer.on('clear-app-data', () => {
|
||||
ipcRenderer.send('clear-app-settings');
|
||||
});
|
||||
|
||||
ipcRenderer.on('switch-server-tab', (event, index) => {
|
||||
this.activateLastTab(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('focus-webview-with-id', (event, webviewId) => {
|
||||
const webviews = document.querySelectorAll('webview');
|
||||
webviews.forEach(webview => {
|
||||
const currentId = webview.getWebContents().id;
|
||||
const tabId = webview.getAttribute('data-tab-id');
|
||||
const concurrentTab = document.querySelector(`div[data-tab-id="${tabId}"]`);
|
||||
if (currentId === webviewId) {
|
||||
concurrentTab.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
const reconnectUtil = new ReconnectUtil(serverManagerView);
|
||||
serverManagerView.init();
|
||||
window.addEventListener('online', () => {
|
||||
reconnectUtil.pollInternetAndReload();
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
reconnectUtil.clearState();
|
||||
console.log('No internet connection, you are offline.');
|
||||
});
|
||||
|
||||
// only start electron-connect (auto reload on change) when its ran
|
||||
// from `npm run dev` or `gulp dev` and not from `npm start` when
|
||||
// app is started `npm start` main process's proces.argv will have
|
||||
// `--no-electron-connect`
|
||||
const mainProcessArgv = remote.getGlobal('process').argv;
|
||||
if (isDev && !mainProcessArgv.includes('--no-electron-connect')) {
|
||||
const electronConnect = require('electron-connect');
|
||||
electronConnect.client.create();
|
||||
}
|
||||
};
|
1187
app/renderer/js/main.ts
Normal file
1187
app/renderer/js/main.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,100 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const url = require('url');
|
||||
const MacNotifier = require('node-mac-notifier');
|
||||
const ConfigUtil = require('../utils/config-util');
|
||||
const {
|
||||
appId, customReply, focusCurrentServer, parseReply, setupReply
|
||||
} = require('./helpers');
|
||||
|
||||
let replyHandler;
|
||||
let clickHandler;
|
||||
class DarwinNotification {
|
||||
constructor(title, opts) {
|
||||
const silent = ConfigUtil.getConfigItem('silent') || false;
|
||||
const { host, protocol } = location;
|
||||
const { icon } = opts;
|
||||
const profilePic = url.resolve(`${protocol}//${host}`, icon);
|
||||
|
||||
this.tag = opts.tag;
|
||||
const notification = new MacNotifier(title, Object.assign(opts, {
|
||||
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() {
|
||||
return; // eslint-disable-line no-useless-return
|
||||
}
|
||||
|
||||
// Override default Notification permission
|
||||
static get permission() {
|
||||
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
|
||||
}
|
||||
|
||||
set onreply(handler) {
|
||||
replyHandler = handler;
|
||||
}
|
||||
|
||||
get onreply() {
|
||||
return replyHandler;
|
||||
}
|
||||
|
||||
set onclick(handler) {
|
||||
clickHandler = handler;
|
||||
}
|
||||
|
||||
get onclick() {
|
||||
return clickHandler;
|
||||
}
|
||||
|
||||
// not something that is common or
|
||||
// used by zulip server but added to be
|
||||
// future proff.
|
||||
addEventListener(event, handler) {
|
||||
if (event === 'click') {
|
||||
clickHandler = handler;
|
||||
}
|
||||
|
||||
if (event === 'reply') {
|
||||
replyHandler = handler;
|
||||
}
|
||||
}
|
||||
|
||||
notificationHandler({ response }) {
|
||||
response = parseReply(response);
|
||||
focusCurrentServer();
|
||||
setupReply(this.tag);
|
||||
|
||||
if (replyHandler) {
|
||||
replyHandler(response);
|
||||
return;
|
||||
}
|
||||
|
||||
customReply(response);
|
||||
}
|
||||
|
||||
// method specific to notification api
|
||||
// used by zulip
|
||||
close() {
|
||||
return; // eslint-disable-line no-useless-return
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DarwinNotification;
|
@@ -1,31 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const ConfigUtil = require('../utils/config-util');
|
||||
const { focusCurrentServer } = require('./helpers');
|
||||
|
||||
const NativeNotification = window.Notification;
|
||||
class BaseNotification extends NativeNotification {
|
||||
constructor(title, opts) {
|
||||
opts.silent = true;
|
||||
super(title, opts);
|
||||
|
||||
this.addEventListener('click', () => {
|
||||
// focus to the server who sent the
|
||||
// notification if not focused already
|
||||
focusCurrentServer();
|
||||
ipcRenderer.send('focus-app');
|
||||
});
|
||||
}
|
||||
|
||||
static requestPermission() {
|
||||
return; // eslint-disable-line no-useless-return
|
||||
}
|
||||
|
||||
// Override default Notification permission
|
||||
static get permission() {
|
||||
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseNotification;
|
@@ -1,149 +0,0 @@
|
||||
const { remote } = require('electron');
|
||||
|
||||
// Do not change this
|
||||
const appId = 'org.zulip.zulip-electron';
|
||||
|
||||
const botsList = [];
|
||||
let botsListLoaded = false;
|
||||
|
||||
// this function load list of bots from the server
|
||||
// sync=True for a synchronous getJSON request
|
||||
// in case botsList isn't already completely loaded when required in parseRely
|
||||
function loadBots(sync = false) {
|
||||
const { $ } = window;
|
||||
botsList.length = 0;
|
||||
if (sync) {
|
||||
$.ajaxSetup({async: false});
|
||||
}
|
||||
$.getJSON('/json/users')
|
||||
.done(data => {
|
||||
const members = data.members;
|
||||
members.forEach(membersRow => {
|
||||
if (membersRow.is_bot) {
|
||||
const bot = `@${membersRow.full_name}`;
|
||||
const mention = `@**${bot.replace(/^@/, '')}**`;
|
||||
botsList.push([bot, mention]);
|
||||
}
|
||||
});
|
||||
botsListLoaded = true;
|
||||
})
|
||||
.fail(error => {
|
||||
console.log('Request failed: ', error.responseText);
|
||||
console.log('Request status: ', error.statusText);
|
||||
});
|
||||
if (sync) {
|
||||
$.ajaxSetup({async: true});
|
||||
}
|
||||
}
|
||||
|
||||
function checkElements(...elements) {
|
||||
let status = true;
|
||||
elements.forEach(element => {
|
||||
if (element === null || element === undefined) {
|
||||
status = false;
|
||||
}
|
||||
});
|
||||
return status;
|
||||
}
|
||||
|
||||
function customReply(reply) {
|
||||
// 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 = document.querySelector('#compose-textarea');
|
||||
const messagebox = document.querySelector(messageboxSelector);
|
||||
const sendButton = 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
|
||||
function focusCurrentServer() {
|
||||
currentWindow.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
|
||||
function parseReply(reply) {
|
||||
const usersDiv = document.querySelectorAll('#user_presences li');
|
||||
const streamHolder = document.querySelectorAll('#stream_filters li');
|
||||
const users = [];
|
||||
const streams = [];
|
||||
|
||||
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 synchronous getJSON request for list
|
||||
if (botsListLoaded === false) {
|
||||
loadBots(true);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
function setupReply(id) {
|
||||
const { narrow } = window;
|
||||
narrow.by_subject(id, { trigger: 'notification' });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
appId,
|
||||
checkElements,
|
||||
customReply,
|
||||
parseReply,
|
||||
setupReply,
|
||||
focusCurrentServer,
|
||||
loadBots
|
||||
};
|
@@ -1,27 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
remote: { app }
|
||||
} = require('electron');
|
||||
|
||||
const DefaultNotification = require('./default-notification');
|
||||
const { appId, loadBots } = require('./helpers');
|
||||
|
||||
// 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);
|
||||
|
||||
window.Notification = DefaultNotification;
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
const DarwinNotification = require('./darwin-notifications');
|
||||
window.Notification = DarwinNotification;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
// Call this function only when user is logged in
|
||||
// eslint-disable-next-line no-undef, camelcase
|
||||
if (page_params.realm_uri) {
|
||||
loadBots();
|
||||
}
|
||||
});
|
41
app/renderer/js/notification/index.ts
Normal file
41
app/renderer/js/notification/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {ipcRenderer} from "../typed-ipc-renderer.js";
|
||||
|
||||
export type NotificationData = {
|
||||
close: () => void;
|
||||
title: string;
|
||||
dir: NotificationDirection;
|
||||
lang: string;
|
||||
body: string;
|
||||
tag: string;
|
||||
icon: string;
|
||||
data: unknown;
|
||||
};
|
||||
|
||||
export function newNotification(
|
||||
title: string,
|
||||
options: NotificationOptions,
|
||||
dispatch: (type: string, eventInit: EventInit) => boolean,
|
||||
): NotificationData {
|
||||
const notification = new Notification(title, {...options, silent: true});
|
||||
for (const type of ["click", "close", "error", "show"]) {
|
||||
notification.addEventListener(type, (ev: Event) => {
|
||||
if (type === "click") ipcRenderer.send("focus-this-webview");
|
||||
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,
|
||||
icon: notification.icon,
|
||||
data: notification.data,
|
||||
};
|
||||
}
|
24
app/renderer/js/pages/about.ts
Normal file
24
app/renderer/js/pages/about.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {app} from "@electron/remote";
|
||||
|
||||
import {bundleUrl} from "../../../common/paths.js";
|
||||
|
||||
export class AboutView {
|
||||
static async create(): Promise<AboutView> {
|
||||
return new AboutView(
|
||||
await (await fetch(new URL("app/renderer/about.html", bundleUrl))).text(),
|
||||
);
|
||||
}
|
||||
|
||||
readonly $view: HTMLElement;
|
||||
|
||||
private constructor(templateHtml: string) {
|
||||
this.$view = document.createElement("div");
|
||||
const $shadow = this.$view.attachShadow({mode: "open"});
|
||||
$shadow.innerHTML = templateHtml;
|
||||
$shadow.querySelector("#version")!.textContent = `v${app.getVersion()}`;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
@@ -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();
|
||||
};
|
13
app/renderer/js/pages/network.ts
Normal file
13
app/renderer/js/pages/network.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {ipcRenderer} from "../typed-ipc-renderer.js";
|
||||
|
||||
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");
|
||||
});
|
||||
}
|
@@ -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(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
|
||||
}
|
||||
if (messageCount === 0) {
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
} else {
|
||||
mainWindow.webContents.send('render-taskbar-icon', messageCount);
|
||||
}
|
||||
}
|
||||
|
||||
updateTaskbarIcon(data, text, mainWindow) {
|
||||
const img = electron.nativeImage.createFromDataURL(data);
|
||||
mainWindow.setOverlayIcon(img, text);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new BadgeSettings();
|
@@ -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;
|
84
app/renderer/js/pages/preference/base-section.ts
Normal file
84
app/renderer/js/pages/preference/base-section.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type {Html} from "../../../../common/html.js";
|
||||
import {html} from "../../../../common/html.js";
|
||||
import {generateNodeFromHtml} from "../../components/base.js";
|
||||
import {ipcRenderer} from "../../typed-ipc-renderer.js";
|
||||
|
||||
type BaseSectionProps = {
|
||||
$element: HTMLElement;
|
||||
disabled?: boolean;
|
||||
value: boolean;
|
||||
clickHandler: () => void;
|
||||
};
|
||||
|
||||
export function generateSettingOption(props: BaseSectionProps): void {
|
||||
const {$element, disabled, value, clickHandler} = props;
|
||||
|
||||
$element.textContent = "";
|
||||
|
||||
const $optionControl = generateNodeFromHtml(
|
||||
generateOptionHtml(value, disabled),
|
||||
);
|
||||
$element.append($optionControl);
|
||||
|
||||
if (!disabled) {
|
||||
$optionControl.addEventListener("click", clickHandler);
|
||||
}
|
||||
}
|
||||
|
||||
export function generateOptionHtml(
|
||||
settingOption: boolean,
|
||||
disabled?: boolean,
|
||||
): Html {
|
||||
const labelHtml = disabled
|
||||
? // eslint-disable-next-line unicorn/template-indent
|
||||
html`<label
|
||||
class="disallowed"
|
||||
title="Setting locked by system administrator."
|
||||
></label>`
|
||||
: html`<label></label>`;
|
||||
if (settingOption) {
|
||||
return html`
|
||||
<div class="action">
|
||||
<div class="switch">
|
||||
<input class="toggle toggle-round" type="checkbox" checked disabled />
|
||||
${labelHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="action">
|
||||
<div class="switch">
|
||||
<input class="toggle toggle-round" type="checkbox" />
|
||||
${labelHtml}
|
||||
</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
|
||||
*/
|
||||
export function generateSelectHtml(
|
||||
options: Record<string, string>,
|
||||
className?: string,
|
||||
idName?: string,
|
||||
): Html {
|
||||
const optionsHtml = html``.join(
|
||||
Object.keys(options).map(
|
||||
(key) => html`
|
||||
<option name="${key}" value="${key}">${options[key]}</option>
|
||||
`,
|
||||
),
|
||||
);
|
||||
return html`
|
||||
<select class="${className}" id="${idName}">
|
||||
${optionsHtml}
|
||||
</select>
|
||||
`;
|
||||
}
|
||||
|
||||
export function reloadApp(): void {
|
||||
ipcRenderer.send("forward-message", "reload-viewer");
|
||||
}
|
67
app/renderer/js/pages/preference/connected-org-section.ts
Normal file
67
app/renderer/js/pages/preference/connected-org-section.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {html} from "../../../../common/html.js";
|
||||
import * as t from "../../../../common/translation-util.js";
|
||||
import {ipcRenderer} from "../../typed-ipc-renderer.js";
|
||||
import * as DomainUtil from "../../utils/domain-util.js";
|
||||
|
||||
import {reloadApp} from "./base-section.js";
|
||||
import {initFindAccounts} from "./find-accounts.js";
|
||||
import {initServerInfoForm} from "./server-info-form.js";
|
||||
|
||||
type ConnectedOrgSectionProps = {
|
||||
$root: Element;
|
||||
};
|
||||
|
||||
export function initConnectedOrgSection({
|
||||
$root,
|
||||
}: ConnectedOrgSectionProps): void {
|
||||
$root.textContent = "";
|
||||
|
||||
const servers = DomainUtil.getDomains();
|
||||
$root.innerHTML = html`
|
||||
<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 organizations 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.__("Find accounts by email")}</div>
|
||||
<div id="find-accounts-container"></div>
|
||||
</div>
|
||||
`.html;
|
||||
|
||||
const $serverInfoContainer = $root.querySelector("#server-info-container")!;
|
||||
const $existingServers = $root.querySelector("#existing-servers")!;
|
||||
const $newOrgButton: HTMLButtonElement =
|
||||
$root.querySelector("#new-org-button")!;
|
||||
const $findAccountsContainer = $root.querySelector(
|
||||
"#find-accounts-container",
|
||||
)!;
|
||||
|
||||
const noServerText = t.__(
|
||||
"All the connected organizations will appear here.",
|
||||
);
|
||||
// Show noServerText if no servers are there otherwise hide it
|
||||
$existingServers.textContent = servers.length === 0 ? noServerText : "";
|
||||
|
||||
for (const [i, server] of servers.entries()) {
|
||||
initServerInfoForm({
|
||||
$root: $serverInfoContainer,
|
||||
server,
|
||||
index: i,
|
||||
onChange: reloadApp,
|
||||
});
|
||||
}
|
||||
|
||||
$newOrgButton.addEventListener("click", () => {
|
||||
ipcRenderer.send("forward-message", "open-org-tab");
|
||||
});
|
||||
|
||||
initFindAccounts({
|
||||
$root: $findAccountsContainer,
|
||||
});
|
||||
}
|
@@ -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">Or 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;
|
66
app/renderer/js/pages/preference/find-accounts.ts
Normal file
66
app/renderer/js/pages/preference/find-accounts.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {html} from "../../../../common/html.js";
|
||||
import * as LinkUtil from "../../../../common/link-util.js";
|
||||
import * as t from "../../../../common/translation-util.js";
|
||||
import {generateNodeFromHtml} from "../../components/base.js";
|
||||
|
||||
type FindAccountsProps = {
|
||||
$root: Element;
|
||||
};
|
||||
|
||||
async function findAccounts(url: string): Promise<void> {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url.startsWith("http")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
await LinkUtil.openBrowser(new URL("/accounts/find", url));
|
||||
}
|
||||
|
||||
export function initFindAccounts(props: FindAccountsProps): void {
|
||||
const $findAccounts = generateNodeFromHtml(html`
|
||||
<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>
|
||||
`);
|
||||
props.$root.append($findAccounts);
|
||||
const $findAccountsButton = $findAccounts.querySelector(
|
||||
"#find-accounts-button",
|
||||
)!;
|
||||
const $serverUrlField: HTMLInputElement = $findAccounts.querySelector(
|
||||
"input.setting-input-value",
|
||||
)!;
|
||||
|
||||
$findAccountsButton.addEventListener("click", async () => {
|
||||
await findAccounts($serverUrlField.value);
|
||||
});
|
||||
|
||||
$serverUrlField.addEventListener("click", () => {
|
||||
if ($serverUrlField.value === "zulipchat.com") {
|
||||
$serverUrlField.setSelectionRange(0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
$serverUrlField.addEventListener("keypress", async (event) => {
|
||||
if (event.key === "Enter") {
|
||||
await findAccounts($serverUrlField.value);
|
||||
}
|
||||
});
|
||||
|
||||
$serverUrlField.addEventListener("input", () => {
|
||||
$serverUrlField.classList.toggle(
|
||||
"invalid-input-value",
|
||||
$serverUrlField.value === "",
|
||||
);
|
||||
});
|
||||
}
|
@@ -1,315 +0,0 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const { ipcRenderer, remote } = require('electron');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const { app, dialog } = remote;
|
||||
const currentBrowserWindow = remote.getCurrentWindow();
|
||||
const BaseSection = require(__dirname + '/base-section.js');
|
||||
const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
|
||||
|
||||
class GeneralSection extends BaseSection {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-pane">
|
||||
<div class="title">Appearance</div>
|
||||
<div id="appearance-option-settings" class="settings-card">
|
||||
<div class="setting-row" id="tray-option">
|
||||
<div class="setting-description">Show app icon in system tray</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="sidebar-option">
|
||||
<div class="setting-description">Show sidebar (<span class="code">Cmd Or Ctrl+S</span>)</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="badge-option">
|
||||
<div class="setting-description">Show app unread badge</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="flash-taskbar-option" style= "display:${process.platform === 'win32' ? '' : 'none'}">
|
||||
<div class="setting-description">Flash taskbar on new message</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">Desktop Notification</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="show-notification-option">
|
||||
<div class="setting-description">Show Desktop Notifications</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="silent-option">
|
||||
<div class="setting-description">Mute all sounds from Zulip</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">App Updates</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="betaupdate-option">
|
||||
<div class="setting-description">Get beta updates</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">Functionality</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="startAtLogin-option">
|
||||
<div class="setting-description">Start app at login</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="start-minimize-option">
|
||||
<div class="setting-description">Always start minimized</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="enable-spellchecker-option">
|
||||
<div class="setting-description">Enable Spellchecker (requires restart)</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">Add custom CSS</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="add-custom-css">
|
||||
<div class="setting-description">
|
||||
This will inject the selected css stylesheet in all the added accounts
|
||||
</div>
|
||||
<button class="custom-css-button blue">Add</button>
|
||||
</div>
|
||||
<div class="setting-row" id="remove-custom-css">
|
||||
<div class="setting-description">
|
||||
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div>
|
||||
</div>
|
||||
<div class="action red" id="css-delete-action">
|
||||
<i class="material-icons">indeterminate_check_box</i>
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">Reset Application Data</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="resetdata-option">
|
||||
<div class="setting-description">This will delete all application data including all added accounts and preferences
|
||||
</div>
|
||||
<button class="reset-data-button blue">Reset App Data</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.updateTrayOption();
|
||||
this.updateBadgeOption();
|
||||
this.updateSilentOption();
|
||||
this.updateUpdateOption();
|
||||
this.updateSidebarOption();
|
||||
this.updateStartAtLoginOption();
|
||||
this.updateResetDataOption();
|
||||
this.showDesktopNotification();
|
||||
this.enableSpellchecker();
|
||||
this.minimizeOnStart();
|
||||
this.addCustomCSS();
|
||||
this.showCustomCSSPath();
|
||||
this.removeCustomCSS();
|
||||
|
||||
// Platform specific settings
|
||||
// Flashing taskbar on Windows
|
||||
if (process.platform === 'win32') {
|
||||
this.updateFlashTaskbar();
|
||||
}
|
||||
}
|
||||
|
||||
updateTrayOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#tray-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('trayIcon', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('trayIcon');
|
||||
ConfigUtil.setConfigItem('trayIcon', newValue);
|
||||
ipcRenderer.send('forward-message', 'toggletray');
|
||||
this.updateTrayOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateBadgeOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#badge-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('badgeOption', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('badgeOption');
|
||||
ConfigUtil.setConfigItem('badgeOption', newValue);
|
||||
ipcRenderer.send('toggle-badge-option', newValue);
|
||||
this.updateBadgeOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateFlashTaskbar() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#flash-taskbar-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('flashTaskbarOnMessage', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('flashTaskbarOnMessage');
|
||||
ConfigUtil.setConfigItem('flashTaskbarOnMessage', newValue);
|
||||
this.updateFlashTaskbar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateUpdateOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#betaupdate-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('betaUpdate', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
|
||||
ConfigUtil.setConfigItem('betaUpdate', newValue);
|
||||
this.updateUpdateOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSilentOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#silent-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('silent', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('silent', true);
|
||||
ConfigUtil.setConfigItem('silent', newValue);
|
||||
this.updateSilentOption();
|
||||
currentBrowserWindow.send('toogle-silent', newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showDesktopNotification() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#show-notification-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('showNotification', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('showNotification', true);
|
||||
ConfigUtil.setConfigItem('showNotification', newValue);
|
||||
this.showDesktopNotification();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSidebarOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#sidebar-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('showSidebar', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('showSidebar');
|
||||
ConfigUtil.setConfigItem('showSidebar', newValue);
|
||||
ipcRenderer.send('forward-message', 'toggle-sidebar', newValue);
|
||||
this.updateSidebarOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateStartAtLoginOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#startAtLogin-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('startAtLogin', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('startAtLogin');
|
||||
ConfigUtil.setConfigItem('startAtLogin', newValue);
|
||||
ipcRenderer.send('toggleAutoLauncher', newValue);
|
||||
this.updateStartAtLoginOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableSpellchecker() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#enable-spellchecker-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('enableSpellchecker', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('enableSpellchecker');
|
||||
ConfigUtil.setConfigItem('enableSpellchecker', newValue);
|
||||
this.enableSpellchecker();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearAppDataDialog() {
|
||||
const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.';
|
||||
const getAppPath = path.join(app.getPath('appData'), app.getName());
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['YES', 'NO'],
|
||||
defaultId: 0,
|
||||
message: 'Are you sure',
|
||||
detail: clearAppDataMessage
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
fs.remove(getAppPath);
|
||||
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
customCssDialog() {
|
||||
const showDialogOptions = {
|
||||
title: 'Select file',
|
||||
defaultId: 1,
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'CSS file', extensions: ['css'] }]
|
||||
};
|
||||
|
||||
dialog.showOpenDialog(showDialogOptions, selectedFile => {
|
||||
if (selectedFile) {
|
||||
ConfigUtil.setConfigItem('customCSS', selectedFile[0]);
|
||||
ipcRenderer.send('forward-message', 'hard-reload');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateResetDataOption() {
|
||||
const resetDataButton = document.querySelector('#resetdata-option .reset-data-button');
|
||||
resetDataButton.addEventListener('click', () => {
|
||||
this.clearAppDataDialog();
|
||||
});
|
||||
}
|
||||
|
||||
minimizeOnStart() {
|
||||
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() {
|
||||
const customCSSButton = document.querySelector('#add-custom-css .custom-css-button');
|
||||
customCSSButton.addEventListener('click', () => {
|
||||
this.customCssDialog();
|
||||
});
|
||||
}
|
||||
|
||||
showCustomCSSPath() {
|
||||
if (!ConfigUtil.getConfigItem('customCSS')) {
|
||||
const cssPATH = document.getElementById('remove-custom-css');
|
||||
cssPATH.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
removeCustomCSS() {
|
||||
const removeCSSButton = document.getElementById('css-delete-action');
|
||||
removeCSSButton.addEventListener('click', () => {
|
||||
ConfigUtil.setConfigItem('customCSS');
|
||||
ipcRenderer.send('forward-message', 'hard-reload');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = GeneralSection;
|
705
app/renderer/js/pages/preference/general-section.ts
Normal file
705
app/renderer/js/pages/preference/general-section.ts
Normal file
@@ -0,0 +1,705 @@
|
||||
import type {OpenDialogOptions} from "electron/renderer";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
import * as remote from "@electron/remote";
|
||||
import {app, dialog, session} from "@electron/remote";
|
||||
import Tagify from "@yaireo/tagify";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import {z} from "zod";
|
||||
|
||||
import supportedLocales from "../../../../../public/translations/supported-locales.json";
|
||||
import * as ConfigUtil from "../../../../common/config-util.js";
|
||||
import * as EnterpriseUtil from "../../../../common/enterprise-util.js";
|
||||
import {html} from "../../../../common/html.js";
|
||||
import * as t from "../../../../common/translation-util.js";
|
||||
import {ipcRenderer} from "../../typed-ipc-renderer.js";
|
||||
|
||||
import {generateSelectHtml, generateSettingOption} from "./base-section.js";
|
||||
|
||||
const currentBrowserWindow = remote.getCurrentWindow();
|
||||
|
||||
type GeneralSectionProps = {
|
||||
$root: Element;
|
||||
};
|
||||
|
||||
export function initGeneralSection({$root}: GeneralSectionProps): void {
|
||||
$root.innerHTML = html`
|
||||
<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.getConfigItem("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.getConfigItem(
|
||||
"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 and accounts.",
|
||||
)}
|
||||
</div>
|
||||
<button class="factory-reset-button red w-150">
|
||||
${t.__("Factory Reset")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`.html;
|
||||
|
||||
updateTrayOption();
|
||||
updateBadgeOption();
|
||||
updateSilentOption();
|
||||
autoUpdateOption();
|
||||
betaUpdateOption();
|
||||
updateSidebarOption();
|
||||
updateStartAtLoginOption();
|
||||
factoryReset();
|
||||
showDesktopNotification();
|
||||
enableSpellchecker();
|
||||
minimizeOnStart();
|
||||
addCustomCss();
|
||||
showCustomCssPath();
|
||||
removeCustomCss();
|
||||
downloadFolder();
|
||||
updateQuitOnCloseOption();
|
||||
updatePromptDownloadOption();
|
||||
enableErrorReporting();
|
||||
setLocale();
|
||||
initSpellChecker();
|
||||
|
||||
// Platform specific settings
|
||||
|
||||
// Flashing taskbar on Windows
|
||||
if (process.platform === "win32") {
|
||||
updateFlashTaskbar();
|
||||
}
|
||||
|
||||
// Dock bounce on macOS
|
||||
if (process.platform === "darwin") {
|
||||
updateDockBouncing();
|
||||
}
|
||||
|
||||
// Auto hide menubar on Windows and Linux
|
||||
if (process.platform !== "darwin") {
|
||||
updateMenubarOption();
|
||||
}
|
||||
|
||||
function updateTrayOption(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#tray-option .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("trayIcon", true),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("trayIcon", true);
|
||||
ConfigUtil.setConfigItem("trayIcon", newValue);
|
||||
ipcRenderer.send("forward-message", "toggletray");
|
||||
updateTrayOption();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateMenubarOption(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#menubar-option .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("autoHideMenubar", false),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("autoHideMenubar", false);
|
||||
ConfigUtil.setConfigItem("autoHideMenubar", newValue);
|
||||
ipcRenderer.send("toggle-menubar", newValue);
|
||||
updateMenubarOption();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateBadgeOption(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#badge-option .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("badgeOption", true),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("badgeOption", true);
|
||||
ConfigUtil.setConfigItem("badgeOption", newValue);
|
||||
ipcRenderer.send("toggle-badge-option", newValue);
|
||||
updateBadgeOption();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateDockBouncing(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#dock-bounce-option .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("dockBouncing", true),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("dockBouncing", true);
|
||||
ConfigUtil.setConfigItem("dockBouncing", newValue);
|
||||
updateDockBouncing();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateFlashTaskbar(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#flash-taskbar-option .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("flashTaskbarOnMessage", true),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem(
|
||||
"flashTaskbarOnMessage",
|
||||
true,
|
||||
);
|
||||
ConfigUtil.setConfigItem("flashTaskbarOnMessage", newValue);
|
||||
updateFlashTaskbar();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function autoUpdateOption(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#autoupdate-option .setting-control")!,
|
||||
disabled: EnterpriseUtil.configItemExists("autoUpdate"),
|
||||
value: ConfigUtil.getConfigItem("autoUpdate", true),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("autoUpdate", true);
|
||||
ConfigUtil.setConfigItem("autoUpdate", newValue);
|
||||
if (!newValue) {
|
||||
ConfigUtil.setConfigItem("betaUpdate", false);
|
||||
betaUpdateOption();
|
||||
}
|
||||
|
||||
autoUpdateOption();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function betaUpdateOption(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#betaupdate-option .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("betaUpdate", false),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("betaUpdate", false);
|
||||
if (ConfigUtil.getConfigItem("autoUpdate", true)) {
|
||||
ConfigUtil.setConfigItem("betaUpdate", newValue);
|
||||
betaUpdateOption();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateSilentOption(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#silent-option .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("silent", false),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("silent", true);
|
||||
ConfigUtil.setConfigItem("silent", newValue);
|
||||
updateSilentOption();
|
||||
ipcRenderer.sendTo(
|
||||
currentBrowserWindow.webContents.id,
|
||||
"toggle-silent",
|
||||
newValue,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function showDesktopNotification(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector(
|
||||
"#show-notification-option .setting-control",
|
||||
)!,
|
||||
value: ConfigUtil.getConfigItem("showNotification", true),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("showNotification", true);
|
||||
ConfigUtil.setConfigItem("showNotification", newValue);
|
||||
showDesktopNotification();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateSidebarOption(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#sidebar-option .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("showSidebar", true),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("showSidebar", true);
|
||||
ConfigUtil.setConfigItem("showSidebar", newValue);
|
||||
ipcRenderer.send("forward-message", "toggle-sidebar", newValue);
|
||||
updateSidebarOption();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateStartAtLoginOption(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#startAtLogin-option .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("startAtLogin", false),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("startAtLogin", false);
|
||||
ConfigUtil.setConfigItem("startAtLogin", newValue);
|
||||
ipcRenderer.send("toggleAutoLauncher", newValue);
|
||||
updateStartAtLoginOption();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateQuitOnCloseOption(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#quitOnClose-option .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("quitOnClose", false),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("quitOnClose", false);
|
||||
ConfigUtil.setConfigItem("quitOnClose", newValue);
|
||||
updateQuitOnCloseOption();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function enableSpellchecker(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector(
|
||||
"#enable-spellchecker-option .setting-control",
|
||||
)!,
|
||||
value: ConfigUtil.getConfigItem("enableSpellchecker", true),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("enableSpellchecker", true);
|
||||
ConfigUtil.setConfigItem("enableSpellchecker", newValue);
|
||||
ipcRenderer.send("configure-spell-checker");
|
||||
enableSpellchecker();
|
||||
const spellcheckerLanguageInput: HTMLElement =
|
||||
$root.querySelector("#spellcheck-langs")!;
|
||||
const spellcheckerNote: HTMLElement = $root.querySelector("#note")!;
|
||||
spellcheckerLanguageInput.style.display =
|
||||
spellcheckerLanguageInput.style.display === "none" ? "" : "none";
|
||||
spellcheckerNote.style.display =
|
||||
spellcheckerNote.style.display === "none" ? "" : "none";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function enableErrorReporting(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector(
|
||||
"#enable-error-reporting .setting-control",
|
||||
)!,
|
||||
value: ConfigUtil.getConfigItem("errorReporting", true),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("errorReporting", true);
|
||||
ConfigUtil.setConfigItem("errorReporting", newValue);
|
||||
enableErrorReporting();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function 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");
|
||||
}
|
||||
}
|
||||
|
||||
function setLocale(): void {
|
||||
const langDiv: HTMLSelectElement = $root.querySelector(".lang-div")!;
|
||||
const langListHtml = generateSelectHtml(supportedLocales, "lang-menu");
|
||||
langDiv.innerHTML += langListHtml.html;
|
||||
// `langMenu` is the select-option dropdown menu formed after executing the previous command
|
||||
const langMenu: HTMLSelectElement = $root.querySelector(".lang-menu")!;
|
||||
|
||||
// The next three lines set the selected language visible on the dropdown button
|
||||
let language = ConfigUtil.getConfigItem("appLanguage", "en");
|
||||
language =
|
||||
language && langMenu.options.namedItem(language) ? language : "en";
|
||||
langMenu.options.namedItem(language)!.selected = true;
|
||||
|
||||
langMenu.addEventListener("change", () => {
|
||||
ConfigUtil.setConfigItem("appLanguage", langMenu.value);
|
||||
});
|
||||
}
|
||||
|
||||
function minimizeOnStart(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#start-minimize-option .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("startMinimized", false),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("startMinimized", false);
|
||||
ConfigUtil.setConfigItem("startMinimized", newValue);
|
||||
minimizeOnStart();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function addCustomCss(): void {
|
||||
const customCssButton = $root.querySelector(
|
||||
"#add-custom-css .custom-css-button",
|
||||
)!;
|
||||
customCssButton.addEventListener("click", async () => {
|
||||
await customCssDialog();
|
||||
});
|
||||
}
|
||||
|
||||
function showCustomCssPath(): void {
|
||||
if (!ConfigUtil.getConfigItem("customCSS", null)) {
|
||||
const cssPath: HTMLElement = $root.querySelector("#remove-custom-css")!;
|
||||
cssPath.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function removeCustomCss(): void {
|
||||
const removeCssButton = $root.querySelector("#css-delete-action")!;
|
||||
removeCssButton.addEventListener("click", () => {
|
||||
ConfigUtil.setConfigItem("customCSS", "");
|
||||
ipcRenderer.send("forward-message", "hard-reload");
|
||||
});
|
||||
}
|
||||
|
||||
async function 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 = $root.querySelector(
|
||||
".download-folder-path",
|
||||
)!;
|
||||
downloadFolderPath.textContent = filePaths[0];
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFolder(): void {
|
||||
const downloadFolder = $root.querySelector(
|
||||
"#download-folder .download-folder-button",
|
||||
)!;
|
||||
downloadFolder.addEventListener("click", async () => {
|
||||
await downloadFolderDialog();
|
||||
});
|
||||
}
|
||||
|
||||
function updatePromptDownloadOption(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#prompt-download .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("promptDownload", false),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("promptDownload", false);
|
||||
ConfigUtil.setConfigItem("promptDownload", newValue);
|
||||
updatePromptDownloadOption();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function 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.promises.rmdir(getAppPath, {recursive: true});
|
||||
setTimeout(() => {
|
||||
ipcRenderer.send("clear-app-settings");
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function factoryReset(): void {
|
||||
const factoryResetButton = $root.querySelector(
|
||||
"#factory-reset-option .factory-reset-button",
|
||||
)!;
|
||||
factoryResetButton.addEventListener("click", async () => {
|
||||
await factoryResetSettings();
|
||||
});
|
||||
}
|
||||
|
||||
function initSpellChecker(): void {
|
||||
// The Electron API is a no-op on macOS and macOS default spellchecker is used.
|
||||
if (process.platform === "darwin") {
|
||||
const note: HTMLElement = $root.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 = $root.querySelector("#note")!;
|
||||
note.append(
|
||||
t.__("You can select a maximum of 3 languages for spellchecking."),
|
||||
);
|
||||
const spellDiv: HTMLElement = $root.querySelector("#spellcheck-langs")!;
|
||||
spellDiv.innerHTML += html`
|
||||
<div class="setting-description">${t.__("Spellchecker Languages")}</div>
|
||||
<div id="spellcheck-langs-value">
|
||||
<input name="spellcheck" placeholder="Enter Languages" />
|
||||
</div>
|
||||
`.html;
|
||||
|
||||
const availableLanguages = session.fromPartition(
|
||||
"persist:webviewsession",
|
||||
).availableSpellCheckerLanguages;
|
||||
let languagePairs = new Map<string, string>();
|
||||
for (const l of availableLanguages) {
|
||||
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: HTMLInputElement = $root.querySelector(
|
||||
"input[name=spellcheck]",
|
||||
)!;
|
||||
const tagify = new Tagify(tagField, {
|
||||
whitelist: [...languagePairs.keys()],
|
||||
enforceWhitelist: true,
|
||||
maxTags: 3,
|
||||
dropdown: {
|
||||
enabled: 0,
|
||||
maxItems: Number.POSITIVE_INFINITY,
|
||||
closeOnSelect: false,
|
||||
highlightFirst: true,
|
||||
position: "manual",
|
||||
classname: "settings-tagify-dropdown",
|
||||
},
|
||||
});
|
||||
tagify.DOM.input.addEventListener("focus", () => {
|
||||
tagify.dropdown.show();
|
||||
$root
|
||||
.querySelector("#spellcheck-langs-value")!
|
||||
.append(tagify.DOM.dropdown);
|
||||
});
|
||||
tagify.DOM.input.addEventListener("blur", () => {
|
||||
tagify.dropdown.hide(true);
|
||||
tagify.DOM.dropdown.remove();
|
||||
});
|
||||
|
||||
const configuredLanguages: string[] = (
|
||||
ConfigUtil.getConfigItem("spellcheckerLanguages", null) ?? []
|
||||
).map(
|
||||
(code: string) =>
|
||||
[...languagePairs].find((pair) => pair[1] === code)![0],
|
||||
);
|
||||
tagify.addTags(configuredLanguages);
|
||||
|
||||
tagField.addEventListener("change", () => {
|
||||
if (tagField.value.length === 0) {
|
||||
ConfigUtil.setConfigItem("spellcheckerLanguages", []);
|
||||
ipcRenderer.send("configure-spell-checker");
|
||||
} else {
|
||||
const data: unknown = JSON.parse(tagField.value);
|
||||
const spellLangs: string[] = z
|
||||
.array(z.object({value: z.string()}))
|
||||
.parse(data)
|
||||
.map((elt) => languagePairs.get(elt.value)!);
|
||||
ConfigUtil.setConfigItem("spellcheckerLanguages", spellLangs);
|
||||
ipcRenderer.send("configure-spell-checker");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Do not display the spellchecker input and note if it is disabled
|
||||
if (!ConfigUtil.getConfigItem("enableSpellchecker", true)) {
|
||||
const spellcheckerLanguageInput: HTMLElement =
|
||||
$root.querySelector("#spellcheck-langs")!;
|
||||
const spellcheckerNote: HTMLElement = $root.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;
|
74
app/renderer/js/pages/preference/nav.ts
Normal file
74
app/renderer/js/pages/preference/nav.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type {Html} from "../../../../common/html.js";
|
||||
import {html} from "../../../../common/html.js";
|
||||
import * as t from "../../../../common/translation-util.js";
|
||||
import type {NavItem} from "../../../../common/types.js";
|
||||
import {generateNodeFromHtml} from "../../components/base.js";
|
||||
|
||||
type PreferenceNavProps = {
|
||||
$root: Element;
|
||||
onItemSelected: (navItem: NavItem) => void;
|
||||
};
|
||||
|
||||
export default class PreferenceNav {
|
||||
navItems: NavItem[];
|
||||
$el: Element;
|
||||
constructor(private readonly props: PreferenceNavProps) {
|
||||
this.navItems = [
|
||||
"General",
|
||||
"Network",
|
||||
"AddServer",
|
||||
"Organizations",
|
||||
"Shortcuts",
|
||||
];
|
||||
|
||||
this.$el = generateNodeFromHtml(this.templateHtml());
|
||||
this.props.$root.append(this.$el);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
templateHtml(): Html {
|
||||
const navItemsHtml = html``.join(
|
||||
this.navItems.map(
|
||||
(navItem) => html`
|
||||
<div class="nav" id="nav-${navItem}">${t.__(navItem)}</div>
|
||||
`,
|
||||
),
|
||||
);
|
||||
|
||||
return html`
|
||||
<div>
|
||||
<div id="settings-header">${t.__("Settings")}</div>
|
||||
<div id="nav-container">${navItemsHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
registerListeners(): void {
|
||||
for (const navItem of this.navItems) {
|
||||
const $item = this.$el.querySelector(`#nav-${CSS.escape(navItem)}`)!;
|
||||
$item.addEventListener("click", () => {
|
||||
this.props.onItemSelected(navItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
select(navItemToSelect: NavItem): void {
|
||||
for (const navItem of this.navItems) {
|
||||
if (navItem === navItemToSelect) {
|
||||
this.activate(navItem);
|
||||
} else {
|
||||
this.deactivate(navItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activate(navItem: NavItem): void {
|
||||
const $item = this.$el.querySelector(`#nav-${CSS.escape(navItem)}`)!;
|
||||
$item.classList.add("active");
|
||||
}
|
||||
|
||||
deactivate(navItem: NavItem): void {
|
||||
const $item = this.$el.querySelector(`#nav-${CSS.escape(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;
|
136
app/renderer/js/pages/preference/network-section.ts
Normal file
136
app/renderer/js/pages/preference/network-section.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as ConfigUtil from "../../../../common/config-util.js";
|
||||
import {html} from "../../../../common/html.js";
|
||||
import * as t from "../../../../common/translation-util.js";
|
||||
import {ipcRenderer} from "../../typed-ipc-renderer.js";
|
||||
|
||||
import {generateSettingOption} from "./base-section.js";
|
||||
|
||||
type NetworkSectionProps = {
|
||||
$root: Element;
|
||||
};
|
||||
|
||||
export function initNetworkSection({$root}: NetworkSectionProps): void {
|
||||
$root.innerHTML = html`
|
||||
<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>
|
||||
`.html;
|
||||
|
||||
const $proxyPac: HTMLInputElement = $root.querySelector(
|
||||
"#proxy-pac-option .setting-input-value",
|
||||
)!;
|
||||
const $proxyRules: HTMLInputElement = $root.querySelector(
|
||||
"#proxy-rules-option .setting-input-value",
|
||||
)!;
|
||||
const $proxyBypass: HTMLInputElement = $root.querySelector(
|
||||
"#proxy-bypass-option .setting-input-value",
|
||||
)!;
|
||||
const $proxySaveAction = $root.querySelector("#proxy-save-action")!;
|
||||
const $manualProxyBlock = $root.querySelector(".manual-proxy-block")!;
|
||||
|
||||
toggleManualProxySettings(ConfigUtil.getConfigItem("useManualProxy", false));
|
||||
updateProxyOption();
|
||||
|
||||
$proxyPac.value = ConfigUtil.getConfigItem("proxyPAC", "");
|
||||
$proxyRules.value = ConfigUtil.getConfigItem("proxyRules", "");
|
||||
$proxyBypass.value = ConfigUtil.getConfigItem("proxyBypass", "");
|
||||
|
||||
$proxySaveAction.addEventListener("click", () => {
|
||||
ConfigUtil.setConfigItem("proxyPAC", $proxyPac.value);
|
||||
ConfigUtil.setConfigItem("proxyRules", $proxyRules.value);
|
||||
ConfigUtil.setConfigItem("proxyBypass", $proxyBypass.value);
|
||||
|
||||
ipcRenderer.send("forward-message", "reload-proxy", true);
|
||||
});
|
||||
|
||||
function toggleManualProxySettings(option: boolean): void {
|
||||
$manualProxyBlock.classList.toggle("hidden", !option);
|
||||
}
|
||||
|
||||
function updateProxyOption(): void {
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#use-system-settings .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("useSystemProxy", false),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("useSystemProxy", false);
|
||||
const manualProxyValue = ConfigUtil.getConfigItem(
|
||||
"useManualProxy",
|
||||
false,
|
||||
);
|
||||
if (manualProxyValue && newValue) {
|
||||
ConfigUtil.setConfigItem("useManualProxy", !manualProxyValue);
|
||||
toggleManualProxySettings(!manualProxyValue);
|
||||
}
|
||||
|
||||
if (!newValue) {
|
||||
// Remove proxy system proxy settings
|
||||
ConfigUtil.setConfigItem("proxyRules", "");
|
||||
ipcRenderer.send("forward-message", "reload-proxy", false);
|
||||
}
|
||||
|
||||
ConfigUtil.setConfigItem("useSystemProxy", newValue);
|
||||
updateProxyOption();
|
||||
},
|
||||
});
|
||||
generateSettingOption({
|
||||
$element: $root.querySelector("#use-manual-settings .setting-control")!,
|
||||
value: ConfigUtil.getConfigItem("useManualProxy", false),
|
||||
clickHandler() {
|
||||
const newValue = !ConfigUtil.getConfigItem("useManualProxy", false);
|
||||
const systemProxyValue = ConfigUtil.getConfigItem(
|
||||
"useSystemProxy",
|
||||
false,
|
||||
);
|
||||
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);
|
||||
updateProxyOption();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,71 +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="title">URL of Zulip organization</div>
|
||||
<div class="server-info-row">
|
||||
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or chat.your-organization.com"/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<div class="action blue server-save-action">
|
||||
<i class="material-icons">add_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() {
|
||||
this.$saveServerButton.children[1].innerHTML = 'Adding...';
|
||||
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
|
||||
DomainUtil.addDomain(serverConf).then(() => {
|
||||
this.props.onChange(this.props.index);
|
||||
});
|
||||
}, errorMessage => {
|
||||
this.$saveServerButton.children[1].innerHTML = 'Add';
|
||||
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;
|
103
app/renderer/js/pages/preference/new-server-form.ts
Normal file
103
app/renderer/js/pages/preference/new-server-form.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import {dialog} from "@electron/remote";
|
||||
|
||||
import {html} from "../../../../common/html.js";
|
||||
import * as LinkUtil from "../../../../common/link-util.js";
|
||||
import * as t from "../../../../common/translation-util.js";
|
||||
import {generateNodeFromHtml} from "../../components/base.js";
|
||||
import {ipcRenderer} from "../../typed-ipc-renderer.js";
|
||||
import * as DomainUtil from "../../utils/domain-util.js";
|
||||
|
||||
type NewServerFormProps = {
|
||||
$root: Element;
|
||||
onChange: () => void;
|
||||
};
|
||||
|
||||
export function initNewServerForm({$root, onChange}: NewServerFormProps): void {
|
||||
const $newServerForm = generateNodeFromHtml(html`
|
||||
<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>
|
||||
`);
|
||||
const $saveServerButton: HTMLButtonElement =
|
||||
$newServerForm.querySelector("#connect")!;
|
||||
$root.textContent = "";
|
||||
$root.append($newServerForm);
|
||||
const $newServerUrl: HTMLInputElement = $newServerForm.querySelector(
|
||||
"input.setting-input-value",
|
||||
)!;
|
||||
|
||||
async function submitFormHandler(): Promise<void> {
|
||||
$saveServerButton.textContent = "Connecting...";
|
||||
let serverConf;
|
||||
try {
|
||||
serverConf = await DomainUtil.checkDomain($newServerUrl.value.trim());
|
||||
} catch (error: unknown) {
|
||||
$saveServerButton.textContent = "Connect";
|
||||
await dialog.showMessageBox({
|
||||
type: "error",
|
||||
message:
|
||||
error instanceof Error
|
||||
? `${error.name}: ${error.message}`
|
||||
: "Unknown error",
|
||||
buttons: ["OK"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await DomainUtil.addDomain(serverConf);
|
||||
onChange();
|
||||
}
|
||||
|
||||
$saveServerButton.addEventListener("click", async () => {
|
||||
await submitFormHandler();
|
||||
});
|
||||
$newServerUrl.addEventListener("keypress", async (event) => {
|
||||
if (event.key === "Enter") {
|
||||
await submitFormHandler();
|
||||
}
|
||||
});
|
||||
|
||||
// Open create new org link in default browser
|
||||
const link = "https://zulip.com/new/";
|
||||
const externalCreateNewOrgElement = $root.querySelector(
|
||||
"#open-create-org-link",
|
||||
)!;
|
||||
externalCreateNewOrgElement.addEventListener("click", async () => {
|
||||
await LinkUtil.openBrowser(new URL(link));
|
||||
});
|
||||
|
||||
const networkSettingsId = $root.querySelector(".server-network-option")!;
|
||||
networkSettingsId.addEventListener("click", () => {
|
||||
ipcRenderer.send("forward-message", "open-network-settings");
|
||||
});
|
||||
}
|
@@ -1,99 +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}`;
|
||||
}
|
||||
|
||||
// Handle toggling and reflect changes in preference page
|
||||
handleToggle(elementName, state) {
|
||||
const inputSelector = `#${elementName} .action .switch input`;
|
||||
const input = document.querySelector(inputSelector);
|
||||
if (input) {
|
||||
input.checked = state;
|
||||
}
|
||||
}
|
||||
|
||||
registerIpcs() {
|
||||
ipcRenderer.on('switch-settings-nav', (event, navItem) => {
|
||||
this.handleNavigation(navItem);
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggle-sidebar', (event, state) => {
|
||||
this.handleToggle('sidebar-option', state);
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggletray', (event, state) => {
|
||||
this.handleToggle('tray-option', state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
const preferenceView = new PreferenceView();
|
||||
preferenceView.init();
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user