mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-23 16:13:37 +00:00
Compare commits
438 Commits
disable-au
...
new-repo-u
Author | SHA1 | Date | |
---|---|---|---|
|
d9e09374c2 | ||
|
04e9936e52 | ||
|
c6b7e9a9ee | ||
|
b4073ba7ac | ||
|
b504f31c26 | ||
|
f71660b3f0 | ||
|
5d0516887d | ||
|
0dd86f0814 | ||
|
eaecc92055 | ||
|
4f65c36a72 | ||
|
2380b650c9 | ||
|
1d713f1df2 | ||
|
7fa9c291cb | ||
|
8a40e36a63 | ||
|
3456720135 | ||
|
43840a5245 | ||
|
d99c29301e | ||
|
6e760973ff | ||
|
88c64e9dd6 | ||
|
e9db11c156 | ||
|
8e8de212d8 | ||
|
0e0d7e7c4a | ||
|
6b98a49245 | ||
|
8e5c326d74 | ||
|
862e9e2c8c | ||
|
8a0b047d8f | ||
|
4a833ef603 | ||
|
fefb7247d8 | ||
|
54a6903236 | ||
|
92ef1bd532 | ||
|
bf2e04b3bb | ||
|
3e389ea5db | ||
|
8edff28daa | ||
|
ef6abbf49e | ||
|
0f638dd4b5 | ||
|
92f0e46927 | ||
|
a0dd3832c2 | ||
|
a2e84595c9 | ||
|
9e33ebae62 | ||
|
9bdc5dd9e4 | ||
|
ac89ac1cb4 | ||
|
31da1131a4 | ||
|
4980c71e5a | ||
|
3e73511357 | ||
|
5f23c8570b | ||
|
12f79e18a1 | ||
|
ad7f760444 | ||
|
7314c1f1dd | ||
|
46c3d352a1 | ||
|
cef8eadc8f | ||
|
d3f742719c | ||
|
171d88755c | ||
|
09ac1bd338 | ||
|
48dd12b738 | ||
|
61bbd1cf22 | ||
|
bf722da390 | ||
|
c24f5b3e45 | ||
|
3d0f4d88af | ||
|
a2f412c3de | ||
|
08559c2f4a | ||
|
1f4509a6d2 | ||
|
06c8b2e8a2 | ||
|
2aa15ee11a | ||
|
3c676672ec | ||
|
c937317ecf | ||
|
44dceda50e | ||
|
e1407cb6f7 | ||
|
6899a6bc20 | ||
|
925fec71d5 | ||
|
f847c565f9 | ||
|
c2a380d308 | ||
|
ac6b206583 | ||
|
988405c665 | ||
|
a209804692 | ||
|
95c4df1898 | ||
|
1048c91d9e | ||
|
d723c5cd1c | ||
|
dcc7cd118c | ||
|
d2649dd5d7 | ||
|
25e6f6d482 | ||
|
bd805b8324 | ||
|
e1d98e0d8e | ||
|
89e5d2892b | ||
|
ab28b56377 | ||
|
71aa7e28ba | ||
|
ba2bfba459 | ||
|
d181b0c2e5 | ||
|
bda5c62928 | ||
|
b2b4fd1003 | ||
|
19770f5fbf | ||
|
4592396c4b | ||
|
19d3c0f5b5 | ||
|
af87789c7f | ||
|
36e710dbfe | ||
|
a6e1c93d81 | ||
|
684d88dd7f | ||
|
45aedbd9db | ||
|
6f411a819c | ||
|
097738332b | ||
|
b777f8a50f | ||
|
50f06239bb | ||
|
94cbc786d6 | ||
|
75abf38515 | ||
|
c1af019182 | ||
|
1ea8aea22a | ||
|
a7e418885a | ||
|
0ac823c85b | ||
|
1096188ddf | ||
|
8d0b68316c | ||
|
c45ad7096f | ||
|
5ca7937e52 | ||
|
481b99a2ab | ||
|
7eb4adf722 | ||
|
4d21a8f8d8 | ||
|
94270af265 | ||
|
177bff6113 | ||
|
b4fa4cacb6 | ||
|
06abf6716a | ||
|
0ad4ea4fdf | ||
|
2ece432d19 | ||
|
10cd7850ca | ||
|
86e3d06e52 | ||
|
6ebc9acbf4 | ||
|
278dc686e1 | ||
|
cec98c030e | ||
|
f10d6eb115 | ||
|
8c5e928e9c | ||
|
8d142c8c3f | ||
|
a09769c560 | ||
|
bb99015fd2 | ||
|
24f5c9b226 | ||
|
dc6c4be5fe | ||
|
50647e330b | ||
|
73dc3db436 | ||
|
09cf21bf49 | ||
|
c30d0cc77b | ||
|
872ad4d3e7 | ||
|
6fd9e1be8b | ||
|
76c7f24161 | ||
|
f9c270492c | ||
|
371c580934 | ||
|
3eec4c2209 | ||
|
debbfb6b7d | ||
|
8bd1492586 | ||
|
1115c6d5c3 | ||
|
9ba279213c | ||
|
89c35cb1d4 | ||
|
21d6eb52c5 | ||
|
aa1538837b | ||
|
ea103380b6 | ||
|
60d10d88d1 | ||
|
124a842bbd | ||
|
7130103999 | ||
|
26a144e1c2 | ||
|
a5c1ae8726 | ||
|
ed5096840d | ||
|
3f6d256910 | ||
|
28421992ba | ||
|
82199dd1c3 | ||
|
952baf1f42 | ||
|
3342d7da91 | ||
|
c0ec292090 | ||
|
8f6fba0b97 | ||
|
d3453a3fe1 | ||
|
18e14fd3f8 | ||
|
c52bfd6a45 | ||
|
428087110a | ||
|
781883a898 | ||
|
7299cd51e6 | ||
|
a45254cff0 | ||
|
4095c121ba | ||
|
6787b8d557 | ||
|
306e0f3b5e | ||
|
f030d5d56b | ||
|
acf7c5a238 | ||
|
da22f75300 | ||
|
df35ffc3b0 | ||
|
eeb0b4a666 | ||
|
0a893c97c7 | ||
|
99a1711bb0 | ||
|
a27cf9e0e7 | ||
|
22d6c6a2f3 | ||
|
c742591471 | ||
|
339a415d60 | ||
|
f7696cc04d | ||
|
1eabf5c5a2 | ||
|
4d020c639b | ||
|
85a6cf6890 | ||
|
ef1ad820aa | ||
|
10f6d80e5f | ||
|
50ef02c324 | ||
|
deed18d75f | ||
|
0e3c6bceeb | ||
|
c2e138c16a | ||
|
8d1b027b0e | ||
|
7bb8d78973 | ||
|
a7c7791bf8 | ||
|
5d120b4416 | ||
|
ceaf13dee2 | ||
|
f81381dfec | ||
|
dbe89cdd09 | ||
|
14c59bdae1 | ||
|
0ac3e3f6d3 | ||
|
d69c1339e6 | ||
|
fe56a20334 | ||
|
cfc97c9b73 | ||
|
2e70b515da | ||
|
51e414a508 | ||
|
8e7a9bf230 | ||
|
6493ddb8ec | ||
|
31edbe0d67 | ||
|
9980fee785 | ||
|
ff9986ec6b | ||
|
f3423d394c | ||
|
a1da199627 | ||
|
537fbe8f9e | ||
|
3fccb33fca | ||
|
5638590c8b | ||
|
29ed00981d | ||
|
d7638c0b95 | ||
|
7fadbe877b | ||
|
32a21889fb | ||
|
c4a961f9da | ||
|
ceaa898570 | ||
|
73fe17041d | ||
|
9f756cad3e | ||
|
6db6b7c482 | ||
|
09c45e75e8 | ||
|
120b80cf65 | ||
|
22f705960d | ||
|
ca8ce1deaa | ||
|
f70432f4e3 | ||
|
60d693700e | ||
|
6e7333eab6 | ||
|
0d8dd1cd90 | ||
|
0ee3757774 | ||
|
811df9f381 | ||
|
8bd02cc7e4 | ||
|
9d5d221371 | ||
|
6006f1a3f8 | ||
|
4f96df4a34 | ||
|
a13558fa16 | ||
|
a1d19a385c | ||
|
c98667236e | ||
|
b80c3d007b | ||
|
70fb198a0b | ||
|
7276bfeaa7 | ||
|
abd71330ba | ||
|
e776222d6b | ||
|
30b05571e7 | ||
|
0a155c63e7 | ||
|
13c750ac6c | ||
|
48799f75d1 | ||
|
29f4e702ad | ||
|
3eb4cf4f64 | ||
|
1a97d8a5b0 | ||
|
2f96ec6199 | ||
|
52de465457 | ||
|
e3039cf5a9 | ||
|
6c120269eb | ||
|
b31fc6b66d | ||
|
435e5f086e | ||
|
603ad7dfcd | ||
|
a47a0e3e90 | ||
|
8e04920f54 | ||
|
4a7b84e483 | ||
|
ffd2ee36fa | ||
|
c11fa93642 | ||
|
a90dc0c82f | ||
|
028bc02d0a | ||
|
d673d5b76c | ||
|
2bf88aa912 | ||
|
c9f179a2ae | ||
|
c11089027c | ||
|
5c45ab7b66 | ||
|
6205ca0aca | ||
|
b83e2dd428 | ||
|
fd421a62d2 | ||
|
fa85241c79 | ||
|
b163c237b6 | ||
|
5351ee10fa | ||
|
d48b6ae80d | ||
|
2d07d40c92 | ||
|
55ae71c00c | ||
|
0c18bb5226 | ||
|
6d213c86a8 | ||
|
0c1cd96ed7 | ||
|
d77b1b5960 | ||
|
2c6c0c8c8d | ||
|
84bf59d32f | ||
|
256c352fb9 | ||
|
da28589c17 | ||
|
c452ee2ef6 | ||
|
3582aa4694 | ||
|
7f7eee2455 | ||
|
c716b8f233 | ||
|
4a40c75127 | ||
|
239631a2b6 | ||
|
89d1344e2f | ||
|
1948ba2cc3 | ||
|
b8da7dd6ee | ||
|
4a0efb7301 | ||
|
aedd95259d | ||
|
c8d7a79877 | ||
|
6e6db42b54 | ||
|
db79284fbb | ||
|
2434f06655 | ||
|
1d611d3382 | ||
|
a746194e9e | ||
|
7cc13f7a26 | ||
|
6a9bb152a0 | ||
|
8b6dcd355f | ||
|
91742a5770 | ||
|
fb74251a2c | ||
|
a920720f91 | ||
|
aa8e99b7a6 | ||
|
e23f8aaa58 | ||
|
5c3208d44c | ||
|
c0b57bbe2b | ||
|
afe4e8901b | ||
|
231e7fd9c2 | ||
|
a0d898a5b7 | ||
|
1abf62555c | ||
|
6befcbaa8f | ||
|
e56a01049b | ||
|
72cb8459ff | ||
|
0b83b22206 | ||
|
267d25e5c4 | ||
|
8401f8f5ce | ||
|
c4a7264f34 | ||
|
9d081ecd5a | ||
|
dc6582fa82 | ||
|
3b412672c6 | ||
|
04083bfa81 | ||
|
562e82d2f1 | ||
|
3b014e0715 | ||
|
13178ebc8f | ||
|
08693bf105 | ||
|
d7a0b63d62 | ||
|
a193ecf229 | ||
|
31f04754a4 | ||
|
ae7374475f | ||
|
7697d5d698 | ||
|
d3e1b5de45 | ||
|
9efa6191f7 | ||
|
d86797d2fc | ||
|
23eef7edb0 | ||
|
a1d5a35ccf | ||
|
62e8dfe180 | ||
|
8322054984 | ||
|
13ae6f07e9 | ||
|
dab92be54c | ||
|
fe9c66d8c2 | ||
|
43b4d511dc | ||
|
aa5a47ad53 | ||
|
ce27f92900 | ||
|
7be051bb6e | ||
|
c2a01adabe | ||
|
770926e6eb | ||
|
10ef627f59 | ||
|
4adba0f4b4 | ||
|
ed590c26e3 | ||
|
6d10291a87 | ||
|
3fe3a3da85 | ||
|
ba64438a99 | ||
|
bcc27894c4 | ||
|
0dd0f593d1 | ||
|
f5e9342f78 | ||
|
083ccdf229 | ||
|
1261786db2 | ||
|
7f567f55c3 | ||
|
dcd2abca6e | ||
|
2fb9efb981 | ||
|
7245b6a110 | ||
|
bcb8ffb55f | ||
|
77094596a5 | ||
|
06ad44bdd7 | ||
|
e719ba139c | ||
|
9853e9226c | ||
|
f2c76b5ca3 | ||
|
e6dbff995b | ||
|
4578d4a5f7 | ||
|
4b895a2312 | ||
|
53c0428a3a | ||
|
0a1866abb5 | ||
|
ce862a4890 | ||
|
1b1ad2cd61 | ||
|
ead7a06308 | ||
|
6659dd5097 | ||
|
ed1f0f6d5b | ||
|
79acf8a6e1 | ||
|
8e0033f03e | ||
|
9144c2630d | ||
|
fae05fc3b1 | ||
|
73603a4fd2 | ||
|
a498ffc7d6 | ||
|
7afcf13401 | ||
|
89a292559d | ||
|
be14517caf | ||
|
3b6c5ae532 | ||
|
40e3ed0f2f | ||
|
5d988858b0 | ||
|
3a974136a3 | ||
|
6ed5a5309c | ||
|
80c37fabb8 | ||
|
79366e19df | ||
|
f409bb0449 | ||
|
45bdde951f | ||
|
6b627780f0 | ||
|
6f67553da5 | ||
|
2e710a9322 | ||
|
91f3afa8fe | ||
|
f784345495 | ||
|
67da435154 | ||
|
c89733610d | ||
|
8f272a67b5 | ||
|
f6c4a76138 | ||
|
b90a4c5254 | ||
|
a06e09e565 | ||
|
ad5bef821e | ||
|
58bbd7bf30 | ||
|
90d080dc96 | ||
|
ad3fcf585e | ||
|
4b8f216bab | ||
|
e620e0c428 | ||
|
50b3151b5d | ||
|
0c32756485 | ||
|
0c0835e364 | ||
|
9e962a5c44 | ||
|
a218f7ea64 | ||
|
13a7f7475a | ||
|
48b17a1549 | ||
|
653598fd9e | ||
|
ddbc282f49 | ||
|
992d92b06d | ||
|
45867ef15e | ||
|
6572c90d49 | ||
|
1ed0011c88 |
17
.gitattributes
vendored
17
.gitattributes
vendored
@@ -1 +1,16 @@
|
||||
* text=auto
|
||||
* text=auto eol=lf
|
||||
|
||||
package-lock.json binary
|
||||
app/package-lock.json binary
|
||||
*.gif binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.eot binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.svg binary
|
||||
*.ttf binary
|
||||
*.png binary
|
||||
*.otf binary
|
||||
*.tif binary
|
||||
*.ogg binary
|
||||
|
8
.github/ISSUE_TEMPLATE.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
<!-- Please Include: -->
|
||||
- **Operating System**:
|
||||
- [ ] Windows
|
||||
- [ ] Linux/Ubuntu
|
||||
- [ ] macOS
|
||||
- **Clear steps to reproduce the issue**:
|
||||
- **Relevant error messages and/or screenshots**:
|
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
<!--
|
||||
Remove the fields that are not appropriate
|
||||
Please include:
|
||||
-->
|
||||
|
||||
**What's this PR do?**
|
||||
|
||||
**Any background context you want to provide?**
|
||||
|
||||
**Screenshots?**
|
||||
|
||||
**You have tested this PR on:**
|
||||
- [ ] Windows
|
||||
- [ ] Linux/Ubuntu
|
||||
- [ ] macOS
|
14
.gitignore
vendored
14
.gitignore
vendored
@@ -7,6 +7,13 @@ node_modules/
|
||||
# Compiled binary build directory
|
||||
dist/
|
||||
|
||||
#snap generated files
|
||||
snap/parts
|
||||
snap/prime
|
||||
snap/snap
|
||||
snap/stage
|
||||
snap/*.snap
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
@@ -23,4 +30,9 @@ yarn-error.log*
|
||||
|
||||
# miscellaneous
|
||||
.idea
|
||||
config.gypi
|
||||
config.gypi
|
||||
|
||||
# Test generated files
|
||||
# tests/package.json
|
||||
|
||||
.python-version
|
||||
|
25
.htmlhintrc
Normal file
25
.htmlhintrc
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"tagname-lowercase": true,
|
||||
"attr-lowercase": true,
|
||||
"attr-value-double-quotes": true,
|
||||
"attr-value-not-empty": false,
|
||||
"attr-no-duplication": true,
|
||||
"doctype-first": true,
|
||||
"tag-pair": true,
|
||||
"empty-tag-not-self-closed": true,
|
||||
"spec-char-escape": true,
|
||||
"id-unique": true,
|
||||
"src-not-empty": true,
|
||||
"title-require": true,
|
||||
"alt-require": false,
|
||||
"doctype-html5": true,
|
||||
"id-class-value": "dash",
|
||||
"style-disabled": false,
|
||||
"inline-style-disabled": false,
|
||||
"inline-script-disabled": false,
|
||||
"space-tab-mixed-disabled": "space4",
|
||||
"id-class-ad-disabled": false,
|
||||
"href-abs-or-rel": false,
|
||||
"attr-unsafe-chars": true,
|
||||
"head-script-disabled": true
|
||||
}
|
@@ -1 +0,0 @@
|
||||
2.7.9
|
67
.stylelintrc
Normal file
67
.stylelintrc
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"rules": {
|
||||
# Stylistic rules for CSS.
|
||||
"function-comma-space-after": "always",
|
||||
"function-comma-space-before": "never",
|
||||
"function-max-empty-lines": 0,
|
||||
"function-whitespace-after": "always",
|
||||
|
||||
"value-keyword-case": "lower",
|
||||
"value-list-comma-newline-after": "always-multi-line",
|
||||
"value-list-comma-space-after": "always-single-line",
|
||||
"value-list-comma-space-before": "never",
|
||||
"value-list-max-empty-lines": 0,
|
||||
|
||||
"unit-case": "lower",
|
||||
"property-case": "lower",
|
||||
"color-hex-case": "lower",
|
||||
|
||||
"declaration-bang-space-before": "always",
|
||||
"declaration-colon-newline-after": "always-multi-line",
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
"declaration-colon-space-before": "never",
|
||||
"declaration-block-semicolon-newline-after": "always",
|
||||
"declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-trailing-semicolon": "always",
|
||||
|
||||
"block-closing-brace-empty-line-before": "never",
|
||||
"block-closing-brace-newline-after": "always",
|
||||
"block-closing-brace-newline-before": "always",
|
||||
"block-opening-brace-newline-after": "always",
|
||||
"block-opening-brace-space-before": "always",
|
||||
|
||||
"selector-attribute-brackets-space-inside": "never",
|
||||
"selector-attribute-operator-space-after": "never",
|
||||
"selector-attribute-operator-space-before": "never",
|
||||
"selector-combinator-space-after": "always",
|
||||
"selector-combinator-space-before": "always",
|
||||
"selector-descendant-combinator-no-non-space": true,
|
||||
"selector-pseudo-class-parentheses-space-inside": "never",
|
||||
"selector-pseudo-element-case": "lower",
|
||||
"selector-pseudo-element-colon-notation": "double",
|
||||
"selector-type-case": "lower",
|
||||
"selector-list-comma-newline-after": "always",
|
||||
"selector-list-comma-space-before": "never",
|
||||
|
||||
"media-feature-colon-space-after": "always",
|
||||
"media-feature-colon-space-before": "never",
|
||||
"media-feature-name-case": "lower",
|
||||
"media-feature-parentheses-space-inside": "never",
|
||||
"media-feature-range-operator-space-after": "always",
|
||||
"media-feature-range-operator-space-before": "always",
|
||||
"media-query-list-comma-newline-after": "always",
|
||||
"media-query-list-comma-space-before": "never",
|
||||
|
||||
"at-rule-name-case": "lower",
|
||||
"at-rule-name-space-after": "always",
|
||||
"at-rule-semicolon-newline-after": "always",
|
||||
"at-rule-semicolon-space-before": "never",
|
||||
|
||||
"comment-whitespace-inside": "always",
|
||||
"indentation": 4,
|
||||
|
||||
# Limit language features
|
||||
"color-no-hex": true,
|
||||
"color-named": "never",
|
||||
}
|
||||
}
|
@@ -15,11 +15,12 @@ addons:
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- '6'
|
||||
- '8'
|
||||
|
||||
before_install:
|
||||
- npm install -g gulp
|
||||
- npm install
|
||||
- ./scripts/travis-xvfb.sh
|
||||
- npm install -g gulp
|
||||
- npm install
|
||||
|
||||
cache:
|
||||
directories:
|
||||
@@ -33,4 +34,4 @@ notifications:
|
||||
urls:
|
||||
- https://zulip.org/zulipbot/travis
|
||||
on_success: always
|
||||
on_failure: always
|
||||
on_failure: always
|
||||
|
@@ -2,20 +2,20 @@
|
||||
|
||||
Thanks for taking the time to contribute!
|
||||
|
||||
The following is a set of guidelines for contributing to Zulip Electron Desktop Client. These are just guidelines, not rules, so use your best judgement and feel free to propose changes to this document in a pull request.
|
||||
The following is a set of guidelines for contributing to Zulip's desktop Client. These are just guidelines, not rules, so use your best judgement and feel free to propose changes to this document in a pull request.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If you are new to Electron, please head over to [this](http://jlord.us/essential-electron/) great article.
|
||||
Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If you are new to Electron, please head over to [this](https://jlord.dev/blog/essential-electron) great article.
|
||||
|
||||
## Community
|
||||
|
||||
* The whole Zulip documentation, such as setting up a development environment, setting up with the Zulip webapp project, and testing, can be read [here](https://zulip.readthedocs.io).
|
||||
|
||||
* If you have any questions regarding zulip-electron, open an [issue](https://github.com/zulip/zulip-electron/issues/new/) or ask it on [chat.zulip.org](https://chat.zulip.org/#narrow/stream/electron).
|
||||
* If you have any questions regarding zulip-desktop, open an [issue](https://github.com/zulip/zulip-desktop/issues/new/) or ask it on [chat.zulip.org](https://chat.zulip.org/#narrow/stream/16-desktop).
|
||||
|
||||
## Issue
|
||||
Ensure the bug was not already reported by searching on GitHub under [issues](https://github.com/zulip/zulip-electron/issues). If you're unable to find an open issue addressing the bug, open a [new issue](https://github.com/zulip/zulip-electron/issues/new).
|
||||
Ensure the bug was not already reported by searching on GitHub under [issues](https://github.com/zulip/zulip-desktop/issues). If you're unable to find an open issue addressing the bug, open a [new issue](https://github.com/zulip/zulip-desktop/issues/new).
|
||||
|
||||
The [zulipbot](https://github.com/zulip/zulipbot) helps to claim an issue by commenting the following in the comment section: "**@zulipbot** claim". **@zulipbot** will assign you to the issue and label the issue as **in progress**. For more details, check out [**@zulipbot**](https://github.com/zulip/zulipbot).
|
||||
|
||||
@@ -38,7 +38,7 @@ For example:
|
||||
|
||||
|
||||
## Pull Requests
|
||||
Pull Requests are always welcome.
|
||||
Pull Requests are always welcome.
|
||||
|
||||
1. When you edit the code, please run `npm run test` to check the formatting of your code before you `git commit`.
|
||||
2. Ensure the PR description clearly describes the problem and solution. It should include:
|
||||
|
@@ -1,7 +0,0 @@
|
||||
---
|
||||
Please include:
|
||||
- `Operating System`
|
||||
- `Clear steps to reproduce the issue`
|
||||
- `Relevant error messages and/or screenshots`
|
||||
|
||||
|
36
README.md
36
README.md
@@ -1,39 +1,27 @@
|
||||
# Zulip Desktop Client
|
||||
[](https://travis-ci.org/zulip/zulip-electron)
|
||||
[](https://ci.appveyor.com/project/akashnimare/zulip-electron/branch/master)
|
||||
[](https://travis-ci.org/zulip/zulip-desktop)
|
||||
[](https://ci.appveyor.com/project/zulip/zulip-desktop/branch/master)
|
||||
[](https://github.com/sindresorhus/xo)
|
||||
[](https://chat.zulip.org)
|
||||
|
||||
Desktop client for Zulip. Available for Mac, Linux and Windows.
|
||||
Desktop client for Zulip. Available for Mac, Linux, and Windows.
|
||||
|
||||
<img src="http://i.imgur.com/ChzTq4F.png"/>
|
||||
|
||||
# Download
|
||||
Please see [installation guide](./how-to-install.md).
|
||||
Please see the [installation guide](https://zulipchat.com/help/desktop-app-install-guide).
|
||||
|
||||
# Features
|
||||
* Sign in to multiple teams
|
||||
* Native desktop Notifications
|
||||
* SpellChecker
|
||||
* OSX/Win/Linux installers
|
||||
* Automatic Updates (macOS/Windows)
|
||||
* Keyboard shortcuts
|
||||
|
||||
Description | Keys
|
||||
-----------------------| -----------------------
|
||||
Default shortcuts | <kbd>Cmd/Ctrl</kbd> <kbd>k</kbd>
|
||||
Manage Zulip Servers | <kbd>Cmd/Ctrl</kbd> <kbd>,</kbd>
|
||||
Back | <kbd>Cmd/Ctrl</kbd> <kbd>[</kbd>
|
||||
Forward | <kbd>Cmd/Ctrl</kbd> <kbd>]</kbd>
|
||||
|
||||
# Development
|
||||
Please see our [development guide](./development.md) to get started and run app locally.
|
||||
* Sign in to multiple organizations
|
||||
* Desktop notifications with inline reply
|
||||
* Tray/dock integration
|
||||
* Multi-language spell checker
|
||||
* Automatic updates
|
||||
|
||||
# Contribute
|
||||
|
||||
If you want to contribute please make sure to read [our documentation about contributing](./CONTRIBUTING.md) first.
|
||||
|
||||
* [Issue Tracker](https://github.com/zulip/zulip-electron/issues)
|
||||
* [Source Code](https://github.com/zulip/zulip-electron/)
|
||||
First, join us on the [Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html)!
|
||||
Also see our [contribution guidelines](./CONTRIBUTING.md) and our [development guide](./development.md).
|
||||
|
||||
# License
|
||||
Released under the [Apache-2.0](./LICENSE) license.
|
||||
|
@@ -1,25 +1,27 @@
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const { app, dialog } = require('electron');
|
||||
const { app, dialog, shell } = require('electron');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
const ConfigUtil = require('./../renderer/js/utils/config-util.js');
|
||||
|
||||
function appUpdater() {
|
||||
// Don't initiate auto-updates in development and on Linux system
|
||||
// since autoUpdater doesn't work on Linux
|
||||
if (isDev || process.platform === 'linux') {
|
||||
function appUpdater(updateFromMenu = false) {
|
||||
// Don't initiate auto-updates in development
|
||||
if (isDev) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.platform === 'linux' && !process.env.APPIMAGE) {
|
||||
const { linuxUpdateNotification } = require('./linuxupdater');
|
||||
linuxUpdateNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
let updateAvailable = false;
|
||||
|
||||
// Create Logs directory
|
||||
const LogsDir = `${app.getPath('userData')}/Logs`;
|
||||
|
||||
if (!fs.existsSync(LogsDir)) {
|
||||
fs.mkdirSync(LogsDir);
|
||||
}
|
||||
|
||||
// Log whats happening
|
||||
const log = require('electron-log');
|
||||
|
||||
@@ -28,7 +30,58 @@ function appUpdater() {
|
||||
autoUpdater.logger = log;
|
||||
|
||||
// Handle auto updates for beta/pre releases
|
||||
autoUpdater.allowPrerelease = ConfigUtil.getConfigItem('betaUpdate') || false;
|
||||
const isBetaUpdate = ConfigUtil.getConfigItem('betaUpdate');
|
||||
|
||||
autoUpdater.allowPrerelease = isBetaUpdate || false;
|
||||
|
||||
const eventsListenerRemove = ['update-available', 'update-not-available'];
|
||||
autoUpdater.on('update-available', info => {
|
||||
if (updateFromMenu) {
|
||||
dialog.showMessageBox({
|
||||
message: `A new version ${info.version}, of Zulip Desktop is available`,
|
||||
detail: 'The update will be downloaded in the background. You will be notified when it is ready to be installed.'
|
||||
});
|
||||
|
||||
updateAvailable = true;
|
||||
|
||||
// This is to prevent removal of 'update-downloaded' and 'error' event listener.
|
||||
eventsListenerRemove.forEach(event => {
|
||||
autoUpdater.removeAllListeners(event);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
if (updateFromMenu) {
|
||||
dialog.showMessageBox({
|
||||
message: 'No updates available',
|
||||
detail: `You are running the latest version of Zulip Desktop.\nVersion: ${app.getVersion()}`
|
||||
});
|
||||
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
|
||||
// listeners don't trigger multiple times.
|
||||
autoUpdater.removeAllListeners();
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('error', error => {
|
||||
if (updateFromMenu) {
|
||||
const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates');
|
||||
dialog.showMessageBox({
|
||||
type: 'error',
|
||||
buttons: ['Manual Download', 'Cancel'],
|
||||
message: messageText,
|
||||
detail: (error).toString() + `\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulipchat.com/apps/.\n
|
||||
Current Version: ${app.getVersion()}`
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
shell.openExternal('https://zulipchat.com/apps/');
|
||||
}
|
||||
});
|
||||
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
|
||||
// listeners don't trigger multiple times.
|
||||
autoUpdater.removeAllListeners();
|
||||
}
|
||||
});
|
||||
|
||||
// Ask the user if update is available
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
@@ -1,18 +1,27 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const electron = require('electron');
|
||||
const electronLocalshortcut = require('electron-localshortcut');
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
const isDev = require('electron-is-dev');
|
||||
const appMenu = require('./menu');
|
||||
const { appUpdater } = require('./autoupdater');
|
||||
|
||||
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');
|
||||
const ProxyUtil = require('./../renderer/js/utils/proxy-util.js');
|
||||
const { sentryInit } = require('./../renderer/js/utils/sentry-util.js');
|
||||
|
||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||
require('electron-debug')();
|
||||
// in development mode
|
||||
if (isDev) {
|
||||
require('electron-debug')();
|
||||
}
|
||||
|
||||
// Prevent window being garbage collected
|
||||
let mainWindow;
|
||||
@@ -23,18 +32,19 @@ 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();
|
||||
const singleInstanceLock = app.requestSingleInstanceLock();
|
||||
if (singleInstanceLock) {
|
||||
app.on('second-instance', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
if (isAlreadyRunning) {
|
||||
return app.quit();
|
||||
});
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
const APP_ICON = path.join(__dirname, '../resources', 'Icon');
|
||||
@@ -46,8 +56,9 @@ const iconPath = () => {
|
||||
function createMainWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1000,
|
||||
defaultHeight: 600
|
||||
defaultWidth: 1100,
|
||||
defaultHeight: 720,
|
||||
path: `${app.getPath('userData')}/config`
|
||||
});
|
||||
|
||||
// Let's keep the window position global so that we can access it in other process
|
||||
@@ -61,12 +72,12 @@ function createMainWindow() {
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 600,
|
||||
minHeight: 500,
|
||||
minWidth: 300,
|
||||
minHeight: 400,
|
||||
webPreferences: {
|
||||
plugins: true,
|
||||
allowDisplayingInsecureContent: true,
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
partition: 'persist:webviewsession'
|
||||
},
|
||||
show: false
|
||||
});
|
||||
@@ -75,10 +86,6 @@ function createMainWindow() {
|
||||
win.webContents.send('focus');
|
||||
});
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
win.show();
|
||||
});
|
||||
|
||||
win.loadURL(mainURL);
|
||||
|
||||
// Keep the app running in background on close event
|
||||
@@ -92,9 +99,6 @@ function createMainWindow() {
|
||||
win.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister all the shortcuts so that they don't interfare with other apps
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
});
|
||||
|
||||
win.setTitle('Zulip');
|
||||
@@ -122,21 +126,12 @@ function createMainWindow() {
|
||||
return win;
|
||||
}
|
||||
|
||||
function registerLocalShortcuts(page) {
|
||||
// Somehow, reload action cannot be overwritten by the menu item
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
|
||||
page.send('reload-viewer');
|
||||
});
|
||||
// Decrease load on GPU (experimental)
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
// Also adding these shortcuts because some users might want to use it instead of CMD/Left-Right
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+[', () => {
|
||||
page.send('back');
|
||||
});
|
||||
|
||||
electronLocalshortcut.register(mainWindow, 'CommandOrControl+]', () => {
|
||||
page.send('forward');
|
||||
});
|
||||
}
|
||||
// Temporary fix for Electron render colors differently
|
||||
// More info here - https://github.com/electron/electron/issues/10732
|
||||
app.commandLine.appendSwitch('force-color-profile', 'srgb');
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||
@@ -144,11 +139,6 @@ app.on('certificate-error', (event, webContents, url, error, certificate, callba
|
||||
callback(true);
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// Unregister all the shortcuts so that they don't interfare with other apps
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (!mainWindow) {
|
||||
mainWindow = createMainWindow();
|
||||
@@ -161,22 +151,47 @@ app.on('ready', () => {
|
||||
});
|
||||
mainWindow = createMainWindow();
|
||||
|
||||
// Auto-hide menu bar on Windows + Linux
|
||||
if (process.platform !== 'darwin') {
|
||||
const shouldHideMenu = ConfigUtil.getConfigItem('autoHideMenubar') || false;
|
||||
mainWindow.setAutoHideMenuBar(shouldHideMenu);
|
||||
mainWindow.setMenuBarVisibility(!shouldHideMenu);
|
||||
}
|
||||
|
||||
// Initialize sentry for main process
|
||||
const errorReporting = ConfigUtil.getConfigItem('errorReporting');
|
||||
if (errorReporting) {
|
||||
sentryInit();
|
||||
}
|
||||
|
||||
const isSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
|
||||
|
||||
if (isSystemProxy) {
|
||||
ProxyUtil.resolveSystemProxy(mainWindow);
|
||||
}
|
||||
|
||||
const page = mainWindow.webContents;
|
||||
|
||||
registerLocalShortcuts(page);
|
||||
|
||||
page.on('dom-ready', () => {
|
||||
mainWindow.show();
|
||||
if (ConfigUtil.getConfigItem('startMinimized')) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
page.once('did-frame-finish-load', () => {
|
||||
// Initate auto-updates on MacOS and Windows
|
||||
appUpdater();
|
||||
// Initiate auto-updates on MacOS and Windows
|
||||
if (ConfigUtil.getConfigItem('autoUpdate')) {
|
||||
appUpdater();
|
||||
}
|
||||
});
|
||||
|
||||
electron.powerMonitor.on('resume', () => {
|
||||
page.send('reload-viewer');
|
||||
});
|
||||
// Temporarily remove this event
|
||||
// electron.powerMonitor.on('resume', () => {
|
||||
// mainWindow.reload();
|
||||
// page.send('destroytray');
|
||||
// });
|
||||
|
||||
ipcMain.on('focus-app', () => {
|
||||
mainWindow.show();
|
||||
@@ -186,6 +201,32 @@ app.on('ready', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
|
||||
// ipcMain.on('pdf-view', (event, url) => {
|
||||
// // Paddings for pdfWindow so that it fits into the main browserWindow
|
||||
// const paddingWidth = 55;
|
||||
// const paddingHeight = 22;
|
||||
|
||||
// // Get the config of main browserWindow
|
||||
// const mainWindowState = global.mainWindowState;
|
||||
|
||||
// // Window to view the pdf file
|
||||
// const pdfWindow = new electron.BrowserWindow({
|
||||
// x: mainWindowState.x + paddingWidth,
|
||||
// y: mainWindowState.y + paddingHeight,
|
||||
// width: mainWindowState.width - paddingWidth,
|
||||
// height: mainWindowState.height - paddingHeight,
|
||||
// webPreferences: {
|
||||
// plugins: true,
|
||||
// partition: 'persist:webviewsession'
|
||||
// }
|
||||
// });
|
||||
// pdfWindow.loadURL(url);
|
||||
|
||||
// // We don't want to have the menu bar in pdf window
|
||||
// pdfWindow.setMenu(null);
|
||||
// });
|
||||
|
||||
// Reload full app not just webview, useful in debugging
|
||||
ipcMain.on('reload-full-app', () => {
|
||||
mainWindow.reload();
|
||||
@@ -199,10 +240,10 @@ app.on('ready', () => {
|
||||
});
|
||||
|
||||
ipcMain.on('toggle-app', () => {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
|
||||
mainWindow.show();
|
||||
} else {
|
||||
mainWindow.hide();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -210,6 +251,11 @@ app.on('ready', () => {
|
||||
BadgeSettings.updateBadge(badgeCount, mainWindow);
|
||||
});
|
||||
|
||||
ipcMain.on('toggle-menubar', (event, showMenubar) => {
|
||||
mainWindow.setAutoHideMenuBar(showMenubar);
|
||||
mainWindow.setMenuBarVisibility(!showMenubar);
|
||||
});
|
||||
|
||||
ipcMain.on('update-badge', (event, messageCount) => {
|
||||
badgeCount = messageCount;
|
||||
BadgeSettings.updateBadge(badgeCount, mainWindow);
|
||||
@@ -226,31 +272,87 @@ app.on('ready', () => {
|
||||
|
||||
ipcMain.on('update-menu', (event, props) => {
|
||||
appMenu.setMenu(props);
|
||||
});
|
||||
|
||||
ipcMain.on('register-server-tab-shortcut', (event, index) => {
|
||||
electronLocalshortcut.register(mainWindow, `CommandOrControl+${index}`, () => {
|
||||
// Array index == Shown index - 1
|
||||
page.send('switch-server-tab', index - 1);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('local-shortcuts', (event, enable) => {
|
||||
if (enable) {
|
||||
registerLocalShortcuts(page);
|
||||
} else {
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
const activeTab = props.tabs[props.activeTabIndex];
|
||||
if (activeTab) {
|
||||
mainWindow.setTitle(`Zulip - ${activeTab.webview.props.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('toggleAutoLauncher', (event, AutoLaunchValue) => {
|
||||
setAutoLaunch(AutoLaunchValue);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
// Unregister all the shortcuts so that they don't interfare with other apps
|
||||
electronLocalshortcut.unregisterAll(mainWindow);
|
||||
ipcMain.on('downloadFile', (event, url, downloadPath) => {
|
||||
page.downloadURL(url);
|
||||
page.session.once('will-download', (event, item) => {
|
||||
const filePath = path.join(downloadPath, item.getFilename());
|
||||
|
||||
const getTimeStamp = () => {
|
||||
const date = new Date();
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
const formatFile = filePath => {
|
||||
const fileExtension = path.extname(filePath);
|
||||
const baseName = path.basename(filePath, fileExtension);
|
||||
return `${baseName}-${getTimeStamp()}${fileExtension}`;
|
||||
};
|
||||
|
||||
// Update the name and path of the file if it already exists
|
||||
|
||||
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
|
||||
|
||||
const setFilePath = fs.existsSync(filePath) ? updatedFilePath : filePath;
|
||||
|
||||
item.setSavePath(setFilePath);
|
||||
|
||||
item.on('updated', (event, state) => {
|
||||
switch (state) {
|
||||
case 'interrupted': {
|
||||
// Can interrupted to due to network error, cancel download then
|
||||
console.log('Download interrupted, cancelling and fallback to dialog download.');
|
||||
item.cancel();
|
||||
break;
|
||||
}
|
||||
case 'progressing': {
|
||||
if (item.isPaused()) {
|
||||
item.cancel();
|
||||
}
|
||||
// This event can also be used to show progress in percentage in future.
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.info('Unknown updated state of download item');
|
||||
}
|
||||
}
|
||||
});
|
||||
item.once('done', (event, state) => {
|
||||
const getFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename();
|
||||
if (state === 'completed') {
|
||||
page.send('downloadFileCompleted', item.getSavePath(), getFileName);
|
||||
} else {
|
||||
console.log('Download failed state: ', state);
|
||||
page.send('downloadFileFailed');
|
||||
}
|
||||
// To stop item for listening to updated events of this file
|
||||
item.removeAllListeners('updated');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('realm-name-changed', (event, serverURL, realmName) => {
|
||||
page.send('update-realm-name', serverURL, realmName);
|
||||
});
|
||||
|
||||
ipcMain.on('realm-icon-changed', (event, serverURL, iconURL) => {
|
||||
page.send('update-realm-icon', serverURL, iconURL);
|
||||
});
|
||||
|
||||
// Using event.sender.send instead of page.send here to
|
||||
// make sure the value of errorReporting is sent only once on load.
|
||||
ipcMain.on('error-reporting', event => {
|
||||
event.sender.send('error-reporting-val', errorReporting);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
|
53
app/main/linuxupdater.js
Normal file
53
app/main/linuxupdater.js
Normal file
@@ -0,0 +1,53 @@
|
||||
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 ProxyUtil = require('../renderer/js/utils/proxy-util');
|
||||
const LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util');
|
||||
const Logger = require('../renderer/js/utils/logger-util');
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'linux-update-util.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
function linuxUpdateNotification() {
|
||||
let url = 'https://api.github.com/repos/zulip/zulip-desktop/releases';
|
||||
url = ConfigUtil.getConfigItem('betaUpdate') ? url : url + '/latest';
|
||||
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
|
||||
|
||||
const options = {
|
||||
url,
|
||||
headers: {'User-Agent': 'request'},
|
||||
proxy: proxyEnabled ? ProxyUtil.getProxy(url) : '',
|
||||
ecdhCurve: 'auto'
|
||||
};
|
||||
|
||||
request(options, (error, response, body) => {
|
||||
if (error) {
|
||||
logger.error('Linux update error.');
|
||||
logger.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 {
|
||||
logger.log('Linux update response status: ', response.statusCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
linuxUpdateNotification
|
||||
};
|
308
app/main/menu.js
308
app/main/menu.js
@@ -1,15 +1,23 @@
|
||||
'use strict';
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { app, shell, BrowserWindow, Menu } = require('electron');
|
||||
const { app, shell, BrowserWindow, Menu, dialog } = require('electron');
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const AdmZip = require('adm-zip');
|
||||
const { appUpdater } = require('./autoupdater');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');
|
||||
const DNDUtil = require(__dirname + '/../renderer/js/utils/dnd-util.js');
|
||||
const Logger = require(__dirname + '/../renderer/js/utils/logger-util.js');
|
||||
|
||||
const appName = app.getName();
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'errors.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
class AppMenu {
|
||||
getHistorySubmenu() {
|
||||
return [{
|
||||
@@ -31,13 +39,72 @@ class AppMenu {
|
||||
}];
|
||||
}
|
||||
|
||||
getToolsSubmenu() {
|
||||
return [{
|
||||
label: `Check for Updates`,
|
||||
click() {
|
||||
AppMenu.checkForUpdate();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: `Release Notes`,
|
||||
click() {
|
||||
shell.openExternal(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`);
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Factory Reset',
|
||||
accelerator: process.platform === 'darwin' ? 'Command+Shift+D' : 'Ctrl+Shift+D',
|
||||
click() {
|
||||
AppMenu.resetAppSettings();
|
||||
}
|
||||
}, {
|
||||
label: 'Download App Logs',
|
||||
click() {
|
||||
const zip = new AdmZip();
|
||||
let date = new Date();
|
||||
date = date.toLocaleDateString().replace(/\//g, '-');
|
||||
|
||||
// Create a zip file of all the logs and config data
|
||||
zip.addLocalFolder(`${app.getPath('appData')}/${appName}/Logs`);
|
||||
zip.addLocalFolder(`${app.getPath('appData')}/${appName}/config`);
|
||||
|
||||
// Put the log file in downloads folder
|
||||
const logFilePath = `${app.getPath('downloads')}/Zulip-logs-${date}.zip`;
|
||||
zip.writeZip(logFilePath);
|
||||
|
||||
// Open and select the log file
|
||||
shell.showItemInFolder(logFilePath);
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Toggle DevTools for Zulip App',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.toggleDevTools();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Toggle DevTools for Active Tab',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('tab-devtools');
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
getViewSubmenu() {
|
||||
return [{
|
||||
label: 'Reload',
|
||||
accelerator: 'CommandOrControl+R',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('reload-viewer');
|
||||
AppMenu.sendAction('reload-current-viewer');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
@@ -54,7 +121,7 @@ class AppMenu {
|
||||
role: 'togglefullscreen'
|
||||
}, {
|
||||
label: 'Zoom In',
|
||||
accelerator: 'CommandOrControl+=',
|
||||
accelerator: process.platform === 'darwin' ? 'Command+Plus' : 'Control+=',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('zoomIn');
|
||||
@@ -87,7 +154,7 @@ class AppMenu {
|
||||
}
|
||||
}, {
|
||||
label: 'Toggle Sidebar',
|
||||
accelerator: 'CommandOrControl+S',
|
||||
accelerator: 'CommandOrControl+Shift+S',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
const newValue = !ConfigUtil.getConfigItem('showSidebar');
|
||||
@@ -95,46 +162,38 @@ class AppMenu {
|
||||
ConfigUtil.setConfigItem('showSidebar', newValue);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Toggle DevTools for Zulip App',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.toggleDevTools();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Toggle DevTools for Active Tab',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('tab-devtools');
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
getHelpSubmenu() {
|
||||
return [{
|
||||
label: `${appName} Website`,
|
||||
click() {
|
||||
shell.openExternal('https://zulipchat.com/help/');
|
||||
}
|
||||
}, {
|
||||
label: `${appName + 'Desktop'} - ${app.getVersion()}`,
|
||||
enabled: false
|
||||
}, {
|
||||
label: 'Report an Issue...',
|
||||
click() {
|
||||
const body = `
|
||||
<!-- Please succinctly describe your issue and steps to reproduce it. -->
|
||||
-
|
||||
${app.getName()} ${app.getVersion()}
|
||||
Electron ${process.versions.electron}
|
||||
${process.platform} ${process.arch} ${os.release()}`;
|
||||
shell.openExternal(`https://github.com/zulip/zulip-electron/issues/new?body=${encodeURIComponent(body)}`);
|
||||
}
|
||||
}];
|
||||
return [
|
||||
{
|
||||
label: `${appName + ' Desktop'} v${app.getVersion()}`,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
label: 'About Zulip',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('open-about');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: `Help Center`,
|
||||
click() {
|
||||
shell.openExternal('https://zulipchat.com/help/');
|
||||
}
|
||||
}, {
|
||||
label: 'Report an Issue',
|
||||
click() {
|
||||
// the goal is to notify the main.html BrowserWindow
|
||||
// which may not be the focused window.
|
||||
BrowserWindow.getAllWindows().forEach(window => {
|
||||
window.webContents.send('open-feedback-modal');
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
getWindowSubmenu(tabs, activeTabIndex) {
|
||||
@@ -150,6 +209,11 @@ class AppMenu {
|
||||
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}`,
|
||||
@@ -159,30 +223,58 @@ class AppMenu {
|
||||
AppMenu.sendAction('switch-server-tab', tabs[i].props.index);
|
||||
}
|
||||
},
|
||||
type: 'radio'
|
||||
type: 'checkbox'
|
||||
});
|
||||
}
|
||||
initialSubmenu.push({
|
||||
type: 'separator'
|
||||
});
|
||||
initialSubmenu.push({
|
||||
label: 'Switch to Next Organization',
|
||||
accelerator: `Ctrl+Tab`,
|
||||
enabled: tabs[activeTabIndex].props.role === 'server',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('switch-server-tab', AppMenu.getNextServer(tabs, activeTabIndex));
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Switch to Previous Organization',
|
||||
accelerator: `Ctrl+Shift+Tab`,
|
||||
enabled: tabs[activeTabIndex].props.role === 'server',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('switch-server-tab', AppMenu.getPreviousServer(tabs, activeTabIndex));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return initialSubmenu;
|
||||
}
|
||||
|
||||
getDarwinTpl(props) {
|
||||
const { tabs, activeTabIndex } = props;
|
||||
const { tabs, activeTabIndex, enableMenu } = props;
|
||||
|
||||
return [{
|
||||
label: `${app.getName()}`,
|
||||
submenu: [{
|
||||
label: 'Zulip Desktop',
|
||||
label: 'Add Organization',
|
||||
accelerator: 'Cmd+Shift+N',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('open-about');
|
||||
AppMenu.sendAction('new-server');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
label: 'Toggle Do Not Disturb',
|
||||
accelerator: 'Cmd+Shift+M',
|
||||
click() {
|
||||
const dndUtil = DNDUtil.toggle();
|
||||
AppMenu.sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
|
||||
}
|
||||
}, {
|
||||
label: 'Settings',
|
||||
label: 'Desktop Settings',
|
||||
accelerator: 'Cmd+,',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
@@ -191,7 +283,8 @@ class AppMenu {
|
||||
}
|
||||
}, {
|
||||
label: 'Keyboard Shortcuts',
|
||||
accelerator: 'Cmd+K',
|
||||
accelerator: 'Cmd+Shift+K',
|
||||
enabled: enableMenu,
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('shortcut');
|
||||
@@ -200,14 +293,17 @@ class AppMenu {
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Reset App Settings',
|
||||
accelerator: 'Command+Shift+D',
|
||||
click() {
|
||||
AppMenu.resetAppSettings();
|
||||
label: 'Copy Zulip URL',
|
||||
accelerator: 'Cmd+Shift+C',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('copy-zulip-url');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Log Out',
|
||||
label: 'Log Out of Organization',
|
||||
accelerator: 'Cmd+L',
|
||||
enabled: enableMenu,
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('log-out');
|
||||
@@ -247,8 +343,6 @@ class AppMenu {
|
||||
role: 'paste'
|
||||
}, {
|
||||
role: 'pasteandmatchstyle'
|
||||
}, {
|
||||
role: 'delete'
|
||||
}, {
|
||||
role: 'selectall'
|
||||
}]
|
||||
@@ -261,6 +355,9 @@ class AppMenu {
|
||||
}, {
|
||||
label: 'Window',
|
||||
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
|
||||
}, {
|
||||
label: 'Tools',
|
||||
submenu: this.getToolsSubmenu()
|
||||
}, {
|
||||
role: 'help',
|
||||
submenu: this.getHelpSubmenu()
|
||||
@@ -268,32 +365,39 @@ class AppMenu {
|
||||
}
|
||||
|
||||
getOtherTpl(props) {
|
||||
const { tabs, activeTabIndex } = props;
|
||||
const { tabs, activeTabIndex, enableMenu } = props;
|
||||
|
||||
return [{
|
||||
label: 'File',
|
||||
label: '&File',
|
||||
submenu: [{
|
||||
label: 'Zulip Desktop',
|
||||
label: 'Add Organization',
|
||||
accelerator: 'Ctrl+Shift+N',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('open-about');
|
||||
AppMenu.sendAction('new-server');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Settings',
|
||||
label: 'Toggle Do Not Disturb',
|
||||
accelerator: 'Ctrl+Shift+M',
|
||||
click() {
|
||||
const dndUtil = DNDUtil.toggle();
|
||||
AppMenu.sendAction('toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
|
||||
}
|
||||
}, {
|
||||
label: 'Desktop Settings',
|
||||
accelerator: 'Ctrl+,',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('open-settings');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Keyboard Shortcuts',
|
||||
accelerator: 'Ctrl+K',
|
||||
accelerator: 'Ctrl+Shift+K',
|
||||
enabled: enableMenu,
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('shortcut');
|
||||
@@ -302,14 +406,17 @@ class AppMenu {
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Reset App Settings',
|
||||
accelerator: 'Ctrl+Shift+D',
|
||||
click() {
|
||||
AppMenu.resetAppSettings();
|
||||
label: 'Copy Zulip URL',
|
||||
accelerator: 'Ctrl+Shift+C',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('copy-zulip-url');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
label: 'Log Out',
|
||||
label: 'Log Out of Organization',
|
||||
accelerator: 'Ctrl+L',
|
||||
enabled: enableMenu,
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
AppMenu.sendAction('log-out');
|
||||
@@ -322,7 +429,7 @@ class AppMenu {
|
||||
accelerator: 'Ctrl+Q'
|
||||
}]
|
||||
}, {
|
||||
label: 'Edit',
|
||||
label: '&Edit',
|
||||
submenu: [{
|
||||
role: 'undo'
|
||||
}, {
|
||||
@@ -337,23 +444,25 @@ class AppMenu {
|
||||
role: 'paste'
|
||||
}, {
|
||||
role: 'pasteandmatchstyle'
|
||||
}, {
|
||||
role: 'delete'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
role: 'selectall'
|
||||
}]
|
||||
}, {
|
||||
label: 'View',
|
||||
label: '&View',
|
||||
submenu: this.getViewSubmenu()
|
||||
}, {
|
||||
label: 'History',
|
||||
label: '&History',
|
||||
submenu: this.getHistorySubmenu()
|
||||
}, {
|
||||
label: 'Window',
|
||||
label: '&Window',
|
||||
submenu: this.getWindowSubmenu(tabs, activeTabIndex)
|
||||
}, {
|
||||
label: '&Tools',
|
||||
submenu: this.getToolsSubmenu()
|
||||
}, {
|
||||
label: '&Help',
|
||||
role: 'help',
|
||||
submenu: this.getHelpSubmenu()
|
||||
}];
|
||||
@@ -369,11 +478,54 @@ class AppMenu {
|
||||
win.webContents.send(action, ...params);
|
||||
}
|
||||
|
||||
static resetAppSettings() {
|
||||
const getAppPath = path.join(app.getPath('appData'), appName, 'window-state.json');
|
||||
static checkForUpdate() {
|
||||
appUpdater(true);
|
||||
}
|
||||
|
||||
fs.unlink(getAppPath, () => {
|
||||
setTimeout(() => AppMenu.sendAction('clear-app-data'), 1000);
|
||||
static getNextServer(tabs, activeTabIndex) {
|
||||
do {
|
||||
activeTabIndex = (activeTabIndex + 1) % tabs.length;
|
||||
}
|
||||
while (tabs[activeTabIndex].props.role !== 'server');
|
||||
return activeTabIndex;
|
||||
}
|
||||
|
||||
static getPreviousServer(tabs, activeTabIndex) {
|
||||
do {
|
||||
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
|
||||
}
|
||||
while (tabs[activeTabIndex].props.role !== 'server');
|
||||
return activeTabIndex;
|
||||
}
|
||||
|
||||
static resetAppSettings() {
|
||||
const resetAppSettingsMessage = 'By proceeding you will be removing all connected organizations and preferences from Zulip.';
|
||||
|
||||
// We save App's settings/configurations in following files
|
||||
const settingFiles = ['config/window-state.json', 'config/domain.json', 'config/settings.json', 'config/certificates.json'];
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['YES', 'NO'],
|
||||
defaultId: 0,
|
||||
message: 'Are you sure?',
|
||||
detail: resetAppSettingsMessage
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
settingFiles.forEach(settingFileName => {
|
||||
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
|
||||
fs.access(getSettingFilesPath, error => {
|
||||
if (error) {
|
||||
logger.error('Error while resetting app settings.');
|
||||
logger.error(error);
|
||||
} else {
|
||||
fs.unlink(getSettingFilesPath, () => {
|
||||
AppMenu.sendAction('clear-app-data');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
1676
app/package-lock.json
generated
Normal file
1676
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,20 @@
|
||||
{
|
||||
"name": "zulip",
|
||||
"productName": "Zulip",
|
||||
"version": "1.4.0",
|
||||
"version": "2.5.0-beta",
|
||||
"description": "Zulip Desktop App",
|
||||
"license": "Apache-2.0",
|
||||
"email": "<svnitakash@gmail.com>",
|
||||
"copyright": "©2017 Kandra Labs, Inc.",
|
||||
"copyright": "Kandra Labs, Inc.",
|
||||
"author": {
|
||||
"name": "Kandra Labs, Inc.",
|
||||
"email": "svnitakash@gmail.com"
|
||||
"email": "support@zulipchat.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zulip/zulip-electron.git"
|
||||
"url": "https://github.com/zulip/zulip-desktop.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/zulip/zulip-electron/issues"
|
||||
"url": "https://github.com/zulip/zulip-desktop/issues"
|
||||
},
|
||||
"main": "main/index.js",
|
||||
"keywords": [
|
||||
@@ -27,16 +26,23 @@
|
||||
"InstantMessaging"
|
||||
],
|
||||
"dependencies": {
|
||||
"electron-debug": "1.4.0",
|
||||
"@electron-elements/send-feedback": "1.0.8",
|
||||
"@sentry/electron": "0.14.0",
|
||||
"adm-zip": "0.4.11",
|
||||
"auto-launch": "5.0.5",
|
||||
"electron-is-dev": "0.3.0",
|
||||
"electron-localshortcut": "2.0.2",
|
||||
"electron-log": "2.2.7",
|
||||
"electron-spellchecker": "1.2.0",
|
||||
"electron-updater": "2.8.9",
|
||||
"node-json-db": "0.7.3",
|
||||
"request": "2.81.0",
|
||||
"wurl": "2.5.0",
|
||||
"electron-window-state": "4.1.1",
|
||||
"auto-launch": "5.0.1"
|
||||
"electron-log": "2.2.14",
|
||||
"electron-spellchecker": "1.1.2",
|
||||
"electron-updater": "4.0.6",
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
"is-online": "7.0.0",
|
||||
"node-json-db": "0.9.1",
|
||||
"request": "2.85.0",
|
||||
"semver": "5.4.1",
|
||||
"wurl": "2.5.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-mac-notifier": "1.1.0"
|
||||
}
|
||||
}
|
||||
|
@@ -1,33 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="css/about.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="about">
|
||||
<img class="logo" src="../resources/zulip.png" />
|
||||
<p class="detail" id="version">version ?.?.?</p>
|
||||
<div class="maintenance-info">
|
||||
<p class="detail maintainer">Maintained by Zulip</p>
|
||||
<p class="detail license">Available under the Apache License</p>
|
||||
<a class="bug" onclick="linkInBrowser()" href="#">Found bug?</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
const app = require('electron').remote.app;
|
||||
const version_tag = document.getElementById('version');
|
||||
version_tag.innerHTML = 'version ' + app.getVersion();
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="css/about.css">
|
||||
<title>Zulip - About</title>
|
||||
</head>
|
||||
|
||||
function linkInBrowser(event) {
|
||||
<body>
|
||||
<div class="about">
|
||||
<img class="logo" src="../resources/zulip.png" />
|
||||
<p class="detail" id="version">v?.?.?</p>
|
||||
<div class="maintenance-info">
|
||||
<p class="detail maintainer">
|
||||
Maintained by
|
||||
<a onclick="linkInBrowser('website')">Zulip</a>
|
||||
</p>
|
||||
<p class="detail license">
|
||||
Available under the
|
||||
<a onclick="linkInBrowser('license')">Apache 2.0 License</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
const shell = require('electron').shell;
|
||||
const { app } = require('electron').remote;
|
||||
const { shell } = require('electron');
|
||||
const version_tag = document.querySelector('#version');
|
||||
version_tag.innerHTML = 'v' + app.getVersion();
|
||||
|
||||
const url = "https://github.com/zulip/zulip-electron/issues/new?body=Please describe your issue and steps to reproduce it."
|
||||
|
||||
shell.openExternal(url);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
function linkInBrowser(type) {
|
||||
let url;
|
||||
switch (type) {
|
||||
case 'website':
|
||||
url = "https://zulipchat.com";
|
||||
break;
|
||||
case 'license':
|
||||
url = "https://github.com/zulip/zulip-desktop/blob/master/LICENSE";
|
||||
break;
|
||||
}
|
||||
shell.openExternal(url);
|
||||
}
|
||||
</script>
|
||||
<script>require('./js/shared/preventdrag.js')</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1,73 +1,66 @@
|
||||
body {
|
||||
background: #fafafa;
|
||||
font-family: menu, "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: rgba(250, 250, 250, 1.000);
|
||||
font-family: menu, "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
margin: -40px auto;
|
||||
}
|
||||
|
||||
#version {
|
||||
color: #aaa;
|
||||
font-size: 0.9em;
|
||||
color: rgba(68, 67, 67, 1.000);
|
||||
font-size: 1.3em;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.about {
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
margin: 25vh auto;
|
||||
height: 25vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about p {
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, 0.62);
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, 0.62);
|
||||
}
|
||||
|
||||
.about img {
|
||||
width: 150px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail.maintainer {
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail.license {
|
||||
font-size: 0.8em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.maintenance-info {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 20px;
|
||||
left: 0px;
|
||||
color: #444;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
color: rgba(68, 68, 68, 1.000);
|
||||
}
|
||||
|
||||
.maintenance-info p {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.maintenance-info .bug {
|
||||
display: inline-block;
|
||||
padding: 8px 15px;
|
||||
margin-top: 30px;
|
||||
text-decoration: none;
|
||||
background-color: #52c2af;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
|
||||
transition: background-color 0.2s ease;
|
||||
p.detail a {
|
||||
color: rgba(53, 95, 76, 1.000);
|
||||
}
|
||||
|
||||
.maintenance-info .bug:hover {
|
||||
background-color: #32a692;
|
||||
p.detail a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
@@ -13,13 +13,10 @@ body {
|
||||
#content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: #eee url(../img/ic_loading.gif) no-repeat;
|
||||
background-size: 60px 60px;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
background: #222c31;
|
||||
.toggle-sidebar {
|
||||
background: rgba(34, 44, 49, 1.000);
|
||||
width: 54px;
|
||||
padding: 27px 0 20px 0;
|
||||
justify-content: space-between;
|
||||
@@ -27,6 +24,44 @@ body {
|
||||
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;
|
||||
}
|
||||
|
||||
.sidebar-hide {
|
||||
width: 0;
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.sidebar-hide div {
|
||||
transform: translateX(-100%);
|
||||
transition: all 0.6s ease-out;
|
||||
}
|
||||
|
||||
#view-controls-container {
|
||||
height: calc(100% - 208px);
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#view-controls-container:hover {
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
||||
#view-controls-container::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
#view-controls-container::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#view-controls-container::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(169, 169, 169, 1.000);
|
||||
outline: 1px solid rgba(169, 169, 169, 1.000);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -38,8 +73,8 @@ body {
|
||||
|
||||
|
||||
/*******************
|
||||
* Left Sidebar *
|
||||
*******************/
|
||||
* Left Sidebar *
|
||||
*******************/
|
||||
|
||||
#tabs-container {
|
||||
display: flex;
|
||||
@@ -66,11 +101,17 @@ body {
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
#actions-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
@@ -78,16 +119,39 @@ body {
|
||||
}
|
||||
|
||||
.action-button i {
|
||||
color: #6c8592;
|
||||
color: rgba(108, 133, 146, 1.000);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.action-button:hover i {
|
||||
color: #98a9b3;
|
||||
color: rgba(152, 169, 179, 1.000);
|
||||
}
|
||||
|
||||
.action-button.disable {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.action-button.disable:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button.disable:hover i {
|
||||
color: rgba(108, 133, 146, 1.000);
|
||||
}
|
||||
|
||||
.action-button.active {
|
||||
/* background-color: rgba(255, 255, 255, 0.25); */
|
||||
background-color: rgba(239, 239, 239, 1.000);
|
||||
opacity: 0.9;
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
.action-button.active i {
|
||||
color: rgba(28, 38, 43, 1.000);
|
||||
}
|
||||
|
||||
.tab:first-child {
|
||||
margin-top: 8px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@@ -101,25 +165,33 @@ body {
|
||||
}
|
||||
|
||||
.tab .server-icons {
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
padding: 3px;
|
||||
height: 30px;
|
||||
width: 35px;
|
||||
vertical-align: top;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab .server-tab {
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
position: relative;
|
||||
margin: 5px 0 2px 0;
|
||||
margin-top: 5px;
|
||||
z-index: 11;
|
||||
line-height: 31px;
|
||||
color: #eee;
|
||||
color: rgba(238, 238, 238, 1.000);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
opacity: 0.6;
|
||||
padding: 2px 0;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.server-tab .alt-icon {
|
||||
font-family: Verdana;
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
border: 2px solid rgba(34, 44, 49, 1.000);
|
||||
margin-left: 17%;
|
||||
width: 35px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab .server-tab:hover {
|
||||
@@ -144,7 +216,7 @@ body {
|
||||
|
||||
.tab.active .server-tab {
|
||||
opacity: 1;
|
||||
background-color: #648478;
|
||||
background-color: rgba(100, 132, 120, 1.000);
|
||||
}
|
||||
|
||||
.tab .server-tab-badge.active {
|
||||
@@ -152,7 +224,7 @@ body {
|
||||
min-width: 11px;
|
||||
padding: 0 3px;
|
||||
height: 17px;
|
||||
background-color: #f44336;
|
||||
background-color: rgba(244, 67, 54, 1.000);
|
||||
font-size: 10px;
|
||||
font-family: sans-serif;
|
||||
position: absolute;
|
||||
@@ -160,7 +232,7 @@ body {
|
||||
z-index: 15;
|
||||
top: 6px;
|
||||
float: right;
|
||||
color: #fff;
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
text-align: center;
|
||||
line-height: 17px;
|
||||
display: block;
|
||||
@@ -188,7 +260,7 @@ body {
|
||||
}
|
||||
|
||||
.tab .server-tab-shortcut {
|
||||
color: #648478;
|
||||
color: rgba(100, 132, 120, 1.000);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-family: sans-serif;
|
||||
@@ -197,8 +269,8 @@ body {
|
||||
|
||||
|
||||
/*******************
|
||||
* Webview Area *
|
||||
*******************/
|
||||
* Webview Area *
|
||||
*******************/
|
||||
|
||||
#webviews-container {
|
||||
display: flex;
|
||||
@@ -206,72 +278,128 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Pseudo element for loading indicator */
|
||||
#webviews-container::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background: rgba(255, 255, 255, 1.000) url(../img/ic_loading.gif) no-repeat;
|
||||
background-size: 60px 60px;
|
||||
background-position: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* When the active webview is loaded */
|
||||
#webviews-container.loaded::before {
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
webview {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in;
|
||||
/* transition: opacity 0.3s ease-in; */
|
||||
flex-grow: 1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
webview.onload {
|
||||
transition: opacity 1s cubic-bezier(0.95, 0.05, 0.795, 0.035);
|
||||
}
|
||||
|
||||
webview.disabled {
|
||||
flex: 0 1;
|
||||
height: 0;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-out;
|
||||
webview.active {
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
webview:focus {
|
||||
webview.disabled {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
webview.focus {
|
||||
outline: 0px solid transparent;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip styling */
|
||||
|
||||
#dnd-tooltip,
|
||||
#back-tooltip,
|
||||
#reload-tooltip,
|
||||
#setting-tooltip {
|
||||
font-family: sans-serif;
|
||||
background: #222c31;
|
||||
margin-left: 68px;
|
||||
background: rgba(34, 44, 49, 1.000);
|
||||
margin-left: 48px;
|
||||
padding: 6px 8px;
|
||||
position: absolute;
|
||||
margin-top: 0px;
|
||||
z-index: 1000;
|
||||
color: white;
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: 55px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#reload-tooltip:after,
|
||||
#setting-tooltip:after {
|
||||
#dnd-tooltip::after,
|
||||
#back-tooltip::after,
|
||||
#reload-tooltip::after,
|
||||
#setting-tooltip::after {
|
||||
content: " ";
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-right: 8px solid #222c31;
|
||||
border-right: 8px solid rgba(34, 44, 49, 1.000);
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 68px;
|
||||
}
|
||||
|
||||
#add-server-tooltip,
|
||||
.server-tooltip {
|
||||
font-family: 'arial';
|
||||
background: rgba(34, 44, 49, 1.000);
|
||||
left: 56px;
|
||||
padding: 10px 20px;
|
||||
position: fixed;
|
||||
margin-top: 11px;
|
||||
z-index: 5000 !important;
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
width: max-content;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#add-server-tooltip::after,
|
||||
.server-tooltip::after {
|
||||
content: " ";
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-right: 8px solid rgba(34, 44, 49, 1.000);
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
#collapse-button {
|
||||
bottom: 30px;
|
||||
left: 20px;
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #222c31;
|
||||
background: rgba(34, 44, 49, 1.000);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
box-shadow: #999 1px 1px;
|
||||
box-shadow: rgba(153, 153, 153, 1.000) 1px 1px;
|
||||
}
|
||||
|
||||
#collapse-button i {
|
||||
color: #efefef;
|
||||
color: rgba(239, 239, 239, 1.000);
|
||||
}
|
||||
|
||||
#main-container {
|
||||
@@ -279,17 +407,21 @@ webview:focus {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Full screen Popup container */
|
||||
|
||||
.popup .popuptext {
|
||||
visibility: hidden;
|
||||
background-color: #555;
|
||||
color: #fff;
|
||||
background-color: rgba(85, 85, 85, 1.000);
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 9px 0;
|
||||
@@ -319,3 +451,26 @@ webview:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
send-feedback {
|
||||
width: 60%;
|
||||
height: 85%;
|
||||
}
|
||||
|
||||
#feedback-modal {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(68, 67, 67, 0.81);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
transition: all 1s ease-out;
|
||||
}
|
||||
|
||||
#feedback-modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
user-select:none;
|
||||
color: rgba(51, 51, 51, 1.000);
|
||||
background: rgba(255, 255, 255, 1.000);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#content {
|
||||
@@ -27,8 +28,8 @@ html, body {
|
||||
|
||||
#reconnect {
|
||||
font-size: 16px;
|
||||
background: #009688;
|
||||
color: #fff;
|
||||
background: rgba(0, 150, 136, 1.000);
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
width: 84px;
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
|
@@ -5,88 +5,105 @@ body {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
font-family: menu, "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #efefef;
|
||||
background: rgba(239, 239, 239, 1.000);
|
||||
letter-spacing: -.08px;
|
||||
line-height: 18px;
|
||||
color: rgba(139, 142, 143, 1.000);
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1em 0.6em;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 12px;
|
||||
font-family: Arial,Helvetica,sans-serif;
|
||||
background-color: #f7f7f7;
|
||||
color: #333;
|
||||
-moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2),0 0 0 2px #ffffff inset;
|
||||
-webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2),0 0 0 2px #ffffff inset;
|
||||
box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2),0 0 0 2px #ffffff inset;
|
||||
-moz-border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
margin: 0 0.1em;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
line-height: 1.4;
|
||||
border: 1px solid rgba(204, 204, 204, 1.000);
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
font-family: Courier New, Courier, monospace;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
background-color: rgba(247, 247, 247, 1.000);
|
||||
color: rgba(51, 51, 51, 1.000);
|
||||
margin: 0 0.1em;
|
||||
padding: 0.3em 0.8em;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 1.000);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid #ddd;
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
color: rgba(56, 52, 48, 1.000);
|
||||
}
|
||||
|
||||
table { width: 85%; }
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) { background-color: #f2f2f2; }
|
||||
table tr:nth-child(even) {
|
||||
background-color: rgba(250, 250, 250, 1.000);
|
||||
}
|
||||
|
||||
td { padding: 5px; }
|
||||
table tr:nth-child(odd) {
|
||||
background-color: rgba(255, 255, 255, 1.000);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
td:nth-child(odd) {
|
||||
text-align: right;
|
||||
width: 50%;
|
||||
text-align: right;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'), local('MaterialIcons-Regular'), url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
src: url(../fonts/Montserrat-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
/* Preferred icon size */
|
||||
font-size: 24px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
/* Preferred icon size */
|
||||
font-size: 24px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
font-family: 'Montserrat';
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 80px;
|
||||
padding: 30px;
|
||||
width: 150px;
|
||||
min-width: 100px;
|
||||
padding: 30px 30px 30px 35px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
background: rgba(242, 242, 242, 1.000);
|
||||
}
|
||||
|
||||
#nav-container {
|
||||
@@ -94,29 +111,37 @@ td:nth-child(odd) {
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding: 5px 0;
|
||||
color: #999;
|
||||
padding: 7px 0;
|
||||
color: rgba(153, 153, 153, 1.000);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav.active {
|
||||
color: #464e5a;
|
||||
color: rgba(78, 191, 172, 1.000);
|
||||
cursor: default;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav.active::before {
|
||||
background: #464e5a;
|
||||
background: rgba(70, 78, 90, 1.000);
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
height: 18px;
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
/* We don't want to show this in nav item since we have the + button for adding an Organization */
|
||||
|
||||
#nav-AddServer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#settings-header {
|
||||
font-size: 22px;
|
||||
color: #5c6166;
|
||||
color: rgba(34, 44, 49, 1.000);
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#settings-container {
|
||||
@@ -127,40 +152,77 @@ td:nth-child(odd) {
|
||||
}
|
||||
|
||||
#new-server-container {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s;
|
||||
padding-left: 42px;
|
||||
padding-top: 25px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 4px 0 6px 0;
|
||||
font-weight: 500;
|
||||
color: rgba(34, 44, 49, 1.000);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: rgba(34, 44, 49, 1.000);
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #1e1e1e;
|
||||
padding: 4px 0 6px 0;
|
||||
}
|
||||
|
||||
.add-server-info-row {
|
||||
display: flex;
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
.add-server-info-right {
|
||||
flex-grow: 1;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
padding: 4px 0 6px 0;
|
||||
font-weight: bold;
|
||||
color: #616161;
|
||||
color: rgba(97, 97, 97, 1.000);
|
||||
}
|
||||
|
||||
img.server-info-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 4px;
|
||||
padding: 4px 4px 4px 10px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.server-info-left {
|
||||
margin: 10px 20px 0 0;
|
||||
margin: 4px 20px 0 0;
|
||||
min-width: 40%;
|
||||
}
|
||||
|
||||
.server-info-right {
|
||||
margin-top: 4px;
|
||||
width: 55%;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.server-info-row {
|
||||
display: flex;
|
||||
margin: 8px 0 0 0;
|
||||
display: inline-block;
|
||||
margin: 5px 0 0 0;
|
||||
}
|
||||
|
||||
.server-info-left .server-info-row {
|
||||
display: inline-flex;
|
||||
align-items: inherit;
|
||||
vertical-align: -2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.server-url {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.server-info-alias {
|
||||
@@ -168,6 +230,10 @@ img.server-info-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.server-url-info {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.setting-input-key {
|
||||
font-size: 14px;
|
||||
height: 27px;
|
||||
@@ -181,26 +247,26 @@ img.server-info-icon {
|
||||
.setting-input-value {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-bottom: #ededed 1px solid;
|
||||
border-radius: 4px;
|
||||
padding: 13px;
|
||||
border: rgba(237, 237, 237, 1.000) 2px solid;
|
||||
outline-width: 0;
|
||||
background: transparent;
|
||||
max-width: 500px;
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
.setting-input-value:focus {
|
||||
border-bottom: #7cb980 1px solid;
|
||||
border: rgba(78, 191, 172, 1.000) 2px solid;
|
||||
}
|
||||
|
||||
.setting-block {
|
||||
width: 100%;
|
||||
.manual-proxy-block {
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: #235d3a;
|
||||
color: rgba(35, 93, 58, 1.000);
|
||||
vertical-align: middle;
|
||||
margin: 10px 0;
|
||||
flex-wrap: wrap;
|
||||
@@ -210,18 +276,18 @@ img.server-info-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
border-radius: 2px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.action i {
|
||||
margin-right: 5px;
|
||||
font-size: 18px;
|
||||
line-height: 27px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.settings-pane {
|
||||
flex-grow: 1;
|
||||
min-width: 550px;
|
||||
}
|
||||
|
||||
.action:hover {
|
||||
@@ -233,7 +299,7 @@ img.server-info-icon {
|
||||
}
|
||||
|
||||
.action.disabled {
|
||||
color: #999;
|
||||
color: rgba(153, 153, 153, 1.000);
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
@@ -241,10 +307,13 @@ img.server-info-icon {
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 30px;
|
||||
margin: 10px 0 20px 0;
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
width: 540px;
|
||||
box-shadow: 1px 2px 4px #bcbcbc;
|
||||
background: rgba(255, 255, 255, 1.000);
|
||||
width: 80%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.settings-card:hover {
|
||||
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 0px 0px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
@@ -253,28 +322,67 @@ img.server-info-icon {
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #ef5350;
|
||||
background: #ffebee;
|
||||
border: 1px solid #ef5350;
|
||||
color: rgb(240, 148, 148);
|
||||
background: rgba(255, 255, 255, 1.000);
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
border: 2px solid rgb(240, 148, 148);
|
||||
padding: 10px;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.2s ease;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.red:hover {
|
||||
background-color: rgb(240, 148, 148);
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
}
|
||||
|
||||
.green {
|
||||
color: #388E3C;
|
||||
background: #E8F5E9;
|
||||
border: 1px solid #388E3C;
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
background: rgba(78, 191, 172, 1.000);
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.2s ease;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.green:hover {
|
||||
background-color: rgba(60, 159, 141, 1.000);
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
}
|
||||
|
||||
.w-150 {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.w-250 {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.grey {
|
||||
color: #9E9E9E;
|
||||
background: #FAFAFA;
|
||||
border: 1px solid #9E9E9E;
|
||||
color: rgba(158, 158, 158, 1.000);
|
||||
background: rgba(250, 250, 250, 1.000);
|
||||
border: 1px solid rgba(158, 158, 158, 1.000);
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.code {
|
||||
@@ -282,33 +390,299 @@ img.server-info-icon {
|
||||
}
|
||||
|
||||
i.open-tab-button {
|
||||
padding: 0 5px;
|
||||
font-size: 18px;
|
||||
padding-left: 5px;
|
||||
font-size: 19px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reset-data-button {
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
.css-delete-action {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.selected-css-path,
|
||||
.download-folder-path {
|
||||
background: rgba(238, 238, 238, 1.000);
|
||||
padding: 5px 10px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
width: 90%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#remove-custom-css {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
#server-info-container {
|
||||
/* min-height: calc(100% - 260px); */
|
||||
}
|
||||
|
||||
#new-org-button {
|
||||
margin: 30px 0px 30px 0px;
|
||||
}
|
||||
|
||||
#create-organization-container {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
#create-organization-container i {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
#open-create-org-link {
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: background-color 0.2s ease;
|
||||
text-decoration: none;
|
||||
|
||||
}
|
||||
|
||||
.reset-data-button:hover {
|
||||
background-color: #32a692;
|
||||
color: #fff;
|
||||
.toggle {
|
||||
position: absolute;
|
||||
margin-left: -9999px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#open-shortcuts-url {
|
||||
color: #08c;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
.toggle + label {
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#open-shortcuts-url:hover {
|
||||
color: #005580;;
|
||||
text-decoration: underline;
|
||||
}
|
||||
input.toggle-round + label {
|
||||
padding: 2px;
|
||||
width: 50px;
|
||||
height: 25px;
|
||||
background-color: rgba(221, 221, 221, 1.000);
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
input.toggle-round + label::before,
|
||||
input.toggle-round + label::after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
content: "";
|
||||
}
|
||||
|
||||
input.toggle-round + label::before {
|
||||
background-color: rgba(241, 241, 241, 1.000);
|
||||
border-radius: 25px;
|
||||
top: 0;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
input.toggle-round + label::after {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-color: rgba(255, 255, 255, 1.000);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
input.toggle-round:checked + label::before {
|
||||
background-color: rgba(78, 191, 172, 1.000);
|
||||
top: 0;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
input.toggle-round:checked + label::after {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
/* Add new server modal */
|
||||
|
||||
.add-server-modal {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
padding-top: 15vh;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* background: rgba(61, 64, 67, 15); */
|
||||
background: linear-gradient(35deg, rgba(0, 59, 82, 1.000), rgba(69, 181, 155, 1.000));
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Modal Content */
|
||||
|
||||
.modal-container {
|
||||
background-color: rgba(244, 247, 248, 1.000);
|
||||
margin: auto;
|
||||
padding: 57px;
|
||||
border: rgba(218, 225, 227, 1.000) 1px solid;
|
||||
width: 550px;
|
||||
height: 370px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.add-server-modal .page-title {
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin-bottom: 30px;
|
||||
margin-top: 30px;
|
||||
color: rgba(125, 135, 138, 1.000);
|
||||
}
|
||||
|
||||
.divider hr {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
width: 44%;
|
||||
}
|
||||
|
||||
.left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.server-center {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
padding-top: 13px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.server-center button {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
margin: auto;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 1.000);
|
||||
background: rgba(78, 191, 172, 1.000);
|
||||
border-color: none;
|
||||
border: none;
|
||||
width: 98%;
|
||||
height: 46px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.server-center button:hover {
|
||||
background: rgba(50, 149, 136, 1.000);
|
||||
}
|
||||
|
||||
.server-center button:focus {
|
||||
background: rgba(50, 149, 136, 1.000);
|
||||
}
|
||||
|
||||
.certificates-card {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.certificate-input {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.certificate-input div {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.certificate-input .setting-input-value {
|
||||
margin-left: 10px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#add-certificate-button {
|
||||
margin: 10px 10px 10px 37px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tip:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.md-14 {
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
#open-hotkeys-link {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* responsive grid */
|
||||
|
||||
@media (max-width: 650px) {
|
||||
.selected-css-path,
|
||||
.download-folder-path {
|
||||
margin-right: 15px;
|
||||
}
|
||||
#css-delete-action {
|
||||
margin-left: 10px;
|
||||
}
|
||||
#css-delete-action span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.modal-container {
|
||||
width: 60vw;
|
||||
padding: 40px;
|
||||
min-width: 300px;
|
||||
}
|
||||
.server-center button {
|
||||
margin-right: -12px;
|
||||
width: 100%;
|
||||
}
|
||||
.divider {
|
||||
margin-right: -8px;
|
||||
}
|
||||
.divider hr {
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
width: 43%;
|
||||
}
|
||||
#new-server-container {
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.divider {
|
||||
margin-left: 4%;
|
||||
}
|
||||
.divider hr {
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.settings-card {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.server-info-right {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.action {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
/* Override css rules */
|
||||
|
||||
.portico-wrap>.header {
|
||||
.portico-wrap > .header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.portico-container>.footer {
|
||||
.portico-container > .footer {
|
||||
display: none;
|
||||
}
|
BIN
app/renderer/fonts/Montserrat-Regular.ttf
Normal file
BIN
app/renderer/fonts/Montserrat-Regular.ttf
Normal file
Binary file not shown.
@@ -4,7 +4,7 @@ const Tab = require(__dirname + '/../components/tab.js');
|
||||
|
||||
class FunctionalTab extends Tab {
|
||||
template() {
|
||||
return `<div class="tab functional-tab">
|
||||
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>
|
||||
@@ -16,10 +16,11 @@ class FunctionalTab extends Tab {
|
||||
|
||||
init() {
|
||||
this.$el = this.generateNodeFromTemplate(this.template());
|
||||
this.props.$root.appendChild(this.$el);
|
||||
|
||||
this.$closeButton = this.$el.getElementsByClassName('server-tab-badge')[0];
|
||||
this.registerListeners();
|
||||
if (this.props.name !== 'Settings') {
|
||||
this.props.$root.appendChild(this.$el);
|
||||
this.$closeButton = this.$el.getElementsByClassName('server-tab-badge')[0];
|
||||
this.registerListeners();
|
||||
}
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
|
80
app/renderer/js/components/handle-external-link.js
Normal file
80
app/renderer/js/components/handle-external-link.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
const { shell, app } = require('electron').remote;
|
||||
const LinkUtil = require('../utils/link-util');
|
||||
const DomainUtil = require('../utils/domain-util');
|
||||
const ConfigUtil = require('../utils/config-util');
|
||||
|
||||
const dingSound = new Audio('../resources/sounds/ding.ogg');
|
||||
|
||||
function handleExternalLink(event) {
|
||||
const { url } = event;
|
||||
const domainPrefix = DomainUtil.getDomain(this.props.index).url;
|
||||
const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`);
|
||||
const shouldShowInFolder = ConfigUtil.getConfigItem('showDownloadFolder', false);
|
||||
|
||||
// Whitelist URLs which are allowed to be opened in the app
|
||||
const {
|
||||
isInternalUrl: isWhiteListURL,
|
||||
isUploadsUrl: isUploadsURL
|
||||
} = LinkUtil.isInternal(domainPrefix, url);
|
||||
|
||||
if (isWhiteListURL) {
|
||||
event.preventDefault();
|
||||
|
||||
// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
|
||||
// Show pdf attachments in a new window
|
||||
// if (LinkUtil.isPDF(url) && isUploadsURL) {
|
||||
// ipcRenderer.send('pdf-view', url);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// download txt, mp3, mp4 etc.. by using downloadURL in the
|
||||
// main process which allows the user to save the files to their desktop
|
||||
// and not trigger webview reload while image in webview will
|
||||
// do nothing and will not save it
|
||||
|
||||
// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
|
||||
// if (!LinkUtil.isImage(url) && !LinkUtil.isPDF(url) && isUploadsURL) {
|
||||
if (!LinkUtil.isImage(url) && isUploadsURL) {
|
||||
ipcRenderer.send('downloadFile', url, downloadPath);
|
||||
ipcRenderer.once('downloadFileCompleted', (event, filePath, fileName) => {
|
||||
const downloadNotification = new Notification('Download Complete', {
|
||||
body: shouldShowInFolder ? `Click to show ${fileName} in folder` : `Click to open ${fileName}`,
|
||||
silent: true // We'll play our own sound - ding.ogg
|
||||
});
|
||||
|
||||
// Play sound to indicate download complete
|
||||
if (!ConfigUtil.getConfigItem('silent')) {
|
||||
dingSound.play();
|
||||
}
|
||||
|
||||
downloadNotification.onclick = () => {
|
||||
if (shouldShowInFolder) {
|
||||
// Reveal file in download folder
|
||||
shell.showItemInFolder(filePath);
|
||||
} else {
|
||||
// Open file in the default native app
|
||||
shell.openItem(filePath);
|
||||
}
|
||||
};
|
||||
ipcRenderer.removeAllListeners('downloadFileFailed');
|
||||
});
|
||||
|
||||
ipcRenderer.once('downloadFileFailed', () => {
|
||||
// Automatic download failed, so show save dialog prompt and download
|
||||
// through webview
|
||||
this.$el.downloadURL(url);
|
||||
ipcRenderer.removeAllListeners('downloadFileCompleted');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// open internal urls inside the current webview.
|
||||
this.$el.loadURL(url);
|
||||
} else {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = handleExternalLink;
|
@@ -7,7 +7,8 @@ const {ipcRenderer} = require('electron');
|
||||
|
||||
class ServerTab extends Tab {
|
||||
template() {
|
||||
return `<div class="tab">
|
||||
return `<div class="tab" data-tab-id="${this.props.tabIndex}">
|
||||
<div class="server-tooltip" style="display:none">${this.props.name}</div>
|
||||
<div class="server-tab-badge"></div>
|
||||
<div class="server-tab">
|
||||
<img class="server-icons" src='${this.props.icon}'/>
|
||||
@@ -49,7 +50,8 @@ class ServerTab extends Tab {
|
||||
shortcutText = `Ctrl+${shownIndex}`;
|
||||
}
|
||||
|
||||
ipcRenderer.send('register-server-tab-shortcut', shownIndex);
|
||||
// Array index == Shown index - 1
|
||||
ipcRenderer.send('switch-server-tab', shownIndex - 1);
|
||||
|
||||
return shortcutText;
|
||||
}
|
||||
|
@@ -21,6 +21,8 @@ class Tab extends BaseComponent {
|
||||
|
||||
registerListeners() {
|
||||
this.$el.addEventListener('click', this.props.onClick);
|
||||
this.$el.addEventListener('mouseover', this.props.onHover);
|
||||
this.$el.addEventListener('mouseout', this.props.onHoverOut);
|
||||
}
|
||||
|
||||
isLoading() {
|
||||
|
@@ -3,13 +3,14 @@
|
||||
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} = require('electron').remote;
|
||||
const { app, dialog } = require('electron').remote;
|
||||
|
||||
const BaseComponent = require(__dirname + '/../components/base.js');
|
||||
const handleExternalLink = require(__dirname + '/../components/handle-external-link.js');
|
||||
|
||||
const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
|
||||
class WebView extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
@@ -17,18 +18,22 @@ class WebView extends BaseComponent {
|
||||
this.props = props;
|
||||
|
||||
this.zoomFactor = 1.0;
|
||||
this.loading = false;
|
||||
this.loading = true;
|
||||
this.badgeCount = 0;
|
||||
this.customCSS = ConfigUtil.getConfigItem('customCSS');
|
||||
this.$webviewsContainer = document.querySelector('#webviews-container').classList;
|
||||
}
|
||||
|
||||
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"
|
||||
name="${this.props.name}"
|
||||
webpreferences="allowRunningInsecureContent, javascript=yes">
|
||||
</webview>`;
|
||||
}
|
||||
@@ -42,33 +47,63 @@ class WebView extends BaseComponent {
|
||||
|
||||
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);
|
||||
}
|
||||
handleExternalLink.call(this, event);
|
||||
});
|
||||
|
||||
if (shouldSilentWebview) {
|
||||
this.$el.addEventListener('dom-ready', () => {
|
||||
this.$el.setAudioMuted(true);
|
||||
});
|
||||
}
|
||||
|
||||
this.$el.addEventListener('page-title-updated', event => {
|
||||
const {title} = event;
|
||||
const { title } = event;
|
||||
this.badgeCount = this.getBadgeCount(title);
|
||||
this.props.onTitleChange();
|
||||
});
|
||||
|
||||
this.$el.addEventListener('did-navigate-in-page', event => {
|
||||
const isSettingPage = event.url.includes('renderer/preference.html');
|
||||
if (isSettingPage) {
|
||||
return;
|
||||
}
|
||||
this.canGoBackButton();
|
||||
});
|
||||
|
||||
this.$el.addEventListener('did-navigate', () => {
|
||||
this.canGoBackButton();
|
||||
});
|
||||
|
||||
this.$el.addEventListener('page-favicon-updated', event => {
|
||||
const { favicons } = event;
|
||||
|
||||
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
|
||||
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
|
||||
if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') {
|
||||
// This api is only supported on macOS
|
||||
app.dock.setBadge('●');
|
||||
// bounce the dock
|
||||
if (ConfigUtil.getConfigItem('dockBouncing')) {
|
||||
app.dock.bounce();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$el.addEventListener('dom-ready', () => {
|
||||
if (this.props.role === 'server') {
|
||||
this.$el.classList.add('onload');
|
||||
}
|
||||
this.loading = false;
|
||||
this.show();
|
||||
|
||||
// Refocus text boxes after reload
|
||||
// Remove when upstream issue https://github.com/electron/electron/issues/14474 is fixed
|
||||
this.$el.blur();
|
||||
this.$el.focus();
|
||||
});
|
||||
|
||||
this.$el.addEventListener('did-fail-load', event => {
|
||||
const {errorDescription} = event;
|
||||
const { errorDescription } = event;
|
||||
const hasConnectivityErr = (SystemUtil.connectivityERR.indexOf(errorDescription) >= 0);
|
||||
if (hasConnectivityErr) {
|
||||
console.error('error', errorDescription);
|
||||
@@ -97,23 +132,51 @@ class WebView extends BaseComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
// To show or hide the loading indicator in the the active tab
|
||||
if (this.loading) {
|
||||
this.$webviewsContainer.remove('loaded');
|
||||
} else {
|
||||
this.$webviewsContainer.add('loaded');
|
||||
}
|
||||
|
||||
this.$el.classList.remove('disabled');
|
||||
this.$el.classList.add('active');
|
||||
setTimeout(() => {
|
||||
if (this.props.role === 'server') {
|
||||
this.$el.classList.remove('onload');
|
||||
}
|
||||
}, 1000);
|
||||
this.focus();
|
||||
this.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()) {
|
||||
// HACK: webContents.isFocused() seems to be true even without the element
|
||||
// being in focus. So, we check against `document.activeElement`.
|
||||
if (webContents && this.$el !== document.activeElement) {
|
||||
// HACK: Looks like blur needs to be called on the previously focused
|
||||
// element to transfer focus correctly, in Electron v3.0.10
|
||||
// See https://github.com/electron/electron/issues/15718
|
||||
document.activeElement.blur();
|
||||
this.$el.focus();
|
||||
webContents.focus();
|
||||
}
|
||||
@@ -121,6 +184,7 @@ class WebView extends BaseComponent {
|
||||
|
||||
hide() {
|
||||
this.$el.classList.add('disabled');
|
||||
this.$el.classList.remove('active');
|
||||
}
|
||||
|
||||
load() {
|
||||
@@ -161,6 +225,16 @@ class WebView extends BaseComponent {
|
||||
back() {
|
||||
if (this.$el.canGoBack()) {
|
||||
this.$el.goBack();
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
canGoBackButton() {
|
||||
const $backButton = document.querySelector('#actions-container #back-action');
|
||||
if (this.$el.canGoBack()) {
|
||||
$backButton.classList.remove('disable');
|
||||
} else {
|
||||
$backButton.classList.add('disable');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +246,9 @@ class WebView extends BaseComponent {
|
||||
|
||||
reload() {
|
||||
this.hide();
|
||||
// Shows the loading indicator till the webview is reloaded
|
||||
this.$webviewsContainer.remove('loaded');
|
||||
this.loading = true;
|
||||
this.$el.reload();
|
||||
}
|
||||
|
||||
|
40
app/renderer/js/electron-bridge.js
Normal file
40
app/renderer/js/electron-bridge.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const events = require('events');
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
// we have and will have some non camelcase stuff
|
||||
// while working with zulip so just turning the rule off
|
||||
// for the whole file.
|
||||
/* eslint-disable camelcase */
|
||||
class ElectronBridge extends events {
|
||||
send_event(...args) {
|
||||
this.emit(...args);
|
||||
}
|
||||
|
||||
on_event(...args) {
|
||||
this.on(...args);
|
||||
}
|
||||
}
|
||||
|
||||
const electron_bridge = new ElectronBridge();
|
||||
|
||||
electron_bridge.on('total_unread_count', (...args) => {
|
||||
ipcRenderer.send('unread-count', ...args);
|
||||
});
|
||||
|
||||
electron_bridge.on('realm_name', realmName => {
|
||||
const serverURL = location.origin;
|
||||
ipcRenderer.send('realm-name-changed', serverURL, realmName);
|
||||
});
|
||||
|
||||
electron_bridge.on('realm_icon_url', iconURL => {
|
||||
const serverURL = location.origin;
|
||||
iconURL = iconURL.includes('http') ? iconURL : `${serverURL}${iconURL}`;
|
||||
ipcRenderer.send('realm-icon-changed', serverURL, iconURL);
|
||||
});
|
||||
|
||||
// this follows node's idiomatic implementation of event
|
||||
// emitters to make event handling more simpler instead of using
|
||||
// functions zulip side will emit event using ElectronBrigde.send_event
|
||||
// which is alias of .emit and on this side we can handle the data by adding
|
||||
// a listener for the event.
|
||||
module.exports = electron_bridge;
|
62
app/renderer/js/feedback.js
Normal file
62
app/renderer/js/feedback.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const { app } = require('electron').remote;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const SendFeedback = require('@electron-elements/send-feedback');
|
||||
|
||||
// make the button color match zulip app's theme
|
||||
SendFeedback.customStyles = `
|
||||
button:hover, button:focus {
|
||||
border-color: #4EBFAC;
|
||||
color: #4EBFAC;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: #f1f1f1;
|
||||
color: #4EBFAC;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #4EBFAC;
|
||||
border-color: #4EBFAC;
|
||||
}
|
||||
`;
|
||||
|
||||
customElements.define('send-feedback', SendFeedback);
|
||||
const sendFeedback = document.querySelector('send-feedback');
|
||||
const feedbackHolder = sendFeedback.parentElement;
|
||||
|
||||
// customize the fields of custom elements
|
||||
sendFeedback.title = 'Report Issue';
|
||||
sendFeedback.titleLabel = 'Issue title:';
|
||||
sendFeedback.titlePlaceholder = 'Enter issue title';
|
||||
sendFeedback.textareaLabel = 'Describe the issue:';
|
||||
sendFeedback.textareaPlaceholder = 'Succinctly describe your issue and steps to reproduce it...';
|
||||
sendFeedback.buttonLabel = 'Report Issue';
|
||||
sendFeedback.loaderSuccessText = '';
|
||||
|
||||
sendFeedback.useReporter('emailReporter', {
|
||||
email: 'akash@zulipchat.com'
|
||||
});
|
||||
|
||||
feedbackHolder.addEventListener('click', e => {
|
||||
// only remove the class if the grey out faded
|
||||
// part is clicked and not the feedback element itself
|
||||
if (e.target === e.currentTarget) {
|
||||
feedbackHolder.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
sendFeedback.addEventListener('feedback-submitted', () => {
|
||||
setTimeout(() => {
|
||||
feedbackHolder.classList.remove('show');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
const dataDir = app.getPath('userData');
|
||||
const logsDir = path.join(dataDir, '/Logs');
|
||||
sendFeedback.logs.push(...fs.readdirSync(logsDir).map(file => path.join(logsDir, file)));
|
||||
|
||||
module.exports = {
|
||||
feedbackHolder,
|
||||
sendFeedback
|
||||
};
|
@@ -1,15 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer, remote, clipboard } = require('electron');
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
const { session, app, Menu, dialog } = remote;
|
||||
const escape = require('escape-html');
|
||||
|
||||
require(__dirname + '/js/tray.js');
|
||||
const { ipcRenderer, remote } = require('electron');
|
||||
|
||||
const { session } = remote;
|
||||
|
||||
const DomainUtil = require(__dirname + '/js/utils/domain-util.js');
|
||||
const WebView = require(__dirname + '/js/components/webview.js');
|
||||
const ServerTab = require(__dirname + '/js/components/server-tab.js');
|
||||
const FunctionalTab = require(__dirname + '/js/components/functional-tab.js');
|
||||
const ConfigUtil = require(__dirname + '/js/utils/config-util.js');
|
||||
const DNDUtil = require(__dirname + '/js/utils/dnd-util.js');
|
||||
const ReconnectUtil = require(__dirname + '/js/utils/reconnect-util.js');
|
||||
const Logger = require(__dirname + '/js/utils/logger-util.js');
|
||||
const CommonUtil = require(__dirname + '/js/utils/common-util.js');
|
||||
|
||||
const { feedbackHolder } = require(__dirname + '/js/feedback.js');
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'errors.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
class ServerManagerView {
|
||||
constructor() {
|
||||
@@ -20,9 +33,16 @@ class ServerManagerView {
|
||||
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.$dndButton = $actionsContainer.querySelector('#dnd-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.$dndTooltip = $actionsContainer.querySelector('#dnd-tooltip');
|
||||
|
||||
this.$sidebar = document.getElementById('sidebar');
|
||||
|
||||
this.$fullscreenPopup = document.getElementById('fullscreen-popup');
|
||||
@@ -32,6 +52,7 @@ class ServerManagerView {
|
||||
this.activeTabIndex = -1;
|
||||
this.tabs = [];
|
||||
this.functionalTabs = {};
|
||||
this.tabIndex = 0;
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -40,12 +61,23 @@ class ServerManagerView {
|
||||
this.initTabs();
|
||||
this.initActions();
|
||||
this.registerIpcs();
|
||||
this.initDefaultSettings();
|
||||
});
|
||||
}
|
||||
|
||||
loadProxy() {
|
||||
return new Promise(resolve => {
|
||||
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
|
||||
// To change proxyEnable to useManualProxy in older versions
|
||||
const proxyEnabledOld = ConfigUtil.isConfigItemExists('useProxy');
|
||||
if (proxyEnabledOld) {
|
||||
const proxyEnableOldState = ConfigUtil.getConfigItem('useProxy');
|
||||
if (proxyEnableOldState) {
|
||||
ConfigUtil.setConfigItem('useManualProxy', true);
|
||||
}
|
||||
ConfigUtil.removeConfigItem('useProxy');
|
||||
}
|
||||
|
||||
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
|
||||
if (proxyEnabled) {
|
||||
session.fromPartition('persist:webviewsession').setProxy({
|
||||
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
|
||||
@@ -62,6 +94,60 @@ class ServerManagerView {
|
||||
});
|
||||
}
|
||||
|
||||
// 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,
|
||||
useManualProxy: false,
|
||||
useSystemProxy: false,
|
||||
showSidebar: true,
|
||||
badgeOption: true,
|
||||
startAtLogin: true,
|
||||
startMinimized: false,
|
||||
enableSpellchecker: true,
|
||||
showNotification: true,
|
||||
autoUpdate: true,
|
||||
betaUpdate: false,
|
||||
errorReporting: true,
|
||||
customCSS: false,
|
||||
silent: false,
|
||||
lastActiveTab: 0,
|
||||
dnd: false,
|
||||
dndPreviousSettings: {
|
||||
showNotification: true,
|
||||
silent: false
|
||||
},
|
||||
downloadsPath: `${app.getPath('downloads')}`,
|
||||
showDownloadFolder: false
|
||||
};
|
||||
|
||||
// Platform specific settings
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Only available on Windows
|
||||
settingOptions.flashTaskbarOnMessage = true;
|
||||
settingOptions.dndPreviousSettings.flashTaskbarOnMessage = true;
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
// Only available on macOS
|
||||
settingOptions.dockBouncing = true;
|
||||
}
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
settingOptions.autoHideMenubar = false;
|
||||
}
|
||||
|
||||
for (const i in settingOptions) {
|
||||
if (ConfigUtil.getConfigItem(i) === null) {
|
||||
ConfigUtil.setConfigItem(i, settingOptions[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initSidebar() {
|
||||
const showSidebar = ConfigUtil.getConfigItem('showSidebar', true);
|
||||
this.toggleSidebar(showSidebar);
|
||||
@@ -75,26 +161,34 @@ class ServerManagerView {
|
||||
DomainUtil.updateSavedServer(servers[i].url, i);
|
||||
this.activateTab(i);
|
||||
}
|
||||
this.activateTab(0);
|
||||
// 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');
|
||||
this.openSettings('AddServer');
|
||||
}
|
||||
|
||||
ipcRenderer.send('local-shortcuts', true);
|
||||
}
|
||||
|
||||
initServer(server, index) {
|
||||
const tabIndex = this.getTabIndex();
|
||||
this.tabs.push(new ServerTab({
|
||||
role: 'server',
|
||||
icon: server.icon,
|
||||
name: server.alias,
|
||||
$root: this.$tabsContainer,
|
||||
onClick: this.activateTab.bind(this, index),
|
||||
onClick: this.activateLastTab.bind(this, index),
|
||||
index,
|
||||
tabIndex,
|
||||
onHover: this.onHover.bind(this, index),
|
||||
onHoverOut: this.onHoverOut.bind(this, index),
|
||||
webview: new WebView({
|
||||
$root: this.$webviewsContainer,
|
||||
index,
|
||||
tabIndex,
|
||||
url: server.url,
|
||||
name: server.alias,
|
||||
role: 'server',
|
||||
name: CommonUtil.decodeString(server.alias),
|
||||
isActive: () => {
|
||||
return index === this.activeTabIndex;
|
||||
},
|
||||
@@ -107,29 +201,120 @@ class ServerManagerView {
|
||||
}
|
||||
|
||||
initActions() {
|
||||
this.initDNDButton();
|
||||
this.initServerActions();
|
||||
this.initLeftSidebarEvents();
|
||||
}
|
||||
|
||||
initServerActions() {
|
||||
const $serverImgs = document.querySelectorAll('.server-icons');
|
||||
$serverImgs.forEach(($serverImg, index) => {
|
||||
this.addContextMenu($serverImg, index);
|
||||
if ($serverImg.src.includes('img/icon.png')) {
|
||||
this.displayInitialCharLogo($serverImg, index);
|
||||
}
|
||||
$serverImg.addEventListener('error', () => {
|
||||
this.displayInitialCharLogo($serverImg, index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initLeftSidebarEvents() {
|
||||
this.$dndButton.addEventListener('click', () => {
|
||||
const dndUtil = DNDUtil.toggle();
|
||||
ipcRenderer.send('forward-message', 'toggle-dnd', dndUtil.dnd, dndUtil.newSettings);
|
||||
});
|
||||
this.$reloadButton.addEventListener('click', () => {
|
||||
this.tabs[this.activeTabIndex].webview.reload();
|
||||
});
|
||||
this.$addServerButton.addEventListener('click', () => {
|
||||
this.openSettings('Servers');
|
||||
this.openSettings('AddServer');
|
||||
});
|
||||
this.$settingsButton.addEventListener('click', () => {
|
||||
this.openSettings('General');
|
||||
});
|
||||
this.$backButton.addEventListener('click', () => {
|
||||
this.tabs[this.activeTabIndex].webview.back();
|
||||
});
|
||||
|
||||
this.sidebarHoverEvent(this.$addServerButton, this.$addServerTooltip, true);
|
||||
this.sidebarHoverEvent(this.$settingsButton, this.$settingsTooltip);
|
||||
this.sidebarHoverEvent(this.$reloadButton, this.$reloadTooltip);
|
||||
this.sidebarHoverEvent(this.$backButton, this.$backTooltip);
|
||||
this.sidebarHoverEvent(this.$dndButton, this.$dndTooltip);
|
||||
}
|
||||
|
||||
sidebarHoverEvent(SidebarButton, SidebarTooltip) {
|
||||
initDNDButton() {
|
||||
const dnd = ConfigUtil.getConfigItem('dnd', false);
|
||||
this.toggleDNDButton(dnd);
|
||||
}
|
||||
|
||||
getTabIndex() {
|
||||
const currentIndex = this.tabIndex;
|
||||
this.tabIndex++;
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
displayInitialCharLogo($img, index) {
|
||||
/*
|
||||
index parameter needed because webview[data-tab-id] can increment
|
||||
beyond size of sidebar org array and throw error
|
||||
*/
|
||||
|
||||
const $altIcon = document.createElement('div');
|
||||
const $parent = $img.parentElement;
|
||||
const $container = $parent.parentElement;
|
||||
const webviewId = $container.dataset.tabId;
|
||||
const $webview = document.querySelector(`webview[data-tab-id="${webviewId}"]`);
|
||||
const realmName = $webview.getAttribute('name');
|
||||
|
||||
if (realmName === null) {
|
||||
$img.src = '/img/icon.png';
|
||||
return;
|
||||
}
|
||||
|
||||
$altIcon.textContent = realmName.charAt(0) || 'Z';
|
||||
$altIcon.classList.add('server-icon');
|
||||
$altIcon.classList.add('alt-icon');
|
||||
|
||||
$parent.removeChild($img);
|
||||
$parent.appendChild($altIcon);
|
||||
|
||||
this.addContextMenu($altIcon, index);
|
||||
}
|
||||
|
||||
sidebarHoverEvent(SidebarButton, SidebarTooltip, addServer = false) {
|
||||
SidebarButton.addEventListener('mouseover', () => {
|
||||
SidebarTooltip.removeAttribute('style');
|
||||
// To handle position of add server tooltip due to scrolling of list of organizations
|
||||
// This could not be handled using CSS, hence the top of the tooltip is made same
|
||||
// as that of its parent element.
|
||||
// This needs to handled only for the add server tooltip and not others.
|
||||
if (addServer) {
|
||||
const { top } = SidebarButton.getBoundingClientRect();
|
||||
SidebarTooltip.style.top = top + 'px';
|
||||
}
|
||||
});
|
||||
SidebarButton.addEventListener('mouseout', () => {
|
||||
SidebarTooltip.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
onHover(index) {
|
||||
// this.$serverIconTooltip[index].innerHTML already has realm name, so we are just
|
||||
// removing the style.
|
||||
this.$serverIconTooltip[index].removeAttribute('style');
|
||||
// To handle position of servers' tooltip due to scrolling of list of organizations
|
||||
// This could not be handled using CSS, hence the top of the tooltip is made same
|
||||
// as that of its parent element.
|
||||
const { top } = this.$serverIconTooltip[index].parentElement.getBoundingClientRect();
|
||||
this.$serverIconTooltip[index].style.top = top + 'px';
|
||||
}
|
||||
|
||||
onHoverOut(index) {
|
||||
this.$serverIconTooltip[index].style.display = 'none';
|
||||
}
|
||||
|
||||
openFunctionalTab(tabProps) {
|
||||
if (this.functionalTabs[tabProps.name] !== undefined) {
|
||||
this.activateTab(this.functionalTabs[tabProps.name]);
|
||||
@@ -138,17 +323,23 @@ class ServerManagerView {
|
||||
|
||||
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,
|
||||
role: 'function',
|
||||
name: tabProps.name,
|
||||
isActive: () => {
|
||||
return this.functionalTabs[tabProps.name] === this.activeTabIndex;
|
||||
@@ -160,6 +351,10 @@ class ServerManagerView {
|
||||
})
|
||||
}));
|
||||
|
||||
// To show loading indicator the first time a functional tab is opened, indicator is
|
||||
// closed when the functional tab DOM is ready, handled in webview.js
|
||||
this.$webviewsContainer.classList.remove('loaded');
|
||||
|
||||
this.activateTab(this.functionalTabs[tabProps.name]);
|
||||
}
|
||||
|
||||
@@ -169,6 +364,7 @@ class ServerManagerView {
|
||||
materialIcon: 'settings',
|
||||
url: `file://${__dirname}/preference.html#${nav}`
|
||||
});
|
||||
this.$settingsButton.classList.add('active');
|
||||
this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav);
|
||||
}
|
||||
|
||||
@@ -188,8 +384,34 @@ class ServerManagerView {
|
||||
});
|
||||
}
|
||||
|
||||
activateLastTab(index) {
|
||||
// Open all the tabs in background, also activate the tab based on the index
|
||||
this.activateTab(index);
|
||||
// Save last active tab
|
||||
ConfigUtil.setConfigItem('lastActiveTab', index);
|
||||
}
|
||||
|
||||
// returns this.tabs in an way that does
|
||||
// not crash app when this.tabs is passed into
|
||||
// ipcRenderer. Something about webview, and props.webview
|
||||
// properties in ServerTab causes the app to crash.
|
||||
get tabsForIpc() {
|
||||
const tabs = [];
|
||||
this.tabs.forEach(tab => {
|
||||
const proto = Object.create(Object.getPrototypeOf(tab));
|
||||
const tabClone = Object.assign(proto, tab);
|
||||
|
||||
tabClone.webview = { props: {} };
|
||||
tabClone.webview.props.name = tab.webview.props.name;
|
||||
delete tabClone.props.webview;
|
||||
tabs.push(tabClone);
|
||||
});
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
activateTab(index, hideOldTab = true) {
|
||||
if (this.tabs[index].webview.loading) {
|
||||
if (!this.tabs[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,16 +419,29 @@ class ServerManagerView {
|
||||
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
|
||||
// JSON stringify this.tabs to avoid a crash
|
||||
// util.inspect is being used to handle circular references
|
||||
tabs: this.tabsForIpc,
|
||||
activeTabIndex: this.activeTabIndex,
|
||||
// Following flag controls whether a menu item should be enabled or not
|
||||
enableMenu: this.tabs[index].props.role === 'server'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -227,6 +462,9 @@ class ServerManagerView {
|
||||
}
|
||||
|
||||
destroyView() {
|
||||
// Show loading indicator
|
||||
this.$webviewsContainer.classList.remove('loaded');
|
||||
|
||||
// Clear global variables
|
||||
this.activeTabIndex = -1;
|
||||
this.tabs = [];
|
||||
@@ -235,14 +473,23 @@ class ServerManagerView {
|
||||
// Clear DOM elements
|
||||
this.$tabsContainer.innerHTML = '';
|
||||
this.$webviewsContainer.innerHTML = '';
|
||||
|
||||
// Destroy shortcuts
|
||||
ipcRenderer.send('local-shortcuts', false);
|
||||
}
|
||||
|
||||
reloadView() {
|
||||
// Save and remember the index of last active tab so that we can use it later
|
||||
const lastActiveTab = this.tabs[this.activeTabIndex].props.index;
|
||||
ConfigUtil.setConfigItem('lastActiveTab', lastActiveTab);
|
||||
|
||||
// Destroy the current view and re-initiate it
|
||||
this.destroyView();
|
||||
this.initTabs();
|
||||
this.initServerActions();
|
||||
}
|
||||
|
||||
// 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() {
|
||||
@@ -260,12 +507,50 @@ class ServerManagerView {
|
||||
|
||||
toggleSidebar(show) {
|
||||
if (show) {
|
||||
this.$sidebar.classList.remove('hidden');
|
||||
this.$sidebar.classList.remove('sidebar-hide');
|
||||
} else {
|
||||
this.$sidebar.classList.add('hidden');
|
||||
this.$sidebar.classList.add('sidebar-hide');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggles the dnd button icon.
|
||||
toggleDNDButton(alert) {
|
||||
this.$dndTooltip.textContent = (alert ? 'Disable' : 'Enable') + ' Do Not Disturb';
|
||||
this.$dndButton.querySelector('i').textContent = alert ? 'notifications_off' : 'notifications';
|
||||
}
|
||||
|
||||
addContextMenu($serverImg, index) {
|
||||
$serverImg.addEventListener('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
const template = [
|
||||
{
|
||||
label: 'Disconnect organization',
|
||||
click: () => {
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['YES', 'NO'],
|
||||
defaultId: 0,
|
||||
message: 'Are you sure you want to disconnect this organization?'
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
DomainUtil.removeDomain(index);
|
||||
ipcRenderer.send('reload-full-app');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Copy Zulip URL',
|
||||
click: () => {
|
||||
clipboard.writeText(DomainUtil.getDomain(index).url);
|
||||
}
|
||||
}
|
||||
];
|
||||
const contextMenu = Menu.buildFromTemplate(template);
|
||||
contextMenu.popup({ window: remote.getCurrentWindow() });
|
||||
});
|
||||
}
|
||||
|
||||
registerIpcs() {
|
||||
const webviewListeners = {
|
||||
'webview-reload': 'reload',
|
||||
@@ -295,7 +580,9 @@ class ServerManagerView {
|
||||
|
||||
ipcRenderer.on('open-about', this.openAbout.bind(this));
|
||||
|
||||
ipcRenderer.on('reload-viewer', this.reloadView.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');
|
||||
@@ -306,19 +593,91 @@ class ServerManagerView {
|
||||
});
|
||||
|
||||
ipcRenderer.on('switch-server-tab', (event, index) => {
|
||||
this.activateTab(index);
|
||||
this.activateLastTab(index);
|
||||
});
|
||||
|
||||
ipcRenderer.on('open-org-tab', () => {
|
||||
this.openSettings('AddServer');
|
||||
});
|
||||
|
||||
ipcRenderer.on('reload-proxy', (event, showAlert) => {
|
||||
this.loadProxy().then(() => {
|
||||
if (showAlert) {
|
||||
alert('Proxy settings saved!');
|
||||
ipcRenderer.send('reload-full-app');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggle-sidebar', (event, show) => {
|
||||
// Toggle the left sidebar
|
||||
this.toggleSidebar(show);
|
||||
|
||||
// Toggle sidebar switch in the general settings
|
||||
const selector = 'webview:not([class*=disabled])';
|
||||
const webview = document.querySelector(selector);
|
||||
const webContents = webview.getWebContents();
|
||||
webContents.send('toggle-sidebar-setting', show);
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggle-silent', (event, state) => {
|
||||
const webviews = document.querySelectorAll('webview');
|
||||
webviews.forEach(webview => {
|
||||
try {
|
||||
webview.setAudioMuted(state);
|
||||
} catch (err) {
|
||||
// webview is not ready yet
|
||||
webview.addEventListener('dom-ready', () => {
|
||||
webview.setAudioMuted(state);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggle-dnd', (event, state, newSettings) => {
|
||||
this.toggleDNDButton(state);
|
||||
ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent);
|
||||
const selector = 'webview:not([class*=disabled])';
|
||||
const webview = document.querySelector(selector);
|
||||
const webContents = webview.getWebContents();
|
||||
webContents.send('toggle-dnd', state, newSettings);
|
||||
});
|
||||
|
||||
ipcRenderer.on('update-realm-name', (event, serverURL, realmName) => {
|
||||
DomainUtil.getDomains().forEach((domain, index) => {
|
||||
if (domain.url.includes(serverURL)) {
|
||||
const serverTooltipSelector = `.tab .server-tooltip`;
|
||||
const serverTooltips = document.querySelectorAll(serverTooltipSelector);
|
||||
serverTooltips[index].innerHTML = escape(realmName);
|
||||
this.tabs[index].props.name = escape(realmName);
|
||||
this.tabs[index].webview.props.name = realmName;
|
||||
|
||||
domain.alias = escape(realmName);
|
||||
DomainUtil.db.push(`/domains[${index}]`, domain, true);
|
||||
DomainUtil.reloadDB();
|
||||
// Update the realm name also on the Window menu
|
||||
ipcRenderer.send('update-menu', {
|
||||
tabs: this.tabsForIpc,
|
||||
activeTabIndex: this.activeTabIndex
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ipcRenderer.on('update-realm-icon', (event, serverURL, iconURL) => {
|
||||
DomainUtil.getDomains().forEach((domain, index) => {
|
||||
if (domain.url.includes(serverURL)) {
|
||||
DomainUtil.saveServerIcon(iconURL).then(localIconUrl => {
|
||||
const serverImgsSelector = `.tab .server-icons`;
|
||||
const serverImgs = document.querySelectorAll(serverImgsSelector);
|
||||
serverImgs[index].src = localIconUrl;
|
||||
|
||||
domain.icon = localIconUrl;
|
||||
DomainUtil.db.push(`/domains[${index}]`, domain, true);
|
||||
DomainUtil.reloadDB();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ipcRenderer.on('enter-fullscreen', () => {
|
||||
@@ -330,6 +689,18 @@ class ServerManagerView {
|
||||
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) {
|
||||
@@ -358,14 +729,41 @@ class ServerManagerView {
|
||||
}
|
||||
ipcRenderer.send('update-taskbar-icon', createOverlayIcon(messageCount).toDataURL(), String(messageCount));
|
||||
});
|
||||
|
||||
ipcRenderer.on('open-feedback-modal', () => {
|
||||
feedbackHolder.classList.add('show');
|
||||
});
|
||||
|
||||
ipcRenderer.on('copy-zulip-url', () => {
|
||||
clipboard.writeText(DomainUtil.getDomain(this.activeTabIndex).url);
|
||||
});
|
||||
|
||||
ipcRenderer.on('new-server', () => {
|
||||
this.openSettings('AddServer');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = () => {
|
||||
const serverManagerView = new ServerManagerView();
|
||||
const reconnectUtil = new ReconnectUtil(serverManagerView);
|
||||
serverManagerView.init();
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
serverManagerView.reloadView();
|
||||
reconnectUtil.pollInternetAndReload();
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
reconnectUtil.clearState();
|
||||
logger.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();
|
||||
}
|
||||
};
|
||||
|
@@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { remote } = require('electron');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/utils/config-util.js');
|
||||
|
||||
const app = remote.app;
|
||||
|
||||
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
|
||||
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
|
||||
app.setAppUserModelId('org.zulip.zulip-electron');
|
||||
|
||||
const NativeNotification = window.Notification;
|
||||
|
||||
class baseNotification extends NativeNotification {
|
||||
constructor(title, opts) {
|
||||
opts.silent = ConfigUtil.getConfigItem('silent') || false;
|
||||
// calling super without passing arguments will fail to constuct Notification
|
||||
// and will result in no notification. It's a hack (not an ideal way) for deleting the window notification
|
||||
ConfigUtil.getConfigItem('showNotification') ? super(title, opts) : super(); // eslint-disable-line no-unused-expressions
|
||||
}
|
||||
}
|
||||
|
||||
window.Notification = baseNotification;
|
100
app/renderer/js/notification/darwin-notifications.js
Normal file
100
app/renderer/js/notification/darwin-notifications.js
Normal file
@@ -0,0 +1,100 @@
|
||||
'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;
|
31
app/renderer/js/notification/default-notification.js
Normal file
31
app/renderer/js/notification/default-notification.js
Normal file
@@ -0,0 +1,31 @@
|
||||
'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;
|
156
app/renderer/js/notification/helpers.js
Normal file
156
app/renderer/js/notification/helpers.js
Normal file
@@ -0,0 +1,156 @@
|
||||
const { remote } = require('electron');
|
||||
const Logger = require('../utils/logger-util.js');
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'errors.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
// Do not change this
|
||||
const appId = 'org.zulip.zulip-electron';
|
||||
|
||||
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 => {
|
||||
logger.log('Load bots request failed: ', error.responseText);
|
||||
logger.log('Load bots 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;
|
||||
const narrowByTopic = narrow.by_topic || narrow.by_subject;
|
||||
narrowByTopic(id, { trigger: 'notification' });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
appId,
|
||||
checkElements,
|
||||
customReply,
|
||||
parseReply,
|
||||
setupReply,
|
||||
focusCurrentServer,
|
||||
loadBots
|
||||
};
|
27
app/renderer/js/notification/index.js
Normal file
27
app/renderer/js/notification/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
remote: { app }
|
||||
} = require('electron');
|
||||
|
||||
const params = require('../utils/params-util.js');
|
||||
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', () => {
|
||||
// eslint-disable-next-line no-undef, camelcase
|
||||
if (params.isPageParams() && page_params.realm_uri) {
|
||||
loadBots();
|
||||
}
|
||||
});
|
92
app/renderer/js/pages/preference/add-certificate.js
Normal file
92
app/renderer/js/pages/preference/add-certificate.js
Normal file
@@ -0,0 +1,92 @@
|
||||
'use-strict';
|
||||
|
||||
const { dialog } = require('electron').remote;
|
||||
const path = require('path');
|
||||
|
||||
const BaseComponent = require(__dirname + '/../../components/base.js');
|
||||
const CertificateUtil = require(__dirname + '/../../utils/certificate-util.js');
|
||||
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
|
||||
|
||||
class AddCertificate extends BaseComponent {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
this._certFile = '';
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-card certificates-card">
|
||||
<div class="certificate-input">
|
||||
<div>Organization URL</div>
|
||||
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
|
||||
</div>
|
||||
<div class="certificate-input">
|
||||
<div>Certificate file</div>
|
||||
<button class="green" id="add-certificate-button">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$addCertificate = this.generateNodeFromTemplate(this.template());
|
||||
this.props.$root.appendChild(this.$addCertificate);
|
||||
this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button');
|
||||
this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0];
|
||||
this.initListeners();
|
||||
}
|
||||
|
||||
validateAndAdd() {
|
||||
const certificate = this._certFile;
|
||||
const serverUrl = this.serverUrl.value;
|
||||
if (certificate !== '' && serverUrl !== '') {
|
||||
const server = encodeURIComponent(DomainUtil.formatUrl(serverUrl));
|
||||
const fileName = path.basename(certificate);
|
||||
const copy = CertificateUtil.copyCertificate(server, certificate, fileName);
|
||||
if (!copy) {
|
||||
return;
|
||||
}
|
||||
CertificateUtil.setCertificate(server, fileName);
|
||||
dialog.showMessageBox({
|
||||
title: 'Success',
|
||||
message: `Certificate saved!`
|
||||
});
|
||||
this.serverUrl.value = '';
|
||||
} else {
|
||||
dialog.showErrorBox('Error', `Please, ${serverUrl === '' ?
|
||||
'Enter an Organization URL' : 'Choose certificate file'}`);
|
||||
}
|
||||
}
|
||||
|
||||
addHandler() {
|
||||
const showDialogOptions = {
|
||||
title: 'Select file',
|
||||
defaultId: 1,
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'crt, pem', extensions: ['crt', 'pem'] }]
|
||||
};
|
||||
dialog.showOpenDialog(showDialogOptions, selectedFile => {
|
||||
if (selectedFile) {
|
||||
this._certFile = selectedFile[0] || '';
|
||||
this.validateAndAdd();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initListeners() {
|
||||
this.addCertificateButton.addEventListener('click', () => {
|
||||
this.addHandler();
|
||||
});
|
||||
|
||||
this.serverUrl.addEventListener('keypress', event => {
|
||||
const EnterkeyCode = event.keyCode;
|
||||
|
||||
if (EnterkeyCode === 13) {
|
||||
this.addHandler();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AddCertificate;
|
@@ -45,7 +45,7 @@ class BadgeSettings {
|
||||
|
||||
updateOverlayIcon(messageCount, mainWindow) {
|
||||
if (!mainWindow.isFocused()) {
|
||||
mainWindow.flashFrame(true);
|
||||
mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
|
||||
}
|
||||
if (messageCount === 0) {
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
|
@@ -19,14 +19,20 @@ class BaseSection extends BaseComponent {
|
||||
generateOptionTemplate(settingOption) {
|
||||
if (settingOption) {
|
||||
return `
|
||||
<div class="action green">
|
||||
<span>On</span>
|
||||
<div class="action">
|
||||
<div class="switch">
|
||||
<input class="toggle toggle-round" type="checkbox" checked>
|
||||
<label></label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="action red">
|
||||
<span>Off</span>
|
||||
<div class="action">
|
||||
<div class="switch">
|
||||
<input class="toggle toggle-round" type="checkbox">
|
||||
<label></label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
72
app/renderer/js/pages/preference/connected-org-section.js
Normal file
72
app/renderer/js/pages/preference/connected-org-section.js
Normal file
@@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
|
||||
const BaseSection = require(__dirname + '/base-section.js');
|
||||
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
|
||||
const ServerInfoForm = require(__dirname + '/server-info-form.js');
|
||||
const AddCertificate = require(__dirname + '/add-certificate.js');
|
||||
|
||||
class ConnectedOrgSection extends BaseSection {
|
||||
constructor(props) {
|
||||
super();
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-pane" id="server-settings-pane">
|
||||
<div class="page-title">Connected organizations</div>
|
||||
<div class="title" id="existing-servers">All the connected orgnizations will appear here.</div>
|
||||
<div id="server-info-container"></div>
|
||||
<div id="new-org-button"><button class="green sea w-250">Connect to another organization</button></div>
|
||||
<div class="page-title">Add Custom Certificates</div>
|
||||
<div id="add-certificate-container"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initServers();
|
||||
}
|
||||
|
||||
initServers() {
|
||||
this.props.$root.innerHTML = '';
|
||||
|
||||
const servers = DomainUtil.getDomains();
|
||||
this.props.$root.innerHTML = this.template();
|
||||
|
||||
this.$serverInfoContainer = document.getElementById('server-info-container');
|
||||
this.$existingServers = document.getElementById('existing-servers');
|
||||
this.$newOrgButton = document.getElementById('new-org-button');
|
||||
this.$addCertificateContainer = document.getElementById('add-certificate-container');
|
||||
|
||||
const noServerText = 'All the connected orgnizations will appear here';
|
||||
// Show noServerText if no servers are there otherwise hide it
|
||||
this.$existingServers.innerHTML = servers.length === 0 ? noServerText : '';
|
||||
|
||||
for (let i = 0; i < servers.length; i++) {
|
||||
new ServerInfoForm({
|
||||
$root: this.$serverInfoContainer,
|
||||
server: servers[i],
|
||||
index: i,
|
||||
onChange: this.reloadApp
|
||||
}).init();
|
||||
}
|
||||
|
||||
this.$newOrgButton.addEventListener('click', () => {
|
||||
// We don't need to import this since it's already imported in other files
|
||||
// eslint-disable-next-line no-undef
|
||||
ipcRenderer.send('forward-message', 'open-org-tab');
|
||||
});
|
||||
|
||||
this.initAddCertificate();
|
||||
}
|
||||
|
||||
initAddCertificate() {
|
||||
new AddCertificate({
|
||||
$root: this.$addCertificateContainer
|
||||
}).init();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ConnectedOrgSection;
|
@@ -1,11 +1,10 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const { app, dialog } = require('electron').remote;
|
||||
|
||||
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');
|
||||
|
||||
@@ -24,19 +23,31 @@ class GeneralSection extends BaseSection {
|
||||
<div class="setting-description">Show app icon in system tray</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="menubar-option" style= "display:${process.platform === 'darwin' ? 'none' : ''}">
|
||||
<div class="setting-description">Auto hide 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">Show sidebar (<span class="code">CmdOrCtrl+S</span>)</div>
|
||||
<div class="setting-description">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">Show app unread badge</div>
|
||||
<div class="setting-control"></div>
|
||||
<div class="setting-description">Show app unread badge</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="dock-bounce-option" style= "display:${process.platform === 'darwin' ? '' : 'none'}">
|
||||
<div class="setting-description">Bounce dock on new private message</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="flash-taskbar-option" style= "display:${process.platform === 'win32' ? '' : 'none'}">
|
||||
<div class="setting-description">Flash taskbar on new message</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">Desktop Notification</div>
|
||||
<div class="title">Desktop Notifications</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="show-notification-option">
|
||||
<div class="setting-description">Show Desktop Notifications</div>
|
||||
<div class="setting-description">Show desktop notifications</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="silent-option">
|
||||
@@ -45,7 +56,11 @@ class GeneralSection extends BaseSection {
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">App Updates</div>
|
||||
<div class="settings-card">
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="autoupdate-option">
|
||||
<div class="setting-description">Enable auto updates</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="betaupdate-option">
|
||||
<div class="setting-description">Get beta updates</div>
|
||||
<div class="setting-control"></div>
|
||||
@@ -57,13 +72,59 @@ class GeneralSection extends BaseSection {
|
||||
<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">Advanced</div>
|
||||
<div class="settings-card">
|
||||
<div class="setting-row" id="enable-error-reporting">
|
||||
<div class="setting-description">Enable error reporting (requires restart)</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="show-download-folder">
|
||||
<div class="setting-description">Show downloaded files in file manager</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-row" id="add-custom-css">
|
||||
<div class="setting-description">
|
||||
Add custom CSS
|
||||
</div>
|
||||
<button class="custom-css-button green">Upload</button>
|
||||
</div>
|
||||
<div class="setting-row" id="remove-custom-css">
|
||||
<div class="setting-description">
|
||||
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div>
|
||||
</div>
|
||||
<div class="action red" id="css-delete-action">
|
||||
<i class="material-icons">indeterminate_check_box</i>
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row" id="download-folder">
|
||||
<div class="setting-description">
|
||||
Default download location
|
||||
</div>
|
||||
<button class="download-folder-button green">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>
|
||||
<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 green">Reset App Data</button>
|
||||
<button class="reset-data-button red w-150">Reset App Data</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,12 +135,37 @@ class GeneralSection extends BaseSection {
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.updateTrayOption();
|
||||
this.updateBadgeOption();
|
||||
this.updateUpdateOption();
|
||||
this.updateSilentOption();
|
||||
this.autoUpdateOption();
|
||||
this.betaUpdateOption();
|
||||
this.updateSidebarOption();
|
||||
this.updateStartAtLoginOption();
|
||||
this.updateResetDataOption();
|
||||
this.showDesktopNotification();
|
||||
this.enableSpellchecker();
|
||||
this.minimizeOnStart();
|
||||
this.addCustomCSS();
|
||||
this.showCustomCSSPath();
|
||||
this.removeCustomCSS();
|
||||
this.downloadFolder();
|
||||
this.showDownloadFolder();
|
||||
this.enableErrorReporting();
|
||||
|
||||
// Platform specific settings
|
||||
|
||||
// Flashing taskbar on Windows
|
||||
if (process.platform === 'win32') {
|
||||
this.updateFlashTaskbar();
|
||||
}
|
||||
// Dock bounce on macOS
|
||||
if (process.platform === 'darwin') {
|
||||
this.updateDockBouncing();
|
||||
}
|
||||
|
||||
// Auto hide menubar on Windows and Linux
|
||||
if (process.platform !== 'darwin') {
|
||||
this.updateMenubarOption();
|
||||
}
|
||||
}
|
||||
|
||||
updateTrayOption() {
|
||||
@@ -95,6 +181,19 @@ class GeneralSection extends BaseSection {
|
||||
});
|
||||
}
|
||||
|
||||
updateMenubarOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#menubar-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('autoHideMenubar', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('autoHideMenubar');
|
||||
ConfigUtil.setConfigItem('autoHideMenubar', newValue);
|
||||
ipcRenderer.send('toggle-menubar', newValue);
|
||||
this.updateMenubarOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateBadgeOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#badge-option .setting-control'),
|
||||
@@ -108,14 +207,56 @@ class GeneralSection extends BaseSection {
|
||||
});
|
||||
}
|
||||
|
||||
updateUpdateOption() {
|
||||
updateDockBouncing() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#dock-bounce-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('dockBouncing', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('dockBouncing');
|
||||
ConfigUtil.setConfigItem('dockBouncing', newValue);
|
||||
this.updateDockBouncing();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateFlashTaskbar() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#flash-taskbar-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('flashTaskbarOnMessage', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('flashTaskbarOnMessage');
|
||||
ConfigUtil.setConfigItem('flashTaskbarOnMessage', newValue);
|
||||
this.updateFlashTaskbar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
autoUpdateOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#autoupdate-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('autoUpdate', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('autoUpdate');
|
||||
ConfigUtil.setConfigItem('autoUpdate', newValue);
|
||||
if (!newValue) {
|
||||
ConfigUtil.setConfigItem('betaUpdate', false);
|
||||
this.betaUpdateOption();
|
||||
}
|
||||
this.autoUpdateOption();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
betaUpdateOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#betaupdate-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('betaUpdate', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('betaUpdate');
|
||||
ConfigUtil.setConfigItem('betaUpdate', newValue);
|
||||
this.updateUpdateOption();
|
||||
if (ConfigUtil.getConfigItem('autoUpdate')) {
|
||||
ConfigUtil.setConfigItem('betaUpdate', newValue);
|
||||
this.betaUpdateOption();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -128,6 +269,7 @@ class GeneralSection extends BaseSection {
|
||||
const newValue = !ConfigUtil.getConfigItem('silent', true);
|
||||
ConfigUtil.setConfigItem('silent', newValue);
|
||||
this.updateSilentOption();
|
||||
currentBrowserWindow.send('toggle-silent', newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -170,6 +312,30 @@ class GeneralSection extends BaseSection {
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableErrorReporting() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#enable-error-reporting .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('errorReporting', true),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('errorReporting');
|
||||
ConfigUtil.setConfigItem('errorReporting', newValue);
|
||||
this.enableErrorReporting();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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());
|
||||
@@ -188,6 +354,22 @@ class GeneralSection extends BaseSection {
|
||||
});
|
||||
}
|
||||
|
||||
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', () => {
|
||||
@@ -195,6 +377,74 @@ class GeneralSection extends BaseSection {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
downloadFolderDialog() {
|
||||
const showDialogOptions = {
|
||||
title: 'Select Download Location',
|
||||
defaultId: 1,
|
||||
properties: ['openDirectory']
|
||||
};
|
||||
|
||||
dialog.showOpenDialog(showDialogOptions, selectedFolder => {
|
||||
if (selectedFolder) {
|
||||
ConfigUtil.setConfigItem('downloadsPath', selectedFolder[0]);
|
||||
const downloadFolderPath = document.querySelector('.download-folder-path');
|
||||
downloadFolderPath.innerText = selectedFolder[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
downloadFolder() {
|
||||
const downloadFolder = document.querySelector('#download-folder .download-folder-button');
|
||||
downloadFolder.addEventListener('click', () => {
|
||||
this.downloadFolderDialog();
|
||||
});
|
||||
}
|
||||
|
||||
showDownloadFolder() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#show-download-folder .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('showDownloadFolder', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('showDownloadFolder');
|
||||
ConfigUtil.setConfigItem('showDownloadFolder', newValue);
|
||||
this.showDownloadFolder();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = GeneralSection;
|
||||
|
@@ -8,7 +8,7 @@ class PreferenceNav extends BaseComponent {
|
||||
|
||||
this.props = props;
|
||||
|
||||
this.navItems = ['General', 'Network', 'Servers', 'Shortcuts'];
|
||||
this.navItems = ['General', 'Network', 'AddServer', 'Organizations', 'Shortcuts'];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
@@ -16,11 +16,15 @@ class NetworkSection extends BaseSection {
|
||||
<div class="settings-pane">
|
||||
<div class="title">Proxy</div>
|
||||
<div id="appearance-option-settings" class="settings-card">
|
||||
<div class="setting-row" id="use-proxy-option">
|
||||
<div class="setting-description">Connect servers through a proxy</div>
|
||||
<div class="setting-row" id="use-system-settings">
|
||||
<div class="setting-description">Use system proxy settings (requires restart)</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="setting-block">
|
||||
<div class="setting-row" id="use-manual-settings">
|
||||
<div class="setting-description">Manual proxy configuration</div>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
<div class="manual-proxy-block">
|
||||
<div class="setting-row" id="proxy-pac-option">
|
||||
<span class="setting-input-key">PAC script</span>
|
||||
<input class="setting-input-value" placeholder="e.g. foobar.com/pacfile.js"/>
|
||||
@@ -35,7 +39,6 @@ class NetworkSection extends BaseSection {
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="action green" id="proxy-save-action">
|
||||
<i class="material-icons">check_box</i>
|
||||
<span>Save</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +54,7 @@ class NetworkSection extends BaseSection {
|
||||
this.$proxyRules = document.querySelector('#proxy-rules-option .setting-input-value');
|
||||
this.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value');
|
||||
this.$proxySaveAction = document.getElementById('proxy-save-action');
|
||||
this.$settingBlock = this.props.$root.querySelector('.setting-block');
|
||||
this.$manualProxyBlock = this.props.$root.querySelector('.manual-proxy-block');
|
||||
this.initProxyOption();
|
||||
|
||||
this.$proxyPAC.value = ConfigUtil.getConfigItem('proxyPAC', '');
|
||||
@@ -68,31 +71,54 @@ class NetworkSection extends BaseSection {
|
||||
}
|
||||
|
||||
initProxyOption() {
|
||||
const proxyEnabled = ConfigUtil.getConfigItem('useProxy', false);
|
||||
this.toggleProxySettings(proxyEnabled);
|
||||
const manualProxyEnabled = ConfigUtil.getConfigItem('useManualProxy', false);
|
||||
this.toggleManualProxySettings(manualProxyEnabled);
|
||||
|
||||
this.updateProxyOption();
|
||||
}
|
||||
|
||||
toggleProxySettings(option) {
|
||||
toggleManualProxySettings(option) {
|
||||
if (option) {
|
||||
this.$settingBlock.classList.remove('hidden');
|
||||
this.$manualProxyBlock.classList.remove('hidden');
|
||||
} else {
|
||||
this.$settingBlock.classList.add('hidden');
|
||||
this.$manualProxyBlock.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
updateProxyOption() {
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#use-proxy-option .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('useProxy', false),
|
||||
$element: document.querySelector('#use-system-settings .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('useSystemProxy', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('useProxy');
|
||||
ConfigUtil.setConfigItem('useProxy', newValue);
|
||||
this.toggleProxySettings(newValue);
|
||||
const newValue = !ConfigUtil.getConfigItem('useSystemProxy');
|
||||
const manualProxyValue = ConfigUtil.getConfigItem('useManualProxy');
|
||||
if (manualProxyValue && newValue) {
|
||||
ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue);
|
||||
this.toggleManualProxySettings(!manualProxyValue);
|
||||
}
|
||||
if (newValue === false) {
|
||||
// Reload proxy if the proxy is turned off
|
||||
// Remove proxy system proxy settings
|
||||
ConfigUtil.setConfigItem('proxyRules', '');
|
||||
ipcRenderer.send('forward-message', 'reload-proxy', false);
|
||||
}
|
||||
ConfigUtil.setConfigItem('useSystemProxy', newValue);
|
||||
this.updateProxyOption();
|
||||
}
|
||||
});
|
||||
this.generateSettingOption({
|
||||
$element: document.querySelector('#use-manual-settings .setting-control'),
|
||||
value: ConfigUtil.getConfigItem('useManualProxy', false),
|
||||
clickHandler: () => {
|
||||
const newValue = !ConfigUtil.getConfigItem('useManualProxy');
|
||||
const systemProxyValue = ConfigUtil.getConfigItem('useSystemProxy');
|
||||
this.toggleManualProxySettings(newValue);
|
||||
if (systemProxyValue && newValue) {
|
||||
ConfigUtil.setConfigItem('useSystemProxy', !systemProxyValue);
|
||||
}
|
||||
ConfigUtil.setConfigItem('proxyRules', '');
|
||||
ConfigUtil.setConfigItem('useManualProxy', newValue);
|
||||
// Reload app only when turning manual proxy off, hence !newValue
|
||||
ipcRenderer.send('forward-message', 'reload-proxy', !newValue);
|
||||
this.updateProxyOption();
|
||||
}
|
||||
});
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
const BaseComponent = require(__dirname + '/../../components/base.js');
|
||||
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
|
||||
const shell = require('electron').shell;
|
||||
|
||||
class NewServerForm extends BaseComponent {
|
||||
constructor(props) {
|
||||
@@ -11,18 +12,26 @@ class NewServerForm extends BaseComponent {
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-card">
|
||||
<div class="server-info-right">
|
||||
<div class="server-info-row">
|
||||
<input class="setting-input-value" autofocus placeholder="Entert the URL of your Zulip organization..."/>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<div class="action green server-save-action">
|
||||
<i class="material-icons">check_box</i>
|
||||
<span>Save</span>
|
||||
</div>
|
||||
<div class="server-input-container">
|
||||
<div class="title">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">
|
||||
<div class="server-save-action">
|
||||
<button id="connect">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-center">
|
||||
<div class="divider">
|
||||
<hr class="left"/>OR<hr class="right" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-center">
|
||||
<div class="server-save-action">
|
||||
<button id="open-create-org-link">Create a new organization</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -42,15 +51,25 @@ class NewServerForm extends BaseComponent {
|
||||
}
|
||||
|
||||
submitFormHandler() {
|
||||
this.$saveServerButton.children[0].innerHTML = 'Connecting...';
|
||||
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
|
||||
DomainUtil.addDomain(serverConf).then(() => {
|
||||
this.props.onChange(this.props.index);
|
||||
});
|
||||
}, errorMessage => {
|
||||
this.$saveServerButton.children[0].innerHTML = 'Connect';
|
||||
alert(errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
openCreateNewOrgExternalLink() {
|
||||
const link = 'https://zulipchat.com/new/';
|
||||
const externalCreateNewOrgEl = document.getElementById('open-create-org-link');
|
||||
externalCreateNewOrgEl.addEventListener('click', () => {
|
||||
shell.openExternal(link);
|
||||
});
|
||||
}
|
||||
|
||||
initActions() {
|
||||
this.$saveServerButton.addEventListener('click', () => {
|
||||
this.submitFormHandler();
|
||||
@@ -62,6 +81,8 @@ class NewServerForm extends BaseComponent {
|
||||
this.submitFormHandler();
|
||||
}
|
||||
});
|
||||
// open create new org link in default browser
|
||||
this.openCreateNewOrgExternalLink();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,12 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const BaseComponent = require(__dirname + '/js/components/base.js');
|
||||
const {ipcRenderer} = require('electron');
|
||||
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 ConnectedOrgSection = require(__dirname + '/js/pages/preference/connected-org-section.js');
|
||||
const ShortcutsSection = require(__dirname + '/js/pages/preference/shortcuts-section.js');
|
||||
|
||||
class PreferenceView extends BaseComponent {
|
||||
@@ -39,7 +40,7 @@ class PreferenceView extends BaseComponent {
|
||||
handleNavigation(navItem) {
|
||||
this.nav.select(navItem);
|
||||
switch (navItem) {
|
||||
case 'Servers': {
|
||||
case 'AddServer': {
|
||||
this.section = new ServersSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
@@ -51,6 +52,12 @@ class PreferenceView extends BaseComponent {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'Organizations': {
|
||||
this.section = new ConnectedOrgSection({
|
||||
$root: this.$settingsContainer
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'Network': {
|
||||
this.section = new NetworkSection({
|
||||
$root: this.$settingsContainer
|
||||
@@ -69,10 +76,36 @@ class PreferenceView extends BaseComponent {
|
||||
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-setting', (event, state) => {
|
||||
this.handleToggle('sidebar-option', state);
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggletray', (event, state) => {
|
||||
this.handleToggle('tray-option', state);
|
||||
});
|
||||
|
||||
ipcRenderer.on('toggle-dnd', (event, state, newSettings) => {
|
||||
this.handleToggle('show-notification-option', newSettings.showNotification);
|
||||
this.handleToggle('silent-option', newSettings.silent);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
this.handleToggle('flash-taskbar-option', newSettings.flashTaskbarOnMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
const {dialog} = require('electron').remote;
|
||||
const {ipcRenderer} = require('electron');
|
||||
const { dialog } = require('electron').remote;
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
const BaseComponent = require(__dirname + '/../../components/base.js');
|
||||
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
|
||||
@@ -16,19 +16,18 @@ class ServerInfoForm extends BaseComponent {
|
||||
<div class="settings-card">
|
||||
<div class="server-info-left">
|
||||
<img class="server-info-icon" src="${this.props.server.icon}"/>
|
||||
</div>
|
||||
<div class="server-info-right">
|
||||
<div class="server-info-row">
|
||||
<span class="server-info-alias">${this.props.server.alias}</span>
|
||||
<i class="material-icons open-tab-button">open_in_new</i>
|
||||
<i class="material-icons open-tab-button">open_in_new</i>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<input class="setting-input-value" disabled value="${this.props.server.url}"/>
|
||||
</div>
|
||||
<div class="server-info-right">
|
||||
<div class="server-info-row server-url">
|
||||
<span class="server-url-info" title="${this.props.server.url}">${this.props.server.url}</span>
|
||||
</div>
|
||||
<div class="server-info-row">
|
||||
<div class="action red server-delete-action">
|
||||
<i class="material-icons">indeterminate_check_box</i>
|
||||
<span>Delete</span>
|
||||
<span>Disconnect</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,6 +43,7 @@ class ServerInfoForm extends BaseComponent {
|
||||
initForm() {
|
||||
this.$serverInfoForm = this.generateNodeFromTemplate(this.template());
|
||||
this.$serverInfoAlias = this.$serverInfoForm.getElementsByClassName('server-info-alias')[0];
|
||||
this.$serverIcon = this.$serverInfoForm.getElementsByClassName('server-info-icon')[0];
|
||||
this.$deleteServerButton = this.$serverInfoForm.getElementsByClassName('server-delete-action')[0];
|
||||
this.$openServerButton = this.$serverInfoForm.getElementsByClassName('open-tab-button')[0];
|
||||
this.props.$root.appendChild(this.$serverInfoForm);
|
||||
@@ -55,7 +55,7 @@ class ServerInfoForm extends BaseComponent {
|
||||
type: 'warning',
|
||||
buttons: ['YES', 'NO'],
|
||||
defaultId: 0,
|
||||
message: 'Are you sure you want to delete this server?'
|
||||
message: 'Are you sure you want to disconnect this organization?'
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
DomainUtil.removeDomain(this.props.index);
|
||||
@@ -71,7 +71,12 @@ class ServerInfoForm extends BaseComponent {
|
||||
this.$serverInfoAlias.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
|
||||
});
|
||||
|
||||
this.$serverIcon.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ServerInfoForm;
|
||||
|
@@ -1,8 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const BaseSection = require(__dirname + '/base-section.js');
|
||||
const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
|
||||
const ServerInfoForm = require(__dirname + '/server-info-form.js');
|
||||
const NewServerForm = require(__dirname + '/new-server-form.js');
|
||||
|
||||
class ServersSection extends BaseSection {
|
||||
@@ -13,12 +11,14 @@ class ServersSection extends BaseSection {
|
||||
|
||||
template() {
|
||||
return `
|
||||
<div class="settings-pane" id="server-settings-pane">
|
||||
<div class="title">Add Server</div>
|
||||
<div id="new-server-container"></div>
|
||||
<div class="title" id="existing-servers"></div>
|
||||
<div id="server-info-container"></div>
|
||||
<div class="add-server-modal">
|
||||
<div class="modal-container">
|
||||
<div class="settings-pane" id="server-settings-pane">
|
||||
<div class="page-title">Add a Zulip organization</div>
|
||||
<div id="new-server-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -29,26 +29,10 @@ class ServersSection extends BaseSection {
|
||||
initServers() {
|
||||
this.props.$root.innerHTML = '';
|
||||
|
||||
const servers = DomainUtil.getDomains();
|
||||
this.props.$root.innerHTML = this.template();
|
||||
this.$serverInfoContainer = document.getElementById('server-info-container');
|
||||
this.$existingServers = document.getElementById('existing-servers');
|
||||
this.$newServerContainer = document.getElementById('new-server-container');
|
||||
this.$newServerButton = document.getElementById('new-server-action');
|
||||
|
||||
this.$serverInfoContainer.innerHTML = servers.length ? '' : 'Add your first server to get started!';
|
||||
// Show Existing servers if servers are there otherwise hide it
|
||||
this.$existingServers.innerHTML = servers.length === 0 ? '' : 'Existing Servers';
|
||||
this.initNewServerForm();
|
||||
|
||||
for (let i = 0; i < servers.length; i++) {
|
||||
new ServerInfoForm({
|
||||
$root: this.$serverInfoContainer,
|
||||
server: servers[i],
|
||||
index: i,
|
||||
onChange: this.reloadApp
|
||||
}).init();
|
||||
}
|
||||
}
|
||||
|
||||
initNewServerForm() {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const BaseSection = require(__dirname + '/base-section.js');
|
||||
const shell = require('electron').shell;
|
||||
|
||||
class ShortcutsSection extends BaseSection {
|
||||
constructor(props) {
|
||||
@@ -13,6 +14,7 @@ class ShortcutsSection extends BaseSection {
|
||||
|
||||
return `
|
||||
<div class="settings-pane">
|
||||
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>Tip: </b>These desktop app shortcuts extend the Zulip webapp's <span id="open-hotkeys-link"> keyboard shortcuts</span>.</p></div>
|
||||
<div class="title">Application Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
@@ -23,6 +25,10 @@ class ShortcutsSection extends BaseSection {
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>K</kbd></td>
|
||||
<td>Keyboard Shortcuts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
|
||||
<td>Toggle Do Not Disturb</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>D</kbd></td>
|
||||
@@ -78,14 +84,6 @@ class ShortcutsSection extends BaseSection {
|
||||
<td><kbd>${userOSKey}</kbd><kbd>A</kbd></td>
|
||||
<td>Select All</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
|
||||
<td>Find</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>G</kbd></td>
|
||||
<td>Find Next</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>Space</kbd></td>
|
||||
<td>Emoji & Symbols</td>
|
||||
@@ -109,7 +107,7 @@ class ShortcutsSection extends BaseSection {
|
||||
<td>Enter Full Screen</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>=</kbd></td>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>+</kbd></td>
|
||||
<td>Zoom In</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -121,7 +119,7 @@ class ShortcutsSection extends BaseSection {
|
||||
<td>Actual Size</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd><kbd>S</kbd></td>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
|
||||
<td>Toggle Sidebar</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -132,6 +130,14 @@ class ShortcutsSection extends BaseSection {
|
||||
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>U</kbd></td>
|
||||
<td>Toggle DevTools for Active Tab</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Ctrl</kbd> + <kbd>Tab</kbd></td>
|
||||
<td>Switch to Next Organization</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
|
||||
<td>Switch to Previous Organization</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
@@ -172,6 +178,7 @@ class ShortcutsSection extends BaseSection {
|
||||
|
||||
return `
|
||||
<div class="settings-pane">
|
||||
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>Tip: </b>These desktop app shortcuts extend the Zulip webapp's <span id="open-hotkeys-link"> keyboard shortcuts</span>.</p></div>
|
||||
<div class="title">Application Shortcuts</div>
|
||||
<div class="settings-card">
|
||||
<table>
|
||||
@@ -182,6 +189,10 @@ class ShortcutsSection extends BaseSection {
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>K</kbd></td>
|
||||
<td>Keyboard Shortcuts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
|
||||
<td>Toggle Do Not Disturb</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>L</kbd></td>
|
||||
@@ -256,7 +267,7 @@ class ShortcutsSection extends BaseSection {
|
||||
<td>Actual Size</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>S</kbd></td>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
|
||||
<td>Toggle Sidebar</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -267,6 +278,14 @@ class ShortcutsSection extends BaseSection {
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>U</kbd></td>
|
||||
<td>Toggle DevTools for Active Tab</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Tab</kbd></td>
|
||||
<td>Switch to Next Organization</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
|
||||
<td>Switch to Previous Organization</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="setting-control"></div>
|
||||
</div>
|
||||
@@ -302,9 +321,17 @@ class ShortcutsSection extends BaseSection {
|
||||
`;
|
||||
}
|
||||
|
||||
openHotkeysExternalLink() {
|
||||
const link = 'https://zulipchat.com/help/keyboard-shortcuts';
|
||||
const externalCreateNewOrgEl = document.getElementById('open-hotkeys-link');
|
||||
externalCreateNewOrgEl.addEventListener('click', () => {
|
||||
shell.openExternal(link);
|
||||
});
|
||||
}
|
||||
init() {
|
||||
this.props.$root.innerHTML = (process.platform === 'darwin') ?
|
||||
this.templateMac() : this.templateWinLin();
|
||||
this.openHotkeysExternalLink();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const { spellChecker } = require('./spellchecker');
|
||||
const { ipcRenderer, shell } = require('electron');
|
||||
const SetupSpellChecker = require('./spellchecker');
|
||||
|
||||
const ConfigUtil = require(__dirname + '/utils/config-util.js');
|
||||
const LinkUtil = require(__dirname + '/utils/link-util.js');
|
||||
const params = require(__dirname + '/utils/params-util.js');
|
||||
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
require('./notification');
|
||||
|
||||
// Prevent drag and drop event in main process which prevents remote code executaion
|
||||
require(__dirname + '/shared/preventdrag.js');
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
window.electron_bridge = require('./electron-bridge');
|
||||
|
||||
const logout = () => {
|
||||
// Create the menu for the below
|
||||
document.querySelector('.dropdown-toggle').click();
|
||||
@@ -17,7 +28,7 @@ const shortcut = () => {
|
||||
// Create the menu for the below
|
||||
const node = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
|
||||
// Additional check
|
||||
if (node.text.trim().toLowerCase() === 'keyboard shortcuts') {
|
||||
if (node.text.trim().toLowerCase() === 'keyboard shortcuts (?)') {
|
||||
node.click();
|
||||
} else {
|
||||
// Atleast click the dropdown
|
||||
@@ -32,14 +43,63 @@ process.once('loaded', () => {
|
||||
|
||||
// To prevent failing this script on linux we need to load it after the document loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Init spellchecker
|
||||
spellChecker();
|
||||
if (params.isPageParams()) {
|
||||
// Get the default language of the server
|
||||
const serverLanguage = page_params.default_language; // eslint-disable-line no-undef, camelcase
|
||||
if (serverLanguage) {
|
||||
// Set spellcheker language
|
||||
ConfigUtil.setConfigItem('spellcheckerLanguage', serverLanguage);
|
||||
// Init spellchecker
|
||||
SetupSpellChecker.init();
|
||||
}
|
||||
// redirect users to network troubleshooting page
|
||||
const getRestartButton = document.querySelector('.restart_get_events_button');
|
||||
if (getRestartButton) {
|
||||
getRestartButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
});
|
||||
}
|
||||
// Open image attachment link in the lightbox instead of opening in the default browser
|
||||
const { $, lightbox } = window;
|
||||
$('#main_div').on('click', '.message_content p a', function (e) {
|
||||
const url = $(this).attr('href');
|
||||
|
||||
// redirect users to network troubleshooting page
|
||||
const getRestartButton = document.querySelector('.restart_get_events_button');
|
||||
if (getRestartButton) {
|
||||
getRestartButton.addEventListener('click', () => {
|
||||
ipcRenderer.send('forward-message', 'reload-viewer');
|
||||
if (LinkUtil.isImage(url)) {
|
||||
const $img = $(this).parent().siblings('.message_inline_image').find('img');
|
||||
|
||||
// prevent the image link from opening in a new page.
|
||||
e.preventDefault();
|
||||
// prevent the message compose dialog from happening.
|
||||
e.stopPropagation();
|
||||
|
||||
// Open image in the default browser if image preview is unavailable
|
||||
if (!$img[0]) {
|
||||
shell.openExternal(window.location.origin + url);
|
||||
}
|
||||
// Open image in lightbox
|
||||
lightbox.open($img);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up spellchecker events after you navigate away from this page;
|
||||
// otherwise, you may experience errors
|
||||
window.addEventListener('beforeunload', () => {
|
||||
SetupSpellChecker.unsubscribeSpellChecker();
|
||||
});
|
||||
|
||||
// electron's globalShortcut can cause unexpected results
|
||||
// so adding the reload shortcut in the old-school way
|
||||
// Zoom from numpad keys is not supported by electron, so adding it through listeners.
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.code === 'F5') {
|
||||
ipcRenderer.send('forward-message', 'hard-reload');
|
||||
} else if (event.ctrlKey && event.code === 'NumpadAdd') {
|
||||
ipcRenderer.send('forward-message', 'zoomIn');
|
||||
} else if (event.ctrlKey && event.code === 'NumpadSubtract') {
|
||||
ipcRenderer.send('forward-message', 'zoomOut');
|
||||
} else if (event.ctrlKey && event.code === 'Numpad0') {
|
||||
ipcRenderer.send('forward-message', 'zoomActualSize');
|
||||
}
|
||||
});
|
||||
|
17
app/renderer/js/shared/preventdrag.js
Normal file
17
app/renderer/js/shared/preventdrag.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
// This is a security fix. Following function prevents drag and drop event in the app
|
||||
// so that attackers can't execute any remote code within the app
|
||||
// It doesn't affect the compose box so that users can still
|
||||
// use drag and drop event to share files etc
|
||||
|
||||
const preventDragAndDrop = () => {
|
||||
const preventEvents = ['dragover', 'drop'];
|
||||
preventEvents.forEach(dragEvents => {
|
||||
document.addEventListener(dragEvents, event => {
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
preventDragAndDrop();
|
@@ -1,29 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
const {SpellCheckHandler, ContextMenuListener, ContextMenuBuilder} = require('electron-spellchecker');
|
||||
const { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } = require('electron-spellchecker');
|
||||
|
||||
function spellChecker() {
|
||||
// Implement spellcheck using electron api
|
||||
window.spellCheckHandler = new SpellCheckHandler();
|
||||
window.spellCheckHandler.attachToInput();
|
||||
const ConfigUtil = require(__dirname + '/utils/config-util.js');
|
||||
const Logger = require(__dirname + '/utils/logger-util.js');
|
||||
|
||||
// Start off as US English
|
||||
window.spellCheckHandler.switchLanguage('en-US');
|
||||
const logger = new Logger({
|
||||
file: 'errors.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
const contextMenuBuilder = new ContextMenuBuilder(window.spellCheckHandler);
|
||||
const contextMenuListener = new ContextMenuListener(info => {
|
||||
contextMenuBuilder.showPopupMenu(info);
|
||||
});
|
||||
class SetupSpellChecker {
|
||||
init() {
|
||||
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
|
||||
this.enableSpellChecker();
|
||||
}
|
||||
this.enableContextMenu();
|
||||
}
|
||||
|
||||
// Clean up events after you navigate away from this page;
|
||||
// otherwise, you may experience errors
|
||||
window.addEventListener('beforeunload', () => {
|
||||
// eslint-disable-next-line no-undef
|
||||
spellCheckHandler.unsubscribe();
|
||||
contextMenuListener.unsubscribe();
|
||||
});
|
||||
enableSpellChecker() {
|
||||
try {
|
||||
this.SpellCheckHandler = new SpellCheckHandler();
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
enableContextMenu() {
|
||||
if (this.SpellCheckHandler) {
|
||||
this.SpellCheckHandler.attachToInput();
|
||||
|
||||
const userLanguage = ConfigUtil.getConfigItem('spellcheckerLanguage');
|
||||
|
||||
this.SpellCheckHandler.switchLanguage(userLanguage);
|
||||
}
|
||||
|
||||
const contextMenuBuilder = new ContextMenuBuilder(this.SpellCheckHandler);
|
||||
this.contextMenuListener = new ContextMenuListener(info => {
|
||||
contextMenuBuilder.showPopupMenu(info);
|
||||
});
|
||||
}
|
||||
|
||||
unsubscribeSpellChecker() {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (this.SpellCheckHandler) {
|
||||
this.SpellCheckHandler.unsubscribe();
|
||||
}
|
||||
if (this.contextMenuListener) {
|
||||
this.contextMenuListener.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
spellChecker
|
||||
};
|
||||
module.exports = new SetupSpellChecker();
|
||||
|
@@ -3,9 +3,9 @@ const path = require('path');
|
||||
|
||||
const electron = require('electron');
|
||||
|
||||
const {ipcRenderer, remote} = electron;
|
||||
const { ipcRenderer, remote } = electron;
|
||||
|
||||
const {Tray, Menu, nativeImage, BrowserWindow} = remote;
|
||||
const { Tray, Menu, nativeImage, BrowserWindow } = remote;
|
||||
|
||||
const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray');
|
||||
|
||||
@@ -98,8 +98,10 @@ const renderNativeImage = function (arg) {
|
||||
return Promise.resolve()
|
||||
.then(() => renderCanvas(arg))
|
||||
.then(canvas => {
|
||||
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPng();
|
||||
return Promise.resolve(nativeImage.createFromBuffer(pngData, config.pixelRatio));
|
||||
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
|
||||
return Promise.resolve(nativeImage.createFromBuffer(pngData, {
|
||||
scaleFactor: config.pixelRatio
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -115,50 +117,36 @@ function sendAction(action) {
|
||||
|
||||
const createTray = function () {
|
||||
window.tray = new Tray(iconPath());
|
||||
const contextMenu = Menu.buildFromTemplate([{
|
||||
label: 'About',
|
||||
click() {
|
||||
// We need to focus the main window first
|
||||
ipcRenderer.send('focus-app');
|
||||
sendAction('open-about');
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Zulip',
|
||||
click() {
|
||||
ipcRenderer.send('focus-app');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
click() {
|
||||
ipcRenderer.send('focus-app');
|
||||
sendAction('open-settings');
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
click() {
|
||||
ipcRenderer.send('quit-app');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Focus',
|
||||
click() {
|
||||
ipcRenderer.send('focus-app');
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
click() {
|
||||
ipcRenderer.send('focus-app');
|
||||
sendAction('open-settings');
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
click() {
|
||||
ipcRenderer.send('quit-app');
|
||||
}
|
||||
}
|
||||
]);
|
||||
window.tray.setContextMenu(contextMenu);
|
||||
window.tray.on('click', () => {
|
||||
// Click event only works on Windows
|
||||
if (process.platform === 'win32') {
|
||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||
window.tray.on('click', () => {
|
||||
ipcRenderer.send('toggle-app');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ipcRenderer.on('destroytray', event => {
|
||||
@@ -197,13 +185,16 @@ ipcRenderer.on('tray', (event, arg) => {
|
||||
});
|
||||
|
||||
function toggleTray() {
|
||||
let state;
|
||||
if (window.tray) {
|
||||
state = false;
|
||||
window.tray.destroy();
|
||||
if (window.tray.isDestroyed()) {
|
||||
window.tray = null;
|
||||
}
|
||||
ConfigUtil.setConfigItem('trayIcon', false);
|
||||
} else {
|
||||
state = true;
|
||||
createTray();
|
||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||
renderNativeImage(unread).then(image => {
|
||||
@@ -213,6 +204,10 @@ function toggleTray() {
|
||||
}
|
||||
ConfigUtil.setConfigItem('trayIcon', true);
|
||||
}
|
||||
const selector = 'webview:not([class*=disabled])';
|
||||
const webview = document.querySelector(selector);
|
||||
const webContents = webview.getWebContents();
|
||||
webContents.send('toggletray', state);
|
||||
}
|
||||
|
||||
ipcRenderer.on('toggletray', toggleTray);
|
||||
|
86
app/renderer/js/utils/certificate-util.js
Normal file
86
app/renderer/js/utils/certificate-util.js
Normal file
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
|
||||
const { app, dialog } = require('electron').remote;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const JsonDB = require('node-json-db');
|
||||
const Logger = require('./logger-util');
|
||||
const { initSetUp } = require('./default-util');
|
||||
|
||||
initSetUp();
|
||||
|
||||
const logger = new Logger({
|
||||
file: `certificate-util.log`,
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
let instance = null;
|
||||
const certificatesDir = `${app.getPath('userData')}/certificates`;
|
||||
|
||||
class CertificateUtil {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
this.reloadDB();
|
||||
return instance;
|
||||
}
|
||||
getCertificate(server, defaultValue = null) {
|
||||
this.reloadDB();
|
||||
const value = this.db.getData('/')[server];
|
||||
if (value === undefined) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
// Function to copy the certificate to userData folder
|
||||
copyCertificate(server, location, fileName) {
|
||||
let copied = false;
|
||||
const filePath = `${certificatesDir}/${fileName}`;
|
||||
try {
|
||||
fs.copyFileSync(location, filePath);
|
||||
copied = true;
|
||||
} catch (err) {
|
||||
dialog.showErrorBox(
|
||||
'Error saving certificate',
|
||||
'We encountered error while saving the certificate.'
|
||||
);
|
||||
logger.error('Error while copying the certificate to certificates folder.');
|
||||
logger.error(err);
|
||||
}
|
||||
return copied;
|
||||
}
|
||||
setCertificate(server, fileName) {
|
||||
const filePath = `${certificatesDir}/${fileName}`;
|
||||
this.db.push(`/${server}`, filePath, true);
|
||||
this.reloadDB();
|
||||
}
|
||||
removeCertificate(server) {
|
||||
this.db.delete(`/${server}`);
|
||||
this.reloadDB();
|
||||
}
|
||||
reloadDB() {
|
||||
const settingsJsonPath = path.join(app.getPath('userData'), '/config/certificates.json');
|
||||
try {
|
||||
const file = fs.readFileSync(settingsJsonPath, 'utf8');
|
||||
JSON.parse(file);
|
||||
} catch (err) {
|
||||
if (fs.existsSync(settingsJsonPath)) {
|
||||
fs.unlinkSync(settingsJsonPath);
|
||||
dialog.showErrorBox(
|
||||
'Error saving settings',
|
||||
'We encountered error while saving the certificate.'
|
||||
);
|
||||
logger.error('Error while JSON parsing certificates.json: ');
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
this.db = new JsonDB(settingsJsonPath, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CertificateUtil();
|
25
app/renderer/js/utils/common-util.js
Normal file
25
app/renderer/js/utils/common-util.js
Normal file
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
let instance = null;
|
||||
|
||||
class CommonUtil {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// unescape already encoded/escaped strings
|
||||
decodeString(string) {
|
||||
const parser = new DOMParser();
|
||||
const dom = parser.parseFromString(
|
||||
'<!doctype html><body>' + string,
|
||||
'text/html');
|
||||
return dom.body.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CommonUtil();
|
@@ -1,16 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const process = require('process');
|
||||
const JsonDB = require('node-json-db');
|
||||
const Logger = require('./logger-util');
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'config-util.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
let instance = null;
|
||||
let dialog = null;
|
||||
let app = null;
|
||||
|
||||
/* To make the util runnable in both main and renderer process */
|
||||
if (process.type === 'renderer') {
|
||||
app = require('electron').remote.app;
|
||||
const remote = require('electron').remote;
|
||||
dialog = remote.dialog;
|
||||
app = remote.app;
|
||||
} else {
|
||||
app = require('electron').app;
|
||||
const electron = require('electron');
|
||||
dialog = electron.dialog;
|
||||
app = electron.app;
|
||||
}
|
||||
|
||||
class ConfigUtil {
|
||||
@@ -26,7 +39,12 @@ class ConfigUtil {
|
||||
}
|
||||
|
||||
getConfigItem(key, defaultValue = null) {
|
||||
this.reloadDB();
|
||||
try {
|
||||
this.db.reload();
|
||||
} catch (err) {
|
||||
logger.error('Error while reloading settings.json: ');
|
||||
logger.error(err);
|
||||
}
|
||||
const value = this.db.getData('/')[key];
|
||||
if (value === undefined) {
|
||||
this.setConfigItem(key, defaultValue);
|
||||
@@ -35,19 +53,46 @@ class ConfigUtil {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
// This function returns whether a key exists in the configuration file (settings.json)
|
||||
isConfigItemExists(key) {
|
||||
try {
|
||||
this.db.reload();
|
||||
} catch (err) {
|
||||
logger.error('Error while reloading settings.json: ');
|
||||
logger.error(err);
|
||||
}
|
||||
const value = this.db.getData('/')[key];
|
||||
return (value !== undefined);
|
||||
}
|
||||
|
||||
setConfigItem(key, value) {
|
||||
this.db.push(`/${key}`, value, true);
|
||||
this.reloadDB();
|
||||
this.db.save();
|
||||
}
|
||||
|
||||
removeConfigItem(key) {
|
||||
this.db.delete(`/${key}`);
|
||||
this.reloadDB();
|
||||
this.db.save();
|
||||
}
|
||||
|
||||
reloadDB() {
|
||||
this.db = new JsonDB(app.getPath('userData') + '/settings.json', true, true);
|
||||
const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.json');
|
||||
try {
|
||||
const file = fs.readFileSync(settingsJsonPath, 'utf8');
|
||||
JSON.parse(file);
|
||||
} catch (err) {
|
||||
if (fs.existsSync(settingsJsonPath)) {
|
||||
fs.unlinkSync(settingsJsonPath);
|
||||
dialog.showErrorBox(
|
||||
'Error saving settings',
|
||||
'We encountered an error while saving the settings.'
|
||||
);
|
||||
logger.error('Error while JSON parsing settings.json: ');
|
||||
logger.error(err);
|
||||
logger.reportSentry(err);
|
||||
}
|
||||
}
|
||||
this.db = new JsonDB(settingsJsonPath, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
78
app/renderer/js/utils/default-util.js
Normal file
78
app/renderer/js/utils/default-util.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const fs = require('fs');
|
||||
|
||||
let app = null;
|
||||
let setupCompleted = false;
|
||||
if (process.type === 'renderer') {
|
||||
app = require('electron').remote.app;
|
||||
} else {
|
||||
app = require('electron').app;
|
||||
}
|
||||
|
||||
const zulipDir = app.getPath('userData');
|
||||
const logDir = `${zulipDir}/Logs/`;
|
||||
const certificatesDir = `${zulipDir}/certificates/`;
|
||||
const configDir = `${zulipDir}/config/`;
|
||||
const initSetUp = () => {
|
||||
// if it is the first time the app is running
|
||||
// create zulip dir in userData folder to
|
||||
// avoid errors
|
||||
if (!setupCompleted) {
|
||||
if (!fs.existsSync(zulipDir)) {
|
||||
fs.mkdirSync(zulipDir);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(certificatesDir)) {
|
||||
fs.mkdirSync(certificatesDir);
|
||||
}
|
||||
|
||||
// Migrate config files from app data folder to config folder inside app
|
||||
// data folder. This will be done once when a user updates to the new version.
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir);
|
||||
const domainJson = `${zulipDir}/domain.json`;
|
||||
const certificatesJson = `${zulipDir}/certificates.json`;
|
||||
const settingsJson = `${zulipDir}/settings.json`;
|
||||
const updatesJson = `${zulipDir}/updates.json`;
|
||||
const windowStateJson = `${zulipDir}/window-state.json`;
|
||||
const configData = [
|
||||
{
|
||||
path: domainJson,
|
||||
fileName: `domain.json`
|
||||
},
|
||||
{
|
||||
path: certificatesJson,
|
||||
fileName: `certificates.json`
|
||||
},
|
||||
{
|
||||
path: settingsJson,
|
||||
fileName: `settings.json`
|
||||
},
|
||||
{
|
||||
path: updatesJson,
|
||||
fileName: `updates.json`
|
||||
}
|
||||
];
|
||||
configData.forEach(data => {
|
||||
if (fs.existsSync(data.path)) {
|
||||
fs.copyFileSync(data.path, configDir + data.fileName);
|
||||
fs.unlinkSync(data.path);
|
||||
}
|
||||
});
|
||||
// window-state.json is only deleted not moved, as the electron-window-state
|
||||
// package will recreate the file in the config folder.
|
||||
if (fs.existsSync(windowStateJson)) {
|
||||
fs.unlinkSync(windowStateJson);
|
||||
}
|
||||
}
|
||||
|
||||
setupCompleted = true;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initSetUp
|
||||
};
|
41
app/renderer/js/utils/dnd-util.js
Normal file
41
app/renderer/js/utils/dnd-util.js
Normal file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const ConfigUtil = require(__dirname + '/config-util.js');
|
||||
|
||||
function toggle() {
|
||||
const dnd = !ConfigUtil.getConfigItem('dnd', false);
|
||||
const dndSettingList = ['showNotification', 'silent'];
|
||||
if (process.platform === 'win32') {
|
||||
dndSettingList.push('flashTaskbarOnMessage');
|
||||
}
|
||||
|
||||
let newSettings;
|
||||
if (dnd) {
|
||||
const oldSettings = {};
|
||||
newSettings = {};
|
||||
|
||||
// Iterate through the dndSettingList.
|
||||
for (const settingName of dndSettingList) {
|
||||
// Store the current value of setting.
|
||||
oldSettings[settingName] = ConfigUtil.getConfigItem(settingName);
|
||||
// New value of setting.
|
||||
newSettings[settingName] = (settingName === 'silent');
|
||||
}
|
||||
|
||||
// Store old value in oldSettings.
|
||||
ConfigUtil.setConfigItem('dndPreviousSettings', oldSettings);
|
||||
} else {
|
||||
newSettings = ConfigUtil.getConfigItem('dndPreviousSettings');
|
||||
}
|
||||
|
||||
for (const settingName of dndSettingList) {
|
||||
ConfigUtil.setConfigItem(settingName, newSettings[settingName]);
|
||||
}
|
||||
|
||||
ConfigUtil.setConfigItem('dnd', dnd);
|
||||
return {dnd, newSettings};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toggle
|
||||
};
|
@@ -5,6 +5,16 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const JsonDB = require('node-json-db');
|
||||
const request = require('request');
|
||||
const escape = require('escape-html');
|
||||
|
||||
const Logger = require('./logger-util');
|
||||
|
||||
const RequestUtil = require(__dirname + '/../utils/request-util.js');
|
||||
|
||||
const logger = new Logger({
|
||||
file: `domain-util.log`,
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
let instance = null;
|
||||
|
||||
@@ -51,9 +61,10 @@ class DomainUtil {
|
||||
}
|
||||
|
||||
addDomain(server) {
|
||||
const ignoreCerts = server.ignoreCerts;
|
||||
return new Promise(resolve => {
|
||||
if (server.icon) {
|
||||
this.saveServerIcon(server.icon).then(localIconUrl => {
|
||||
this.saveServerIcon(server, ignoreCerts).then(localIconUrl => {
|
||||
server.icon = localIconUrl;
|
||||
this.db.push('/domains[]', server, true);
|
||||
this.reloadDB();
|
||||
@@ -90,58 +101,74 @@ class DomainUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
checkDomain(domain, silent = false) {
|
||||
// ignoreCerts parameter helps in fetching server icon and
|
||||
// other server details when user chooses to ignore certificate warnings
|
||||
checkDomain(domain, ignoreCerts = false, silent = false) {
|
||||
if (!silent && this.duplicateDomain(domain)) {
|
||||
// Do not check duplicate in silent mode
|
||||
alert('This server has been added.');
|
||||
return;
|
||||
return Promise.reject('This server has been added.');
|
||||
}
|
||||
|
||||
domain = this.formatUrl(domain);
|
||||
|
||||
const checkDomain = domain + '/static/audio/zulip.ogg';
|
||||
const checkDomain = {
|
||||
url: domain + '/static/audio/zulip.ogg',
|
||||
...RequestUtil.requestOptions(domain, ignoreCerts)
|
||||
};
|
||||
|
||||
const serverConf = {
|
||||
icon: defaultIconUrl,
|
||||
url: domain,
|
||||
alias: domain
|
||||
alias: domain,
|
||||
ignoreCerts
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(checkDomain, (error, response) => {
|
||||
const certsError =
|
||||
['Error: self signed certificate',
|
||||
'Error: unable to verify the first certificate'
|
||||
];
|
||||
if (!error && response.statusCode !== 404) {
|
||||
// If the domain contains following strings we just bypass the server
|
||||
const whitelistDomains = [
|
||||
'zulipdev.org'
|
||||
];
|
||||
|
||||
// make sure that error is an error or string not undefined
|
||||
// so validation does not throw error.
|
||||
error = error || '';
|
||||
|
||||
const certsError = error.toString().includes('certificate');
|
||||
if (!error && response.statusCode < 400) {
|
||||
// Correct
|
||||
this.getServerSettings(domain).then(serverSettings => {
|
||||
this.getServerSettings(domain, serverConf.ignoreCerts).then(serverSettings => {
|
||||
resolve(serverSettings);
|
||||
}, () => {
|
||||
resolve(serverConf);
|
||||
});
|
||||
} else if (certsError.indexOf(error.toString()) >= 0) {
|
||||
} else if (domain.indexOf(whitelistDomains) >= 0 || certsError) {
|
||||
if (silent) {
|
||||
this.getServerSettings(domain).then(serverSettings => {
|
||||
this.getServerSettings(domain, serverConf.ignoreCerts).then(serverSettings => {
|
||||
resolve(serverSettings);
|
||||
}, () => {
|
||||
resolve(serverConf);
|
||||
});
|
||||
} else {
|
||||
// Report error to sentry to get idea of possible certificate errors
|
||||
// users get when adding the servers
|
||||
logger.reportSentry(new Error(error));
|
||||
const certErrorMessage = `Do you trust certificate from ${domain}? \n ${error}`;
|
||||
const certErrorDetail = `The server you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
|
||||
\n Unless you have a good reason to believe otherwise, you should not proceed.
|
||||
\n You can click here if you'd like to proceed with the connection.`;
|
||||
const certErrorDetail = `The organization you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
|
||||
\nIf you have a valid certificate please add it from Settings>Organizations and try to add the organization again.
|
||||
\nUnless you have a good reason to believe otherwise, you should not proceed.
|
||||
\nYou can click here if you'd like to proceed with the connection.`;
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['Yes', 'No'],
|
||||
defaultId: 0,
|
||||
defaultId: 1,
|
||||
message: certErrorMessage,
|
||||
detail: certErrorDetail
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
this.getServerSettings(domain).then(serverSettings => {
|
||||
// set ignoreCerts parameter to true in case user responds with yes
|
||||
serverConf.ignoreCerts = true;
|
||||
this.getServerSettings(domain, serverConf.ignoreCerts).then(serverSettings => {
|
||||
resolve(serverSettings);
|
||||
}, () => {
|
||||
resolve(serverConf);
|
||||
@@ -152,23 +179,33 @@ class DomainUtil {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
reject('Not a valid Zulip server');
|
||||
const invalidZulipServerError = `${domain} does not appear to be a valid Zulip server. Make sure that \
|
||||
\n (1) you can connect to that URL in a web browser and \n (2) if you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings \n (3) its a zulip server \
|
||||
\n (4) the server has a valid certificate, you can add custom certificates in Settings>Organizations`;
|
||||
reject(invalidZulipServerError);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getServerSettings(domain) {
|
||||
const serverSettingsUrl = domain + '/api/v1/server_settings';
|
||||
getServerSettings(domain, ignoreCerts = false) {
|
||||
const serverSettingsOptions = {
|
||||
url: domain + '/api/v1/server_settings',
|
||||
...RequestUtil.requestOptions(domain, ignoreCerts)
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(serverSettingsUrl, (error, response) => {
|
||||
request(serverSettingsOptions, (error, response) => {
|
||||
if (!error && response.statusCode === 200) {
|
||||
const data = JSON.parse(response.body);
|
||||
if (data.hasOwnProperty('realm_icon') && data.realm_icon) {
|
||||
resolve({
|
||||
icon: data.realm_uri + data.realm_icon,
|
||||
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
|
||||
// Following check handles both the cases
|
||||
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
|
||||
url: data.realm_uri,
|
||||
alias: data.realm_name
|
||||
alias: escape(data.realm_name),
|
||||
ignoreCerts
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -178,26 +215,40 @@ class DomainUtil {
|
||||
});
|
||||
}
|
||||
|
||||
saveServerIcon(url) {
|
||||
saveServerIcon(server, ignoreCerts = false) {
|
||||
const url = server.icon;
|
||||
const domain = server.url;
|
||||
|
||||
const serverIconOptions = {
|
||||
url,
|
||||
...RequestUtil.requestOptions(domain, ignoreCerts)
|
||||
};
|
||||
|
||||
// The save will always succeed. If url is invalid, downgrade to default icon.
|
||||
return new Promise(resolve => {
|
||||
const filePath = this.generateFilePath(url);
|
||||
const file = fs.createWriteStream(filePath);
|
||||
try {
|
||||
request(url).on('response', response => {
|
||||
request(serverIconOptions).on('response', response => {
|
||||
response.on('error', err => {
|
||||
console.log(err);
|
||||
logger.log('Could not get server icon.');
|
||||
logger.log(err);
|
||||
logger.reportSentry(err);
|
||||
resolve(defaultIconUrl);
|
||||
});
|
||||
response.pipe(file).on('finish', () => {
|
||||
resolve(filePath);
|
||||
});
|
||||
}).on('error', err => {
|
||||
console.log(err);
|
||||
logger.log('Could not get server icon.');
|
||||
logger.log(err);
|
||||
logger.reportSentry(err);
|
||||
resolve(defaultIconUrl);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
logger.log('Could not get server icon.');
|
||||
logger.log(err);
|
||||
logger.reportSentry(err);
|
||||
resolve(defaultIconUrl);
|
||||
}
|
||||
});
|
||||
@@ -205,8 +256,9 @@ class DomainUtil {
|
||||
|
||||
updateSavedServer(url, index) {
|
||||
// Does not promise successful update
|
||||
this.checkDomain(url, true).then(newServerConf => {
|
||||
this.saveServerIcon(newServerConf.icon).then(localIconUrl => {
|
||||
const ignoreCerts = this.getDomain(index).ignoreCerts;
|
||||
this.checkDomain(url, ignoreCerts, true).then(newServerConf => {
|
||||
this.saveServerIcon(newServerConf, ignoreCerts).then(localIconUrl => {
|
||||
newServerConf.icon = localIconUrl;
|
||||
this.updateDomain(index, newServerConf);
|
||||
this.reloadDB();
|
||||
@@ -215,7 +267,24 @@ class DomainUtil {
|
||||
}
|
||||
|
||||
reloadDB() {
|
||||
this.db = new JsonDB(app.getPath('userData') + '/domain.json', true, true);
|
||||
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
|
||||
try {
|
||||
const file = fs.readFileSync(domainJsonPath, 'utf8');
|
||||
JSON.parse(file);
|
||||
} catch (err) {
|
||||
if (fs.existsSync(domainJsonPath)) {
|
||||
fs.unlinkSync(domainJsonPath);
|
||||
dialog.showErrorBox(
|
||||
'Error saving new organization',
|
||||
'There seems to be error while saving new organization, ' +
|
||||
'you may have to re-add your previous organizations back.'
|
||||
);
|
||||
logger.error('Error while JSON parsing domain.json: ');
|
||||
logger.error(err);
|
||||
logger.reportSentry(err);
|
||||
}
|
||||
}
|
||||
this.db = new JsonDB(domainJsonPath, true, true);
|
||||
}
|
||||
|
||||
generateFilePath(url) {
|
||||
|
@@ -19,7 +19,26 @@ class LinkUtil {
|
||||
const currentDomain = wurl('hostname', currentUrl);
|
||||
const newDomain = wurl('hostname', newUrl);
|
||||
|
||||
return (currentDomain === newDomain) && newUrl.includes('/#narrow');
|
||||
const sameDomainUrl = (currentDomain === newDomain || newUrl === currentUrl + '/');
|
||||
const isUploadsUrl = newUrl.includes(currentUrl + '/user_uploads/');
|
||||
const isInternalUrl = newUrl.includes('/#narrow') || isUploadsUrl;
|
||||
|
||||
return {
|
||||
isInternalUrl: sameDomainUrl && isInternalUrl,
|
||||
isUploadsUrl
|
||||
};
|
||||
}
|
||||
|
||||
isImage(url) {
|
||||
// test for images extension as well as urls like .png?s=100
|
||||
const isImageUrl = /\.(bmp|gif|jpg|jpeg|png|webp)\?*.*$/i;
|
||||
return isImageUrl.test(url);
|
||||
}
|
||||
|
||||
isPDF(url) {
|
||||
// test for pdf extension
|
||||
const isPDFUrl = /\.(pdf)\?*.*$/i;
|
||||
return isPDFUrl.test(url);
|
||||
}
|
||||
}
|
||||
|
||||
|
74
app/renderer/js/utils/linux-update-util.js
Normal file
74
app/renderer/js/utils/linux-update-util.js
Normal file
@@ -0,0 +1,74 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const process = require('process');
|
||||
const remote =
|
||||
process.type === 'renderer' ? require('electron').remote : require('electron');
|
||||
const JsonDB = require('node-json-db');
|
||||
const Logger = require('./logger-util');
|
||||
|
||||
const logger = new Logger({
|
||||
file: 'linux-update-util.log',
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
/* To make the util runnable in both main and renderer process */
|
||||
const { dialog, app } = remote;
|
||||
|
||||
let instance = null;
|
||||
|
||||
class LinuxUpdateUtil {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
this.reloadDB();
|
||||
return instance;
|
||||
}
|
||||
|
||||
getUpdateItem(key, defaultValue = null) {
|
||||
this.reloadDB();
|
||||
const value = this.db.getData('/')[key];
|
||||
if (value === undefined) {
|
||||
this.setUpdateItem(key, defaultValue);
|
||||
return defaultValue;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
setUpdateItem(key, value) {
|
||||
this.db.push(`/${key}`, value, true);
|
||||
this.reloadDB();
|
||||
}
|
||||
|
||||
removeUpdateItem(key) {
|
||||
this.db.delete(`/${key}`);
|
||||
this.reloadDB();
|
||||
}
|
||||
|
||||
reloadDB() {
|
||||
const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/config/updates.json');
|
||||
try {
|
||||
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
|
||||
JSON.parse(file);
|
||||
} catch (err) {
|
||||
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(err);
|
||||
}
|
||||
}
|
||||
this.db = new JsonDB(linuxUpdateJsonPath, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new LinuxUpdateUtil();
|
134
app/renderer/js/utils/logger-util.js
Normal file
134
app/renderer/js/utils/logger-util.js
Normal file
@@ -0,0 +1,134 @@
|
||||
const NodeConsole = require('console').Console;
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const isDev = require('electron-is-dev');
|
||||
const { initSetUp } = require('./default-util');
|
||||
const { sentryInit, captureException } = require('./sentry-util');
|
||||
|
||||
initSetUp();
|
||||
|
||||
let app = null;
|
||||
let reportErrors = true;
|
||||
if (process.type === 'renderer') {
|
||||
app = require('electron').remote.app;
|
||||
|
||||
// Report Errors to Sentry only if it is enabled in settings
|
||||
// Gets the value of reportErrors from config-util for renderer process
|
||||
// For main process, sentryInit() is handled in index.js
|
||||
const { ipcRenderer } = require('electron');
|
||||
ipcRenderer.send('error-reporting');
|
||||
ipcRenderer.on('error-reporting-val', (event, errorReporting) => {
|
||||
reportErrors = errorReporting;
|
||||
if (reportErrors) {
|
||||
sentryInit();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
app = require('electron').app;
|
||||
}
|
||||
|
||||
const browserConsole = console;
|
||||
const logDir = `${app.getPath('userData')}/Logs`;
|
||||
|
||||
class Logger {
|
||||
constructor(opts = {}) {
|
||||
let {
|
||||
timestamp = true,
|
||||
file = 'console.log',
|
||||
level = true,
|
||||
logInDevMode = false
|
||||
} = opts;
|
||||
|
||||
file = `${logDir}/${file}`;
|
||||
if (timestamp === true) {
|
||||
timestamp = this.getTimestamp;
|
||||
}
|
||||
|
||||
// Trim log according to type of process
|
||||
if (process.type === 'renderer') {
|
||||
requestIdleCallback(() => this.trimLog(file));
|
||||
} else {
|
||||
process.nextTick(() => this.trimLog(file));
|
||||
}
|
||||
|
||||
const fileStream = fs.createWriteStream(file, { flags: 'a' });
|
||||
const nodeConsole = new NodeConsole(fileStream);
|
||||
|
||||
this.nodeConsole = nodeConsole;
|
||||
this.timestamp = timestamp;
|
||||
this.level = level;
|
||||
this.logInDevMode = logInDevMode;
|
||||
this.setUpConsole();
|
||||
}
|
||||
|
||||
_log(type, ...args) {
|
||||
const {
|
||||
nodeConsole, timestamp, level, logInDevMode
|
||||
} = this;
|
||||
let nodeConsoleLog;
|
||||
|
||||
/* eslint-disable no-fallthrough */
|
||||
switch (true) {
|
||||
case typeof timestamp === 'function':
|
||||
args.unshift(timestamp() + ' |\t');
|
||||
|
||||
case (level !== false):
|
||||
args.unshift(type.toUpperCase() + ' |');
|
||||
|
||||
case isDev || logInDevMode:
|
||||
nodeConsoleLog = nodeConsole[type] || nodeConsole.log;
|
||||
nodeConsoleLog.apply(null, args);
|
||||
|
||||
default: break;
|
||||
}
|
||||
/* eslint-enable no-fallthrough */
|
||||
|
||||
browserConsole[type].apply(null, args);
|
||||
}
|
||||
|
||||
setUpConsole() {
|
||||
for (const type in browserConsole) {
|
||||
this.setupConsoleMethod(type);
|
||||
}
|
||||
}
|
||||
|
||||
setupConsoleMethod(type) {
|
||||
this[type] = (...args) => {
|
||||
this._log(type, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
getTimestamp() {
|
||||
const date = new Date();
|
||||
const timestamp =
|
||||
`${date.getMonth()}/${date.getDate()} ` +
|
||||
`${date.getMinutes()}:${date.getSeconds()}`;
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
reportSentry(err) {
|
||||
if (reportErrors) {
|
||||
captureException(err);
|
||||
}
|
||||
}
|
||||
|
||||
trimLog(file) {
|
||||
fs.readFile(file, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
const MAX_LOG_FILE_LINES = 500;
|
||||
const logs = data.split(os.EOL);
|
||||
const logLength = logs.length - 1;
|
||||
|
||||
// Keep bottom MAX_LOG_FILE_LINES of each log instance
|
||||
if (logLength > MAX_LOG_FILE_LINES) {
|
||||
const trimmedLogs = logs.slice(logLength - MAX_LOG_FILE_LINES);
|
||||
const toWrite = trimmedLogs.join(os.EOL);
|
||||
fs.writeFileSync(file, toWrite);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Logger;
|
15
app/renderer/js/utils/params-util.js
Normal file
15
app/renderer/js/utils/params-util.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// This util function returns the page params if they're present else returns null
|
||||
function isPageParams() {
|
||||
let webpageParams = null;
|
||||
try {
|
||||
// eslint-disable-next-line no-undef, camelcase
|
||||
webpageParams = page_params;
|
||||
} catch (err) {
|
||||
webpageParams = null;
|
||||
}
|
||||
return webpageParams;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isPageParams
|
||||
};
|
130
app/renderer/js/utils/proxy-util.js
Normal file
130
app/renderer/js/utils/proxy-util.js
Normal file
@@ -0,0 +1,130 @@
|
||||
'use strict';
|
||||
|
||||
const url = require('url');
|
||||
const ConfigUtil = require('./config-util.js');
|
||||
|
||||
let instance = null;
|
||||
|
||||
class ProxyUtil {
|
||||
constructor() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Return proxy to be used for a particular uri, to be used for request
|
||||
getProxy(uri) {
|
||||
uri = url.parse(uri);
|
||||
const proxyRules = ConfigUtil.getConfigItem('proxyRules', '').split(';');
|
||||
// If SPS is on and system uses no proxy then request should not try to use proxy from
|
||||
// environment. NO_PROXY = '*' makes request ignore all environment proxy variables.
|
||||
if (proxyRules[0] === '') {
|
||||
process.env.NO_PROXY = '*';
|
||||
return;
|
||||
}
|
||||
|
||||
const proxyRule = {};
|
||||
if (uri.protocol === 'http:') {
|
||||
proxyRules.forEach(proxy => {
|
||||
if (proxy.includes('http=')) {
|
||||
proxyRule.hostname = proxy.split('http=')[1].trim().split(':')[0];
|
||||
proxyRule.port = proxy.split('http=')[1].trim().split(':')[1];
|
||||
}
|
||||
});
|
||||
return proxyRule;
|
||||
}
|
||||
|
||||
if (uri.protocol === 'https:') {
|
||||
proxyRules.forEach(proxy => {
|
||||
if (proxy.includes('https=')) {
|
||||
proxyRule.hostname = proxy.split('https=')[1].trim().split(':')[0];
|
||||
proxyRule.port = proxy.split('https=')[1].trim().split(':')[1];
|
||||
}
|
||||
});
|
||||
return proxyRule;
|
||||
}
|
||||
}
|
||||
|
||||
resolveSystemProxy(mainWindow) {
|
||||
const page = mainWindow.webContents;
|
||||
const ses = page.session;
|
||||
const resolveProxyUrl = 'www.example.com';
|
||||
|
||||
// Check HTTP Proxy
|
||||
const httpProxy = new Promise(resolve => {
|
||||
ses.resolveProxy('http://' + resolveProxyUrl, proxy => {
|
||||
let httpString = '';
|
||||
if (proxy !== 'DIRECT') {
|
||||
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
|
||||
// for all other HTTP or direct url:port both uses PROXY
|
||||
if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
|
||||
httpString = 'http=' + proxy.split('PROXY')[1] + ';';
|
||||
}
|
||||
}
|
||||
resolve(httpString);
|
||||
});
|
||||
});
|
||||
// Check HTTPS Proxy
|
||||
const httpsProxy = new Promise(resolve => {
|
||||
ses.resolveProxy('https://' + resolveProxyUrl, proxy => {
|
||||
let httpsString = '';
|
||||
if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) {
|
||||
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
|
||||
// for all other HTTP or direct url:port both uses PROXY
|
||||
if (proxy.includes('PROXY' || proxy.includes('HTTPS'))) {
|
||||
httpsString += 'https=' + proxy.split('PROXY')[1] + ';';
|
||||
}
|
||||
}
|
||||
resolve(httpsString);
|
||||
});
|
||||
});
|
||||
|
||||
// Check FTP Proxy
|
||||
const ftpProxy = new Promise(resolve => {
|
||||
ses.resolveProxy('ftp://' + resolveProxyUrl, proxy => {
|
||||
let ftpString = '';
|
||||
if (proxy !== 'DIRECT') {
|
||||
if (proxy.includes('PROXY')) {
|
||||
ftpString += 'ftp=' + proxy.split('PROXY')[1] + ';';
|
||||
}
|
||||
}
|
||||
resolve(ftpString);
|
||||
});
|
||||
});
|
||||
|
||||
// Check SOCKS Proxy
|
||||
const socksProxy = new Promise(resolve => {
|
||||
ses.resolveProxy('socks4://' + resolveProxyUrl, proxy => {
|
||||
let socksString = '';
|
||||
if (proxy !== 'DIRECT') {
|
||||
if (proxy.includes('SOCKS5')) {
|
||||
socksString += 'socks=' + proxy.split('SOCKS5')[1] + ';';
|
||||
} else if (proxy.includes('SOCKS4')) {
|
||||
socksString += 'socks=' + proxy.split('SOCKS4')[1] + ';';
|
||||
} else if (proxy.includes('PROXY')) {
|
||||
socksString += 'socks=' + proxy.split('PROXY')[1] + ';';
|
||||
}
|
||||
}
|
||||
resolve(socksString);
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all([httpProxy, httpsProxy, ftpProxy, socksProxy]).then(values => {
|
||||
let proxyString = '';
|
||||
values.forEach(proxy => {
|
||||
proxyString += proxy;
|
||||
});
|
||||
ConfigUtil.setConfigItem('systemProxyRules', proxyString);
|
||||
const useSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
|
||||
if (useSystemProxy) {
|
||||
ConfigUtil.setConfigItem('proxyRules', proxyString);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ProxyUtil();
|
60
app/renderer/js/utils/reconnect-util.js
Normal file
60
app/renderer/js/utils/reconnect-util.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const isOnline = require('is-online');
|
||||
const Logger = require('./logger-util');
|
||||
|
||||
const logger = new Logger({
|
||||
file: `domain-util.log`,
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
class ReconnectUtil {
|
||||
constructor(serverManagerView) {
|
||||
this.serverManagerView = serverManagerView;
|
||||
this.alreadyReloaded = false;
|
||||
}
|
||||
|
||||
clearState() {
|
||||
this.alreadyReloaded = false;
|
||||
}
|
||||
|
||||
pollInternetAndReload() {
|
||||
const pollInterval = setInterval(() => {
|
||||
this._checkAndReload()
|
||||
.then(status => {
|
||||
if (status) {
|
||||
this.alreadyReloaded = true;
|
||||
clearInterval(pollInterval);
|
||||
}
|
||||
});
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
_checkAndReload() {
|
||||
return new Promise(resolve => {
|
||||
if (!this.alreadyReloaded) { // eslint-disable-line no-negated-condition
|
||||
isOnline()
|
||||
.then(online => {
|
||||
if (online) {
|
||||
if (!this.alreadyReloaded) {
|
||||
this.serverManagerView.reloadView();
|
||||
}
|
||||
logger.log('You\'re back online.');
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
logger.log('There is no internet connection, try checking network cables, modem and router.');
|
||||
const errMsgHolder = document.querySelector('#description');
|
||||
if (errMsgHolder) {
|
||||
errMsgHolder.innerHTML = `
|
||||
<div>Your internet connection doesn't seem to work properly!</div>
|
||||
<div>Verify that it works and then click try again.</div>`;
|
||||
}
|
||||
return resolve(false);
|
||||
});
|
||||
} else {
|
||||
return resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ReconnectUtil;
|
62
app/renderer/js/utils/request-util.js
Normal file
62
app/renderer/js/utils/request-util.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const fs = require('fs');
|
||||
const Logger = require('./logger-util');
|
||||
|
||||
const CertificateUtil = require(__dirname + '/certificate-util.js');
|
||||
const ProxyUtil = require(__dirname + '/proxy-util.js');
|
||||
const ConfigUtil = require(__dirname + '/config-util.js');
|
||||
const SystemUtil = require(__dirname + '/../utils/system-util.js');
|
||||
|
||||
const logger = new Logger({
|
||||
file: `request-util.log`,
|
||||
timestamp: true
|
||||
});
|
||||
|
||||
let instance = null;
|
||||
|
||||
class RequestUtil {
|
||||
constructor() {
|
||||
if (!instance) {
|
||||
instance = this;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// ignoreCerts parameter helps in fetching server icon and
|
||||
// other server details when user chooses to ignore certificate warnings
|
||||
requestOptions(domain, ignoreCerts) {
|
||||
domain = this.formatUrl(domain);
|
||||
const certificate = CertificateUtil.getCertificate(
|
||||
encodeURIComponent(domain)
|
||||
);
|
||||
let certificateLocation = '';
|
||||
if (certificate) {
|
||||
// To handle case where certificate has been moved from the location in certificates.json
|
||||
try {
|
||||
certificateLocation = fs.readFileSync(certificate);
|
||||
} catch (err) {
|
||||
logger.warn('Error while trying to get certificate: ' + err);
|
||||
}
|
||||
}
|
||||
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
|
||||
// If certificate for the domain exists add it as a ca key in the request's parameter else consider only domain as the parameter for request
|
||||
// Add proxy as a parameter if it is being used.
|
||||
return {
|
||||
ca: certificateLocation ? certificateLocation : '',
|
||||
proxy: proxyEnabled ? ProxyUtil.getProxy(domain) : '',
|
||||
ecdhCurve: 'auto',
|
||||
headers: { 'User-Agent': SystemUtil.getUserAgent() },
|
||||
rejectUnauthorized: !ignoreCerts
|
||||
};
|
||||
}
|
||||
|
||||
formatUrl(domain) {
|
||||
const hasPrefix = (domain.indexOf('http') === 0);
|
||||
if (hasPrefix) {
|
||||
return domain;
|
||||
} else {
|
||||
return (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new RequestUtil();
|
20
app/renderer/js/utils/sentry-util.js
Normal file
20
app/renderer/js/utils/sentry-util.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { init, captureException } = require('@sentry/electron');
|
||||
const isDev = require('electron-is-dev');
|
||||
|
||||
const sentryInit = () => {
|
||||
if (!isDev) {
|
||||
init({
|
||||
dsn: 'SENTRY_DSN',
|
||||
// We should ignore this error since it's harmless and we know the reason behind this
|
||||
// This error mainly comes from the console logs.
|
||||
// This is a temp solution until Sentry supports disabling the console logs
|
||||
ignoreErrors: ['does not appear to be a valid Zulip server'],
|
||||
sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sentryInit,
|
||||
captureException
|
||||
};
|
@@ -1,43 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="responsive desktop">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip</title>
|
||||
<link rel="stylesheet" href="css/main.css" type="text/css" media="screen">
|
||||
</head>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip</title>
|
||||
<link rel="stylesheet" href="css/main.css" type="text/css" media="screen">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="content">
|
||||
<div class="popup">
|
||||
<span class="popuptext hidden" id="fullscreen-popup"></span>
|
||||
</div>
|
||||
<div id="sidebar">
|
||||
<div id="view-controls-container">
|
||||
<div id="tabs-container"></div>
|
||||
<div id="add-tab" class="tab functional-tab">
|
||||
<div class="server-tab" id="add-action">
|
||||
<i class="material-icons">add</i>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div class="popup">
|
||||
<span class="popuptext hidden" id="fullscreen-popup"></span>
|
||||
</div>
|
||||
<div id="sidebar" class="toggle-sidebar">
|
||||
<div id="view-controls-container">
|
||||
<div id="tabs-container"></div>
|
||||
<div id="add-tab" class="tab functional-tab">
|
||||
<div class="server-tab" id="add-action">
|
||||
<i class="material-icons">add</i>
|
||||
</div>
|
||||
<span id="add-server-tooltip" style="display:none">Add organization</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="actions-container">
|
||||
<div class="action-button" id="dnd-action">
|
||||
<i class="material-icons md-48">notifications</i>
|
||||
<span id="dnd-tooltip" style="display:none">Do Not Disturb</span>
|
||||
</div>
|
||||
<div class="action-button" id="reload-action">
|
||||
<i class="material-icons md-48">refresh</i>
|
||||
<span id="reload-tooltip" style="display:none">Reload</span>
|
||||
</div>
|
||||
<div class="action-button disable" id="back-action">
|
||||
<i class="material-icons md-48">arrow_back</i>
|
||||
<span id="back-tooltip" style="display:none">Go Back</span>
|
||||
</div>
|
||||
<div class="action-button" id="settings-action">
|
||||
<i class="material-icons md-48">settings</i>
|
||||
<span id="setting-tooltip" style="display:none">Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-container">
|
||||
<div id="webviews-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="actions-container">
|
||||
<div class="action-button" id="reload-action">
|
||||
<i class="material-icons md-48">refresh</i>
|
||||
<span id="reload-tooltip" style="display:none">Reload</span>
|
||||
</div>
|
||||
<div class="action-button" id="settings-action">
|
||||
<i class="material-icons md-48">settings</i>
|
||||
<span id="setting-tooltip" style="display:none">Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-container">
|
||||
<div id="webviews-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
</html>
|
||||
<div id="feedback-modal">
|
||||
<send-feedback></send-feedback>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/main.js"></script>
|
||||
<script>require('./js/shared/preventdrag.js')</script>
|
||||
</html>
|
||||
|
@@ -1,21 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="responsive desktop">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip - Network Troubleshooting</title>
|
||||
<link rel="stylesheet" href="css/network.css" type="text/css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div id="picture"><img src="img/zulip_network.png"></div>
|
||||
<div id="title">Zulip can't connect</div>
|
||||
<div id="description">
|
||||
<div>Your computer seems to be offline.</div>
|
||||
<div>We will keep trying to reconnect, or you can try now.</div>
|
||||
</div>
|
||||
<div id="reconnect">Try now</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/pages/network.js"></script>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip - Network Troubleshooting</title>
|
||||
<link rel="stylesheet" href="css/network.css" type="text/css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div id="picture"><img src="img/zulip_network.png"></div>
|
||||
<div id="title">Zulip can't connect</div>
|
||||
<div id="description">
|
||||
<div>Your computer seems to be offline.</div>
|
||||
<div>We will keep trying to reconnect, or you can try now.</div>
|
||||
</div>
|
||||
<div id="reconnect">Try now</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/pages/network.js"></script>
|
||||
<script>require('./js/shared/preventdrag.js')</script>
|
||||
</html>
|
||||
|
@@ -1,16 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="responsive desktop">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip - Settings</title>
|
||||
<link rel="stylesheet" href="css/preference.css" type="text/css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div id="sidebar"></div>
|
||||
<div id="settings-container"></div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/pages/preference/preference.js"></script>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Zulip - Settings</title>
|
||||
<link rel="stylesheet" href="css/preference.css" type="text/css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div id="sidebar"></div>
|
||||
<div id="settings-container"></div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="js/pages/preference/preference.js"></script>
|
||||
<script>require('./js/shared/preventdrag.js')</script>
|
||||
</html>
|
||||
|
BIN
app/resources/sounds/ding.ogg
Normal file
BIN
app/resources/sounds/ding.ogg
Normal file
Binary file not shown.
BIN
app/resources/tray/trayosx@2x.png
Normal file
BIN
app/resources/tray/trayosx@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
app/resources/tray/trayosx@3x.png
Normal file
BIN
app/resources/tray/trayosx@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
app/resources/tray/trayosx@4x.png
Normal file
BIN
app/resources/tray/trayosx@4x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
39
app/translations/bg.json
Normal file
39
app/translations/bg.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "За Зулип",
|
||||
"Actual Size": "Реален размер",
|
||||
"Back": "обратно",
|
||||
"Close": "Близо",
|
||||
"Copy": "копие",
|
||||
"Cut": "Разрез",
|
||||
"Delete": "Изтрий",
|
||||
"Desktop App Settings": "Настройки за настолни приложения",
|
||||
"Edit": "редактиране",
|
||||
"File": "досие",
|
||||
"Forward": "напред",
|
||||
"Hard Reload": "Трудно презареждане",
|
||||
"Help": "Помогне",
|
||||
"History": "история",
|
||||
"Keyboard Shortcuts": "Комбинация от клавиши",
|
||||
"Log Out": "Излез от профила си",
|
||||
"Minimize": "Минимизиране",
|
||||
"Paste": "паста",
|
||||
"Paste and Match Style": "Поставяне и съвпадение на стила",
|
||||
"Quit": "напускам",
|
||||
"Redo": "ремонтирам",
|
||||
"Reload": "Презареди",
|
||||
"Report an issue...": "Подаване на сигнал за проблем ...",
|
||||
"Reset App Settings": "Нулирайте настройките на приложението",
|
||||
"Select All": "Избери всички",
|
||||
"Show App Logs": "Показване на регистрационните файлове на приложения",
|
||||
"Toggle DevTools for Active Tab": "Превключете DevTools за активен раздел",
|
||||
"Toggle DevTools for Zulip App": "Превключете DevTools за Zulip App",
|
||||
"Toggle Full Screen": "Превключване на цял екран",
|
||||
"Toggle Sidebar": "Превключване на страничната лента",
|
||||
"Toggle Tray Icon": "Превключване на иконата на тавата",
|
||||
"Undo": "премахвам",
|
||||
"View": "изглед",
|
||||
"Window": "прозорец",
|
||||
"Zoom In": "Увеличавам",
|
||||
"Zoom Out": "Отдалечавам",
|
||||
"Zulip Help": "Помощ за Zulip"
|
||||
}
|
39
app/translations/cs.json
Normal file
39
app/translations/cs.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "O společnosti Zulip",
|
||||
"Actual Size": "Aktuální velikost",
|
||||
"Back": "Zadní",
|
||||
"Close": "Zavřít",
|
||||
"Copy": "kopírovat",
|
||||
"Cut": "Střih",
|
||||
"Delete": "Vymazat",
|
||||
"Desktop App Settings": "Nastavení aplikace Desktop",
|
||||
"Edit": "Upravit",
|
||||
"File": "Soubor",
|
||||
"Forward": "Vpřed",
|
||||
"Hard Reload": "Hard Reload",
|
||||
"Help": "Pomoc",
|
||||
"History": "Dějiny",
|
||||
"Keyboard Shortcuts": "Klávesové zkratky",
|
||||
"Log Out": "Odhlásit se",
|
||||
"Minimize": "Minimalizujte",
|
||||
"Paste": "Vložit",
|
||||
"Paste and Match Style": "Vložit a přizpůsobit styl",
|
||||
"Quit": "Přestat",
|
||||
"Redo": "Předělat",
|
||||
"Reload": "Znovu načíst",
|
||||
"Report an issue...": "Nahlásit problém...",
|
||||
"Reset App Settings": "Obnovit nastavení aplikace",
|
||||
"Select All": "Vybrat vše",
|
||||
"Show App Logs": "Zobrazit protokoly aplikace",
|
||||
"Toggle DevTools for Active Tab": "Přepínač DevTools pro aktivní kartu",
|
||||
"Toggle DevTools for Zulip App": "Přepnout nástroj DevTools pro aplikaci Zulip App",
|
||||
"Toggle Full Screen": "Přepnout na celou obrazovku",
|
||||
"Toggle Sidebar": "Přepnout postranní panel",
|
||||
"Toggle Tray Icon": "Přepnout ikonu zásobníku",
|
||||
"Undo": "vrátit",
|
||||
"View": "Pohled",
|
||||
"Window": "Okno",
|
||||
"Zoom In": "Přiblížit",
|
||||
"Zoom Out": "Oddálit",
|
||||
"Zulip Help": "Zulip Nápověda"
|
||||
}
|
39
app/translations/de.json
Normal file
39
app/translations/de.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "Über Zulip",
|
||||
"Actual Size": "Tatsächliche Größe",
|
||||
"Back": "Zurück",
|
||||
"Close": "Schließen",
|
||||
"Copy": "Kopieren",
|
||||
"Cut": "Schnitt",
|
||||
"Delete": "Löschen",
|
||||
"Desktop App Settings": "Desktop App Einstellungen",
|
||||
"Edit": "Bearbeiten",
|
||||
"File": "Datei",
|
||||
"Forward": "Nach vorne",
|
||||
"Hard Reload": "Hard Reload",
|
||||
"Help": "Hilfe",
|
||||
"History": "Geschichte",
|
||||
"Keyboard Shortcuts": "Tastatürkürzel",
|
||||
"Log Out": "Ausloggen",
|
||||
"Minimize": "Minimieren",
|
||||
"Paste": "Einfügen",
|
||||
"Paste and Match Style": "Einfügen und Anpassen von Stilen",
|
||||
"Quit": "Verlassen",
|
||||
"Redo": "Wiederholen",
|
||||
"Reload": "Neu laden",
|
||||
"Report an issue...": "Ein Problem melden...",
|
||||
"Reset App Settings": "App-Einstellungen zurücksetzen",
|
||||
"Select All": "Wählen Sie Alle",
|
||||
"Show App Logs": "App-Logs anzeigen",
|
||||
"Toggle DevTools for Active Tab": "Schalten Sie DevTools für Active Tab ein",
|
||||
"Toggle DevTools for Zulip App": "Umschalt DevTools für Zulip App",
|
||||
"Toggle Full Screen": "Vollbild umschalten",
|
||||
"Toggle Sidebar": "Toggle Seitenleiste",
|
||||
"Toggle Tray Icon": "Toggle Tray-Symbol",
|
||||
"Undo": "Rückgängig machen",
|
||||
"View": "Aussicht",
|
||||
"Window": "Fenster",
|
||||
"Zoom In": "Hineinzoomen",
|
||||
"Zoom Out": "Rauszoomen",
|
||||
"Zulip Help": "Zulip Hilfe"
|
||||
}
|
39
app/translations/en.json
Normal file
39
app/translations/en.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "About Zulip",
|
||||
"Actual Size": "Actual Size",
|
||||
"Back": "Back",
|
||||
"Close": "Close",
|
||||
"Copy": "Copy",
|
||||
"Cut": "Cut",
|
||||
"Delete": "Delete",
|
||||
"Desktop App Settings": "Desktop App Settings",
|
||||
"Edit": "Edit",
|
||||
"File": "File",
|
||||
"Forward": "Forward",
|
||||
"Hard Reload": "Hard Reload",
|
||||
"Help": "Help",
|
||||
"History": "History",
|
||||
"Keyboard Shortcuts": "Keyboard Shortcuts",
|
||||
"Log Out": "Log Out",
|
||||
"Minimize": "Minimize",
|
||||
"Paste": "Paste",
|
||||
"Paste and Match Style": "Paste and Match Style",
|
||||
"Quit": "Quit",
|
||||
"Redo": "Redo",
|
||||
"Reload": "Reload",
|
||||
"Report an issue...": "Report an issue...",
|
||||
"Reset App Settings": "Reset App Settings",
|
||||
"Select All": "Select All",
|
||||
"Show App Logs": "Show App Logs",
|
||||
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
|
||||
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
|
||||
"Toggle Full Screen": "Toggle Full Screen",
|
||||
"Toggle Sidebar": "Toggle Sidebar",
|
||||
"Toggle Tray Icon": "Toggle Tray Icon",
|
||||
"Undo": "Undo",
|
||||
"View": "View",
|
||||
"Window": "Window",
|
||||
"Zoom In": "Zoom In",
|
||||
"Zoom Out": "Zoom Out",
|
||||
"Zulip Help": "Zulip Help"
|
||||
}
|
39
app/translations/es.json
Normal file
39
app/translations/es.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "Acerca de Zulip",
|
||||
"Actual Size": "Tamaño real",
|
||||
"Back": "Espalda",
|
||||
"Close": "Cerca",
|
||||
"Copy": "Dupdo",
|
||||
"Cut": "Cortar",
|
||||
"Delete": "Borrar",
|
||||
"Desktop App Settings": "Configuración de la aplicación de escritorio",
|
||||
"Edit": "Editar",
|
||||
"File": "Archivo",
|
||||
"Forward": "Adelante",
|
||||
"Hard Reload": "Recargar duro",
|
||||
"Help": "Ayuda",
|
||||
"History": "Historia",
|
||||
"Keyboard Shortcuts": "Atajos de teclado",
|
||||
"Log Out": "Cerrar sesión",
|
||||
"Minimize": "Minimizar",
|
||||
"Paste": "Pegar",
|
||||
"Paste and Match Style": "Pegar y combinar estilo",
|
||||
"Quit": "Dejar",
|
||||
"Redo": "Rehacer",
|
||||
"Reload": "Recargar",
|
||||
"Report an issue...": "Reportar un problema...",
|
||||
"Reset App Settings": "Restablecer configuraciones",
|
||||
"Select All": "Seleccionar todo",
|
||||
"Show App Logs": "Mostrar registros de aplicaciones",
|
||||
"Toggle DevTools for Active Tab": "Alternar DevTools para Active Tab",
|
||||
"Toggle DevTools for Zulip App": "Alternar DevTools para la aplicación Zulip",
|
||||
"Toggle Full Screen": "Alternar pantalla completa",
|
||||
"Toggle Sidebar": "Alternar barra lateral",
|
||||
"Toggle Tray Icon": "Alternar el icono de la bandeja",
|
||||
"Undo": "Deshacer",
|
||||
"View": "Ver",
|
||||
"Window": "Ventana",
|
||||
"Zoom In": "Acercarse",
|
||||
"Zoom Out": "Disminuir el zoom",
|
||||
"Zulip Help": "Ayuda de Zulip"
|
||||
}
|
39
app/translations/fr.json
Normal file
39
app/translations/fr.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "À propos de Zulip",
|
||||
"Actual Size": "Taille actuelle",
|
||||
"Back": "Arrière",
|
||||
"Close": "Fermer",
|
||||
"Copy": "Copie",
|
||||
"Cut": "Couper",
|
||||
"Delete": "Effacer",
|
||||
"Desktop App Settings": "Paramètres de l'application de bureau",
|
||||
"Edit": "modifier",
|
||||
"File": "Fichier",
|
||||
"Forward": "Vers l'avant",
|
||||
"Hard Reload": "Rechargement dur",
|
||||
"Help": "Aidez-moi",
|
||||
"History": "Histoire",
|
||||
"Keyboard Shortcuts": "Raccourcis clavier",
|
||||
"Log Out": "Connectez - Out",
|
||||
"Minimize": "Minimiser",
|
||||
"Paste": "Coller",
|
||||
"Paste and Match Style": "Coller et assortir le style",
|
||||
"Quit": "Quitter",
|
||||
"Redo": "Refaire",
|
||||
"Reload": "Recharger",
|
||||
"Report an issue...": "Signaler un problème...",
|
||||
"Reset App Settings": "Réinitialiser les paramètres",
|
||||
"Select All": "Tout sélectionner",
|
||||
"Show App Logs": "Afficher les journaux d'applications",
|
||||
"Toggle DevTools for Active Tab": "Basculer DevTools pour l'onglet actif",
|
||||
"Toggle DevTools for Zulip App": "Basculer DevTools pour l'application Zulip",
|
||||
"Toggle Full Screen": "Basculer en plein écran",
|
||||
"Toggle Sidebar": "Basculer la barre latérale",
|
||||
"Toggle Tray Icon": "Toggle Tray Icône",
|
||||
"Undo": "annuler",
|
||||
"View": "Vue",
|
||||
"Window": "Fenêtre",
|
||||
"Zoom In": "Agrandir",
|
||||
"Zoom Out": "Dézoomer",
|
||||
"Zulip Help": "Aide Zulip"
|
||||
}
|
39
app/translations/hi.json
Normal file
39
app/translations/hi.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "Zulip के बारे में",
|
||||
"Actual Size": "वास्तविक आकार",
|
||||
"Back": "वापस",
|
||||
"Close": "बंद करे",
|
||||
"Copy": "प्रतिलिपि",
|
||||
"Cut": "कट गया",
|
||||
"Delete": "हटाना",
|
||||
"Desktop App Settings": "डेस्कटॉप ऐप सेटिंग्स",
|
||||
"Edit": "संपादित करें",
|
||||
"File": "फ़ाइल",
|
||||
"Forward": "आगे",
|
||||
"Hard Reload": "कठिन पुन: लोड करें",
|
||||
"Help": "मदद",
|
||||
"History": "इतिहास",
|
||||
"Keyboard Shortcuts": "कुंजीपटल अल्प मार्ग",
|
||||
"Log Out": "लोग आउट",
|
||||
"Minimize": "छोटा करना",
|
||||
"Paste": "चिपकाएं",
|
||||
"Paste and Match Style": "चिपकाएं और शैली का मिलान करें",
|
||||
"Quit": "छोड़ना",
|
||||
"Redo": "फिर से करना",
|
||||
"Reload": "सीमा से अधिक लादना",
|
||||
"Report an issue...": "मामले की रिपोर्ट करें...",
|
||||
"Reset App Settings": "ऐप सेटिंग्स रीसेट करें",
|
||||
"Select All": "सभी का चयन करे",
|
||||
"Show App Logs": "ऐप लॉग दिखाएं",
|
||||
"Toggle DevTools for Active Tab": "सक्रिय टैब के लिए DevTools टॉगल करें",
|
||||
"Toggle DevTools for Zulip App": "Zulip ऐप के लिए DevTools टॉगल करें",
|
||||
"Toggle Full Screen": "पूर्णस्क्रीन चालू करें",
|
||||
"Toggle Sidebar": "साइडबार टॉगल करें",
|
||||
"Toggle Tray Icon": "ट्रे आइकन टॉगल करें",
|
||||
"Undo": "पूर्ववत करें",
|
||||
"View": "राय",
|
||||
"Window": "खिड़की",
|
||||
"Zoom In": "ज़ूम इन",
|
||||
"Zoom Out": "ज़ूम आउट",
|
||||
"Zulip Help": "Zulip सहायता"
|
||||
}
|
39
app/translations/hu.json
Normal file
39
app/translations/hu.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "A Zulipról",
|
||||
"Actual Size": "Valódi méret",
|
||||
"Back": "Hát",
|
||||
"Close": "Bezárás",
|
||||
"Copy": "Másolat",
|
||||
"Cut": "Vágott",
|
||||
"Delete": "Töröl",
|
||||
"Desktop App Settings": "Asztali alkalmazások beállításai",
|
||||
"Edit": "szerkesztése",
|
||||
"File": "fájl",
|
||||
"Forward": "Előre",
|
||||
"Hard Reload": "Hard Reload",
|
||||
"Help": "Segítség",
|
||||
"History": "Történelem",
|
||||
"Keyboard Shortcuts": "Gyorsbillentyűket",
|
||||
"Log Out": "Kijelentkezés",
|
||||
"Minimize": "Kis méret",
|
||||
"Paste": "Paszta",
|
||||
"Paste and Match Style": "Beillesztés és stílusok illesztése",
|
||||
"Quit": "quit",
|
||||
"Redo": "Újra",
|
||||
"Reload": "Reload",
|
||||
"Report an issue...": "Hibajelentés ...",
|
||||
"Reset App Settings": "Az alkalmazás beállításainak visszaállítása",
|
||||
"Select All": "Mindet kiválaszt",
|
||||
"Show App Logs": "Alkalmazásnaplók megjelenítése",
|
||||
"Toggle DevTools for Active Tab": "A DevTools aktiválása az Aktív laphoz",
|
||||
"Toggle DevTools for Zulip App": "Kapcsolja a DevTools-ot a Zulip App-hoz",
|
||||
"Toggle Full Screen": "Teljes képernyőre váltás",
|
||||
"Toggle Sidebar": "Oldalsáv átkapcsolása",
|
||||
"Toggle Tray Icon": "Tálcaikon átkapcsolása",
|
||||
"Undo": "kibont",
|
||||
"View": "Kilátás",
|
||||
"Window": "Ablak",
|
||||
"Zoom In": "Nagyítás",
|
||||
"Zoom Out": "Kicsinyítés",
|
||||
"Zulip Help": "Zulip Súgó"
|
||||
}
|
39
app/translations/id.json
Normal file
39
app/translations/id.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "Tentang Zulip",
|
||||
"Actual Size": "Ukuran sebenarnya",
|
||||
"Back": "Kembali",
|
||||
"Close": "Dekat",
|
||||
"Copy": "Salinan",
|
||||
"Cut": "Memotong",
|
||||
"Delete": "Menghapus",
|
||||
"Desktop App Settings": "Setelan Aplikasi Desktop",
|
||||
"Edit": "Edit",
|
||||
"File": "Mengajukan",
|
||||
"Forward": "Meneruskan",
|
||||
"Hard Reload": "Hard Reload",
|
||||
"Help": "Membantu",
|
||||
"History": "Sejarah",
|
||||
"Keyboard Shortcuts": "Cara pintas keyboard",
|
||||
"Log Out": "Keluar",
|
||||
"Minimize": "Memperkecil",
|
||||
"Paste": "Pasta",
|
||||
"Paste and Match Style": "Tempel dan Cocokkan Gaya",
|
||||
"Quit": "Berhenti",
|
||||
"Redo": "Mengulangi",
|
||||
"Reload": "Reload",
|
||||
"Report an issue...": "Laporkan masalah ...",
|
||||
"Reset App Settings": "Setel ulang Pengaturan Aplikasi",
|
||||
"Select All": "Pilih Semua",
|
||||
"Show App Logs": "Tampilkan log aplikasi",
|
||||
"Toggle DevTools for Active Tab": "Toggle DevTools untuk Tab Aktif",
|
||||
"Toggle DevTools for Zulip App": "Toggle DevTools untuk Aplikasi Zulip",
|
||||
"Toggle Full Screen": "Toggle Full Screen",
|
||||
"Toggle Sidebar": "Toggle Sidebar",
|
||||
"Toggle Tray Icon": "Toggle Tray Icon",
|
||||
"Undo": "Membuka",
|
||||
"View": "Melihat",
|
||||
"Window": "Jendela",
|
||||
"Zoom In": "Perbesar",
|
||||
"Zoom Out": "Zoom Out",
|
||||
"Zulip Help": "Zulip Help"
|
||||
}
|
39
app/translations/ja.json
Normal file
39
app/translations/ja.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "チューリップについて",
|
||||
"Actual Size": "実際のサイズ",
|
||||
"Back": "バック",
|
||||
"Close": "閉じる",
|
||||
"Copy": "コピー",
|
||||
"Cut": "カット",
|
||||
"Delete": "削除",
|
||||
"Desktop App Settings": "デスクトップアプリケーションの設定",
|
||||
"Edit": "編集",
|
||||
"File": "ファイル",
|
||||
"Forward": "フォワード",
|
||||
"Hard Reload": "ハードリロード",
|
||||
"Help": "助けて",
|
||||
"History": "歴史",
|
||||
"Keyboard Shortcuts": "キーボードショートカット",
|
||||
"Log Out": "ログアウト",
|
||||
"Minimize": "最小化する",
|
||||
"Paste": "ペースト",
|
||||
"Paste and Match Style": "スタイルの貼り付けと一致",
|
||||
"Quit": "終了する",
|
||||
"Redo": "やり直し",
|
||||
"Reload": "リロード",
|
||||
"Report an issue...": "問題を報告する...",
|
||||
"Reset App Settings": "アプリの設定をリセットする",
|
||||
"Select All": "すべて選択",
|
||||
"Show App Logs": "アプリケーションログを表示する",
|
||||
"Toggle DevTools for Active Tab": "DevTools for Activeタブを切り替える",
|
||||
"Toggle DevTools for Zulip App": "Zulip AppのDevToolsの切り替え",
|
||||
"Toggle Full Screen": "フルスクリーン切り替え",
|
||||
"Toggle Sidebar": "サイドバーの切り替え",
|
||||
"Toggle Tray Icon": "トレイアイコンを切り替える",
|
||||
"Undo": "元に戻す",
|
||||
"View": "ビュー",
|
||||
"Window": "窓",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウトする",
|
||||
"Zulip Help": "チューリップヘルプ"
|
||||
}
|
39
app/translations/ko.json
Normal file
39
app/translations/ko.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "튤립 소개",
|
||||
"Actual Size": "실제 크기",
|
||||
"Back": "뒤로",
|
||||
"Close": "닫기",
|
||||
"Copy": "부",
|
||||
"Cut": "절단",
|
||||
"Delete": "지우다",
|
||||
"Desktop App Settings": "데스크톱 앱 설정",
|
||||
"Edit": "편집하다",
|
||||
"File": "파일",
|
||||
"Forward": "앞으로",
|
||||
"Hard Reload": "하드 다시로드",
|
||||
"Help": "도움",
|
||||
"History": "역사",
|
||||
"Keyboard Shortcuts": "키보드 단축키",
|
||||
"Log Out": "로그 아웃",
|
||||
"Minimize": "최소화",
|
||||
"Paste": "풀",
|
||||
"Paste and Match Style": "붙여 넣기 및 스타일 일치",
|
||||
"Quit": "떠나다",
|
||||
"Redo": "다시 하다",
|
||||
"Reload": "다시로드",
|
||||
"Report an issue...": "문제 신고 ...",
|
||||
"Reset App Settings": "앱 설정 재설정",
|
||||
"Select All": "모두 선택",
|
||||
"Show App Logs": "앱 로그 표시",
|
||||
"Toggle DevTools for Active Tab": "DevTools for Active Tab 토글",
|
||||
"Toggle DevTools for Zulip App": "Zulip App 용 DevTools 토글",
|
||||
"Toggle Full Screen": "전체 화면 토글",
|
||||
"Toggle Sidebar": "사이드 바 전환",
|
||||
"Toggle Tray Icon": "트레이 아이콘 토글",
|
||||
"Undo": "끄르다",
|
||||
"View": "전망",
|
||||
"Window": "창문",
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소",
|
||||
"Zulip Help": "튤립 도움말"
|
||||
}
|
39
app/translations/ml.json
Normal file
39
app/translations/ml.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "സുലിപ്",
|
||||
"Actual Size": "യഥാർത്ഥ വലുപ്പം",
|
||||
"Back": "തിരികെ",
|
||||
"Close": "അടയ്ക്കുക",
|
||||
"Copy": "പകർത്തുക",
|
||||
"Cut": "മുറിക്കുക",
|
||||
"Delete": "ഇല്ലാതാക്കുക",
|
||||
"Desktop App Settings": "ഡെസ്ക്ടോപ്പ് അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങൾ",
|
||||
"Edit": "എഡിറ്റുചെയ്യുക",
|
||||
"File": "ഫയൽ",
|
||||
"Forward": "മുന്നോട്ട്",
|
||||
"Hard Reload": "ഹാർഡ് റീലോഡ്",
|
||||
"Help": "സഹായിക്കൂ",
|
||||
"History": "ചരിത്രം",
|
||||
"Keyboard Shortcuts": "കീബോർഡ് കുറുക്കുവഴികൾ",
|
||||
"Log Out": "ലോഗ് ഔട്ട്",
|
||||
"Minimize": "ചെറുതാക്കുക",
|
||||
"Paste": "പേസ്റ്റ്",
|
||||
"Paste and Match Style": "ശൈലി ഒട്ടിക്കുകയും പൊരുത്തപ്പെടുത്തുകയും ചെയ്യുക",
|
||||
"Quit": "പുറത്തുകടക്കുക",
|
||||
"Redo": "വീണ്ടും ചെയ്യുക",
|
||||
"Reload": "വീണ്ടും ലോഡുചെയ്യുക",
|
||||
"Report an issue...": "ഒരു പ്രശ്നം റിപ്പോർട്ടുചെയ്യുക ...",
|
||||
"Reset App Settings": "അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങൾ പുനഃസജ്ജമാക്കുക",
|
||||
"Select All": "എല്ലാം തിരഞ്ഞെടുക്കുക",
|
||||
"Show App Logs": "അപ്ലിക്കേഷൻ ലോഗുകൾ കാണിക്കുക",
|
||||
"Toggle DevTools for Active Tab": "സജീവ ടാബിനായി DevTools ടോഗിൾ ചെയ്യുക",
|
||||
"Toggle DevTools for Zulip App": "സുലിപ്പ് ആപ്ലിക്കേഷനായി DevTools ടോഗിൾ ചെയ്യുക",
|
||||
"Toggle Full Screen": "പൂർണ്ണ സ്ക്രീൻ ടോഗിൾ ചെയ്യുക",
|
||||
"Toggle Sidebar": "സൈഡ്ബാർ ടോഗിൾ ചെയ്യുക",
|
||||
"Toggle Tray Icon": "ട്രേ ഐക്കൺ ടോഗിൾ ചെയ്യുക",
|
||||
"Undo": "പഴയപടിയാക്കുക",
|
||||
"View": "കാണുക",
|
||||
"Window": "ജാലകം",
|
||||
"Zoom In": "വലുതാക്കുക",
|
||||
"Zoom Out": "സൂം ഔട്ട്",
|
||||
"Zulip Help": "സുലിപ്പ് സഹായം"
|
||||
}
|
39
app/translations/nl.json
Normal file
39
app/translations/nl.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "Over Zulip",
|
||||
"Actual Size": "Daadwerkelijke grootte",
|
||||
"Back": "Terug",
|
||||
"Close": "Dichtbij",
|
||||
"Copy": "Kopiëren",
|
||||
"Cut": "Besnoeiing",
|
||||
"Delete": "Verwijder",
|
||||
"Desktop App Settings": "Desktop-app-instellingen",
|
||||
"Edit": "Bewerk",
|
||||
"File": "het dossier",
|
||||
"Forward": "Vooruit",
|
||||
"Hard Reload": "Moeilijke herladen",
|
||||
"Help": "Helpen",
|
||||
"History": "Geschiedenis",
|
||||
"Keyboard Shortcuts": "Toetsenbord sneltoetsen",
|
||||
"Log Out": "Uitloggen",
|
||||
"Minimize": "verkleinen",
|
||||
"Paste": "Plakken",
|
||||
"Paste and Match Style": "Plak en match stijl",
|
||||
"Quit": "ophouden",
|
||||
"Redo": "Opnieuw doen",
|
||||
"Reload": "vernieuwen",
|
||||
"Report an issue...": "Een probleem melden...",
|
||||
"Reset App Settings": "App-instellingen opnieuw instellen",
|
||||
"Select All": "Selecteer alles",
|
||||
"Show App Logs": "App-logs weergeven",
|
||||
"Toggle DevTools for Active Tab": "DevTools voor actieve tabblad omschakelen",
|
||||
"Toggle DevTools for Zulip App": "DevTools voor Zulip-app omschakelen",
|
||||
"Toggle Full Screen": "Volledig scherm activeren",
|
||||
"Toggle Sidebar": "Zijbalk verschuiven",
|
||||
"Toggle Tray Icon": "Pictogram Lade wisselen",
|
||||
"Undo": "ongedaan maken",
|
||||
"View": "Uitzicht",
|
||||
"Window": "Venster",
|
||||
"Zoom In": "In zoomen",
|
||||
"Zoom Out": "Uitzoomen",
|
||||
"Zulip Help": "Zulip Help"
|
||||
}
|
39
app/translations/pl.json
Normal file
39
app/translations/pl.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "O Zulip",
|
||||
"Actual Size": "Rzeczywisty rozmiar",
|
||||
"Back": "Plecy",
|
||||
"Close": "Blisko",
|
||||
"Copy": "Kopiuj",
|
||||
"Cut": "Ciąć",
|
||||
"Delete": "Kasować",
|
||||
"Desktop App Settings": "Ustawienia aplikacji na komputer",
|
||||
"Edit": "Edytować",
|
||||
"File": "Plik",
|
||||
"Forward": "Naprzód",
|
||||
"Hard Reload": "Trudne przeładowanie",
|
||||
"Help": "Wsparcie",
|
||||
"History": "Historia",
|
||||
"Keyboard Shortcuts": "Skróty klawiszowe",
|
||||
"Log Out": "Wyloguj",
|
||||
"Minimize": "Zminimalizować",
|
||||
"Paste": "Pasta",
|
||||
"Paste and Match Style": "Wklej i dopasuj styl",
|
||||
"Quit": "Porzucić",
|
||||
"Redo": "Przerobić",
|
||||
"Reload": "Przeładować",
|
||||
"Report an issue...": "Zgłoś problem...",
|
||||
"Reset App Settings": "Zresetuj ustawienia aplikacji",
|
||||
"Select All": "Zaznacz wszystko",
|
||||
"Show App Logs": "Pokaż dzienniki aplikacji",
|
||||
"Toggle DevTools for Active Tab": "Przełącz DevTools dla Active Tab",
|
||||
"Toggle DevTools for Zulip App": "Przełącz DevTools dla aplikacji Zulip",
|
||||
"Toggle Full Screen": "Przełącz tryb pełnoekranowy",
|
||||
"Toggle Sidebar": "Przełącz boczny pasek",
|
||||
"Toggle Tray Icon": "Przełącz ikonę tacy",
|
||||
"Undo": "Cofnij",
|
||||
"View": "Widok",
|
||||
"Window": "Okno",
|
||||
"Zoom In": "Zbliżenie",
|
||||
"Zoom Out": "Pomniejsz",
|
||||
"Zulip Help": "Zulip Help"
|
||||
}
|
39
app/translations/pt.json
Normal file
39
app/translations/pt.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "Sobre Zulip",
|
||||
"Actual Size": "Tamanho atual",
|
||||
"Back": "Costas",
|
||||
"Close": "Fechar",
|
||||
"Copy": "cópia de",
|
||||
"Cut": "Cortar",
|
||||
"Delete": "Excluir",
|
||||
"Desktop App Settings": "Configurações da aplicação Desktop",
|
||||
"Edit": "Editar",
|
||||
"File": "Arquivo",
|
||||
"Forward": "frente",
|
||||
"Hard Reload": "Hard Recargar",
|
||||
"Help": "Socorro",
|
||||
"History": "História",
|
||||
"Keyboard Shortcuts": "Atalhos do teclado",
|
||||
"Log Out": "Sair",
|
||||
"Minimize": "Minimizar",
|
||||
"Paste": "Colar",
|
||||
"Paste and Match Style": "Estilo de colar e combinar",
|
||||
"Quit": "Sair",
|
||||
"Redo": "Refazer",
|
||||
"Reload": "recarregar",
|
||||
"Report an issue...": "Relatar um problema ...",
|
||||
"Reset App Settings": "Redefinir as configurações da aplicação",
|
||||
"Select All": "Selecionar tudo",
|
||||
"Show App Logs": "Mostrar logs de aplicativos",
|
||||
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
|
||||
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
|
||||
"Toggle Full Screen": "Alternar para o modo tela cheia",
|
||||
"Toggle Sidebar": "Toggle Sidebar",
|
||||
"Toggle Tray Icon": "Ícone Toggle Tray",
|
||||
"Undo": "Desfazer",
|
||||
"View": "Visão",
|
||||
"Window": "Janela",
|
||||
"Zoom In": "Mais Zoom",
|
||||
"Zoom Out": "Reduzir o zoom",
|
||||
"Zulip Help": "Zulip Help"
|
||||
}
|
39
app/translations/ru.json
Normal file
39
app/translations/ru.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "О пользователе Zulip",
|
||||
"Actual Size": "Фактический размер",
|
||||
"Back": "назад",
|
||||
"Close": "Закрыть",
|
||||
"Copy": "копия",
|
||||
"Cut": "Порез",
|
||||
"Delete": "Удалить",
|
||||
"Desktop App Settings": "Настройки настольных приложений",
|
||||
"Edit": "редактировать",
|
||||
"File": "файл",
|
||||
"Forward": "Вперед",
|
||||
"Hard Reload": "Hard Reload",
|
||||
"Help": "Помогите",
|
||||
"History": "история",
|
||||
"Keyboard Shortcuts": "Горячие клавиши",
|
||||
"Log Out": "Выйти",
|
||||
"Minimize": "Минимизировать",
|
||||
"Paste": "Вставить",
|
||||
"Paste and Match Style": "Стиль вставки и совпадения",
|
||||
"Quit": "Уволиться",
|
||||
"Redo": "переделывать",
|
||||
"Reload": "перезагружать",
|
||||
"Report an issue...": "Сообщить о проблеме...",
|
||||
"Reset App Settings": "Сбросить настройки приложения",
|
||||
"Select All": "Выбрать все",
|
||||
"Show App Logs": "Показать журналы приложений",
|
||||
"Toggle DevTools for Active Tab": "Toggle DevTools для активной вкладки",
|
||||
"Toggle DevTools for Zulip App": "Toggle DevTools для приложения Zulip",
|
||||
"Toggle Full Screen": "Включить полноэкранный режим",
|
||||
"Toggle Sidebar": "Переключить боковую панель",
|
||||
"Toggle Tray Icon": "Иконка Toggle Tray",
|
||||
"Undo": "расстегивать",
|
||||
"View": "Посмотреть",
|
||||
"Window": "Окно",
|
||||
"Zoom In": "Приблизить",
|
||||
"Zoom Out": "Уменьшить",
|
||||
"Zulip Help": "Zulip Help"
|
||||
}
|
39
app/translations/sr.json
Normal file
39
app/translations/sr.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "О Зулипу",
|
||||
"Actual Size": "Стварна величина",
|
||||
"Back": "Назад",
|
||||
"Close": "Близу",
|
||||
"Copy": "Копирај",
|
||||
"Cut": "Цут",
|
||||
"Delete": "Избриши",
|
||||
"Desktop App Settings": "Подешавања за десктоп апликације",
|
||||
"Edit": "Уредити",
|
||||
"File": "Филе",
|
||||
"Forward": "Напријед",
|
||||
"Hard Reload": "Хард Релоад",
|
||||
"Help": "Помоћ",
|
||||
"History": "Историја",
|
||||
"Keyboard Shortcuts": "Пречице на тастатури",
|
||||
"Log Out": "Одјавити се",
|
||||
"Minimize": "Минимизирај",
|
||||
"Paste": "Пасте",
|
||||
"Paste and Match Style": "Пасте и Матцх Стиле",
|
||||
"Quit": "Одустати",
|
||||
"Redo": "Поново",
|
||||
"Reload": "Освежи",
|
||||
"Report an issue...": "Пријавите проблем...",
|
||||
"Reset App Settings": "Поново поставите подешавања апликације",
|
||||
"Select All": "Изабери све",
|
||||
"Show App Logs": "Прикажи евиденције апликација",
|
||||
"Toggle DevTools for Active Tab": "Пребаците ДевТоолс за Ацтиве Таб",
|
||||
"Toggle DevTools for Zulip App": "Пребаците ДевТоолс за Зулип Апп",
|
||||
"Toggle Full Screen": "Пребаците цео екран",
|
||||
"Toggle Sidebar": "Пребаците бочну траку",
|
||||
"Toggle Tray Icon": "Тоггле Траи Ицон",
|
||||
"Undo": "Ундо",
|
||||
"View": "Поглед",
|
||||
"Window": "Прозор",
|
||||
"Zoom In": "Увеличати",
|
||||
"Zoom Out": "Зумирај",
|
||||
"Zulip Help": "Зулип Хелп"
|
||||
}
|
39
app/translations/ta.json
Normal file
39
app/translations/ta.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "ஜுலிப் பற்றி",
|
||||
"Actual Size": "உண்மையான அளவு",
|
||||
"Back": "மீண்டும்",
|
||||
"Close": "நெருக்கமான",
|
||||
"Copy": "நகல்",
|
||||
"Cut": "வெட்டு",
|
||||
"Delete": "அழி",
|
||||
"Desktop App Settings": "டெஸ்க்டாப் பயன்பாட்டு அமைப்புகள்",
|
||||
"Edit": "தொகு",
|
||||
"File": "கோப்பு",
|
||||
"Forward": "முன்னோக்கி",
|
||||
"Hard Reload": "கடினமாக மீண்டும் ஏற்றவும்",
|
||||
"Help": "உதவி",
|
||||
"History": "வரலாறு",
|
||||
"Keyboard Shortcuts": "விசைப்பலகை குறுக்குவழிகள்",
|
||||
"Log Out": "வெளியேறு",
|
||||
"Minimize": "குறைத்தல்",
|
||||
"Paste": "ஒட்டு",
|
||||
"Paste and Match Style": "உடை ஒட்டு மற்றும் பொருந்தும்",
|
||||
"Quit": "விட்டுவிட",
|
||||
"Redo": "மீண்டும் செய்",
|
||||
"Reload": "ஏற்றவும்",
|
||||
"Report an issue...": "சிக்கலைப் புகார ...",
|
||||
"Reset App Settings": "பயன்பாட்டு அமைப்புகளை மீட்டமைக்கவும்",
|
||||
"Select All": "அனைத்தையும் தெரிவுசெய்",
|
||||
"Show App Logs": "பயன்பாட்டு பதிவுகள் காட்டு",
|
||||
"Toggle DevTools for Active Tab": "செயலில் தாவலுக்கு DevTools ஐ மாற்று",
|
||||
"Toggle DevTools for Zulip App": "Zulip பயன்பாட்டிற்கான DevTools ஐ மாற்று",
|
||||
"Toggle Full Screen": "மாற்று முழுத்திரை",
|
||||
"Toggle Sidebar": "பக்கப்பட்டி மாறு",
|
||||
"Toggle Tray Icon": "ட்ரே ஐகானை மாற்று",
|
||||
"Undo": "செயல்தவிர்",
|
||||
"View": "காண்க",
|
||||
"Window": "ஜன்னல்",
|
||||
"Zoom In": "பெரிதாக்க",
|
||||
"Zoom Out": "பெரிதாக்குக",
|
||||
"Zulip Help": "Zulip உதவி"
|
||||
}
|
39
app/translations/tr.json
Normal file
39
app/translations/tr.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"About Zulip": "Zulip Hakkinda",
|
||||
"Actual Size": "Gerçek boyutu",
|
||||
"Back": "Geri",
|
||||
"Close": "Kapat",
|
||||
"Copy": "kopya",
|
||||
"Cut": "Kesmek",
|
||||
"Delete": "silmek",
|
||||
"Desktop App Settings": "Masaüstü Uygulama Ayarları",
|
||||
"Edit": "Düzenle",
|
||||
"File": "Dosya",
|
||||
"Forward": "ileri",
|
||||
"Hard Reload": "Sert Yeniden Yükle",
|
||||
"Help": "yardım et",
|
||||
"History": "Tarihçe",
|
||||
"Keyboard Shortcuts": "Klavye kısayolları",
|
||||
"Log Out": "Çıkış Yap",
|
||||
"Minimize": "küçültmek",
|
||||
"Paste": "Yapıştırmak",
|
||||
"Paste and Match Style": "Stili Yapıştır ve Eşleştir",
|
||||
"Quit": "çıkmak",
|
||||
"Redo": "yeniden yapmak",
|
||||
"Reload": "Tekrar yükle",
|
||||
"Report an issue...": "Bir sorun bildir ...",
|
||||
"Reset App Settings": "Uygulama Ayarlarını Sıfırla",
|
||||
"Select All": "Hepsini seç",
|
||||
"Show App Logs": "Uygulama Günlüğlerini Göster",
|
||||
"Toggle DevTools for Active Tab": "Aktif Sekme İçin DevTools'a Geçiş Yap",
|
||||
"Toggle DevTools for Zulip App": "Zulip App için DevTools'a Geçiş Yap",
|
||||
"Toggle Full Screen": "Tam ekrana geç",
|
||||
"Toggle Sidebar": "Kenar Çubuğunu Aç / Kapat",
|
||||
"Toggle Tray Icon": "Tepsi Simgesini Aç / Kapat",
|
||||
"Undo": "Geri alma",
|
||||
"View": "Görünüm",
|
||||
"Window": "pencere",
|
||||
"Zoom In": "Yakınlaştır",
|
||||
"Zoom Out": "Uzaklaştır",
|
||||
"Zulip Help": "Zulip Yardımı"
|
||||
}
|
@@ -6,9 +6,9 @@ os: Previous Visual Studio 2015
|
||||
|
||||
cache:
|
||||
- node_modules
|
||||
|
||||
|
||||
install:
|
||||
- ps: Install-Product node 6 x64
|
||||
- ps: Install-Product node 8 x64
|
||||
- git reset --hard HEAD
|
||||
- npm install npm -g
|
||||
- node --version
|
||||
@@ -17,7 +17,8 @@ install:
|
||||
- npm install
|
||||
- npm install -g gulp
|
||||
|
||||
build: off
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- npm run test
|
||||
# - npm run test-e2e
|
||||
|
BIN
build/appdmg.png
BIN
build/appdmg.png
Binary file not shown.
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 12 KiB |
764
changelog.md
Normal file
764
changelog.md
Normal file
@@ -0,0 +1,764 @@
|
||||
# Version History
|
||||
|
||||
All notable changes to the Zulip desktop app are documented in this file.
|
||||
|
||||
### v2.3.82 --2018-09-25
|
||||
|
||||
**New features**:
|
||||
* UI enhancements.
|
||||
* Updated some menu items.
|
||||
|
||||
**Fixes**:
|
||||
* Fix about page not opening up.
|
||||
|
||||
<hr>
|
||||
|
||||
### v2.3.8 --2018-09-25
|
||||
|
||||
**New features**:
|
||||
* Auto hide menubar on Windows/Linux. Add a setting option for the same.
|
||||
* Improve design of setting page.
|
||||
* Toggle app on clicking the tray icon (Linux).
|
||||
* Update sidebar realm name when it's changed in webapp.
|
||||
* left-sidebar: Add initial character of realm name instead of default icon.
|
||||
|
||||
**Fixes**:
|
||||
* linux: Fix ALT+SHIFT opening menu items on Linux.
|
||||
* sentry: Add ignore errors to sentry configuration.
|
||||
* Linux: Add label for help menu item.
|
||||
* file-attachments: Allow multiple downloads of same file name.
|
||||
|
||||
**Module Updates**
|
||||
* electron: Update electron to v2.0.9.
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
### v2.3.7-beta --2018-09-03
|
||||
|
||||
**New features**:
|
||||
* Add a feature to show and view pdf file in the app.
|
||||
|
||||
**Fixes**:
|
||||
* Use package reload instead of custom reload. This is an experimental fix for setting files getting corrupted issue.
|
||||
* Unescape server name in window menu item.
|
||||
|
||||
<hr>
|
||||
|
||||
### v2.3.6 --2018-08-27
|
||||
|
||||
**New features**:
|
||||
* Add proxy details while validating a server. This fixes the server validating issue for users who are using the proxy settings.
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Fix youtube video not playing in lightbox.
|
||||
* Fix realm name not escaped properly.
|
||||
|
||||
<hr>
|
||||
|
||||
### v2.3.5 --2018-08-03
|
||||
|
||||
**New features**:
|
||||
* Add a setting option to show downloaded file in file manager.
|
||||
* Added electron bridge to communicate with webapp in real time.
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Fix failing attached file downloads.
|
||||
* Fix page_params error.
|
||||
* gulpfile: Update syntax and methods for gulp v4.x.
|
||||
|
||||
<hr>
|
||||
|
||||
### v2.3.4-beta --2018-07-24
|
||||
|
||||
**Fixes**:
|
||||
* Fix downloading functionality of file attachments.
|
||||
* Fix null of downloadPath when settings.json fails.
|
||||
|
||||
<hr>
|
||||
|
||||
### v2.3.3 --2018-07-14
|
||||
|
||||
**Enhancements**:
|
||||
* Add dock bounce effect on macOS
|
||||
* Add a setting option to use the system proxy settings
|
||||
* Add support for self/custom signed certificate
|
||||
* Add Sentry support to get the bug reports
|
||||
* Show a notification when a user clicks on file attachments and open the same in default native app
|
||||
|
||||
|
||||
**Fixes**:
|
||||
* Fix auto-updates on Windows
|
||||
* Fix image attachments not opening up in the app
|
||||
* Security fix - Do proper HTML escaping for server data to avoid XSS attacks
|
||||
* Other minor fixes
|
||||
|
||||
**Updated dependencies**:
|
||||
|
||||
electron-builder: v20.20.4
|
||||
|
||||
electron-updater: v2.23.3
|
||||
|
||||
<hr>
|
||||
|
||||
### v2.3.2 --2018-05-28
|
||||
|
||||
|
||||
This is a quick release since we recently updated the certificate for code signing the app and we want to make sure the previous versions of the app get auto-updates without any conflicts.
|
||||
|
||||
<hr>
|
||||
|
||||
### v2.3.1 --2018-05-23
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Add a new setting to disable auto-updates
|
||||
|
||||
* Add a menu item to check for updates manually
|
||||
|
||||
* Other minor improvements
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Fix app not rendering colors properly
|
||||
|
||||
* **Security fix** - Do proper HTML escaping for server data to avoid XSS attacks
|
||||
|
||||
* Fix loading indicator when server is loaded
|
||||
* Other minor fixes
|
||||
|
||||
|
||||
|
||||
|
||||
**Updated dependencies**:
|
||||
|
||||
electron - `v2.0.1`
|
||||
|
||||
electron-builder - `v20.13.4`
|
||||
|
||||
electron-updater - `v2.21.10`
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
### v2.2.0-beta --2018-05-08
|
||||
|
||||
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Added do not disturb feature
|
||||
|
||||
* New DMG installer image
|
||||
|
||||
* Minor improvements
|
||||
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Fix download functionality of file attachment links
|
||||
|
||||
* Fix tray icon not showing unread counts
|
||||
|
||||
|
||||
|
||||
|
||||
**Updated dependencies**:
|
||||
|
||||
electron - `v2.0.0`
|
||||
|
||||
electron-builder - `v2.21.10`
|
||||
|
||||
electron-updater - `v2.21.8`
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v2.0.0 -- 2018-04-20
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* [Snap](https://snapcraft.io/zulip) support for Linux
|
||||
|
||||
* Add an option to download the file attachments instead of opening it in the browser
|
||||
|
||||
* Open image link in webapp lightbox
|
||||
|
||||
* Add scrollbar for list of organizations on overflow
|
||||
|
||||
* Better report issue UX
|
||||
|
||||
* Add F5 shortcut for reloading the app
|
||||
|
||||
* Responsive UI for connected orgs in smaller window sizes
|
||||
|
||||
* Minor improvements
|
||||
|
||||
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
|
||||
|
||||
* Remove unused shortcuts
|
||||
|
||||
* Update toggle sidebar shortcut to CMD/CTRL+SHIFT+S
|
||||
|
||||
* Warning dialog box for Reset App Settings
|
||||
|
||||
* Fix reinstall script for working across all platforms
|
||||
|
||||
* Other minor fixes
|
||||
|
||||
|
||||
|
||||
**Updated dependencies**:
|
||||
|
||||
* Update electron-builder to `v20.8.1`
|
||||
<hr>
|
||||
|
||||
|
||||
|
||||
### v1.9.0 --2018-03-23
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Major UI changes in the add new server and setting page
|
||||
|
||||
* Some users wanted to change the look of the Zulip. Now you have the power. Feel free to add your own CSS using the all-new setting option **Add Custom CSS**
|
||||
|
||||
* Added i18n locale helper script. Internalization is coming in the next release
|
||||
|
||||
* Added **What's new** in `help` submenu so that you can see all the latest changes in the app
|
||||
|
||||
* Other small improvements
|
||||
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Add Zulip binary link to the bin. It was a regression we missed somehow. You can now start the app using `zulip` from a terminal [Linux]
|
||||
|
||||
* Fix app settings overriding issue. This bug was unnoticed from `v1.4.0`. The bug was overriding the app settings which was pretty bad. Don't worry this works as expected now
|
||||
|
||||
|
||||
|
||||
* Prevent drag and drop events to avoid few security risks
|
||||
|
||||
* Do not allow running insecure contents in the app
|
||||
|
||||
* The ugly white flickering in the webview is now fixed
|
||||
|
||||
* Fix position of left-sidebar on toggle
|
||||
|
||||
* Fix actions tooltip not visible on hover
|
||||
|
||||
* Other small fixes
|
||||
|
||||
|
||||
|
||||
|
||||
**Updated dependencies**:
|
||||
|
||||
* electron - `v1.8.4`
|
||||
|
||||
* Spectron - `v3.8.0`
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v1.8.2 --2018-02-27
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Add bots mention support to reply option
|
||||
|
||||
* Show a notification when a new update available [Linux]
|
||||
|
||||
* Add back button in left-sidebar
|
||||
|
||||
* Add a Loading indication for new server button
|
||||
|
||||
* Improve help menu
|
||||
|
||||
* Add menu-item to reveal app logs in file manager
|
||||
|
||||
* Setting page and left-sidebar UI improvements
|
||||
|
||||
* Other minor improvements
|
||||
|
||||
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Fix broken last active tab
|
||||
|
||||
* Fix Zoom In shortcut
|
||||
|
||||
* Check whether the internet is working before reloading
|
||||
|
||||
* New organization link overlapping existing servers
|
||||
|
||||
* Disable electron-connect when it's run from npm start
|
||||
|
||||
* Do not auto-reload app when the system comes back from sleep
|
||||
|
||||
* Only toggle the setting state if the element is present
|
||||
|
||||
* Other minor fixes
|
||||
|
||||
|
||||
|
||||
**Updated dependencies**:
|
||||
|
||||
* electron - `v1.8.2`
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v1.8.1 --2018-01-19
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Reply from notifications [macOS]
|
||||
|
||||
* Add a setting option to start the app in the background
|
||||
|
||||
* 32-bit Debian installer
|
||||
|
||||
* 32-bit AppImage installer
|
||||
|
||||
* Automatically add apt-repo and GPG public keys on installing Debian file [Linux]
|
||||
|
||||
* Remove app data, config files etc on uninstalling Debian installer [Linux]
|
||||
|
||||
* Add logger utility for debugging that makes easier to sharing the apps
|
||||
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Handle corrupted config files
|
||||
|
||||
* Reload full app on system hibernation
|
||||
* Load default icon if organization icon is not available
|
||||
|
||||
* Focus app when a notification is triggered
|
||||
|
||||
* Reflect changes in the preference page (#362)
|
||||
|
||||
* Set the default value of flash taskbar setting [Windows]
|
||||
|
||||
* Rename Zoom In keyboard shortcut
|
||||
|
||||
* Fix checkDomain, so it checks all error codes
|
||||
|
||||
* Disable hardware acceleration to decrease the load on GPU
|
||||
|
||||
* Minor bug fixes
|
||||
|
||||
|
||||
|
||||
**Updated dependencies**:
|
||||
|
||||
* electron - `v1.7.10`
|
||||
|
||||
* electron-builder - `v19.53.6`
|
||||
|
||||
* electron-updater - `v2.18.2`
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
|
||||
### v1.7.0 --2017-11-24
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Updated to Electron `v1.6.15`, electron-builder `v19.46.4`
|
||||
|
||||
* Improved setting page for new users
|
||||
|
||||
* On the developer side, we have added few tests to make sure app doesn't fail
|
||||
|
||||
* Minor improvements
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
### 1.6.0-beta --2017-11-16
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Added setting option to enable/disable spellcheck
|
||||
|
||||
* Added setting option to control Windows taskbar flashing [Windows]
|
||||
|
||||
* Auto-updates for Linux [AppImage]
|
||||
|
||||
* Better tray icons for retina display
|
||||
|
||||
* Reset app settings from Menu item
|
||||
|
||||
* Show detailed error message on invalid Zulip server
|
||||
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Fixed a bug which was caused by app's shortcuts. From now on our shortcuts won't hijack other apps shortcuts
|
||||
|
||||
* Removed [electron-localshortcut](https://github.com/parro-it/electron-localshortcut) completely. Now we only depend on menu accelerators for keyboard shortcuts
|
||||
|
||||
* Handle certificate issue properly
|
||||
|
||||
* Other minor fixes
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
### v1.5.0 --2017-10-11
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Added an option to clear app data
|
||||
|
||||
* Added an option to show/hide desktop notifications
|
||||
|
||||
* Redesigned setting page
|
||||
|
||||
* Red dot over dock icon for PM [macOS]
|
||||
|
||||
* Show server-info on hovering the server icon
|
||||
|
||||
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Fetch correct organization icon from server_settings API
|
||||
|
||||
* Minor improvements
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v1.4.0 --2017-09-04
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Added proxy support
|
||||
|
||||
* Added setting option for badges
|
||||
|
||||
* Start app at login setting option
|
||||
|
||||
* Added app category for Linux
|
||||
|
||||
* Minor improvements in settings page
|
||||
|
||||
* apt-repo on bintray for linux - [here](https://bintray.com/zulip/debian/zulip-elec)
|
||||
|
||||
|
||||
|
||||
**Fixes**
|
||||
|
||||
* Fixed auto-updates
|
||||
|
||||
* Better warning message on certificate error
|
||||
|
||||
* Don't allow duplicate servers
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
### 1.3.0-beta -- 2017-08-09
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Added back/forward option under `History` submenu
|
||||
|
||||
* Added taskbar overlay icon [Windows]
|
||||
|
||||
* Added `Window` submenu by which users can switch to other servers
|
||||
|
||||
* Added flashing taskbar icon on incoming message [Windows]
|
||||
|
||||
* Option to hide the left sidebar
|
||||
|
||||
* Staged rollouts
|
||||
|
||||
* Minor style improvements
|
||||
|
||||
|
||||
|
||||
|
||||
**Fixes**
|
||||
|
||||
* Handle focus event properly
|
||||
|
||||
* Open external links in default browser
|
||||
|
||||
* Allow user to change installation directory
|
||||
|
||||
* Fixed server-icon path error [Windows]
|
||||
|
||||
* Fixed server validation error
|
||||
|
||||
* Fetch server details from Zulip api
|
||||
|
||||
* Added tooltip in left sidebar
|
||||
|
||||
* Added publisher name in windows installer
|
||||
|
||||
|
||||
|
||||
**Updated dependencies**:
|
||||
|
||||
* electron - `v1.6.11`
|
||||
|
||||
* spellchecker - `v1.2.0`
|
||||
|
||||
* electron-builder - `v19.9.1`
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v1.2.0-beta -- 2017-07-12
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
|
||||
|
||||
* Added new setting page
|
||||
|
||||
* Setting for beta-updates. You can opt in for beta updates via selecting it from setting page
|
||||
|
||||
* Added Keyboard shortcuts to switch between multiple servers
|
||||
|
||||
* Desktop notifications support on Windows
|
||||
|
||||
|
||||
|
||||
**Fixes**
|
||||
|
||||
* Unregister keyboard shortcuts on windows close
|
||||
|
||||
* Show warning dialog while deleting server
|
||||
|
||||
* Allow server which is signed by root cert
|
||||
|
||||
* Other minor bug fixes
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
### v1.1.0-beta --2017-06-23
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* New network error page. App will auto-reload once the network comes back
|
||||
* Quit shortcut - CTRL+Q [Windows]
|
||||
* Improved preferences
|
||||
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Fixed power-monitor module error
|
||||
* Fixed code signing error [macOS]
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
|
||||
### v1.0.0-beta --2017-06-21
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Sign in to multiple teams
|
||||
* Windows 7 desktop notification support
|
||||
* Show badge count for each Zulip server
|
||||
* Toggle window on clicking tray icon [Windows]
|
||||
|
||||
* Windows installer is properly code signed using [DigiCert](https://www.digicert.com/) certificate. You may get a warning regarding the certificate, please make it false positive
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Close/hide app properly
|
||||
* Keep app running in background on clicking X
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v0.5.10 --2017-05-12
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Added self-signed server support
|
||||
* Unread message counts in tray icon
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v0.5.9 --2017-04-12
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
* Properly signed app for macOS
|
||||
* Toggle tray icon
|
||||
* Better error handling when no internet connection
|
||||
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Fixed permission issue on windows
|
||||
* Fixed wrong keyboard shortcuts
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v0.5.8 --2017-02-13
|
||||
|
||||
**Enhancements**:
|
||||
Smaller Windows installer size
|
||||
|
||||
<hr>
|
||||
|
||||
### v0.5.7 --2017-02-08
|
||||
|
||||
Minor improvements
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v0.5.6 --2017-02-07
|
||||
|
||||
|
||||
**Enhancement**:
|
||||
|
||||
- Using NSIS instead of [Squirrel.Windows](https://github.com/Squirrel/Squirrel.Windows) on Windows
|
||||
|
||||
- Autoupdates: now using [electron-updater](https://www.npmjs.com/package/electron-updater)
|
||||
|
||||
- Removed nuts dependency
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
- Windows installer error
|
||||
- Compile and build error in native node modules + spellchecker [Windows]
|
||||
- Missing icon on installing [Windows]
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
### v0.5.4 --2017-01-09
|
||||
|
||||
**Fixes**:
|
||||
- Fixed :
|
||||
- Auto-updates
|
||||
- Spellchecker
|
||||
- Zooming functionality
|
||||
|
||||
- Removed unused node modules
|
||||
- Using stable version for node modules
|
||||
- Added icon for AppImage
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
### v0.5.3 --2016-12-24
|
||||
**Enhancements**:
|
||||
- 🎉 Added automatic update support on Windows
|
||||
|
||||
**Fixes**:
|
||||
|
||||
- Fixed javascript error when electron has been left running and try to right-click
|
||||
|
||||
- Fixed error - Unregister Spellchecker while window is closed
|
||||
|
||||
- Fixed - Keyboard shortcuts not getting unregistered
|
||||
|
||||
- Minor UI Changes
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v0.5.2 --2016-12-13
|
||||
|
||||
**Enhancements**:
|
||||
|
||||
- ⚡️ Added automatic app updates for macOS
|
||||
- Added windows installer for 32 bit machine
|
||||
|
||||
- Better tray icons
|
||||
|
||||
**Fixes**:
|
||||
- Blurry Unity icon fixed
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v0.5.1 --2016-11-23
|
||||
|
||||
**Enhancement**:
|
||||
|
||||
- Added Spellchecker support with correct spell suggestions
|
||||
|
||||
- Added Code-Signing on MAC
|
||||
|
||||
- Added Win/linux/Mac installers
|
||||
|
||||
- Added Continuous Integration on travis and appveyor
|
||||
|
||||
- Open internal links in app only and external in default browser
|
||||
|
||||
- Better icons
|
||||
|
||||
- Electron version updated to `v1.4.7`
|
||||
|
||||
- Using two package.json structure
|
||||
|
||||
- Node integration disabled in main window due to jquery error
|
||||
|
||||
- Now using electron-builder for packaging instead of electron-packager
|
||||
|
||||
- Removed electron-context-menu and simple-spellchecker dependency
|
||||
|
||||
|
||||
|
||||
**Fixes**:
|
||||
|
||||
- Better error handling when user enters wrong zulip server
|
||||
|
||||
- Fixed close Zulip window JavaScript error
|
||||
|
||||
- Fixed opening image in default browser
|
||||
|
||||
- Fixed native notifications issue on linux
|
||||
|
||||
|
||||
<hr>
|
||||
|
||||
### v0.0.1-alpha -- 2016-08-31
|
||||
|
||||
**Enhancement**:
|
||||
|
||||
* Added DMG installer for macOS
|
190
development.md
190
development.md
@@ -1,84 +1,152 @@
|
||||
# Improve development guide
|
||||
|
||||
# Development setup
|
||||
|
||||
This is a guide to running the Zulip desktop app from a source tree,
|
||||
in order to contribute to developing it.
|
||||
This is a guide to running the Zulip desktop app from a source tree, in order to contribute to developing it. The Zulip electron development environment can be installed on **macOS, Windows, and Linux** (Debian or Ubuntu recommended). You’ll need at least **2GB of available RAM**. Installing the Zulip electron development environment requires downloading several hundred megabytes of dependencies, so you will need an **active, reasonably fast, internet connection throughout the entire installation processes.**
|
||||
|
||||
## Prerequisites
|
||||
# Set up Git & GitHub
|
||||
|
||||
To build and run the app from source, you'll need the following:
|
||||
You can skip this step if you already have Git, GitHub.
|
||||
|
||||
* [Git](http://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [Node.js](https://nodejs.org) >= v6.9.0
|
||||
* [NPM](https://www.npmjs.com/get-npm) and
|
||||
[node-gyp](https://github.com/nodejs/node-gyp#installation),
|
||||
if they don't come bundled with your Node.js installation
|
||||
* [Python](https://www.python.org/downloads/release/python-2713/)
|
||||
(v2.7.x recommended)
|
||||
* A C++ compiler compatible with C++11
|
||||
* Development headers for the libXext, libXtst, and libxkbfile libraries
|
||||
Follow our [Git Guide](https://zulip.readthedocs.io/en/latest/git/setup.html) in order to install Git, set up a GitHub account
|
||||
|
||||
### Debian/Ubuntu and friends
|
||||
|
||||
On a system running Debian, Ubuntu, or another Debian-based Linux
|
||||
distribution, you can install all dependencies through the package
|
||||
manager (see [here][nodesource-install] for more on the first command):
|
||||
# Install Prerequisites
|
||||
|
||||
```sh
|
||||
$ curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
|
||||
$ sudo apt install git nodejs python build-essential libxext-dev libxtst-dev libxkbfile-dev
|
||||
```
|
||||
Jump to:
|
||||
|
||||
[nodesource-install]: https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions
|
||||
- [MacOS](https://github.com/zulip/zulip-desktop/blob/master/development.md#macos)
|
||||
- [Ubuntu/Debian](https://github.com/zulip/zulip-desktop/blob/master/development.md#ubuntudebian)
|
||||
- [Windows](https://github.com/zulip/zulip-desktop/blob/master/development.md#windows)
|
||||
|
||||
### Other OSes
|
||||
|
||||
Other developers run the app on Windows, macOS, and possibly other OSes.
|
||||
PRs to add specific instructions to this doc are welcome!
|
||||
## MacOS
|
||||
|
||||
On Windows, your C++ compiler should be Visual Studio 2015 or later.
|
||||
|
||||
## Download, build, and run
|
||||
**Node JS**
|
||||
Go to the [Node.js Downloads page](https://nodejs.org/en/download/). Download Node.js for MacOS (`v6.9.0` or above recommended). Run the downloaded Node.js `.pkg` Installer. You're finished! To ensure Node.js has been installed, run `node -v` in your terminal - you should get something like `v6.9.0` or above
|
||||
|
||||
Clone the source locally:
|
||||
```sh
|
||||
$ git clone https://github.com/zulip/zulip-electron
|
||||
$ cd zulip-electron
|
||||
```
|
||||
|
||||
Install project dependencies:
|
||||
```sh
|
||||
$ npm install
|
||||
```
|
||||
**if** [**NPM**](https://www.npmjs.com/get-npm) **and** [**node-gyp**](https://github.com/nodejs/node-gyp#installation) **don't come bundled with your Node.js installation, Download manually**
|
||||
|
||||
Start the app:
|
||||
```sh
|
||||
$ npm start
|
||||
```
|
||||
|
||||
Start and watch changes:
|
||||
```sh
|
||||
$ npm run dev
|
||||
```
|
||||
Now you are ready for next step [: Get Zulip Desktop Code.](https://github.com/zulip/zulip-desktop/blob/master/development.md#get-zulip-desktop-code)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you have any problems running the app, see the [most common
|
||||
issues](./troubleshooting.md).
|
||||
## Ubuntu/Debian
|
||||
|
||||
## Making a release
|
||||
|
||||
To package the app into an installer:
|
||||
```
|
||||
npm run dist
|
||||
```
|
||||
If you’re in a hurry, you can copy and paste the following into your terminal
|
||||
|
||||
This command will produce distributable packages or installers for the
|
||||
operating system you're running on:
|
||||
* on Windows, a Windows installer file
|
||||
* on macOS, a `.dmg` file
|
||||
* on Linux, a plain `.zip` file as well as a `.deb` file and an
|
||||
`AppImage` file.
|
||||
To generate all three types, you will need all three operating
|
||||
systems.
|
||||
sudo apt install git nodejs node-gyp python build-essential snapcraft libxext-dev libxtst-dev lib xkbfile-dev libgconf-2-4
|
||||
|
||||
The output files appear in the `dist/` directory.
|
||||
after pasting you can jump to next step [: Get Zulip Desktop Code](https://github.com/zulip/zulip-desktop/blob/master/development.md#get-zulip-desktop-code).
|
||||
|
||||
|
||||
**For a step-by-step explanation, read on.**
|
||||
|
||||
1. **Node JS**
|
||||
|
||||
`$ sudo apt-get install nodejs`
|
||||
|
||||
2. **Install** [**Node-gyp**](https://github.com/nodejs/node-gyp#installation)
|
||||
|
||||
3. **Python (v2.7.x recommended)**
|
||||
|
||||
`$ sudo apt install python2.7`
|
||||
|
||||
4. **C++ compiler compatible with C++11**
|
||||
|
||||
`$ sudo apt install build-essential`
|
||||
|
||||
5. **Snapcraft**
|
||||
|
||||
`$ sudo apt install snapcraft`
|
||||
|
||||
6. **Development** **headers**
|
||||
|
||||
`$ sudo apt install libxext-dev libxtst-dev libxkbfile-dev libgconf-2-4`
|
||||
|
||||
|
||||
**if** [**NPM**](https://www.npmjs.com/get-npm) **don't come bundled with your Node.js installation, Download manually**
|
||||
|
||||
|
||||
Now you are ready for next step [: Get Zulip Desktop Code.](https://github.com/zulip/zulip-desktop/blob/master/development.md#get-zulip-desktop-code)
|
||||
|
||||
|
||||
## Windows
|
||||
|
||||
**Node JS**
|
||||
Go to the [Node.js Downloads page](https://nodejs.org/en/download/). Download Node.js for windows (`v6.9.0` or above recommended). Run the downloaded Node.js `.msi` Installer. You're finished! To ensure Node.js has been installed, run `node -v` in your terminal - you should get something like `v6.9.0` or above
|
||||
|
||||
|
||||
**Followings are optional yet recommended prerequisites -**
|
||||
|
||||
**Cmder**
|
||||
1. Download the [latest release](https://github.com/cmderdev/cmder/releases/)
|
||||
2. Extract the archive. *Note: This path should not be* `C:\Program Files` *or anywhere else that would require Administrator access for modifying configuration files*
|
||||
3. (optional) Place your own executable files into the `%cmder_root%\bin` folder to be injected into your PATH.
|
||||
4. Run `Cmder.exe`
|
||||
|
||||
**Chocolatey**
|
||||
You can download chocolatey from here https://chocolatey.org/ and for Installing Chocolatey on your machine follow this steps
|
||||
1. First, ensure that you are using an administrative shell.
|
||||
2. Copy the text specific to your command shell - [cmd.exe](https://chocolatey.org/install#install-with-cmdexe) or [powershell.exe](https://chocolatey.org/install#install-with-powershellexe).
|
||||
3. Paste the copied text into your shell and press Enter.
|
||||
4. Wait a few seconds for the command to complete.
|
||||
5. If you don't see any errors, you are ready to use Chocolatey! Type `choco` or `choco -?`
|
||||
|
||||
|
||||
**System specific dependencies**
|
||||
|
||||
- use only 32bit or 64bit for all of the installers, do not mix architectures
|
||||
- install using default settings
|
||||
- open Windows Powershell as Admin and paste this
|
||||
C:\Windows\system32> npm install --global --production windows-build-tools
|
||||
|
||||
|
||||
**if** [**NPM**](https://www.npmjs.com/get-npm) **and** [**node-gyp**](https://github.com/nodejs/node-gyp#installation) **don't come bundled with your Node.js installation, Download manually**
|
||||
|
||||
Now you are ready for next step [: Get Zulip Desktop Code.](https://github.com/zulip/zulip-desktop/blob/master/development.md#get-zulip-desktop-code)
|
||||
|
||||
|
||||
# Get Zulip Desktop Code
|
||||
|
||||
1. In your browser, visit https://github.com/zulip/zulip-desktop and click the `fork` button. You will need to be logged in to GitHub to do this.
|
||||
2. Open Terminal (macOS/Ubuntu) or Git BASH (Windows; must **run as an Administrator**).
|
||||
3. In Terminal/Git BASH, [clone your fork of the zulip-desktop repository](https://github.com/zulip/zulip-desktop/blob/master/development.md#clone-to-your-machine) and [connect the zulip-desktop upstream repository](https://github.com/zulip/zulip-desktop/blob/master/development.md#connect-your-fork-to-zulip-desktop-upstream)
|
||||
|
||||
|
||||
## Clone to your machine
|
||||
1. On GitHub, navigate to the main page of your fork repository.
|
||||
2. Under the repository name, click **Clone or download**.
|
||||
3. In the Clone with HTTPs section, click to copy the clone URL for the repository.
|
||||
4. Open Terminal, Change the current working directory to the location where you want the cloned directory to be made.
|
||||
|
||||
git clone https://github.com/YOURUSERNAME/zulip-desktop.git
|
||||
|
||||
Don’t forget to replace YOURUSERNAME with your git username
|
||||
|
||||
|
||||
## Connect your fork to zulip-desktop upstream
|
||||
|
||||
cd zulip-desktop
|
||||
git remote add -f upstream https://github.com/zulip/zulip-desktop.git
|
||||
|
||||
|
||||
# build and run
|
||||
|
||||
|
||||
## Install project dependencies:
|
||||
$ npm install
|
||||
|
||||
|
||||
## There two ways to start the app:
|
||||
|
||||
**vanilla method**
|
||||
|
||||
$ npm start
|
||||
|
||||
**start and watch changes recommended for dev’s**
|
||||
|
||||
$ npm run dev
|
||||
|
@@ -21,8 +21,8 @@ C:\Windows\system32> npm install --global --production windows-build-tools
|
||||
Clone the source locally:
|
||||
|
||||
```sh
|
||||
$ git clone https://github.com/zulip/zulip-electron
|
||||
$ cd zulip-electron
|
||||
$ git clone https://github.com/zulip/zulip-desktop
|
||||
$ cd zulip-desktop
|
||||
```
|
||||
|
||||
Install project dependencies:
|
||||
|
@@ -1,3 +1,3 @@
|
||||
### Want to contribute to this Wiki?
|
||||
|
||||
[Edit `/docs` files and send a pull request.](https://github.com/zulip/zulip-electron/tree/master/docs)
|
||||
[Edit `/docs` files and send a pull request.](https://github.com/zulip/zulip-desktop/tree/master/docs)
|
||||
|
21
gulpfile.js
21
gulpfile.js
@@ -1,19 +1,20 @@
|
||||
'use strict';
|
||||
const gulp = require('gulp');
|
||||
const mocha = require('gulp-mocha');
|
||||
const electron = require('electron-connect').server.create({
|
||||
verbose: true
|
||||
});
|
||||
const tape = require('gulp-tape');
|
||||
const tapColorize = require('tap-colorize');
|
||||
|
||||
gulp.task('dev', () => {
|
||||
// Start browser process
|
||||
electron.start();
|
||||
// Restart browser process
|
||||
gulp.watch('app/main/*.js', ['restart:browser']);
|
||||
gulp.watch('app/main/*.js', gulp.series('restart:browser'));
|
||||
// Reload renderer process
|
||||
gulp.watch('app/renderer/css/*.css', ['reload:renderer']);
|
||||
gulp.watch('app/renderer/*.html', ['reload:renderer']);
|
||||
gulp.watch('app/renderer/js/*.js', ['reload:renderer']);
|
||||
gulp.watch('app/renderer/css/*.css', gulp.series('reload:renderer'));
|
||||
gulp.watch('app/renderer/*.html', gulp.series('reload:renderer'));
|
||||
gulp.watch('app/renderer/js/**/*.js', gulp.series('reload:renderer'));
|
||||
});
|
||||
|
||||
gulp.task('restart:browser', done => {
|
||||
@@ -28,9 +29,11 @@ gulp.task('reload:renderer', done => {
|
||||
done();
|
||||
});
|
||||
|
||||
// Test app using mocha+spectron
|
||||
gulp.task('test', () => {
|
||||
return gulp.src('tests/index.js').pipe(mocha());
|
||||
gulp.task('test-e2e', () => {
|
||||
return gulp.src('tests/*.js')
|
||||
.pipe(tape({
|
||||
reporter: tapColorize()
|
||||
}));
|
||||
});
|
||||
|
||||
gulp.task('default', ['dev', 'test']);
|
||||
gulp.task('default', gulp.parallel('dev', 'test-e2e'));
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user