Compare commits

..

119 Commits

Author SHA1 Message Date
Akash Nimare
d4d3805be8 release: New release v5.4.2. 2020-08-11 16:09:23 +05:30
Akash Nimare
e853af40c4 electron: Update electron to v9.2.0. 2020-08-11 15:37:13 +05:30
Manav Mehta
941200cf3b changelog: Update changelog for release 5.4.1-beta. 2020-07-29 16:22:11 +05:30
Akash Nimare
cf1f659ebf release: New beta release v5.4.1-beta. 2020-07-29 13:40:59 +05:30
Akash Nimare
eb381a87bc electron-builder: Update builder to latest version. 2020-07-29 01:54:48 +05:30
Manav Mehta
68bc0ae4a0 readme: Add new screenshot URLs.
Update the screenshots to accomodate new Zulip logo and both the day and night modes
2020-07-29 01:31:57 +05:30
Manav Mehta
178bc7f401 macos: Update dock icon.
The icon in macOS was stretched to the boundaries making it larger than the other icons.
A padding of 30px on all sides makes it coherent with the others.

Fixes: #1003.
2020-07-27 01:12:27 +05:30
Anders Kaseorg
0f1245b975 Upgrade dependencies, including Electron 9.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-24 01:37:41 -07:00
Anders Kaseorg
960312a932 notification: Move loadBots call to preload, to break an import cycle.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-24 01:37:07 -07:00
Anders Kaseorg
0e00f3bbce Commit package-lock.json update missed in v5.4.0 release.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-24 00:50:45 -07:00
Anders Kaseorg
ec205f68a6 Send only needed data from tabs over IPC.
Fixes exceptions from the structured clone algorithm raised by
Electron 9.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-24 00:39:38 -07:00
Anders Kaseorg
5fe5989710 xo: Enable import/newline-after-import.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-23 23:18:25 -07:00
Anders Kaseorg
69141b5395 Remove spurious 'use-strict' [sic] directives.
The directive is 'use strict'.  It’s not necessary in TypeScript.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-23 23:09:12 -07:00
Anders Kaseorg
8d66f05924 xo: Sort imports with import/order.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-23 23:06:41 -07:00
Manav Mehta
e7330dbff8 Update changelog for v5.4.0 and the license year to 2020 (#1000)
Co-authored-by: Akash Nimare <akashnimare@users.noreply.github.com>
2020-07-21 22:05:04 +05:30
Akash Nimare
67fa9cca8c release: New release v5.4.0. 2020-07-21 18:44:45 +05:30
Manav Mehta
a90bf1af08 macOS: Change the logo to Z without the circle. 2020-07-21 17:00:07 +05:30
Manav Mehta
cb145acc73 Update icons introducing the new logo. 2020-07-19 01:56:17 +05:30
Akash Nimare
099e10673c translation: Sync strings from Transifex. 2020-07-18 21:28:59 +05:30
Anders Kaseorg
4b3608fc1e dependencies: Upgrade dependencies.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-17 18:57:33 -07:00
Manav Mehta
6128c0e12a appLanguage: add 'sk' and correct some locale names.
* sk was not in supported locales and thus not in dropdown menu
* el-GR is not the correct name and thus not applicable
* zh-Hant/Hans are exact names, though case didn't hamper the translation
2020-07-14 15:51:38 +05:30
Manav Mehta
14a1f5d3e1 request: Replace deprecated request module with net.request.
Co-authored-by: Anders Kaseorg <anders@zulip.com>

Fixes: #886.
2020-07-14 15:50:52 +05:30
Anders Kaseorg
9cf26f4890 dependencies: Upgrade dependencies.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-01 15:01:47 -07:00
Anders Kaseorg
397a7381b8 main: Use import() syntax for electron-connect.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-01 15:01:38 -07:00
Anders Kaseorg
24b28f9ded proxy-util: Fix misuse of void.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-01 15:01:29 -07:00
Anders Kaseorg
9ceabe02d5 Use optional catch binding.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-01 15:01:29 -07:00
Anders Kaseorg
b207ee57de main: Avoid object type.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-01 15:01:25 -07:00
Anders Kaseorg
cf9d0c8aa2 main: Remove mainWindowState from globals.
(No, globals are not accessible from the other process.)

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-01 14:47:09 -07:00
Manav Mehta
e97ab2e6dd Replace deprecated getWebContents API 2020-06-30 20:33:41 -07:00
Samyak Gaur
6a7f26d7e8 settings: Update font size for spellchecker explanation.
Fixes: #978.
2020-07-01 03:11:53 +05:30
Anders Kaseorg
b6e11f623a darwin-notifications: Avoid no-useless-return warnings.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-06-26 18:56:07 -07:00
Anders Kaseorg
1c60c335fd logger-util: Avoid no-fallthrough warnings.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-06-26 18:56:03 -07:00
Anders Kaseorg
c9249b1724 context-menu: Avoid no-unnecessary-boolean-literal-compare warning.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-06-26 15:56:23 -07:00
Anders Kaseorg
9e957ba704 context-menu: Copy the email address, not the link text.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-06-26 15:47:38 -07:00
Anders Kaseorg
6c37e30233 Accomodate WebSocket URLs in certificate-error handler.
Our certificates are indexed by https: URLs.  Fixes #988.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-06-26 15:24:55 -07:00
Abhigyan Khaund
addfe2e414 context-menu: Remove trailing and leading separators when not required.
Fixes: #979.
2020-06-27 02:38:03 +05:30
Abhigyan Khaund
bda0dd29df context-menu: Fix bug in Copy Link and add copy Email.
Fixes: #986
2020-06-27 02:27:44 +05:30
Abhigyan Khaund
01926e1234 context-menu: Enable copy only when copy is possible. 2020-06-27 02:27:44 +05:30
Manav Mehta
9138bbfaf2 Update changelog for release v5.3.0. 2020-06-25 00:32:37 +05:30
Akash Nimare
596561b731 release: New release v5.3.0. 2020-06-24 14:47:54 +05:30
Akash Nimare
5943c21814 Update electron to v8.3.3. 2020-06-21 01:06:05 +05:30
Akash Nimare
2456bba5ae translations: Sync translations from transifex. 2020-06-20 23:06:50 +05:30
Manav Mehta
0fff6336c7 spellchecker: Use Electron 8 built-in spellchecker.
* Using electron built-in spellchecker
* Added the custom context menu

Co-authored-by: Anders Kaseorg <anders@zulipchat.com>

Fixes: #504
2020-06-18 18:14:23 +05:30
Samyak Gaur
4261874e29 docs: Update issue template.
Explanation lines commented as the titles are self explanatory.
2020-06-17 14:50:45 +05:30
Akash Nimare
f976270d33 lint: Fix linting errors. 2020-06-17 01:44:08 +05:30
Samyak Gaur
4782f1cfd3 Docs: Update issue template.
Fixes: #926.
2020-06-17 00:20:20 +05:30
Akash Nimare
2f70621255 windows: Turn off start at login by default.
Fixes: #960.
2020-06-16 20:10:23 +05:30
Manav Mehta
b6f4e2b83c zoom: Fix zoom issues.
The zoom issues seemed to be caused due to electron roles.
We also had our own definitions firing up along with the roles which might be clashing and the other instances of roles not being much effective in our use case.

* Remove roles from zoom options
* Add accelerator for zoomIn

Fixes: #911.
2020-06-13 12:59:43 +05:30
Manav Mehta
25f4c3aba8 translations: Prevent i18n to automatically add strings.
Now that we have moved to Transifex, auto updation of translation strings is not required.
We would only need to update the resource file (en.json) and push it to Transifex and pulling the rest <lang>.json
2020-06-12 13:18:04 +05:30
Anders Kaseorg
1b23468375 Update zulipchat.com emails to zulip.com.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-06-08 14:02:08 -07:00
Anders Kaseorg
5d775405d9 Update zulipchat.com links to zulip.com.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-06-08 14:01:40 -07:00
Akash Nimare
7f13d9162a mac: Support pkg installer on macOS. 2020-06-05 23:26:10 +05:30
Anders Kaseorg
e0013c22ff package-lock.json: Update.
Unclear why npm wants to change this now.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-31 02:13:55 -07:00
Samyak Gaur
5c41afdccd window: Responsive fixes for main window.
Fixes: #963.
2020-05-27 13:23:40 +05:30
Anders Kaseorg
99a3530238 xo: Add arrow-body-style rule
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-26 11:25:24 -07:00
Manav Mehta
a5ceffc856 transifex: Setup Transifex for better translation process. 2020-05-20 22:11:13 +05:30
Manav Mehta
f55570f2f5 Handle Reset options gracefully.
Fixes: #903.
2020-05-20 21:54:36 +05:30
Manav Mehta
4f890c0316 macOS: Use electron API to get dark tray icon for the light theme. 2020-05-19 21:03:13 +05:30
Priyank Patel
bf651dece0 report-issue: Remove unnessacary instructions from the placeholder.
It looks like the removed instructions were directly copied from GitHub
issue template and did not fit here. Specifically, the first point about
including the platform is automatically included in the email we prepare for
the user and we also include browser and electron version. Then the second
bullet point re-iterates the instructions present. Lastly, the third point
asks for screenshot however user cannot add screenshot in the UI but they can
in the email.

We also remove an comment disabling a eslint rule. The rule was disable for no
reason because we should have used template literal instead of the undocumented
multi-str format which is not cleaner.
2020-05-19 11:02:39 -04:00
Priyank Patel
1babd8da42 report-issue: Darken the color of report issue button.
This is done so it compliments the newly added cancel button.
This change empahsizes the report issue button rather than the
cancel button.
2020-05-19 11:02:39 -04:00
Priyank Patel
bfd146f2d9 send-feedback: Move custom css to a css file.
It just makes it a bit cleaner and easier to update css.
During this transition we update the color hex values to rgb
because of stylelint.
2020-05-19 11:02:39 -04:00
Sanskar Bajpai
3126510245 report-issue: Add a cancel button to the modal.
A user found the lack of a cancel button in the UI a bad
experience. Previously, we would close it when the overlay
was clicked but it does not seem informative in terms of it's
function.

Fixes: #966.
2020-05-19 11:02:39 -04:00
Anders Kaseorg
d661895545 Remove the insecure ignoreCerts option.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-13 04:06:50 -07:00
Akash Nimare
ca9ab6168e Update support email. 2020-05-13 12:19:45 +05:30
Anders Kaseorg
bbdf2c6017 mailmap: Add Git .mailmap for zulipchat.com → zulip.com.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-12 22:57:22 -07:00
Manav Mehta
109a9bbd10 document: Remove inessential comment inherited from #902 and #909
The comment was created to document a piece of code in #902 but #909 addressed code quality
The comment is no longer essential and hence ought to be removed
2020-05-06 17:01:47 -07:00
Anders Kaseorg
22d7ef5615 main: Turn electron-connect off by default.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-06 16:58:25 -07:00
Anders Kaseorg
e03de26137 release: New release v5.2.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-05 12:54:35 -07:00
Anders Kaseorg
983254c310 CVE-2020-12637: Do not ignore certificate errors without ignoreCerts.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-05 12:54:35 -07:00
Anders Kaseorg
b6059077d8 Generalize permission-response mechanism to renderer-callback.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-05 12:54:35 -07:00
Anders Kaseorg
cafff9a008 new-server-form: Add missing string conversion for error message.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-05 12:54:35 -07:00
Anders Kaseorg
190204b2e5 notarize: Fix lint errors.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-05 12:54:09 -07:00
Akash Nimare
4c25c99abc notarization: Pass team short name info in code-signing. 2020-05-06 01:13:29 +05:30
Anders Kaseorg
55be93b906 dependencies: Upgrade dependencies.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-04 01:56:35 -07:00
Anders Kaseorg
34e2b3a3d0 typescript: Eradicate most any annotations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-04 01:08:05 -07:00
Anders Kaseorg
e5ece8db9e package.json: Fix ‘npm start’ to do something useful.
The old run-dev script raced electron and tsc against each other in
parallel, which usually meant electron would run stale JS files.  We
should overhaul the build process at some point, but whatever we do,
this script isn’t going to be part of it.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 20:35:21 -07:00
Anders Kaseorg
40b26dbb0e dnd-util: Refine typing.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 20:03:41 -07:00
Anders Kaseorg
ae4f03f4ba logger-util: Clean up typing disaster zone.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 20:03:41 -07:00
Anders Kaseorg
8ea32a7a96 feedback: Stub send-feedback types.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 19:33:52 -07:00
Anders Kaseorg
6b7cce0366 ServerManagerView: Convert loading from object to Set.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 19:16:05 -07:00
Anders Kaseorg
73fec72e6d registerIpcs: Convert webviewListeners to array with functions.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 19:09:22 -07:00
Anders Kaseorg
920adfb169 CI: Clean unused steps from CI configuration.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 15:02:58 -07:00
Manav Mehta
98174fdcaf help: Update Help Center URL.
Earlier the menu option used to redirect to *.zulipchat.com/help which is already reachable from the webapp.
This will enable the desktop app to redirect only to zulipchat.com/help instead of the help page of the corresponding organization.

Fixes: #948.
2020-05-02 20:17:25 +05:30
Anders Kaseorg
a0c033431e electron-bridge: Add decrypt_clipboard helper.
This one helper allows us to implement browser-based social login
entirely on the server side.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-01 15:00:02 -07:00
Abhigyan Khaund
82421d843a downloadFiles: Fix issue of showing two Save As dialog box.
Currently, there are two dialog boxes shown while downloading files (in Ubuntu). One by default behavior of electron and other by the dialog box for save as feature.
This PR fixes this issue by using electron's save as dialog box.

Fixes: #947.
2020-05-01 17:55:46 +05:30
Anders Kaseorg
d9afee3330 ServerManagerView: Convert functionalTabs from object to Map.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
a46f2ed618 xo: Enable import/no-mutable-exports.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
9f3b4ff408 xo: Remove unicorn/string-content exclusion.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
fb800f7862 xo: Enable @typescript-eslint/no-unused-vars.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
ba191c3699 xo: Enable object-curly-spacing.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
e49a880ed6 xo: Enable capitalized-comments.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
4bfa7c9265 xo: Enable no-else-return.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
39c6fa4ace xo: Enable @typescript-eslint/member-ordering.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
963c2e5388 xo: Enable @typescript-eslint/restrict-plus-operands.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
849df4adaf xo: Enable @typescript-eslint/restrict-template-expressions.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
fc6ff83485 xo: Enable no-alert.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
5ae2a717fa xo: Enable padding-line-between-statements.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
cfdc08a038 xo: Enable unicorn/catch-error-name.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
b76467529d xo: Upgrade xo to 0.30.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
bb88a7b7a8 xo: Handle floating promises.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
0225778050 typescript: Annotate queueDomain.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
2154b191c8 typescript: Annotate url in WebviewProps.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
4093304b4d typescript: Annotate electron-updater events.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
2e03f779e8 typescript: Annotate setMenu props.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Manav Mehta
9464390070 docs: Update changelog for release 5.1.0. 2020-04-30 00:48:31 +05:30
Manav Mehta
16f0af8853 Remove storing of User-Agent on disk.
Fixes: #921.

Co-authored-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-26 13:30:39 -07:00
am2505
bb6d90671f typescript: Complete most TypeScript todos. 2020-04-25 15:49:10 -07:00
Anders Kaseorg
e536a03fab dependencies: Fix package-lock.json stability.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-25 13:07:28 -07:00
Anders Kaseorg
ee60702276 release: Update package-lock.json for new release v5.1.0.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-25 13:07:24 -07:00
Akash Nimare
e721211619 release: New release v5.1.0 2020-04-25 13:53:44 +05:30
Anders Kaseorg
ff671b53ef dependencies: Upgrade dependencies.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-24 22:15:20 -07:00
Akash Nimare
340eb8da99 server-icon: Handle server icon update gracefully.
Don't trigger user-facing network error for updating the server icon.
2020-04-24 11:48:27 +05:30
Anders Kaseorg
ae689ad6bb general-section: Import supported-locales.json as a JSON module.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 13:06:00 -07:00
Anders Kaseorg
7ffddded5d Configure language by value, not by index.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 13:05:59 -07:00
Anders Kaseorg
714cd926ae generateSelectTemplate: Add missing HTML escaping.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 13:05:15 -07:00
Manav Mehta
b2f4af0f49 Add feature to set application language.
User can now select application language without changing the language in the operating system.

Fixes: #855.
2020-04-22 18:08:49 +05:30
Aditya Mudgil
d7136aef25 macOS: Update dock icon.
* Added inner padding to .icns dock icon for mac
* Change inner padding of the image to 20px for each side

Fixes: #787.
2020-04-14 20:20:48 +05:30
Anders Kaseorg
73f8b21a9f scripts: Synchronize Debian scripts with electron-builder 22.4.1.
Fixes #912.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-08 14:15:43 -07:00
Akash Nimare
4ce08cb5a2 dock: Do not toggle window on clicking dock.
We are reverting back our decision after getting a lot of feedback
on this behaviour. Fromm now on, when you click on the dock icon we'll
show the app window and won't hide it after clicking again on dock.

Fixes: #914.
2020-04-07 20:16:50 +05:30
168 changed files with 9114 additions and 7566 deletions

View File

@@ -1,8 +0,0 @@
---
<!-- Please Include: -->
- **Operating System**:
- [ ] Windows
- [ ] Linux/Ubuntu
- [ ] macOS
- **Clear steps to reproduce the issue**:
- **Relevant error messages and/or screenshots**:

25
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,25 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
<!-- Clear steps to reproduce the issue. -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Desktop (please complete the following information):**
- Operating System:
<!-- (Platform and Version) e.g. macOS 10.13.6 / Windows 10 (1803) / Ubuntu 18.04 x64 -->
- Zulip Desktop Version:
<!-- e.g. 5.2.0 -->
**Additional context**
<!-- Add any other context about the problem here. -->

6
.github/ISSUE_TEMPLATE/custom.md vendored Normal file
View File

@@ -0,0 +1,6 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
---

View File

@@ -0,0 +1,16 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Problem Description**
<!-- Please add a clear and concise description of what the problem is. -->
**Proposed Solution**
<!-- Describe the solution you'd like in a clear and concise manner -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

3
.gitignore vendored
View File

@@ -4,6 +4,9 @@
# npm cache directory # npm cache directory
.npm .npm
# transifexrc - if user prefers it to be in working tree
.transifexrc
# Compiled binary build directory # Compiled binary build directory
dist/ dist/

5
.mailmap Normal file
View File

@@ -0,0 +1,5 @@
Anders Kaseorg <anders@zulip.com> <anders@zulipchat.com>
Rishi Gupta <rishig@zulip.com> <rishig@zulipchat.com>
Rishi Gupta <rishig@zulip.com> <rishig@users.noreply.github.com>
Tim Abbott <tabbott@zulip.com> <tabbott@zulipchat.com>
Tim Abbott <tabbott@zulip.com> <tabbott@mit.edu>

View File

@@ -1,6 +1,3 @@
sudo: required
dist: trusty
os: os:
- osx - osx
- linux - linux
@@ -18,20 +15,9 @@ node_js:
- '10' - '10'
- '12' - '12'
before_install:
- ./scripts/travis-xvfb.sh
- npm install -g gulp
- npm ci
cache: cache:
directories: directories:
- node_modules - node_modules
script: script:
- npm run travis - npm run test
notifications:
webhooks:
urls:
- https://zulip.org/zulipbot/travis
on_success: always
on_failure: always

9
.tx/config Normal file
View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[zulip.desktopjson]
file_filter = app/translations/<lang>.json
minimum_perc = 0
source_file = app/translations/en.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -6,10 +6,11 @@
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"/> <img src="https://i.imgur.com/s1o6TRA.png"/>
<img src="https://i.imgur.com/vekKnW4.png"/>
# Download # Download
Please see the [installation guide](https://zulipchat.com/help/desktop-app-install-guide). Please see the [installation guide](https://zulip.com/help/desktop-app-install-guide).
# Features # Features
* Sign in to multiple organizations * Sign in to multiple organizations

View File

@@ -1,20 +1,26 @@
import { app, dialog } from 'electron'; import {app, dialog, session} from 'electron';
import { autoUpdater } from 'electron-updater'; import util from 'util';
import { linuxUpdateNotification } from './linuxupdater'; // Required only in case of linux
import log from 'electron-log';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import log from 'electron-log';
import {UpdateDownloadedEvent, UpdateInfo, autoUpdater} from 'electron-updater';
import * as ConfigUtil from '../renderer/js/utils/config-util'; import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as LinkUtil from '../renderer/js/utils/link-util'; import * as LinkUtil from '../renderer/js/utils/link-util';
export function appUpdater(updateFromMenu = false): void { import {linuxUpdateNotification} from './linuxupdater'; // Required only in case of linux
const sleep = util.promisify(setTimeout);
export async function appUpdater(updateFromMenu = false): Promise<void> {
// Don't initiate auto-updates in development // Don't initiate auto-updates in development
if (isDev) { if (isDev) {
return; return;
} }
if (process.platform === 'linux' && !process.env.APPIMAGE) { if (process.platform === 'linux' && !process.env.APPIMAGE) {
linuxUpdateNotification(); const ses = session.fromPartition('persist:webviewsession');
await linuxUpdateNotification(ses);
return; return;
} }
@@ -34,55 +40,57 @@ export function appUpdater(updateFromMenu = false): void {
autoUpdater.allowPrerelease = isBetaUpdate || false; autoUpdater.allowPrerelease = isBetaUpdate || false;
const eventsListenerRemove = ['update-available', 'update-not-available']; const eventsListenerRemove = ['update-available', 'update-not-available'];
autoUpdater.on('update-available', info => { autoUpdater.on('update-available', async (info: UpdateInfo) => {
if (updateFromMenu) { 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; updateAvailable = true;
// This is to prevent removal of 'update-downloaded' and 'error' event listener. // This is to prevent removal of 'update-downloaded' and 'error' event listener.
eventsListenerRemove.forEach(event => { eventsListenerRemove.forEach(event => {
autoUpdater.removeAllListeners(event); autoUpdater.removeAllListeners(event);
}); });
await dialog.showMessageBox({
message: `A new version ${info.version}, of Zulip Desktop is available`,
detail: 'The update will be downloaded in the background. You will be notified when it is ready to be installed.'
});
} }
}); });
autoUpdater.on('update-not-available', () => { autoUpdater.on('update-not-available', async () => {
if (updateFromMenu) { 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 // Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times. // listeners don't trigger multiple times.
autoUpdater.removeAllListeners(); autoUpdater.removeAllListeners();
await dialog.showMessageBox({
message: 'No updates available',
detail: `You are running the latest version of Zulip Desktop.\nVersion: ${app.getVersion()}`
});
} }
}); });
autoUpdater.on('error', async error => { autoUpdater.on('error', async (error: Error) => {
if (updateFromMenu) { if (updateFromMenu) {
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates'); const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates');
const {response} = await dialog.showMessageBox({ const {response} = await dialog.showMessageBox({
type: 'error', type: 'error',
buttons: ['Manual Download', 'Cancel'], buttons: ['Manual Download', 'Cancel'],
message: messageText, message: messageText,
detail: (error).toString() + `\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulipchat.com/apps/.\n detail: `Error: ${error.message}\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulip.com/apps/.\n
Current Version: ${app.getVersion()}` Current Version: ${app.getVersion()}`
}); });
if (response === 0) { if (response === 0) {
LinkUtil.openBrowser(new URL('https://zulipchat.com/apps/')); await LinkUtil.openBrowser(new URL('https://zulip.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 // Ask the user if update is available
autoUpdater.on('update-downloaded', async event => { autoUpdater.on('update-downloaded', async (event: UpdateDownloadedEvent) => {
// Ask user to update the app // Ask user to update the app
const {response} = await dialog.showMessageBox({ const {response} = await dialog.showMessageBox({
type: 'question', type: 'question',
@@ -92,13 +100,12 @@ export function appUpdater(updateFromMenu = false): void {
detail: 'It will be installed the next time you restart the application' detail: 'It will be installed the next time you restart the application'
}); });
if (response === 0) { if (response === 0) {
setTimeout(() => { await sleep(1000);
autoUpdater.quitAndInstall(); autoUpdater.quitAndInstall();
// force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app. // Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit(); app.quit();
}, 1000);
} }
}); });
// Init for updates // Init for updates
autoUpdater.checkForUpdates(); await autoUpdater.checkForUpdates();
} }

View File

@@ -1,22 +1,22 @@
import { sentryInit } from '../renderer/js/utils/sentry-util';
import { appUpdater } from './autoupdater'; import electron, {app, dialog, ipcMain, session} from 'electron';
import { setAutoLaunch } from './startup'; import fs from 'fs';
import path from 'path';
import windowStateKeeper from 'electron-window-state'; import windowStateKeeper from 'electron-window-state';
import path from 'path';
import fs from 'fs';
import electron, { app, ipcMain, session, dialog } from 'electron';
import * as AppMenu from './menu';
import * as BadgeSettings from '../renderer/js/pages/preference/badge-settings'; import * as BadgeSettings from '../renderer/js/pages/preference/badge-settings';
import * as CertificateUtil from '../renderer/js/utils/certificate-util';
import * as ConfigUtil from '../renderer/js/utils/config-util'; import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as ProxyUtil from '../renderer/js/utils/proxy-util'; import * as ProxyUtil from '../renderer/js/utils/proxy-util';
import {sentryInit} from '../renderer/js/utils/sentry-util';
interface PatchedGlobal extends NodeJS.Global { import {appUpdater} from './autoupdater';
mainWindowState: windowStateKeeper.State; import * as AppMenu from './menu';
} import {_getServerSettings, _saveServerIcon, _isOnline} from './request';
import {setAutoLaunch} from './startup';
const globalPatched = global as PatchedGlobal; let mainWindowState: windowStateKeeper.State;
// Prevent window being garbage collected // Prevent window being garbage collected
let mainWindow: Electron.BrowserWindow; let mainWindow: Electron.BrowserWindow;
@@ -42,13 +42,24 @@ if (singleInstanceLock) {
app.quit(); app.quit();
} }
const rendererCallbacks = new Map();
let nextRendererCallbackId = 0;
ipcMain.on('renderer-callback', (event: Event, rendererCallbackId: number, ...args: any[]) => {
rendererCallbacks.get(rendererCallbackId)(...args);
rendererCallbacks.delete(rendererCallbackId);
});
function makeRendererCallback(callback: (...args: any[]) => void): number {
rendererCallbacks.set(nextRendererCallbackId, callback);
return nextRendererCallbackId++;
}
const APP_ICON = path.join(__dirname, '../resources', 'Icon'); const APP_ICON = path.join(__dirname, '../resources', 'Icon');
const iconPath = (): string => { const iconPath = (): string => APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
};
// toggle the app window // Toggle the app window
const toggleApp = (): void => { const toggleApp = (): void => {
if (!mainWindow.isVisible() || mainWindow.isMinimized()) { if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
mainWindow.show(); mainWindow.show();
@@ -65,9 +76,6 @@ function createMainWindow(): Electron.BrowserWindow {
path: `${app.getPath('userData')}/config` path: `${app.getPath('userData')}/config`
}); });
// Let's keep the window position global so that we can access it in other process
globalPatched.mainWindowState = mainWindowState;
const win = new electron.BrowserWindow({ const win = new electron.BrowserWindow({
// This settings needs to be saved in config // This settings needs to be saved in config
title: 'Zulip', title: 'Zulip',
@@ -76,7 +84,7 @@ function createMainWindow(): Electron.BrowserWindow {
y: mainWindowState.y, y: mainWindowState.y,
width: mainWindowState.width, width: mainWindowState.width,
height: mainWindowState.height, height: mainWindowState.height,
minWidth: 300, minWidth: 500,
minHeight: 400, minHeight: 400,
webPreferences: { webPreferences: {
plugins: true, plugins: true,
@@ -91,13 +99,14 @@ function createMainWindow(): Electron.BrowserWindow {
win.webContents.send('focus'); win.webContents.send('focus');
}); });
win.loadURL(mainURL); (async () => win.loadURL(mainURL))();
// Keep the app running in background on close event // Keep the app running in background on close event
win.on('close', event => { win.on('close', event => {
if (ConfigUtil.getConfigItem('quitOnClose')) { if (ConfigUtil.getConfigItem('quitOnClose')) {
app.quit(); app.quit();
} }
if (!isQuitting) { if (!isQuitting) {
event.preventDefault(); event.preventDefault();
@@ -141,22 +150,23 @@ app.disableHardwareAcceleration();
// More info here - https://github.com/electron/electron/issues/10732 // More info here - https://github.com/electron/electron/issues/10732
app.commandLine.appendSwitch('force-color-profile', 'srgb'); app.commandLine.appendSwitch('force-color-profile', 'srgb');
// eslint-disable-next-line max-params // This event is only available on macOS. Triggers when you click on the dock icon.
app.on('certificate-error', (event: Event, _webContents: Electron.WebContents, _url: string, _error: string, _certificate: any, callback) => {
event.preventDefault();
callback(true);
});
app.on('activate', () => { app.on('activate', () => {
if (mainWindow) { if (mainWindow) {
// if there is already a window toggle the app // If there is already a window show it
toggleApp(); mainWindow.show();
} else { } else {
mainWindow = createMainWindow(); mainWindow = createMainWindow();
} }
}); });
app.on('ready', () => { app.on('ready', () => {
const ses = session.fromPartition('persist:webviewsession');
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);
ipcMain.on('set-spellcheck-langs', () => {
ses.setSpellCheckerLanguages(ConfigUtil.getConfigItem('spellcheckerLanguages'));
});
AppMenu.setMenu({ AppMenu.setMenu({
tabs: [] tabs: []
}); });
@@ -178,7 +188,7 @@ app.on('ready', () => {
const isSystemProxy = ConfigUtil.getConfigItem('useSystemProxy'); const isSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
if (isSystemProxy) { if (isSystemProxy) {
ProxyUtil.resolveSystemProxy(mainWindow); (async () => ProxyUtil.resolveSystemProxy(mainWindow))();
} }
const page = mainWindow.webContents; const page = mainWindow.webContents;
@@ -189,38 +199,76 @@ app.on('ready', () => {
} else { } else {
mainWindow.show(); mainWindow.show();
} }
if (!ConfigUtil.isConfigItemExists('userAgent')) {
const userAgent = session.fromPartition('webview:persistsession').getUserAgent();
ConfigUtil.setConfigItem('userAgent', userAgent);
}
}); });
page.once('did-frame-finish-load', () => { ipcMain.on('fetch-user-agent', event => {
event.returnValue = session.fromPartition('persist:webviewsession').getUserAgent();
});
ipcMain.handle('get-server-settings', async (event, domain: string) => _getServerSettings(domain, ses));
ipcMain.handle('save-server-icon', async (event, url: string) => _saveServerIcon(url, ses));
ipcMain.handle('is-online', async (event, url: string) => _isOnline(url, ses));
page.once('did-frame-finish-load', async () => {
// Initiate auto-updates on MacOS and Windows // Initiate auto-updates on MacOS and Windows
if (ConfigUtil.getConfigItem('autoUpdate')) { if (ConfigUtil.getConfigItem('autoUpdate')) {
appUpdater(); await appUpdater();
} }
}); });
const permissionCallbacks = new Map(); app.on('certificate-error', (
let nextPermissionId = 0; event: Event,
webContents: Electron.WebContents,
urlString: string,
error: string,
certificate: Electron.Certificate,
callback: (isTrusted: boolean) => void
) /* eslint-disable-line max-params */ => {
// TODO: The entire concept of selectively ignoring certificate errors
// is ill-conceived, and this handler needs to be deleted.
const url = new URL(urlString);
if (url.protocol === 'wss:') {
url.protocol = 'https:';
}
const filename = CertificateUtil.getCertificate(encodeURIComponent(url.origin));
if (filename !== undefined) {
try {
const savedCertificate = fs.readFileSync(
path.join(`${app.getPath('userData')}/certificates`, filename),
'utf8'
);
if (certificate.data.replace(/[\r\n]/g, '') ===
savedCertificate.replace(/[\r\n]/g, '')) {
event.preventDefault();
callback(true);
return;
}
} catch (error) {
console.error(`Error reading certificate file ${filename}:`, error);
}
}
dialog.showErrorBox(
'Certificate error',
`The server presented an invalid certificate for ${url.origin}:
${error}`
);
});
page.session.setPermissionRequestHandler((webContents, permission, callback, details) => { page.session.setPermissionRequestHandler((webContents, permission, callback, details) => {
const {origin} = new URL(details.requestingUrl); const {origin} = new URL(details.requestingUrl);
permissionCallbacks.set(nextPermissionId, callback); page.send('permission-request', {
page.send('permission-request', nextPermissionId, {
webContentsId: webContents.id === mainWindow.webContents.id ? webContentsId: webContents.id === mainWindow.webContents.id ?
null : null :
webContents.id, webContents.id,
origin, origin,
permission permission
}); }, makeRendererCallback(callback));
nextPermissionId++;
});
ipcMain.on('permission-response', (event: Event, permissionId: number, grant: boolean) => {
permissionCallbacks.get(permissionId)(grant);
permissionCallbacks.delete(permissionId);
}); });
// Temporarily remove this event // Temporarily remove this event
@@ -270,7 +318,7 @@ app.on('ready', () => {
}); });
ipcMain.on('clear-app-settings', () => { ipcMain.on('clear-app-settings', () => {
globalPatched.mainWindowState.unmanage(); mainWindowState.unmanage();
app.relaunch(); app.relaunch();
app.exit(); app.exit();
}); });
@@ -299,39 +347,30 @@ app.on('ready', () => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow); BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
}); });
ipcMain.on('forward-message', (_event: Electron.IpcMainEvent, listener: string, ...parameters: any[]) => { ipcMain.on('forward-message', (_event: Electron.IpcMainEvent, listener: string, ...parameters: unknown[]) => {
page.send(listener, ...parameters); page.send(listener, ...parameters);
}); });
ipcMain.on('update-menu', (_event: Electron.IpcMainEvent, props: any) => { ipcMain.on('update-menu', (_event: Electron.IpcMainEvent, props: AppMenu.MenuProps) => {
AppMenu.setMenu(props); AppMenu.setMenu(props);
const activeTab = props.tabs[props.activeTabIndex]; const activeTab = props.tabs[props.activeTabIndex];
if (activeTab) { if (activeTab) {
mainWindow.setTitle(`Zulip - ${activeTab.webview.props.name}`); mainWindow.setTitle(`Zulip - ${activeTab.webviewName}`);
} }
}); });
ipcMain.on('toggleAutoLauncher', (_event: Electron.IpcMainEvent, AutoLaunchValue: boolean) => { ipcMain.on('toggleAutoLauncher', async (_event: Electron.IpcMainEvent, AutoLaunchValue: boolean) => {
setAutoLaunch(AutoLaunchValue); await setAutoLaunch(AutoLaunchValue);
}); });
ipcMain.on('downloadFile', (_event: Electron.IpcMainEvent, url: string, downloadPath: string) => { ipcMain.on('downloadFile', (_event: Electron.IpcMainEvent, url: string, downloadPath: string) => {
page.downloadURL(url); page.downloadURL(url);
page.session.once('will-download', async (_event: Event, item) => { page.session.once('will-download', async (_event: Event, item) => {
let setFilePath: string;
let shortFileName: string;
if (ConfigUtil.getConfigItem('promptDownload', false)) { if (ConfigUtil.getConfigItem('promptDownload', false)) {
const showDialogOptions: object = { const showDialogOptions: electron.SaveDialogOptions = {
defaultPath: path.join(downloadPath, item.getFilename()) defaultPath: path.join(downloadPath, item.getFilename())
}; };
item.setSaveDialogOptions(showDialogOptions);
const result = await dialog.showSaveDialog(mainWindow, showDialogOptions);
if (result.canceled) {
item.cancel();
return;
}
setFilePath = result.filePath;
shortFileName = path.basename(setFilePath);
} else { } else {
const getTimeStamp = (): number => { const getTimeStamp = (): number => {
const date = new Date(); const date = new Date();
@@ -348,11 +387,9 @@ app.on('ready', () => {
// Update the name and path of the file if it already exists // Update the name and path of the file if it already exists
const updatedFilePath = path.join(downloadPath, formatFile(filePath)); const updatedFilePath = path.join(downloadPath, formatFile(filePath));
setFilePath = fs.existsSync(filePath) ? updatedFilePath : filePath; const setFilePath: string = fs.existsSync(filePath) ? updatedFilePath : filePath;
shortFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename();
}
item.setSavePath(setFilePath); item.setSavePath(setFilePath);
}
const updatedListener = (_event: Event, state: string): void => { const updatedListener = (_event: Event, state: string): void => {
switch (state) { switch (state) {
@@ -362,26 +399,31 @@ app.on('ready', () => {
item.cancel(); item.cancel();
break; break;
} }
case 'progressing': { case 'progressing': {
if (item.isPaused()) { if (item.isPaused()) {
item.cancel(); item.cancel();
} }
// This event can also be used to show progress in percentage in future. // This event can also be used to show progress in percentage in future.
break; break;
} }
default: { default: {
console.info('Unknown updated state of download item'); console.info('Unknown updated state of download item');
} }
} }
}; };
item.on('updated', updatedListener); item.on('updated', updatedListener);
item.once('done', (_event: Event, state) => { item.once('done', (_event: Event, state) => {
if (state === 'completed') { if (state === 'completed') {
page.send('downloadFileCompleted', item.getSavePath(), shortFileName); page.send('downloadFileCompleted', item.getSavePath(), path.basename(item.getSavePath()));
} else { } else {
console.log('Download failed state:', state); console.log('Download failed state:', state);
page.send('downloadFileFailed'); page.send('downloadFileFailed', state);
} }
// To stop item for listening to updated events of this file // To stop item for listening to updated events of this file
item.removeListener('updated', updatedListener); item.removeListener('updated', updatedListener);
}); });

View File

@@ -1,48 +1,45 @@
import { app, Notification } from 'electron'; import {app, Notification, net} from 'electron';
import request from 'request'; import getStream from 'get-stream';
import semver from 'semver'; import semver from 'semver';
import * as ConfigUtil from '../renderer/js/utils/config-util'; import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as ProxyUtil from '../renderer/js/utils/proxy-util';
import * as LinuxUpdateUtil from '../renderer/js/utils/linux-update-util'; import * as LinuxUpdateUtil from '../renderer/js/utils/linux-update-util';
import Logger from '../renderer/js/utils/logger-util'; import Logger from '../renderer/js/utils/logger-util';
import {fetchResponse} from './request';
const logger = new Logger({ const logger = new Logger({
file: 'linux-update-util.log', file: 'linux-update-util.log',
timestamp: true timestamp: true
}); });
export function linuxUpdateNotification(): void { export async function linuxUpdateNotification(session: Electron.session): Promise<void> {
let url = 'https://api.github.com/repos/zulip/zulip-desktop/releases'; let url = 'https://api.github.com/repos/zulip/zulip-desktop/releases';
url = ConfigUtil.getConfigItem('betaUpdate') ? url : url + '/latest'; url = ConfigUtil.getConfigItem('betaUpdate') ? url : url + '/latest';
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
const options = { try {
url, const response = await fetchResponse(net.request({url, session}));
headers: {'User-Agent': 'request'}, if (response.statusCode !== 200) {
proxy: proxyEnabled ? ProxyUtil.getProxy(url) : '', logger.log('Linux update response status: ', response.statusCode);
ecdhCurve: 'auto'
};
request(options, (error: any, response: any, body: any) => {
if (error) {
logger.error('Linux update error.');
logger.error(error);
return; return;
} }
if (response.statusCode < 400) {
const data = JSON.parse(body); const data = JSON.parse(await getStream(response));
const latestVersion = ConfigUtil.getConfigItem('betaUpdate') ? data[0].tag_name : data.tag_name; const latestVersion = ConfigUtil.getConfigItem('betaUpdate') ? data[0].tag_name : data.tag_name;
if (typeof latestVersion !== 'string') {
throw new TypeError('Expected string for tag_name');
}
if (semver.gt(latestVersion, app.getVersion())) { if (semver.gt(latestVersion, app.getVersion())) {
const notified = LinuxUpdateUtil.getUpdateItem(latestVersion); const notified = LinuxUpdateUtil.getUpdateItem(latestVersion);
if (notified === null) { if (notified === null) {
new Notification({title: 'Zulip Update', body: 'A new version ' + latestVersion + ' is available. Please update using your package manager.'}).show(); new Notification({title: 'Zulip Update', body: `A new version ${latestVersion} is available. Please update using your package manager.`}).show();
LinuxUpdateUtil.setUpdateItem(latestVersion, true); LinuxUpdateUtil.setUpdateItem(latestVersion, true);
} }
} }
} else { } catch (error) {
logger.log('Linux update response status: ', response.statusCode); logger.error('Linux update error.');
logger.error(error);
} }
});
} }

View File

@@ -1,28 +1,29 @@
import { app, shell, BrowserWindow, Menu, dialog } from 'electron'; import {app, shell, BrowserWindow, Menu} from 'electron';
import { appUpdater } from './autoupdater';
import AdmZip from 'adm-zip'; import AdmZip from 'adm-zip';
import fs from 'fs-extra';
import path from 'path'; import type {TabData} from '../renderer/js/main';
import * as DNDUtil from '../renderer/js/utils/dnd-util';
import Logger from '../renderer/js/utils/logger-util';
import * as ConfigUtil from '../renderer/js/utils/config-util'; import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as DNDUtil from '../renderer/js/utils/dnd-util';
import * as LinkUtil from '../renderer/js/utils/link-util'; import * as LinkUtil from '../renderer/js/utils/link-util';
import * as t from '../renderer/js/utils/translation-util'; import * as t from '../renderer/js/utils/translation-util';
const appName = app.name; import {appUpdater} from './autoupdater';
const logger = new Logger({ export interface MenuProps {
file: 'errors.log', tabs: TabData[];
timestamp: true activeTabIndex?: number;
}); enableMenu?: boolean;
}
const appName = app.name;
function getHistorySubmenu(enableMenu: boolean): Electron.MenuItemConstructorOptions[] { function getHistorySubmenu(enableMenu: boolean): Electron.MenuItemConstructorOptions[] {
return [{ return [{
label: t.__('Back'), label: t.__('Back'),
accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left', accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left',
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('back'); sendAction('back');
} }
@@ -31,7 +32,7 @@ function getHistorySubmenu(enableMenu: boolean): Electron.MenuItemConstructorOpt
label: t.__('Forward'), label: t.__('Forward'),
accelerator: process.platform === 'darwin' ? 'Command+Right' : 'Alt+Right', accelerator: process.platform === 'darwin' ? 'Command+Right' : 'Alt+Right',
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('forward'); sendAction('forward');
} }
@@ -42,26 +43,19 @@ function getHistorySubmenu(enableMenu: boolean): Electron.MenuItemConstructorOpt
function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] { function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
return [{ return [{
label: t.__('Check for Updates'), label: t.__('Check for Updates'),
click() { async click() {
checkForUpdate(); await checkForUpdate();
} }
}, },
{ {
label: t.__('Release Notes'), label: t.__('Release Notes'),
click() { async click() {
LinkUtil.openBrowser(new URL(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`)); await LinkUtil.openBrowser(new URL(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`));
} }
}, },
{ {
type: 'separator' type: 'separator'
}, },
{
label: t.__('Factory Reset'),
accelerator: process.platform === 'darwin' ? 'Command+Shift+D' : 'Ctrl+Shift+D',
click() {
resetAppSettings();
}
},
{ {
label: t.__('Download App Logs'), label: t.__('Download App Logs'),
click() { click() {
@@ -87,7 +81,7 @@ function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
{ {
label: t.__('Toggle DevTools for Zulip App'), label: t.__('Toggle DevTools for Zulip App'),
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.webContents.openDevTools({mode: 'undocked'}); focusedWindow.webContents.openDevTools({mode: 'undocked'});
} }
@@ -96,7 +90,7 @@ function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
{ {
label: t.__('Toggle DevTools for Active Tab'), label: t.__('Toggle DevTools for Active Tab'),
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U', accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('tab-devtools'); sendAction('tab-devtools');
} }
@@ -108,7 +102,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
return [{ return [{
label: t.__('Reload'), label: t.__('Reload'),
accelerator: 'CommandOrControl+R', accelerator: 'CommandOrControl+R',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('reload-current-viewer'); sendAction('reload-current-viewer');
} }
@@ -116,7 +110,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
}, { }, {
label: t.__('Hard Reload'), label: t.__('Hard Reload'),
accelerator: 'CommandOrControl+Shift+R', accelerator: 'CommandOrControl+Shift+R',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('hard-reload'); sendAction('hard-reload');
} }
@@ -128,26 +122,24 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
role: 'togglefullscreen' role: 'togglefullscreen'
}, { }, {
label: t.__('Zoom In'), label: t.__('Zoom In'),
role: 'zoomIn', accelerator: 'CommandOrControl+=',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('zoomIn'); sendAction('zoomIn');
} }
} }
}, { }, {
label: t.__('Zoom Out'), label: t.__('Zoom Out'),
role: 'zoomOut',
accelerator: 'CommandOrControl+-', accelerator: 'CommandOrControl+-',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('zoomOut'); sendAction('zoomOut');
} }
} }
}, { }, {
label: t.__('Actual Size'), label: t.__('Actual Size'),
role: 'resetZoom',
accelerator: 'CommandOrControl+0', accelerator: 'CommandOrControl+0',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('zoomActualSize'); sendAction('zoomActualSize');
} }
@@ -156,7 +148,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
type: 'separator' type: 'separator'
}, { }, {
label: t.__('Toggle Tray Icon'), label: t.__('Toggle Tray Icon'),
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
focusedWindow.webContents.send('toggletray'); focusedWindow.webContents.send('toggletray');
} }
@@ -164,7 +156,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
}, { }, {
label: t.__('Toggle Sidebar'), label: t.__('Toggle Sidebar'),
accelerator: 'CommandOrControl+Shift+S', accelerator: 'CommandOrControl+Shift+S',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem('showSidebar'); const newValue = !ConfigUtil.getConfigItem('showSidebar');
focusedWindow.webContents.send('toggle-sidebar', newValue); focusedWindow.webContents.send('toggle-sidebar', newValue);
@@ -175,7 +167,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
label: t.__('Auto hide Menu bar'), label: t.__('Auto hide Menu bar'),
checked: ConfigUtil.getConfigItem('autoHideMenubar', false), checked: ConfigUtil.getConfigItem('autoHideMenubar', false),
visible: process.platform !== 'darwin', visible: process.platform !== 'darwin',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem('autoHideMenubar'); const newValue = !ConfigUtil.getConfigItem('autoHideMenubar');
focusedWindow.autoHideMenuBar = newValue; focusedWindow.autoHideMenuBar = newValue;
@@ -196,7 +188,7 @@ function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
}, },
{ {
label: t.__('About Zulip'), label: t.__('About Zulip'),
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('open-about'); sendAction('open-about');
} }
@@ -213,7 +205,7 @@ function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
{ {
label: t.__('Report an Issue'), label: t.__('Report an Issue'),
click() { click() {
// the goal is to notify the main.html BrowserWindow // The goal is to notify the main.html BrowserWindow
// which may not be the focused window. // which may not be the focused window.
BrowserWindow.getAllWindows().forEach(window => { BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('open-feedback-modal'); window.webContents.send('open-feedback-modal');
@@ -223,7 +215,7 @@ function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
]; ];
} }
function getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boolean): Electron.MenuItemConstructorOptions[] { function getWindowSubmenu(tabs: TabData[], activeTabIndex: number): Electron.MenuItemConstructorOptions[] {
const initialSubmenu: Electron.MenuItemConstructorOptions[] = [{ const initialSubmenu: Electron.MenuItemConstructorOptions[] = [{
label: t.__('Minimize'), label: t.__('Minimize'),
role: 'minimize' role: 'minimize'
@@ -239,17 +231,17 @@ function getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boole
}); });
tabs.forEach(tab => { tabs.forEach(tab => {
// Do not add functional tab settings to list of windows in menu bar // Do not add functional tab settings to list of windows in menu bar
if (tab.props.role === 'function' && tab.props.name === 'Settings') { if (tab.role === 'function' && tab.name === 'Settings') {
return; return;
} }
initialSubmenu.push({ initialSubmenu.push({
label: tab.props.name, label: tab.name,
accelerator: tab.props.role === 'function' ? '' : `${ShortcutKey} + ${tab.props.index + 1}`, accelerator: tab.role === 'function' ? '' : `${ShortcutKey} + ${tab.index + 1}`,
checked: tab.props.index === activeTabIndex, checked: tab.index === activeTabIndex,
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('switch-server-tab', tab.props.index); sendAction('switch-server-tab', tab.index);
} }
}, },
type: 'checkbox' type: 'checkbox'
@@ -262,7 +254,7 @@ function getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boole
label: t.__('Switch to Next Organization'), label: t.__('Switch to Next Organization'),
accelerator: 'Ctrl+Tab', accelerator: 'Ctrl+Tab',
enabled: tabs.length > 1, enabled: tabs.length > 1,
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('switch-server-tab', getNextServer(tabs, activeTabIndex)); sendAction('switch-server-tab', getNextServer(tabs, activeTabIndex));
} }
@@ -271,7 +263,7 @@ function getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boole
label: t.__('Switch to Previous Organization'), label: t.__('Switch to Previous Organization'),
accelerator: 'Ctrl+Shift+Tab', accelerator: 'Ctrl+Shift+Tab',
enabled: tabs.length > 1, enabled: tabs.length > 1,
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('switch-server-tab', getPreviousServer(tabs, activeTabIndex)); sendAction('switch-server-tab', getPreviousServer(tabs, activeTabIndex));
} }
@@ -282,7 +274,7 @@ function getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boole
return initialSubmenu; return initialSubmenu;
} }
function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] { function getDarwinTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu} = props; const {tabs, activeTabIndex, enableMenu} = props;
return [{ return [{
@@ -290,7 +282,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
submenu: [{ submenu: [{
label: t.__('Add Organization'), label: t.__('Add Organization'),
accelerator: 'Cmd+Shift+N', accelerator: 'Cmd+Shift+N',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('new-server'); sendAction('new-server');
} }
@@ -305,7 +297,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
}, { }, {
label: t.__('Desktop Settings'), label: t.__('Desktop Settings'),
accelerator: 'Cmd+,', accelerator: 'Cmd+,',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('open-settings'); sendAction('open-settings');
} }
@@ -314,7 +306,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Keyboard Shortcuts'), label: t.__('Keyboard Shortcuts'),
accelerator: 'Cmd+Shift+K', accelerator: 'Cmd+Shift+K',
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('shortcut'); sendAction('shortcut');
} }
@@ -325,7 +317,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Copy Zulip URL'), label: t.__('Copy Zulip URL'),
accelerator: 'Cmd+Shift+C', accelerator: 'Cmd+Shift+C',
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('copy-zulip-url'); sendAction('copy-zulip-url');
} }
@@ -334,7 +326,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Log Out of Organization'), label: t.__('Log Out of Organization'),
accelerator: 'Cmd+L', accelerator: 'Cmd+L',
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('log-out'); sendAction('log-out');
} }
@@ -373,7 +365,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
submenu: [{ submenu: [{
label: t.__('Undo'), label: t.__('Undo'),
accelerator: 'Cmd+Z', accelerator: 'Cmd+Z',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('undo'); sendAction('undo');
} }
@@ -381,7 +373,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
}, { }, {
label: t.__('Redo'), label: t.__('Redo'),
accelerator: 'Cmd+Shift+Z', accelerator: 'Cmd+Shift+Z',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('redo'); sendAction('redo');
} }
@@ -412,7 +404,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
submenu: getHistorySubmenu(enableMenu) submenu: getHistorySubmenu(enableMenu)
}, { }, {
label: t.__('Window'), label: t.__('Window'),
submenu: getWindowSubmenu(tabs, activeTabIndex, enableMenu) submenu: getWindowSubmenu(tabs, activeTabIndex)
}, { }, {
label: t.__('Tools'), label: t.__('Tools'),
submenu: getToolsSubmenu() submenu: getToolsSubmenu()
@@ -423,14 +415,14 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
}]; }];
} }
function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] { function getOtherTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu} = props; const {tabs, activeTabIndex, enableMenu} = props;
return [{ return [{
label: t.__('File'), label: t.__('File'),
submenu: [{ submenu: [{
label: t.__('Add Organization'), label: t.__('Add Organization'),
accelerator: 'Ctrl+Shift+N', accelerator: 'Ctrl+Shift+N',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('new-server'); sendAction('new-server');
} }
@@ -447,7 +439,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
}, { }, {
label: t.__('Desktop Settings'), label: t.__('Desktop Settings'),
accelerator: 'Ctrl+,', accelerator: 'Ctrl+,',
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('open-settings'); sendAction('open-settings');
} }
@@ -456,7 +448,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Keyboard Shortcuts'), label: t.__('Keyboard Shortcuts'),
accelerator: 'Ctrl+Shift+K', accelerator: 'Ctrl+Shift+K',
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('shortcut'); sendAction('shortcut');
} }
@@ -467,7 +459,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Copy Zulip URL'), label: t.__('Copy Zulip URL'),
accelerator: 'Ctrl+Shift+C', accelerator: 'Ctrl+Shift+C',
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('copy-zulip-url'); sendAction('copy-zulip-url');
} }
@@ -476,7 +468,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Log Out of Organization'), label: t.__('Log Out of Organization'),
accelerator: 'Ctrl+L', accelerator: 'Ctrl+L',
enabled: enableMenu, enabled: enableMenu,
click(_item: any, focusedWindow: any) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('log-out'); sendAction('log-out');
} }
@@ -530,7 +522,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
submenu: getHistorySubmenu(enableMenu) submenu: getHistorySubmenu(enableMenu)
}, { }, {
label: t.__('Window'), label: t.__('Window'),
submenu: getWindowSubmenu(tabs, activeTabIndex, enableMenu) submenu: getWindowSubmenu(tabs, activeTabIndex)
}, { }, {
label: t.__('Tools'), label: t.__('Tools'),
submenu: getToolsSubmenu() submenu: getToolsSubmenu()
@@ -541,7 +533,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
}]; }];
} }
function sendAction(action: string, ...parameters: any[]): void { function sendAction(action: string, ...parameters: unknown[]): void {
const win = BrowserWindow.getAllWindows()[0]; const win = BrowserWindow.getAllWindows()[0];
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
@@ -551,57 +543,29 @@ function sendAction(action: string, ...parameters: any[]): void {
win.webContents.send(action, ...parameters); win.webContents.send(action, ...parameters);
} }
function checkForUpdate(): void { async function checkForUpdate(): Promise<void> {
appUpdater(true); await appUpdater(true);
} }
function getNextServer(tabs: any[], activeTabIndex: number): number { function getNextServer(tabs: TabData[], activeTabIndex: number): number {
do { do {
activeTabIndex = (activeTabIndex + 1) % tabs.length; activeTabIndex = (activeTabIndex + 1) % tabs.length;
} }
while (tabs[activeTabIndex].props.role !== 'server'); while (tabs[activeTabIndex].role !== 'server');
return activeTabIndex; return activeTabIndex;
} }
function getPreviousServer(tabs: any[], activeTabIndex: number): number { function getPreviousServer(tabs: TabData[], activeTabIndex: number): number {
do { do {
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length; activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
} }
while (tabs[activeTabIndex].props.role !== 'server'); while (tabs[activeTabIndex].role !== 'server');
return activeTabIndex; return activeTabIndex;
} }
async function resetAppSettings(): Promise<void> { export function setMenu(props: MenuProps): void {
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'];
const { response } = await dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
message: 'Are you sure?',
detail: resetAppSettingsMessage
});
if (response === 0) {
settingFiles.forEach(settingFileName => {
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
fs.access(getSettingFilesPath, (error: any) => {
if (error) {
logger.error('Error while resetting app settings.');
logger.error(error);
} else {
fs.unlink(getSettingFilesPath, () => {
sendAction('clear-app-data');
});
}
});
});
}
}
export function setMenu(props: any): void {
const tpl = process.platform === 'darwin' ? getDarwinTpl(props) : getOtherTpl(props); const tpl = process.platform === 'darwin' ? getDarwinTpl(props) : getOtherTpl(props);
const menu = Menu.buildFromTemplate(tpl); const menu = Menu.buildFromTemplate(tpl);
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);

113
app/main/request.ts Normal file
View File

@@ -0,0 +1,113 @@
import {ClientRequest, IncomingMessage, app, net} from 'electron';
import fs from 'fs';
import path from 'path';
import stream from 'stream';
import util from 'util';
import escape from 'escape-html';
import getStream from 'get-stream';
import {ServerConf} from '../renderer/js/utils/domain-util';
import Logger from '../renderer/js/utils/logger-util';
import * as Messages from '../resources/messages';
export async function fetchResponse(request: ClientRequest): Promise<IncomingMessage> {
return new Promise((resolve, reject) => {
request.on('response', resolve);
request.on('abort', () => reject(new Error('Request aborted')));
request.on('error', reject);
request.end();
});
}
const pipeline = util.promisify(stream.pipeline);
/* Request: domain-util */
const defaultIconUrl = '../renderer/img/icon.png';
const logger = new Logger({
file: 'domain-util.log',
timestamp: true
});
const generateFilePath = (url: string): string => {
const dir = `${app.getPath('userData')}/server-icons`;
const extension = path.extname(url).split('?')[0];
let hash = 5381;
let {length} = url;
while (length) {
hash = (hash * 33) ^ url.charCodeAt(--length);
}
// Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return `${dir}/${hash >>> 0}${extension}`;
};
export const _getServerSettings = async (domain: string, session: Electron.session): Promise<ServerConf> => {
const response = await fetchResponse(net.request({
url: domain + '/api/v1/server_settings',
session
}));
if (response.statusCode !== 200) {
throw new Error(Messages.invalidZulipServerError(domain));
}
const {realm_name, realm_uri, realm_icon} = JSON.parse(await getStream(response));
if (
typeof realm_name !== 'string' ||
typeof realm_uri !== 'string' ||
typeof realm_icon !== 'string'
) {
throw new TypeError(Messages.noOrgsError(domain));
}
return {
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases
icon: realm_icon.startsWith('/') ? realm_uri + realm_icon : realm_icon,
url: realm_uri,
alias: escape(realm_name)
};
};
export const _saveServerIcon = async (url: string, session: Electron.session): Promise<string> => {
try {
const response = await fetchResponse(net.request({url, session}));
if (response.statusCode !== 200) {
logger.log('Could not get server icon.');
return defaultIconUrl;
}
const filePath = generateFilePath(url);
await pipeline(response, fs.createWriteStream(filePath));
return filePath;
} catch (error) {
logger.log('Could not get server icon.');
logger.log(error);
logger.reportSentry(error);
return defaultIconUrl;
}
};
/* Request: reconnect-util */
export const _isOnline = async (url: string, session: Electron.session): Promise<boolean> => {
try {
const response = await fetchResponse(net.request({
url: `${url}/static/favicon.ico`,
session
}));
const isValidResponse = response.statusCode >= 200 && response.statusCode < 400;
return isValidResponse;
} catch (error) {
logger.log(error);
return false;
}
};

View File

@@ -2,9 +2,10 @@ import { app } from 'electron';
import AutoLaunch from 'auto-launch'; import AutoLaunch from 'auto-launch';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import * as ConfigUtil from '../renderer/js/utils/config-util'; import * as ConfigUtil from '../renderer/js/utils/config-util';
export const setAutoLaunch = (AutoLaunchValue: boolean): void => { export const setAutoLaunch = async (AutoLaunchValue: boolean): Promise<void> => {
// Don't run this in development // Don't run this in development
if (isDev) { if (isDev) {
return; return;
@@ -12,16 +13,16 @@ export const setAutoLaunch = (AutoLaunchValue: boolean): void => {
const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue); const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue);
// setLoginItemSettings doesn't support linux // `setLoginItemSettings` doesn't support linux
if (process.platform === 'linux') { if (process.platform === 'linux') {
const ZulipAutoLauncher = new AutoLaunch({ const ZulipAutoLauncher = new AutoLaunch({
name: 'Zulip', name: 'Zulip',
isHidden: false isHidden: false
}); });
if (autoLaunchOption) { if (autoLaunchOption) {
ZulipAutoLauncher.enable(); await ZulipAutoLauncher.enable();
} else { } else {
ZulipAutoLauncher.disable(); await ZulipAutoLauncher.disable();
} }
} else { } else {
app.setLoginItemSettings({ app.setLoginItemSettings({

View File

@@ -14,7 +14,7 @@
<div class="maintenance-info"> <div class="maintenance-info">
<p class="detail maintainer"> <p class="detail maintainer">
Maintained by Maintained by
<a href="https://zulipchat.com" target="_blank" rel="noopener noreferrer">Zulip</a> <a href="https://zulip.com" target="_blank" rel="noopener noreferrer">Zulip</a>
</p> </p>
<p class="detail license"> <p class="detail license">
Available under the Available under the

View File

@@ -0,0 +1,19 @@
:host {
--button-color: rgb(69, 166, 149);
}
button:hover,
button:focus {
border-color: var(--button-color);
color: var(--button-color);
}
button:active {
background-color: rgb(241, 241, 241);
color: var(--button-color);
}
button {
background-color: var(--button-color);
border-color: var(--button-color);
}

View File

@@ -389,6 +389,10 @@ img.server-info-icon {
margin: 6px; margin: 6px;
} }
#note {
font-size: 10px;
}
.code { .code {
font-family: Courier New, Courier, monospace; font-family: Courier New, Courier, monospace;
} }
@@ -663,6 +667,26 @@ i.open-network-button {
} }
/* responsive grid */ /* responsive grid */
@media (min-width: 500px) and (max-width: 720px) {
#new-server-container {
padding-left: 0px;
width: 60vw;
padding-right: 4vh;
}
.page-title {
width: 60vw;
}
}
@media (max-width: 500px) {
#new-server-container {
padding-left: 0px;
width: 54%;
}
.page-title {
width: 54%;
}
}
@media (max-width: 650px) { @media (max-width: 650px) {
.selected-css-path, .selected-css-path,
@@ -695,9 +719,6 @@ i.open-network-button {
margin-right: 6px; margin-right: 6px;
width: 43%; width: 43%;
} }
#new-server-container {
padding-left: 0px;
}
} }
@media (max-width: 600px) { @media (max-width: 600px) {
@@ -724,3 +745,13 @@ i.open-network-button {
margin-top: 10px; margin-top: 10px;
} }
} }
.lang-menu {
font-size: 13px;
font-weight: bold;
background: rgba(78, 191, 172, 1.000);
width: 100px;
height: 38px;
color: rgba(255, 255, 255, 1.000);
border-color: rgba(0, 0, 0, 0);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,82 @@
import crypto from 'crypto';
import {clipboard} from 'electron';
// This helper is exposed via electron_bridge for use in the social
// login flow.
//
// It consists of a key and a promised token. The in-app page sends
// the key to the server, and opens the users browser to a page where
// they can log in and get a token encrypted to that key. When the
// user copies the encrypted token from their browser to the
// clipboard, we decrypt it and resolve the promise. The in-app page
// then uses the decrypted token to log the user in within the app.
//
// The encryption is authenticated (AES-GCM) to guarantee that we
// dont leak anything from the users clipboard other than the token
// intended for us.
export class ClipboardDecrypterImpl implements ClipboardDecrypter {
version: number;
key: Uint8Array;
pasted: Promise<string>;
constructor(_: number) {
// At this time, the only version is 1.
this.version = 1;
this.key = crypto.randomBytes(32);
this.pasted = new Promise(resolve => {
let interval: NodeJS.Timeout | null = null;
const startPolling = () => {
if (interval === null) {
interval = setInterval(poll, 1000);
}
poll();
};
const stopPolling = () => {
if (interval !== null) {
clearInterval(interval);
interval = null;
}
};
const poll = () => {
let plaintext;
try {
const data = Buffer.from(clipboard.readText(), 'hex');
const iv = data.slice(0, 12);
const ciphertext = data.slice(12, -16);
const authTag = data.slice(-16);
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
this.key,
iv,
{authTagLength: 16}
);
decipher.setAuthTag(authTag);
plaintext =
decipher.update(ciphertext, undefined, 'utf8') +
decipher.final('utf8');
} catch {
// If the parsing or decryption failed in any way,
// the correct token hasnt been copied yet; try
// again next time.
return;
}
window.removeEventListener('focus', startPolling);
window.removeEventListener('blur', stopPolling);
stopPolling();
resolve(plaintext);
};
window.addEventListener('focus', startPolling);
window.addEventListener('blur', stopPolling);
if (document.hasFocus()) {
startPolling();
}
});
}
}

View File

@@ -0,0 +1,115 @@
import {remote, ContextMenuParams} from 'electron';
import * as t from '../utils/translation-util';
const {clipboard, Menu} = remote;
export const contextMenu = (webContents: Electron.WebContents, event: Event, props: ContextMenuParams) => {
const isText = props.selectionText !== '';
const isLink = props.linkURL !== '';
const linkURL = isLink ? new URL(props.linkURL) : undefined;
const isEmailAddress = isLink ? linkURL.protocol === 'mailto:' : undefined;
const makeSuggestion = (suggestion: string) => ({
label: suggestion,
visible: true,
async click() {
await webContents.insertText(suggestion);
}
});
let menuTemplate: Electron.MenuItemConstructorOptions[] = [{
label: t.__('Add to Dictionary'),
visible: props.isEditable && isText && props.misspelledWord.length !== 0,
click(_item) {
webContents.session.addWordToSpellCheckerDictionary(props.misspelledWord);
}
}, {
type: 'separator',
visible: props.isEditable && isText && props.misspelledWord.length !== 0
}, {
label: `${t.__('Look Up')} "${props.selectionText}"`,
visible: process.platform === 'darwin' && isText,
click(_item) {
webContents.showDefinitionForSelection();
}
}, {
type: 'separator',
visible: process.platform === 'darwin' && isText
}, {
label: t.__('Cut'),
visible: isText,
enabled: props.isEditable,
accelerator: 'CommandOrControl+X',
click(_item) {
webContents.cut();
}
}, {
label: t.__('Copy'),
accelerator: 'CommandOrControl+C',
enabled: props.editFlags.canCopy,
click(_item) {
webContents.copy();
}
}, {
label: t.__('Paste'), // Bug: Paste replaces text
accelerator: 'CommandOrControl+V',
enabled: props.isEditable,
click() {
webContents.paste();
}
}, {
type: 'separator'
}, {
label: isEmailAddress ? t.__('Copy Email Address') : t.__('Copy Link'),
visible: isLink,
click(_item) {
clipboard.write({
bookmark: props.linkText,
text: isEmailAddress ? linkURL.pathname : props.linkURL
});
}
}, {
label: t.__('Copy Image'),
visible: props.mediaType === 'image',
click(_item) {
webContents.copyImageAt(props.x, props.y);
}
}, {
label: t.__('Copy Image URL'),
visible: props.mediaType === 'image',
click(_item) {
clipboard.write({
bookmark: props.srcURL,
text: props.srcURL
});
}
}, {
type: 'separator',
visible: isLink || props.mediaType === 'image'
}, {
label: t.__('Services'),
visible: process.platform === 'darwin',
role: 'services'
}];
if (props.misspelledWord) {
if (props.dictionarySuggestions.length > 0) {
const suggestions: Electron.MenuItemConstructorOptions[] = props.dictionarySuggestions.map((suggestion: string) => makeSuggestion(suggestion));
menuTemplate = suggestions.concat(menuTemplate);
} else {
menuTemplate.unshift({
label: t.__('No Suggestion Found'),
enabled: false
});
}
}
// Hide the invisible separators on Linux and Windows
// Electron has a bug which ignores visible: false parameter for separator menuitems. So we remove them here.
// https://github.com/electron/electron/issues/5869
// https://github.com/electron/electron/issues/6906
const filteredMenuTemplate = menuTemplate.filter(menuItem => menuItem.visible ?? true);
const menu = Menu.buildFromTemplate(filteredMenuTemplate);
menu.popup();
};

View File

@@ -1,7 +1,13 @@
import Tab from './tab'; import Tab, {TabProps} from './tab';
export default class FunctionalTab extends Tab { export default class FunctionalTab extends Tab {
$closeButton: Element; $closeButton: Element;
constructor(props: TabProps) {
super(props);
this.init();
}
template(): string { template(): string {
return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}"> return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tab-badge close-button"> <div class="server-tab-badge close-button">
@@ -13,12 +19,6 @@ export default class FunctionalTab extends Tab {
</div>`; </div>`;
} }
// TODO: Typescript - This type for props should be TabProps
constructor(props: any) {
super(props);
this.init();
}
init(): void { init(): void {
this.$el = this.generateNodeFromTemplate(this.template()); this.$el = this.generateNodeFromTemplate(this.template());
if (this.props.name !== 'Settings') { if (this.props.name !== 'Settings') {

View File

@@ -1,7 +1,8 @@
import {ipcRenderer, remote} from 'electron'; import {ipcRenderer, remote} from 'electron';
import * as LinkUtil from '../utils/link-util';
import * as ConfigUtil from '../utils/config-util'; import * as ConfigUtil from '../utils/config-util';
import * as LinkUtil from '../utils/link-util';
import type WebView from './webview'; import type WebView from './webview';
const {shell, app} = remote; const {shell, app} = remote;
@@ -16,29 +17,31 @@ export default function handleExternalLink(this: WebView, event: Electron.NewWin
if (LinkUtil.isUploadsUrl(this.props.url, url)) { if (LinkUtil.isUploadsUrl(this.props.url, url)) {
ipcRenderer.send('downloadFile', url.href, downloadPath); ipcRenderer.send('downloadFile', url.href, downloadPath);
ipcRenderer.once('downloadFileCompleted', (_event: Event, filePath: string, fileName: string) => { ipcRenderer.once('downloadFileCompleted', async (_event: Event, filePath: string, fileName: string) => {
const downloadNotification = new Notification('Download Complete', { const downloadNotification = new Notification('Download Complete', {
body: `Click to show ${fileName} in folder`, body: `Click to show ${fileName} in folder`,
silent: true // We'll play our own sound - ding.ogg silent: true // We'll play our own sound - ding.ogg
}); });
// Play sound to indicate download complete
if (!ConfigUtil.getConfigItem('silent')) {
dingSound.play();
}
downloadNotification.addEventListener('click', () => { downloadNotification.addEventListener('click', () => {
// Reveal file in download folder // Reveal file in download folder
shell.showItemInFolder(filePath); shell.showItemInFolder(filePath);
}); });
ipcRenderer.removeAllListeners('downloadFileFailed'); ipcRenderer.removeAllListeners('downloadFileFailed');
// Play sound to indicate download complete
if (!ConfigUtil.getConfigItem('silent')) {
await dingSound.play();
}
}); });
ipcRenderer.once('downloadFileFailed', () => { ipcRenderer.once('downloadFileFailed', (_event: Event, state: string) => {
// Automatic download failed, so show save dialog prompt and download // Automatic download failed, so show save dialog prompt and download
// through webview // through webview
// Only do this if it is the automatic download, otherwise show an error (so we aren't showing two save // Only do this if it is the automatic download, otherwise show an error (so we aren't showing two save
// prompts right after each other) // prompts right after each other)
// Check that the download is not cancelled by user
if (state !== 'cancelled') {
if (ConfigUtil.getConfigItem('promptDownload', false)) { if (ConfigUtil.getConfigItem('promptDownload', false)) {
// We need to create a "new Notification" to display it, but just `Notification(...)` on its own // We need to create a "new Notification" to display it, but just `Notification(...)` on its own
// doesn't work // doesn't work
@@ -48,9 +51,11 @@ export default function handleExternalLink(this: WebView, event: Electron.NewWin
} else { } else {
this.$el.downloadURL(url.href); this.$el.downloadURL(url.href);
} }
}
ipcRenderer.removeAllListeners('downloadFileCompleted'); ipcRenderer.removeAllListeners('downloadFileCompleted');
}); });
} else { } else {
LinkUtil.openBrowser(url); (async () => LinkUtil.openBrowser(url))();
} }
} }

View File

@@ -1,11 +1,17 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import Tab from './tab';
import * as SystemUtil from '../utils/system-util'; import * as SystemUtil from '../utils/system-util';
import Tab, {TabProps} from './tab';
export default class ServerTab extends Tab { export default class ServerTab extends Tab {
$badge: Element; $badge: Element;
constructor(props: TabProps) {
super(props);
this.init();
}
template(): string { template(): string {
return `<div class="tab" data-tab-id="${this.props.tabIndex}"> return `<div class="tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tooltip" style="display:none">${this.props.name}</div> <div class="server-tooltip" style="display:none">${this.props.name}</div>
@@ -17,12 +23,6 @@ export default class ServerTab extends Tab {
</div>`; </div>`;
} }
// TODO: Typescript - This type for props should be TabProps
constructor(props: any) {
super(props);
this.init();
}
init(): void { init(): void {
this.$el = this.generateNodeFromTemplate(this.template()); this.$el = this.generateNodeFromTemplate(this.template());
this.props.$root.append(this.$el); this.props.$root.append(this.$el);

View File

@@ -1,9 +1,19 @@
import WebView from './webview';
import BaseComponent from './base'; import BaseComponent from './base';
import WebView from './webview';
// TODO: TypeScript - Type annotate props export interface TabProps {
interface TabProps { role: string;
[key: string]: any; icon?: string;
name: string;
$root: Element;
onClick: () => void;
index: number;
tabIndex: number;
onHover?: () => void;
onHoverOut?: () => void;
webview: WebView;
materialIcon?: string;
onDestroy?: () => void;
} }
export default class Tab extends BaseComponent { export default class Tab extends BaseComponent {
@@ -38,7 +48,7 @@ export default class Tab extends BaseComponent {
} }
destroy(): void { destroy(): void {
this.$el.parentNode.removeChild(this.$el); this.$el.remove();
this.webview.$el.parentNode.removeChild(this.webview.$el); this.webview.$el.remove();
} }
} }

View File

@@ -1,23 +1,36 @@
import {ipcRenderer, remote} from 'electron'; import {ipcRenderer, remote} from 'electron';
import path from 'path';
import fs from 'fs'; import fs from 'fs';
import path from 'path';
import * as ConfigUtil from '../utils/config-util'; import * as ConfigUtil from '../utils/config-util';
import * as SystemUtil from '../utils/system-util'; import * as SystemUtil from '../utils/system-util';
import BaseComponent from './base'; import BaseComponent from './base';
import {contextMenu} from './context-menu';
import handleExternalLink from './handle-external-link'; import handleExternalLink from './handle-external-link';
const {app, dialog} = remote; const {app, dialog} = remote;
const shouldSilentWebview = ConfigUtil.getConfigItem('silent'); const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
// TODO: TypeScript - Type annotate WebViewProps.
interface WebViewProps { interface WebViewProps {
[key: string]: any; $root: Element;
index: number;
tabIndex: number;
url: string;
role: string;
name: string;
isActive: () => boolean;
switchLoading: (loading: boolean, url: string) => void;
onNetworkError: (index: number) => void;
nodeIntegration: boolean;
preload: boolean;
onTitleChange: () => void;
hasPermission?: (origin: string, permission: string) => boolean;
} }
export default class WebView extends BaseComponent { export default class WebView extends BaseComponent {
props: any; props: WebViewProps;
zoomFactor: number; zoomFactor: number;
badgeCount: number; badgeCount: number;
loading: boolean; loading: boolean;
@@ -26,10 +39,6 @@ export default class WebView extends BaseComponent {
$el: Electron.WebviewTag; $el: Electron.WebviewTag;
domReady?: Promise<void>; domReady?: Promise<void>;
// This is required because in main.js we access WebView.method as
// webview[method].
[key: string]: any;
constructor(props: WebViewProps) { constructor(props: WebViewProps) {
super(); super();
@@ -50,7 +59,11 @@ export default class WebView extends BaseComponent {
${this.props.preload ? 'preload="js/preload.js"' : ''} ${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession" partition="persist:webviewsession"
name="${this.props.name}" name="${this.props.name}"
webpreferences="${this.props.nodeIntegration ? '' : 'contextIsolation, '}javascript=yes"> webpreferences="
${this.props.nodeIntegration ? '' : 'contextIsolation,'}
${ConfigUtil.getConfigItem('enableSpellchecker') ? 'spellcheck,' : ''}
javascript
">
</webview>`; </webview>`;
} }
@@ -86,6 +99,7 @@ export default class WebView extends BaseComponent {
if (isSettingPage) { if (isSettingPage) {
return; return;
} }
this.canGoBackButton(); this.canGoBackButton();
}); });
@@ -101,7 +115,7 @@ export default class WebView extends BaseComponent {
if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') { if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') {
// This api is only supported on macOS // This api is only supported on macOS
app.dock.setBadge('●'); app.dock.setBadge('●');
// bounce the dock // Bounce the dock
if (ConfigUtil.getConfigItem('dockBouncing')) { if (ConfigUtil.getConfigItem('dockBouncing')) {
app.dock.bounce(); app.dock.bounce();
} }
@@ -109,9 +123,15 @@ export default class WebView extends BaseComponent {
}); });
this.$el.addEventListener('dom-ready', () => { this.$el.addEventListener('dom-ready', () => {
const webContents = remote.webContents.fromId(this.$el.getWebContentsId());
webContents.addListener('context-menu', (event, menuParameters) => {
contextMenu(webContents, event, menuParameters);
});
if (this.props.role === 'server') { if (this.props.role === 'server') {
this.$el.classList.add('onload'); this.$el.classList.add('onload');
} }
this.loading = false; this.loading = false;
this.props.switchLoading(false, this.props.url); this.props.switchLoading(false, this.props.url);
this.show(); this.show();
@@ -138,12 +158,6 @@ export default class WebView extends BaseComponent {
if (!isSettingPage) { if (!isSettingPage) {
this.props.switchLoading(true, this.props.url); this.props.switchLoading(true, this.props.url);
} }
let userAgent = SystemUtil.getUserAgent();
if (!userAgent) {
SystemUtil.setUserAgent(this.$el.getUserAgent());
userAgent = SystemUtil.getUserAgent();
}
this.$el.setUserAgent(userAgent);
}); });
this.$el.addEventListener('did-stop-loading', () => { this.$el.addEventListener('did-stop-loading', () => {
@@ -183,9 +197,9 @@ export default class WebView extends BaseComponent {
this.focus(); this.focus();
this.props.onTitleChange(); this.props.onTitleChange();
// Injecting preload css in webview to override some css rules // Injecting preload css in webview to override some css rules
this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8')); (async () => this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8')))();
// get customCSS again from config util to avoid warning user again // Get customCSS again from config util to avoid warning user again
this.customCSS = ConfigUtil.getConfigItem('customCSS'); this.customCSS = ConfigUtil.getConfigItem('customCSS');
if (this.customCSS) { if (this.customCSS) {
if (!fs.existsSync(this.customCSS)) { if (!fs.existsSync(this.customCSS)) {
@@ -197,13 +211,13 @@ export default class WebView extends BaseComponent {
return; return;
} }
this.$el.insertCSS(fs.readFileSync(path.resolve(__dirname, this.customCSS), 'utf8')); (async () => this.$el.insertCSS(fs.readFileSync(path.resolve(__dirname, this.customCSS), 'utf8')))();
} }
} }
focus(): void { focus(): void {
// focus Webview and it's contents when Window regain focus. // Focus Webview and it's contents when Window regain focus.
const webContents = this.$el.getWebContents(); const webContents = remote.webContents.fromId(this.$el.getWebContentsId());
// HACK: webContents.isFocused() seems to be true even without the element // HACK: webContents.isFocused() seems to be true even without the element
// being in focus. So, we check against `document.activeElement`. // being in focus. So, we check against `document.activeElement`.
if (webContents && this.$el !== document.activeElement) { if (webContents && this.$el !== document.activeElement) {
@@ -291,8 +305,8 @@ export default class WebView extends BaseComponent {
this.init(); this.init();
} }
async send(channel: string, ...parameters: any[]): Promise<void> { async send(channel: string, ...parameters: unknown[]): Promise<void> {
await this.domReady; await this.domReady;
this.$el.send(channel, ...parameters); await this.$el.send(channel, ...parameters);
} }
} }

View File

@@ -1,12 +1,12 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import {ClipboardDecrypterImpl} from './clipboard-decrypter';
import {NotificationData, newNotification} from './notification'; import {NotificationData, newNotification} from './notification';
type ListenerType = ((...args: any[]) => void); type ListenerType = ((...args: any[]) => void);
class ElectronBridge extends EventEmitter { class ElectronBridgeImpl extends EventEmitter implements ElectronBridge {
send_notification_reply_message_supported: boolean; send_notification_reply_message_supported: boolean;
idle_on_system: boolean; idle_on_system: boolean;
last_active_on_system: number; last_active_on_system: number;
@@ -21,7 +21,7 @@ class ElectronBridge extends EventEmitter {
this.last_active_on_system = Date.now(); this.last_active_on_system = Date.now();
} }
send_event = (eventName: string | symbol, ...args: any[]): void => { send_event = (eventName: string | symbol, ...args: unknown[]): void => {
this.emit(eventName, ...args); this.emit(eventName, ...args);
}; };
@@ -46,9 +46,12 @@ class ElectronBridge extends EventEmitter {
set_send_notification_reply_message_supported = (value: boolean): void => { set_send_notification_reply_message_supported = (value: boolean): void => {
this.send_notification_reply_message_supported = value; this.send_notification_reply_message_supported = value;
}; };
decrypt_clipboard = (version: number): ClipboardDecrypterImpl =>
new ClipboardDecrypterImpl(version);
} }
const electron_bridge = new ElectronBridge(); const electron_bridge = new ElectronBridgeImpl();
electron_bridge.on('total_unread_count', (...args) => { electron_bridge.on('total_unread_count', (...args) => {
ipcRenderer.send('unread-count', ...args); ipcRenderer.send('unread-count', ...args);
@@ -59,13 +62,17 @@ electron_bridge.on('realm_name', realmName => {
ipcRenderer.send('realm-name-changed', serverURL, realmName); ipcRenderer.send('realm-name-changed', serverURL, realmName);
}); });
electron_bridge.on('realm_icon_url', iconURL => { electron_bridge.on('realm_icon_url', (iconURL: unknown) => {
if (typeof iconURL !== 'string') {
throw new TypeError('Expected string for iconURL');
}
const serverURL = location.origin; const serverURL = location.origin;
iconURL = iconURL.includes('http') ? iconURL : `${serverURL}${iconURL}`; iconURL = iconURL.includes('http') ? iconURL : `${serverURL}${iconURL}`;
ipcRenderer.send('realm-icon-changed', serverURL, iconURL); ipcRenderer.send('realm-icon-changed', serverURL, iconURL);
}); });
// this follows node's idiomatic implementation of event // This follows node's idiomatic implementation of event
// emitters to make event handling more simpler instead of using // emitters to make event handling more simpler instead of using
// functions zulip side will emit event using ElectronBrigde.send_event // 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 // which is alias of .emit and on this side we can handle the data by adding

View File

@@ -1,68 +1,34 @@
import {remote} from 'electron'; import {remote} from 'electron';
import SendFeedback from '@electron-elements/send-feedback';
import path from 'path';
import fs from 'fs'; import fs from 'fs';
import path from 'path';
import SendFeedback from '@electron-elements/send-feedback';
const {app} = remote; const {app} = remote;
interface SendFeedback extends HTMLElement {
[key: string]: any;
}
type SendFeedbackType = SendFeedback;
// 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); customElements.define('send-feedback', SendFeedback);
export const sendFeedback: SendFeedbackType = document.querySelector('send-feedback'); export const sendFeedback: SendFeedback = document.querySelector('send-feedback');
export const feedbackHolder = sendFeedback.parentElement; export const feedbackHolder = sendFeedback.parentElement;
/* eslint-disable no-multi-str */ // Make the button color match zulip app's theme
sendFeedback.customStylesheet = 'css/feedback.css';
// customize the fields of custom elements // Customize the fields of custom elements
sendFeedback.title = 'Report Issue'; sendFeedback.title = 'Report Issue';
sendFeedback.titleLabel = 'Issue title:'; sendFeedback.titleLabel = 'Issue title:';
sendFeedback.titlePlaceholder = 'Enter issue title'; sendFeedback.titlePlaceholder = 'Enter issue title';
sendFeedback.textareaLabel = 'Describe the issue:'; sendFeedback.textareaLabel = 'Describe the issue:';
sendFeedback.textareaPlaceholder = 'Succinctly describe your issue and steps to reproduce it...\n\n\ sendFeedback.textareaPlaceholder = 'Succinctly describe your issue and steps to reproduce it...';
---\n\
<!-- Please Include: -->\n\
- **Operating System**:\n\
- [ ] Windows\n\
- [ ] Linux/Ubuntu\n\
- [ ] macOS\n\
- **Clear steps to reproduce the issue**:\n\
- **Relevant error messages and/or screenshots**:\n\
';
/* eslint-enable no-multi-str */
sendFeedback.buttonLabel = 'Report Issue'; sendFeedback.buttonLabel = 'Report Issue';
sendFeedback.loaderSuccessText = ''; sendFeedback.loaderSuccessText = '';
sendFeedback.useReporter('emailReporter', { sendFeedback.useReporter('emailReporter', {
email: 'akash@zulipchat.com' email: 'support@zulip.com'
}); });
feedbackHolder.addEventListener('click', (event: Event) => { feedbackHolder.addEventListener('click', (event: Event) => {
// only remove the class if the grey out faded // Only remove the class if the grey out faded
// part is clicked and not the feedback element itself // part is clicked and not the feedback element itself
if (event.target === event.currentTarget) { if (event.target === event.currentTarget) {
feedbackHolder.classList.remove('show'); feedbackHolder.classList.remove('show');
@@ -75,6 +41,10 @@ sendFeedback.addEventListener('feedback-submitted', () => {
}, 1000); }, 1000);
}); });
sendFeedback.addEventListener('feedback-cancelled', () => {
feedbackHolder.classList.remove('show');
});
const dataDir = app.getPath('userData'); const dataDir = app.getPath('userData');
const logsDir = path.join(dataDir, '/Logs'); const logsDir = path.join(dataDir, '/Logs');
sendFeedback.logs.push(...fs.readdirSync(logsDir).map(file => path.join(logsDir, file))); sendFeedback.logs.push(...fs.readdirSync(logsDir).map(file => path.join(logsDir, file)));

View File

@@ -1,25 +1,36 @@
'use strict'; 'use strict';
interface CompatElectronBridge extends ElectronBridge {
readonly idle_on_system: boolean;
readonly last_active_on_system: number;
send_notification_reply_message_supported: boolean;
}
(() => { (() => {
const zulipWindow = window as typeof window & { const zulipWindow = window as typeof window & {
electron_bridge: any; electron_bridge: CompatElectronBridge;
narrow: any; narrow: {
page_params: any; by_subject?: (target_id: number, opts: {trigger?: string}) => void;
raw_electron_bridge: any; by_topic?: (target_id: number, opts: {trigger?: string}) => void;
};
page_params?: {
default_language?: string;
};
raw_electron_bridge: ElectronBridge;
}; };
const electron_bridge = { const electron_bridge: CompatElectronBridge = {
...zulipWindow.raw_electron_bridge, ...zulipWindow.raw_electron_bridge,
get idle_on_system() { get idle_on_system(): boolean {
return this.get_idle_on_system(); return this.get_idle_on_system();
}, },
get last_active_on_system() { get last_active_on_system(): number {
return this.get_last_active_on_system(); return this.get_last_active_on_system();
}, },
get send_notification_reply_message_supported() { get send_notification_reply_message_supported(): boolean {
return this.get_send_notification_reply_message_supported(); return this.get_send_notification_reply_message_supported();
}, },
@@ -41,13 +52,11 @@
const {page_params} = zulipWindow; const {page_params} = zulipWindow;
if (page_params) { if (page_params) {
electron_bridge.send_event('zulip-loaded', { electron_bridge.send_event('zulip-loaded');
serverLanguage: page_params.default_language
});
} }
})(); })();
electron_bridge.on_event('narrow-by-topic', (id: string) => { electron_bridge.on_event('narrow-by-topic', (id: number) => {
const {narrow} = zulipWindow; const {narrow} = zulipWindow;
const narrowByTopic = narrow.by_topic || narrow.by_subject; const narrowByTopic = narrow.by_topic || narrow.by_subject;
narrowByTopic(id, {trigger: 'notification'}); narrowByTopic(id, {trigger: 'notification'});
@@ -73,6 +82,7 @@
if (!(symbol in this)) { if (!(symbol in this)) {
this.addEventListener(type, listener); this.addEventListener(type, listener);
} }
(this as any)[symbol] = value; (this as any)[symbol] = value;
} else if (symbol in this) { } else if (symbol in this) {
this.removeEventListener(type, listener); this.removeEventListener(type, listener);
@@ -107,6 +117,7 @@
if (callback) { if (callback) {
callback(await Promise.resolve(NativeNotification.permission)); callback(await Promise.resolve(NativeNotification.permission));
} }
return NativeNotification.permission; return NativeNotification.permission;
} }
} }

View File

@@ -1,26 +1,29 @@
import {ipcRenderer, remote, clipboard} from 'electron'; import {ipcRenderer, remote, clipboard} from 'electron';
import { feedbackHolder } from './feedback';
import path from 'path'; import path from 'path';
import escape from 'escape-html';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
const { session, app, Menu, dialog } = remote; import escape from 'escape-html';
import * as Messages from '../../resources/messages';
import FunctionalTab from './components/functional-tab';
import ServerTab from './components/server-tab';
import WebView from './components/webview';
import {feedbackHolder} from './feedback';
import * as CommonUtil from './utils/common-util';
import * as ConfigUtil from './utils/config-util';
import * as DNDUtil from './utils/dnd-util';
import type {DNDSettings} from './utils/dnd-util';
import * as DomainUtil from './utils/domain-util';
import * as EnterpriseUtil from './utils/enterprise-util';
import * as LinkUtil from './utils/link-util';
import Logger from './utils/logger-util';
import ReconnectUtil from './utils/reconnect-util';
// eslint-disable-next-line import/no-unassigned-import // eslint-disable-next-line import/no-unassigned-import
import './tray'; import './tray';
import * as DomainUtil from './utils/domain-util'; const {session, app, Menu, dialog} = remote;
import WebView from './components/webview';
import ServerTab from './components/server-tab';
import FunctionalTab from './components/functional-tab';
import * as ConfigUtil from './utils/config-util';
import * as DNDUtil from './utils/dnd-util';
import ReconnectUtil from './utils/reconnect-util';
import Logger from './utils/logger-util';
import * as CommonUtil from './utils/common-util';
import * as EnterpriseUtil from './utils/enterprise-util';
import * as LinkUtil from './utils/link-util';
import * as Messages from '../../resources/messages';
interface FunctionalTabProps { interface FunctionalTabProps {
name: string; name: string;
@@ -28,11 +31,7 @@ interface FunctionalTabProps {
url: string; url: string;
} }
interface AnyObject { interface SettingsOptions extends DNDSettings {
[key: string]: any;
}
interface SettingsOptions {
autoHideMenubar: boolean; autoHideMenubar: boolean;
trayIcon: boolean; trayIcon: boolean;
useManualProxy: boolean; useManualProxy: boolean;
@@ -42,25 +41,18 @@ interface SettingsOptions {
startAtLogin: boolean; startAtLogin: boolean;
startMinimized: boolean; startMinimized: boolean;
enableSpellchecker: boolean; enableSpellchecker: boolean;
showNotification: boolean;
autoUpdate: boolean; autoUpdate: boolean;
betaUpdate: boolean; betaUpdate: boolean;
errorReporting: boolean; errorReporting: boolean;
customCSS: boolean; customCSS: boolean;
silent: boolean;
lastActiveTab: number; lastActiveTab: number;
dnd: boolean; dnd: boolean;
dndPreviousSettings: { dndPreviousSettings: DNDSettings;
showNotification: boolean;
silent: boolean;
flashTaskbarOnMessage?: boolean;
};
downloadsPath: string; downloadsPath: string;
quitOnClose: boolean; quitOnClose: boolean;
promptDownload: boolean; promptDownload: boolean;
flashTaskbarOnMessage?: boolean;
dockBouncing?: boolean; dockBouncing?: boolean;
loading?: AnyObject; spellcheckerLanguages?: string[];
} }
const logger = new Logger({ const logger = new Logger({
@@ -71,6 +63,13 @@ const logger = new Logger({
const rendererDirectory = path.resolve(__dirname, '..'); const rendererDirectory = path.resolve(__dirname, '..');
type ServerOrFunctionalTab = ServerTab | FunctionalTab; type ServerOrFunctionalTab = ServerTab | FunctionalTab;
export interface TabData {
role: string;
name: string;
index: number;
webviewName: string;
}
class ServerManagerView { class ServerManagerView {
$addServerButton: HTMLButtonElement; $addServerButton: HTMLButtonElement;
$tabsContainer: Element; $tabsContainer: Element;
@@ -90,10 +89,10 @@ class ServerManagerView {
$sidebar: Element; $sidebar: Element;
$fullscreenPopup: Element; $fullscreenPopup: Element;
$fullscreenEscapeKey: string; $fullscreenEscapeKey: string;
loading: AnyObject; loading: Set<string>;
activeTabIndex: number; activeTabIndex: number;
tabs: ServerOrFunctionalTab[]; tabs: ServerOrFunctionalTab[];
functionalTabs: AnyObject; functionalTabs: Map<string, number>;
tabIndex: number; tabIndex: number;
presetOrgs: string[]; presetOrgs: string[];
constructor() { constructor() {
@@ -128,11 +127,11 @@ class ServerManagerView {
this.$fullscreenEscapeKey = process.platform === 'darwin' ? '^⌘F' : 'F11'; this.$fullscreenEscapeKey = process.platform === 'darwin' ? '^⌘F' : 'F11';
this.$fullscreenPopup.innerHTML = `Press ${this.$fullscreenEscapeKey} to exit full screen`; this.$fullscreenPopup.innerHTML = `Press ${this.$fullscreenEscapeKey} to exit full screen`;
this.loading = {}; this.loading = new Set();
this.activeTabIndex = -1; this.activeTabIndex = -1;
this.tabs = []; this.tabs = [];
this.presetOrgs = []; this.presetOrgs = [];
this.functionalTabs = {}; this.functionalTabs = new Map();
this.tabIndex = 0; this.tabIndex = 0;
} }
@@ -140,12 +139,15 @@ class ServerManagerView {
await this.loadProxy(); await this.loadProxy();
this.initDefaultSettings(); this.initDefaultSettings();
this.initSidebar(); this.initSidebar();
if (EnterpriseUtil.configFile) { this.removeUAfromDisk();
this.initPresetOrgs(); if (EnterpriseUtil.hasConfigFile()) {
await this.initPresetOrgs();
} }
this.initTabs();
await this.initTabs();
this.initActions(); this.initActions();
this.registerIpcs(); this.registerIpcs();
ipcRenderer.send('set-spellcheck-langs');
} }
async loadProxy(): Promise<void> { async loadProxy(): Promise<void> {
@@ -156,6 +158,7 @@ class ServerManagerView {
if (proxyEnableOldState) { if (proxyEnableOldState) {
ConfigUtil.setConfigItem('useManualProxy', true); ConfigUtil.setConfigItem('useManualProxy', true);
} }
ConfigUtil.removeConfigItem('useProxy'); ConfigUtil.removeConfigItem('useProxy');
} }
@@ -187,7 +190,7 @@ class ServerManagerView {
useSystemProxy: false, useSystemProxy: false,
showSidebar: true, showSidebar: true,
badgeOption: true, badgeOption: true,
startAtLogin: true, startAtLogin: false,
startMinimized: false, startMinimized: false,
enableSpellchecker: true, enableSpellchecker: true,
showNotification: true, showNotification: true,
@@ -222,10 +225,11 @@ class ServerManagerView {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
settingOptions.autoHideMenubar = false; settingOptions.autoHideMenubar = false;
settingOptions.spellcheckerLanguages = ['en-US'];
} }
for (const [setting, value] of Object.entries(settingOptions)) { for (const [setting, value] of Object.entries(settingOptions)) {
// give preference to defaults defined in global_config.json // Give preference to defaults defined in global_config.json
if (EnterpriseUtil.configItemExists(setting)) { if (EnterpriseUtil.configItemExists(setting)) {
ConfigUtil.setConfigItem(setting, EnterpriseUtil.getConfigItem(setting), true); ConfigUtil.setConfigItem(setting, EnterpriseUtil.getConfigItem(setting), true);
} else if (ConfigUtil.getConfigItem(setting) === null) { } else if (ConfigUtil.getConfigItem(setting) === null) {
@@ -239,39 +243,47 @@ class ServerManagerView {
this.toggleSidebar(showSidebar); this.toggleSidebar(showSidebar);
} }
async queueDomain(domain: any): Promise<boolean> { // Remove the stale UA string from the disk if the app is not freshly
// allows us to start adding multiple domains to the app simultaneously // installed. This should be removed in a further release.
removeUAfromDisk(): void {
ConfigUtil.removeConfigItem('userAgent');
}
async queueDomain(domain: string): Promise<boolean> {
// Allows us to start adding multiple domains to the app simultaneously
// promise of addition resolves in both cases, but we consider it rejected // promise of addition resolves in both cases, but we consider it rejected
// if the resolved value is false // if the resolved value is false
try { try {
const serverConf = await DomainUtil.checkDomain(domain); const serverConf = await DomainUtil.checkDomain(domain);
await DomainUtil.addDomain(serverConf); await DomainUtil.addDomain(serverConf);
return true; return true;
} catch (err) { } catch (error) {
logger.error(err); logger.error(error);
logger.error('Could not add ' + domain + '. Please contact your system administrator.'); logger.error(`Could not add ${domain}. Please contact your system administrator.`);
return false; return false;
} }
} }
async initPresetOrgs(): Promise<void> { async initPresetOrgs(): Promise<void> {
// read preset organizations from global_config.json and queues them // Read preset organizations from global_config.json and queues them
// for addition to the app's domains // for addition to the app's domains
const preAddedDomains = DomainUtil.getDomains(); const preAddedDomains = DomainUtil.getDomains();
this.presetOrgs = EnterpriseUtil.getConfigItem('presetOrganizations', []); this.presetOrgs = EnterpriseUtil.getConfigItem('presetOrganizations', []);
// set to true if at least one new domain is added // Set to true if at least one new domain is added
const domainPromises = []; const domainPromises = [];
for (const url of this.presetOrgs) { for (const url of this.presetOrgs) {
if (DomainUtil.duplicateDomain(url)) { if (DomainUtil.duplicateDomain(url)) {
continue; continue;
} }
domainPromises.push(this.queueDomain(url)); domainPromises.push(this.queueDomain(url));
} }
const domainsAdded = await Promise.all(domainPromises); const domainsAdded = await Promise.all(domainPromises);
if (domainsAdded.includes(true)) { if (domainsAdded.includes(true)) {
// at least one domain was resolved // At least one domain was resolved
if (preAddedDomains.length > 0) { if (preAddedDomains.length > 0) {
// user already has servers added // User already has servers added
// ask them before reloading the app // ask them before reloading the app
const {response} = await dialog.showMessageBox({ const {response} = await dialog.showMessageBox({
type: 'question', type: 'question',
@@ -286,57 +298,62 @@ class ServerManagerView {
ipcRenderer.send('reload-full-app'); ipcRenderer.send('reload-full-app');
} }
} else if (domainsAdded.length > 0) { } else if (domainsAdded.length > 0) {
// find all orgs that failed // Find all orgs that failed
const failedDomains: string[] = []; const failedDomains: string[] = [];
for (const org of this.presetOrgs) { for (const org of this.presetOrgs) {
if (DomainUtil.duplicateDomain(org)) { if (DomainUtil.duplicateDomain(org)) {
continue; continue;
} }
failedDomains.push(org); failedDomains.push(org);
} }
const {title, content} = Messages.enterpriseOrgError(domainsAdded.length, failedDomains); const {title, content} = Messages.enterpriseOrgError(domainsAdded.length, failedDomains);
dialog.showErrorBox(title, content); dialog.showErrorBox(title, content);
if (DomainUtil.getDomains().length === 0) { if (DomainUtil.getDomains().length === 0) {
// no orgs present, stop showing loading gif // No orgs present, stop showing loading gif
await this.openSettings('AddServer'); await this.openSettings('AddServer');
} }
} }
} }
initTabs(): void { async initTabs(): Promise<void> {
const servers = DomainUtil.getDomains(); const servers = DomainUtil.getDomains();
if (servers.length > 0) { if (servers.length > 0) {
for (const [i, server] of servers.entries()) { for (const [i, server] of servers.entries()) {
this.initServer(server, i); this.initServer(server, i);
} }
// Open last active tab // Open last active tab
let lastActiveTab = ConfigUtil.getConfigItem('lastActiveTab'); let lastActiveTab = ConfigUtil.getConfigItem('lastActiveTab');
if (lastActiveTab >= servers.length) { if (lastActiveTab >= servers.length) {
lastActiveTab = 0; lastActiveTab = 0;
} }
// checkDomain() and webview.load() for lastActiveTab before the others
DomainUtil.updateSavedServer(servers[lastActiveTab].url, lastActiveTab); // `checkDomain()` and `webview.load()` for lastActiveTab before the others
await DomainUtil.updateSavedServer(servers[lastActiveTab].url, lastActiveTab);
this.activateTab(lastActiveTab); this.activateTab(lastActiveTab);
for (const [i, server] of servers.entries()) { await Promise.all(servers.map(async (server, i) => {
// after the lastActiveTab is activated, we load the others in the background // After the lastActiveTab is activated, we load the others in the background
// without activating them, to prevent flashing of server icons // without activating them, to prevent flashing of server icons
if (i === lastActiveTab) { if (i === lastActiveTab) {
continue; return;
} }
DomainUtil.updateSavedServer(server.url, i);
await DomainUtil.updateSavedServer(server.url, i);
this.tabs[i].webview.load(); this.tabs[i].webview.load();
} }));
// Remove focus from the settings icon at sidebar bottom // Remove focus from the settings icon at sidebar bottom
this.$settingsButton.classList.remove('active'); this.$settingsButton.classList.remove('active');
} else if (this.presetOrgs.length === 0) { } else if (this.presetOrgs.length === 0) {
// not attempting to add organisations in the background // Not attempting to add organisations in the background
this.openSettings('AddServer'); await this.openSettings('AddServer');
} else { } else {
this.showLoading(true); this.showLoading(true);
} }
} }
initServer(server: any, index: number): void { initServer(server: DomainUtil.ServerConf, index: number): void {
const tabIndex = this.getTabIndex(); const tabIndex = this.getTabIndex();
this.tabs.push(new ServerTab({ this.tabs.push(new ServerTab({
role: 'server', role: 'server',
@@ -357,16 +374,15 @@ class ServerManagerView {
name: CommonUtil.decodeString(server.alias), name: CommonUtil.decodeString(server.alias),
hasPermission: (origin: string, permission: string) => hasPermission: (origin: string, permission: string) =>
origin === server.url && permission === 'notifications', origin === server.url && permission === 'notifications',
isActive: () => { isActive: () => index === this.activeTabIndex,
return index === this.activeTabIndex;
},
switchLoading: (loading: boolean, url: string) => { switchLoading: (loading: boolean, url: string) => {
if (!loading && this.loading[url]) { if (loading) {
this.loading[url] = false; this.loading.add(url);
} else if (loading && !this.loading[url]) { } else {
this.loading[url] = true; this.loading.delete(url);
} }
this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]);
this.showLoading(this.loading.has(this.tabs[this.activeTabIndex].webview.props.url));
}, },
onNetworkError: (index: number) => this.openNetworkTroubleshooting(index), onNetworkError: (index: number) => this.openNetworkTroubleshooting(index),
onTitleChange: this.updateBadge.bind(this), onTitleChange: this.updateBadge.bind(this),
@@ -374,7 +390,7 @@ class ServerManagerView {
preload: true preload: true
}) })
})); }));
this.loading[server.url] = true; this.loading.add(server.url);
} }
initActions(): void { initActions(): void {
@@ -390,6 +406,7 @@ class ServerManagerView {
if ($serverImg.src.includes('img/icon.png')) { if ($serverImg.src.includes('img/icon.png')) {
this.displayInitialCharLogo($serverImg, index); this.displayInitialCharLogo($serverImg, index);
} }
$serverImg.addEventListener('error', () => { $serverImg.addEventListener('error', () => {
this.displayInitialCharLogo($serverImg, index); this.displayInitialCharLogo($serverImg, index);
}); });
@@ -404,11 +421,11 @@ class ServerManagerView {
this.$reloadButton.addEventListener('click', () => { this.$reloadButton.addEventListener('click', () => {
this.tabs[this.activeTabIndex].webview.reload(); this.tabs[this.activeTabIndex].webview.reload();
}); });
this.$addServerButton.addEventListener('click', () => { this.$addServerButton.addEventListener('click', async () => {
this.openSettings('AddServer'); await this.openSettings('AddServer');
}); });
this.$settingsButton.addEventListener('click', () => { this.$settingsButton.addEventListener('click', async () => {
this.openSettings('General'); await this.openSettings('General');
}); });
this.$backButton.addEventListener('click', () => { this.$backButton.addEventListener('click', () => {
this.tabs[this.activeTabIndex].webview.back(); this.tabs[this.activeTabIndex].webview.back();
@@ -438,10 +455,9 @@ class ServerManagerView {
} }
displayInitialCharLogo($img: HTMLImageElement, index: number): void { displayInitialCharLogo($img: HTMLImageElement, index: number): void {
/* // The index parameter is needed because webview[data-tab-id] can
index parameter needed because webview[data-tab-id] can increment // increment beyond the size of the sidebar org array and throw an
beyond size of sidebar org array and throw error // error
*/
const $altIcon = document.createElement('div'); const $altIcon = document.createElement('div');
const $parent = $img.parentElement; const $parent = $img.parentElement;
@@ -474,7 +490,7 @@ class ServerManagerView {
// This needs to handled only for the add server tooltip and not others. // This needs to handled only for the add server tooltip and not others.
if (addServer) { if (addServer) {
const {top} = SidebarButton.getBoundingClientRect(); const {top} = SidebarButton.getBoundingClientRect();
SidebarTooltip.style.top = top + 'px'; SidebarTooltip.style.top = `${top}px`;
} }
}); });
SidebarButton.addEventListener('mouseout', () => { SidebarButton.addEventListener('mouseout', () => {
@@ -483,14 +499,14 @@ class ServerManagerView {
} }
onHover(index: number): void { onHover(index: number): void {
// this.$serverIconTooltip[index].innerHTML already has realm name, so we are just // `this.$serverIconTooltip[index].innerHTML` already has realm name, so we are just
// removing the style. // removing the style.
this.$serverIconTooltip[index].removeAttribute('style'); this.$serverIconTooltip[index].removeAttribute('style');
// To handle position of servers' tooltip due to scrolling of list of organizations // 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 // This could not be handled using CSS, hence the top of the tooltip is made same
// as that of its parent element. // as that of its parent element.
const {top} = this.$serverIconTooltip[index].parentElement.getBoundingClientRect(); const {top} = this.$serverIconTooltip[index].parentElement.getBoundingClientRect();
this.$serverIconTooltip[index].style.top = top + 'px'; this.$serverIconTooltip[index].style.top = `${top}px`;
} }
onHoverOut(index: number): void { onHoverOut(index: number): void {
@@ -498,12 +514,12 @@ class ServerManagerView {
} }
openFunctionalTab(tabProps: FunctionalTabProps): void { openFunctionalTab(tabProps: FunctionalTabProps): void {
if (this.functionalTabs[tabProps.name] !== undefined) { if (this.functionalTabs.has(tabProps.name)) {
this.activateTab(this.functionalTabs[tabProps.name]); this.activateTab(this.functionalTabs.get(tabProps.name));
return; return;
} }
this.functionalTabs[tabProps.name] = this.tabs.length; this.functionalTabs.set(tabProps.name, this.tabs.length);
const tabIndex = this.getTabIndex(); const tabIndex = this.getTabIndex();
@@ -512,27 +528,26 @@ class ServerManagerView {
materialIcon: tabProps.materialIcon, materialIcon: tabProps.materialIcon,
name: tabProps.name, name: tabProps.name,
$root: this.$tabsContainer, $root: this.$tabsContainer,
index: this.functionalTabs[tabProps.name], index: this.functionalTabs.get(tabProps.name),
tabIndex, tabIndex,
onClick: this.activateTab.bind(this, this.functionalTabs[tabProps.name]), onClick: this.activateTab.bind(this, this.functionalTabs.get(tabProps.name)),
onDestroy: this.destroyTab.bind(this, tabProps.name, this.functionalTabs[tabProps.name]), onDestroy: this.destroyTab.bind(this, tabProps.name, this.functionalTabs.get(tabProps.name)),
webview: new WebView({ webview: new WebView({
$root: this.$webviewsContainer, $root: this.$webviewsContainer,
index: this.functionalTabs[tabProps.name], index: this.functionalTabs.get(tabProps.name),
tabIndex, tabIndex,
url: tabProps.url, url: tabProps.url,
role: 'function', role: 'function',
name: tabProps.name, name: tabProps.name,
isActive: () => { isActive: () => this.functionalTabs.get(tabProps.name) === this.activeTabIndex,
return this.functionalTabs[tabProps.name] === this.activeTabIndex; switchLoading: (loading: boolean, url: string) => {
}, if (loading) {
switchLoading: (loading: AnyObject, url: string) => { this.loading.add(url);
if (!loading && this.loading[url]) { } else {
this.loading[url] = false; this.loading.delete(url);
} else if (loading && !this.loading[url]) {
this.loading[url] = true;
} }
this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]);
this.showLoading(this.loading.has(this.tabs[this.activeTabIndex].webview.props.url));
}, },
onNetworkError: (index: number) => this.openNetworkTroubleshooting(index), onNetworkError: (index: number) => this.openNetworkTroubleshooting(index),
onTitleChange: this.updateBadge.bind(this), onTitleChange: this.updateBadge.bind(this),
@@ -545,7 +560,7 @@ class ServerManagerView {
// closed when the functional tab DOM is ready, handled in webview.js // closed when the functional tab DOM is ready, handled in webview.js
this.$webviewsContainer.classList.remove('loaded'); this.$webviewsContainer.classList.remove('loaded');
this.activateTab(this.functionalTabs[tabProps.name]); this.activateTab(this.functionalTabs.get(tabProps.name));
} }
async openSettings(nav = 'General'): Promise<void> { async openSettings(nav = 'General'): Promise<void> {
@@ -555,7 +570,7 @@ class ServerManagerView {
url: `file://${rendererDirectory}/preference.html#${nav}` url: `file://${rendererDirectory}/preference.html#${nav}`
}); });
this.$settingsButton.classList.add('active'); this.$settingsButton.classList.add('active');
await this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav); await this.tabs[this.functionalTabs.get('Settings')].webview.send('switch-settings-nav', nav);
} }
openAbout(): void { openAbout(): void {
@@ -580,23 +595,17 @@ class ServerManagerView {
ipcRenderer.send('save-last-tab', index); ipcRenderer.send('save-last-tab', index);
} }
// returns this.tabs in an way that does // Returns this.tabs in an way that does
// not crash app when this.tabs is passed into // not crash app when this.tabs is passed into
// ipcRenderer. Something about webview, and props.webview // ipcRenderer. Something about webview, and props.webview
// properties in ServerTab causes the app to crash. // properties in ServerTab causes the app to crash.
get tabsForIpc(): ServerOrFunctionalTab[] { get tabsForIpc(): TabData[] {
const tabs: ServerOrFunctionalTab[] = []; return this.tabs.map(tab => ({
this.tabs.forEach((tab: ServerOrFunctionalTab) => { role: tab.props.role,
const proto = Object.create(Object.getPrototypeOf(tab)); name: tab.props.name,
const tabClone = Object.assign(proto, tab); index: tab.props.index,
webviewName: tab.webview.props.name
tabClone.webview = { props: {} }; }));
tabClone.webview.props.name = tab.webview.props.name;
delete tabClone.props.webview;
tabs.push(tabClone);
});
return tabs;
} }
activateTab(index: number, hideOldTab = true): void { activateTab(index: number, hideOldTab = true): void {
@@ -607,24 +616,27 @@ class ServerManagerView {
if (this.activeTabIndex !== -1) { if (this.activeTabIndex !== -1) {
if (this.activeTabIndex === index) { if (this.activeTabIndex === index) {
return; return;
} else if (hideOldTab) { }
if (hideOldTab) {
// If old tab is functional tab Settings, remove focus from the settings icon at sidebar bottom // 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') { if (this.tabs[this.activeTabIndex].props.role === 'function' && this.tabs[this.activeTabIndex].props.name === 'Settings') {
this.$settingsButton.classList.remove('active'); this.$settingsButton.classList.remove('active');
} }
this.tabs[this.activeTabIndex].deactivate(); this.tabs[this.activeTabIndex].deactivate();
} }
} }
try { try {
this.tabs[index].webview.canGoBackButton(); this.tabs[index].webview.canGoBackButton();
} catch (err) { } catch {
} }
this.activeTabIndex = index; this.activeTabIndex = index;
this.tabs[index].activate(); this.tabs[index].activate();
this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]); this.showLoading(this.loading.has(this.tabs[this.activeTabIndex].webview.props.url));
ipcRenderer.send('update-menu', { ipcRenderer.send('update-menu', {
// JSON stringify this.tabs to avoid a crash // JSON stringify this.tabs to avoid a crash
@@ -654,7 +666,7 @@ class ServerManagerView {
this.tabs[index].destroy(); this.tabs[index].destroy();
delete this.tabs[index]; delete this.tabs[index];
delete this.functionalTabs[name]; this.functionalTabs.delete(name);
// Issue #188: If the functional tab was not focused, do not activate another tab. // Issue #188: If the functional tab was not focused, do not activate another tab.
if (this.activeTabIndex === index) { if (this.activeTabIndex === index) {
@@ -669,21 +681,21 @@ class ServerManagerView {
// Clear global variables // Clear global variables
this.activeTabIndex = -1; this.activeTabIndex = -1;
this.tabs = []; this.tabs = [];
this.functionalTabs = {}; this.functionalTabs.clear();
// Clear DOM elements // Clear DOM elements
this.$tabsContainer.innerHTML = ''; this.$tabsContainer.innerHTML = '';
this.$webviewsContainer.innerHTML = ''; this.$webviewsContainer.innerHTML = '';
} }
reloadView(): void { async reloadView(): Promise<void> {
// Save and remember the index of last active tab so that we can use it later // Save and remember the index of last active tab so that we can use it later
const lastActiveTab = this.tabs[this.activeTabIndex].props.index; const lastActiveTab = this.tabs[this.activeTabIndex].props.index;
ConfigUtil.setConfigItem('lastActiveTab', lastActiveTab); ConfigUtil.setConfigItem('lastActiveTab', lastActiveTab);
// Destroy the current view and re-initiate it // Destroy the current view and re-initiate it
this.destroyView(); this.destroyView();
this.initTabs(); await this.initTabs();
this.initServerActions(); this.initServerActions();
} }
@@ -706,9 +718,9 @@ class ServerManagerView {
ipcRenderer.send('update-badge', messageCountAll); ipcRenderer.send('update-badge', messageCountAll);
} }
updateGeneralSettings(setting: string, value: any): void { updateGeneralSettings(setting: string, value: unknown): void {
if (this.getActiveWebview()) { if (this.getActiveWebview()) {
const webContents = this.getActiveWebview().getWebContents(); const webContents = remote.webContents.fromId(this.getActiveWebview().getWebContentsId());
webContents.send(setting, value); webContents.send(setting, value);
} }
} }
@@ -765,7 +777,7 @@ class ServerManagerView {
label: 'Notification settings', label: 'Notification settings',
enabled: this.isLoggedIn(index), enabled: this.isLoggedIn(index),
click: () => { click: () => {
// switch to tab whose icon was right-clicked // Switch to tab whose icon was right-clicked
this.activateTab(index); this.activateTab(index);
this.tabs[index].webview.showNotificationSettings(); this.tabs[index].webview.showNotificationSettings();
} }
@@ -783,41 +795,42 @@ class ServerManagerView {
} }
registerIpcs(): void { registerIpcs(): void {
const webviewListeners: AnyObject = { const webviewListeners: Array<[string, (webview: WebView) => void]> = [
'webview-reload': 'reload', ['webview-reload', webview => webview.reload()],
back: 'back', ['back', webview => webview.back()],
focus: 'focus', ['focus', webview => webview.focus()],
forward: 'forward', ['forward', webview => webview.forward()],
zoomIn: 'zoomIn', ['zoomIn', webview => webview.zoomIn()],
zoomOut: 'zoomOut', ['zoomOut', webview => webview.zoomOut()],
zoomActualSize: 'zoomActualSize', ['zoomActualSize', webview => webview.zoomActualSize()],
'log-out': 'logOut', ['log-out', webview => webview.logOut()],
shortcut: 'showShortcut', ['shortcut', webview => webview.showShortcut()],
'tab-devtools': 'openDevTools' ['tab-devtools', webview => webview.openDevTools()]
}; ];
for (const [key, method] of Object.entries(webviewListeners)) { for (const [channel, listener] of webviewListeners) {
ipcRenderer.on(key, () => { ipcRenderer.on(channel, () => {
const activeWebview = this.tabs[this.activeTabIndex].webview; const activeWebview = this.tabs[this.activeTabIndex].webview;
if (activeWebview) { if (activeWebview) {
activeWebview[method](); listener(activeWebview);
} }
}); });
} }
ipcRenderer.on('permission-request', ( ipcRenderer.on('permission-request', (
event: Event, event: Event,
permissionId: number,
{webContentsId, origin, permission}: { {webContentsId, origin, permission}: {
webContentsId: number | null; webContentsId: number | null;
origin: string; origin: string;
permission: string; permission: string;
} },
rendererCallbackId: number
) => { ) => {
const grant = webContentsId === null ? const grant = webContentsId === null ?
origin === 'null' && permission === 'notifications' : origin === 'null' && permission === 'notifications' :
this.tabs.some( this.tabs.some(
({webview}) => ({webview}) =>
!webview.loading &&
webview.$el.getWebContentsId() === webContentsId && webview.$el.getWebContentsId() === webContentsId &&
webview.props.hasPermission?.(origin, permission) webview.props.hasPermission?.(origin, permission)
); );
@@ -825,22 +838,22 @@ class ServerManagerView {
grant ? 'Granted' : 'Denied', 'permissions request for', grant ? 'Granted' : 'Denied', 'permissions request for',
permission, 'from', origin permission, 'from', origin
); );
ipcRenderer.send('permission-response', permissionId, grant); ipcRenderer.send('renderer-callback', rendererCallbackId, grant);
}); });
ipcRenderer.on('show-network-error', (event: Event, index: number) => { ipcRenderer.on('show-network-error', (event: Event, index: number) => {
this.openNetworkTroubleshooting(index); this.openNetworkTroubleshooting(index);
}); });
ipcRenderer.on('open-settings', (event: Event, settingNav: string) => { ipcRenderer.on('open-settings', async (event: Event, settingNav: string) => {
this.openSettings(settingNav); await this.openSettings(settingNav);
}); });
ipcRenderer.on('open-about', this.openAbout.bind(this)); ipcRenderer.on('open-about', this.openAbout.bind(this));
ipcRenderer.on('open-help', () => { ipcRenderer.on('open-help', async () => {
// Open help page of current active server // Open help page of current active server
LinkUtil.openBrowser(new URL('/help', this.getCurrentActiveServer())); await LinkUtil.openBrowser(new URL('https://zulip.com/help/'));
}); });
ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index)); ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index));
@@ -851,22 +864,21 @@ class ServerManagerView {
ipcRenderer.send('reload-full-app'); ipcRenderer.send('reload-full-app');
}); });
ipcRenderer.on('clear-app-data', () => {
ipcRenderer.send('clear-app-settings');
});
ipcRenderer.on('switch-server-tab', (event: Event, index: number) => { ipcRenderer.on('switch-server-tab', (event: Event, index: number) => {
this.activateLastTab(index); this.activateLastTab(index);
}); });
ipcRenderer.on('open-org-tab', () => { ipcRenderer.on('open-org-tab', async () => {
this.openSettings('AddServer'); await this.openSettings('AddServer');
}); });
ipcRenderer.on('reload-proxy', async (event: Event, showAlert: boolean) => { ipcRenderer.on('reload-proxy', async (event: Event, showAlert: boolean) => {
await this.loadProxy(); await this.loadProxy();
if (showAlert) { if (showAlert) {
alert('Proxy settings saved!'); await dialog.showMessageBox({
message: 'Proxy settings saved!',
buttons: ['OK']
});
ipcRenderer.send('reload-full-app'); ipcRenderer.send('reload-full-app');
} }
}); });
@@ -884,8 +896,8 @@ class ServerManagerView {
webviews.forEach(webview => { webviews.forEach(webview => {
try { try {
webview.setAudioMuted(state); webview.setAudioMuted(state);
} catch (err) { } catch {
// webview is not ready yet // Webview is not ready yet
webview.addEventListener('dom-ready', () => { webview.addEventListener('dom-ready', () => {
webview.setAudioMuted(state); webview.setAudioMuted(state);
}); });
@@ -901,19 +913,19 @@ class ServerManagerView {
}); });
return; return;
} }
this.updateGeneralSettings('toggle-menubar-setting', autoHideMenubar); this.updateGeneralSettings('toggle-menubar-setting', autoHideMenubar);
}); });
ipcRenderer.on('toggle-dnd', (event: Event, state: boolean, newSettings: SettingsOptions) => { ipcRenderer.on('toggle-dnd', (event: Event, state: boolean, newSettings: DNDSettings) => {
this.toggleDNDButton(state); this.toggleDNDButton(state);
ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent); ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent);
const webContents = this.getActiveWebview().getWebContents(); const webContents = remote.webContents.fromId(this.getActiveWebview().getWebContentsId());
webContents.send('toggle-dnd', state, newSettings); webContents.send('toggle-dnd', state, newSettings);
}); });
ipcRenderer.on('update-realm-name', (event: Event, serverURL: string, realmName: string) => { ipcRenderer.on('update-realm-name', (event: Event, serverURL: string, realmName: string) => {
// TODO: TypeScript - Type annotate getDomains() or this domain paramter. DomainUtil.getDomains().forEach((domain: DomainUtil.ServerConf, index: number) => {
DomainUtil.getDomains().forEach((domain: any, index: number) => {
if (domain.url.includes(serverURL)) { if (domain.url.includes(serverURL)) {
const serverTooltipSelector = '.tab .server-tooltip'; const serverTooltipSelector = '.tab .server-tooltip';
const serverTooltips = document.querySelectorAll(serverTooltipSelector); const serverTooltips = document.querySelectorAll(serverTooltipSelector);
@@ -922,8 +934,7 @@ class ServerManagerView {
this.tabs[index].webview.props.name = realmName; this.tabs[index].webview.props.name = realmName;
domain.alias = escape(realmName); domain.alias = escape(realmName);
DomainUtil.db.push(`/domains[${index}]`, domain, true); DomainUtil.updateDomain(index, domain);
DomainUtil.reloadDB();
// Update the realm name also on the Window menu // Update the realm name also on the Window menu
ipcRenderer.send('update-menu', { ipcRenderer.send('update-menu', {
tabs: this.tabsForIpc, tabs: this.tabsForIpc,
@@ -944,8 +955,7 @@ class ServerManagerView {
const serverImgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(serverImgsSelector); const serverImgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(serverImgsSelector);
serverImgs[index].src = localIconUrl; serverImgs[index].src = localIconUrl;
domain.icon = localIconUrl; domain.icon = localIconUrl;
DomainUtil.db.push(`/domains[${index}]`, domain, true); DomainUtil.updateDomain(index, domain);
DomainUtil.reloadDB();
} }
}); });
}); });
@@ -962,7 +972,7 @@ class ServerManagerView {
ipcRenderer.on('focus-webview-with-id', (event: Event, webviewId: number) => { ipcRenderer.on('focus-webview-with-id', (event: Event, webviewId: number) => {
const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview'); const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => { webviews.forEach(webview => {
const currentId = webview.getWebContents().id; const currentId = webview.getWebContentsId();
const tabId = webview.getAttribute('data-tab-id'); const tabId = webview.getAttribute('data-tab-id');
const concurrentTab: HTMLButtonElement = document.querySelector(`div[data-tab-id="${tabId}"]`); const concurrentTab: HTMLButtonElement = document.querySelector(`div[data-tab-id="${tabId}"]`);
if (currentId === webviewId) { if (currentId === webviewId) {
@@ -995,8 +1005,10 @@ class ServerManagerView {
ctx.font = '85px Helvetica'; ctx.font = '85px Helvetica';
ctx.fillText(String(Math.min(99, messageCount)), 64, 90); ctx.fillText(String(Math.min(99, messageCount)), 64, 90);
} }
return canvas; return canvas;
} }
ipcRenderer.send('update-taskbar-icon', createOverlayIcon(messageCount).toDataURL(), String(messageCount)); ipcRenderer.send('update-taskbar-icon', createOverlayIcon(messageCount).toDataURL(), String(messageCount));
}); });
@@ -1008,50 +1020,41 @@ class ServerManagerView {
clipboard.writeText(this.getCurrentActiveServer()); clipboard.writeText(this.getCurrentActiveServer());
}); });
ipcRenderer.on('new-server', () => { ipcRenderer.on('new-server', async () => {
this.openSettings('AddServer'); await this.openSettings('AddServer');
}); });
// Redo and undo functionality since the default API doesn't work on macOS // Redo and undo functionality since the default API doesn't work on macOS
ipcRenderer.on('undo', () => { ipcRenderer.on('undo', () => this.getActiveWebview().undo());
return this.getActiveWebview().undo();
});
ipcRenderer.on('redo', () => { ipcRenderer.on('redo', () => this.getActiveWebview().redo());
return this.getActiveWebview().redo();
});
ipcRenderer.on('set-active', () => { ipcRenderer.on('set-active', async () => {
const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview'); const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => { await Promise.all([...webviews].map(async webview => webview.send('set-active')));
webview.send('set-active');
});
}); });
ipcRenderer.on('set-idle', () => { ipcRenderer.on('set-idle', async () => {
const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview'); const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => { await Promise.all([...webviews].map(async webview => webview.send('set-idle')));
webview.send('set-idle');
});
}); });
ipcRenderer.on('open-network-settings', () => { ipcRenderer.on('open-network-settings', async () => {
this.openSettings('Network'); await this.openSettings('Network');
}); });
} }
} }
window.addEventListener('load', () => { window.addEventListener('load', async () => {
// Only start electron-connect (auto reload on change) when its ran
// from `npm run dev` or `gulp dev` and not from `npm start`
if (isDev && remote.getGlobal('process').argv.includes('--electron-connect')) {
// eslint-disable-next-line node/no-unsupported-features/es-syntax
(await import('electron-connect')).client.create();
}
const serverManagerView = new ServerManagerView(); const serverManagerView = new ServerManagerView();
serverManagerView.init(); await serverManagerView.init();
// 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')) {
require('electron-connect').client.create();
}
}); });
export { }; export { };

View File

@@ -1,12 +1,14 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import MacNotifier from 'node-mac-notifier';
import electron_bridge from '../electron-bridge';
import * as ConfigUtil from '../utils/config-util';
import { import {
appId, customReply, focusCurrentServer, parseReply appId, customReply, focusCurrentServer, parseReply
} from './helpers'; } from './helpers';
import MacNotifier from 'node-mac-notifier';
import * as ConfigUtil from '../utils/config-util';
import electron_bridge from '../electron-bridge';
type ReplyHandler = (response: string) => void; type ReplyHandler = (response: string) => void;
type ClickHandler = () => void; type ClickHandler = () => void;
let replyHandler: ReplyHandler; let replyHandler: ReplyHandler;
@@ -17,14 +19,14 @@ interface NotificationHandlerArgs {
} }
class DarwinNotification { class DarwinNotification {
tag: string; tag: number;
constructor(title: string, options: NotificationOptions) { constructor(title: string, options: NotificationOptions) {
const silent: boolean = ConfigUtil.getConfigItem('silent') || false; const silent: boolean = ConfigUtil.getConfigItem('silent') || false;
const {icon} = options; const {icon} = options;
const profilePic = new URL(icon, location.href).href; const profilePic = new URL(icon, location.href).href;
this.tag = options.tag; this.tag = Number.parseInt(options.tag, 10);
const notification = new MacNotifier(title, Object.assign(options, { const notification = new MacNotifier(title, Object.assign(options, {
bundleId: appId, bundleId: appId,
canReply: true, canReply: true,
@@ -33,7 +35,7 @@ class DarwinNotification {
})); }));
notification.addEventListener('click', () => { notification.addEventListener('click', () => {
// focus to the server who sent the // Focus to the server who sent the
// notification if not focused already // notification if not focused already
if (clickHandler) { if (clickHandler) {
clickHandler(); clickHandler();
@@ -47,7 +49,7 @@ class DarwinNotification {
} }
static requestPermission(): void { static requestPermission(): void {
return; // eslint-disable-line no-useless-return // Do nothing
} }
// Override default Notification permission // Override default Notification permission
@@ -71,7 +73,7 @@ class DarwinNotification {
clickHandler = handler; clickHandler = handler;
} }
// not something that is common or // Not something that is common or
// used by zulip server but added to be // used by zulip server but added to be
// future proff. // future proff.
addEventListener(event: string, handler: ClickHandler | ReplyHandler): void { addEventListener(event: string, handler: ClickHandler | ReplyHandler): void {
@@ -101,10 +103,10 @@ class DarwinNotification {
customReply(response); customReply(response);
} }
// method specific to notification api // Method specific to notification api
// used by zulip // used by zulip
close(): void { close(): void {
return; // eslint-disable-line no-useless-return // Do nothing
} }
} }

View File

@@ -1,8 +1,9 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import { focusCurrentServer } from './helpers';
import * as ConfigUtil from '../utils/config-util'; import * as ConfigUtil from '../utils/config-util';
import {focusCurrentServer} from './helpers';
const NativeNotification = window.Notification; const NativeNotification = window.Notification;
export default class BaseNotification extends NativeNotification { export default class BaseNotification extends NativeNotification {
constructor(title: string, options: NotificationOptions) { constructor(title: string, options: NotificationOptions) {
@@ -10,7 +11,7 @@ export default class BaseNotification extends NativeNotification {
super(title, options); super(title, options);
this.addEventListener('click', () => { this.addEventListener('click', () => {
// focus to the server who sent the // Focus to the server who sent the
// notification if not focused already // notification if not focused already
focusCurrentServer(); focusCurrentServer();
ipcRenderer.send('focus-app'); ipcRenderer.send('focus-app');

View File

@@ -14,16 +14,16 @@ export type BotListItem = [string, string];
const botsList: BotListItem[] = []; const botsList: BotListItem[] = [];
let botsListLoaded = false; let botsListLoaded = false;
// this function load list of bots from the server // This function load list of bots from the server
// in case botsList isn't already completely loaded when required in parseRely // in case botsList isn't already completely loaded when required in parseRely
export async function loadBots(): Promise<void> { export async function loadBots(): Promise<void> {
botsList.length = 0; botsList.length = 0;
const response = await fetch('/json/users'); const response = await fetch('/json/users');
if (response.ok) { if (response.ok) {
const {members} = await response.json(); const {members} = await response.json();
members.forEach((membersRow: any) => { members.forEach(({is_bot, full_name}: {[key: string]: unknown}) => {
if (membersRow.is_bot) { if (is_bot && typeof full_name === 'string') {
const bot = `@${membersRow.full_name}`; const bot = `@${full_name}`;
const mention = `@**${bot.replace(/^@/, '')}**`; const mention = `@**${bot.replace(/^@/, '')}**`;
botsList.push([bot, mention]); botsList.push([bot, mention]);
} }
@@ -35,7 +35,7 @@ export async function loadBots(): Promise<void> {
} }
} }
export function checkElements(...elements: any[]): boolean { export function checkElements(...elements: unknown[]): boolean {
let status = true; let status = true;
elements.forEach(element => { elements.forEach(element => {
if (element === null || element === undefined) { if (element === null || element === undefined) {
@@ -46,14 +46,14 @@ export function checkElements(...elements: any[]): boolean {
} }
export function customReply(reply: string): void { export function customReply(reply: string): void {
// server does not support notification reply yet. // Server does not support notification reply yet.
const buttonSelector = '.messagebox #send_controls button[type=submit]'; const buttonSelector = '.messagebox #send_controls button[type=submit]';
const messageboxSelector = '.selected_message .messagebox .messagebox-border .messagebox-content'; const messageboxSelector = '.selected_message .messagebox .messagebox-border .messagebox-content';
const textarea: HTMLInputElement = document.querySelector('#compose-textarea'); const textarea: HTMLInputElement = document.querySelector('#compose-textarea');
const messagebox: HTMLButtonElement = document.querySelector(messageboxSelector); const messagebox: HTMLButtonElement = document.querySelector(messageboxSelector);
const sendButton: HTMLButtonElement = document.querySelector(buttonSelector); const sendButton: HTMLButtonElement = document.querySelector(buttonSelector);
// sanity check for old server versions // Sanity check for old server versions
const elementsExists = checkElements(textarea, messagebox, sendButton); const elementsExists = checkElements(textarea, messagebox, sendButton);
if (!elementsExists) { if (!elementsExists) {
return; return;
@@ -68,12 +68,13 @@ const currentWindow = remote.getCurrentWindow();
const webContents = remote.getCurrentWebContents(); const webContents = remote.getCurrentWebContents();
const webContentsId = webContents.id; const webContentsId = webContents.id;
// this function will focus the server that sent // This function will focus the server that sent
// the notification. Main function implemented in main.js // the notification. Main function implemented in main.js
export function focusCurrentServer(): void { export function focusCurrentServer(): void {
currentWindow.webContents.send('focus-webview-with-id', webContentsId); currentWindow.webContents.send('focus-webview-with-id', webContentsId);
} }
// this function parses the reply from to notification
// This function parses the reply from to notification
// making it easier to reply from notification eg // making it easier to reply from notification eg
// @username in reply will be converted to @**username** // @username in reply will be converted to @**username**
// #stream in reply will be converted to #**stream** // #stream in reply will be converted to #**stream**

View File

@@ -1,8 +1,8 @@
import {remote} from 'electron'; import {remote} from 'electron';
import electron_bridge from '../electron-bridge';
import { appId, loadBots } from './helpers';
import DefaultNotification from './default-notification'; import DefaultNotification from './default-notification';
import {appId} from './helpers';
const {app} = remote; const {app} = remote;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid // From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
@@ -16,7 +16,7 @@ if (process.platform === 'darwin') {
} }
export interface NotificationData { export interface NotificationData {
close(): void; close: () => void;
title: string; title: string;
dir: NotificationDirection; dir: NotificationDirection;
lang: string; lang: string;
@@ -47,6 +47,7 @@ export function newNotification(
} }
}); });
} }
return { return {
close: () => notification.close(), close: () => notification.close(),
title: notification.title, title: notification.title,
@@ -66,7 +67,3 @@ export function newNotification(
actions: notification.actions actions: notification.actions
}; };
} }
electron_bridge.once('zulip-loaded', () => {
loadBots();
});

View File

@@ -1,23 +1,24 @@
'use-strict';
import {remote, OpenDialogOptions} from 'electron'; import {remote, OpenDialogOptions} from 'electron';
import path from 'path'; import path from 'path';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import * as CertificateUtil from '../../utils/certificate-util'; import * as CertificateUtil from '../../utils/certificate-util';
import * as DomainUtil from '../../utils/domain-util'; import * as DomainUtil from '../../utils/domain-util';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
interface AddCertificateProps {
$root: Element;
}
const {dialog} = remote; const {dialog} = remote;
export default class AddCertificate extends BaseComponent { export default class AddCertificate extends BaseComponent {
// TODO: TypeScript - Here props should be object type props: AddCertificateProps;
props: any;
_certFile: string; _certFile: string;
$addCertificate: Element | null; $addCertificate: Element | null;
addCertificateButton: Element | null; addCertificateButton: Element | null;
serverUrl: HTMLInputElement | null; serverUrl: HTMLInputElement | null;
constructor(props: any) { constructor(props: AddCertificateProps) {
super(); super();
this.props = props; this.props = props;
this._certFile = ''; this._certFile = '';
@@ -46,7 +47,7 @@ export default class AddCertificate extends BaseComponent {
this.initListeners(); this.initListeners();
} }
validateAndAdd(): void { async validateAndAdd(): Promise<void> {
const certificate = this._certFile; const certificate = this._certFile;
const serverUrl = this.serverUrl.value; const serverUrl = this.serverUrl.value;
if (certificate !== '' && serverUrl !== '') { if (certificate !== '' && serverUrl !== '') {
@@ -56,12 +57,13 @@ export default class AddCertificate extends BaseComponent {
if (!copy) { if (!copy) {
return; return;
} }
CertificateUtil.setCertificate(server, fileName); CertificateUtil.setCertificate(server, fileName);
dialog.showMessageBox({ this.serverUrl.value = '';
await dialog.showMessageBox({
title: 'Success', title: 'Success',
message: 'Certificate saved!' message: 'Certificate saved!'
}); });
this.serverUrl.value = '';
} else { } else {
dialog.showErrorBox('Error', `Please, ${serverUrl === '' ? dialog.showErrorBox('Error', `Please, ${serverUrl === '' ?
'Enter an Organization URL' : 'Choose certificate file'}`); 'Enter an Organization URL' : 'Choose certificate file'}`);
@@ -77,18 +79,18 @@ export default class AddCertificate extends BaseComponent {
const {filePaths, canceled} = await dialog.showOpenDialog(showDialogOptions); const {filePaths, canceled} = await dialog.showOpenDialog(showDialogOptions);
if (!canceled) { if (!canceled) {
this._certFile = filePaths[0] || ''; this._certFile = filePaths[0] || '';
this.validateAndAdd(); await this.validateAndAdd();
} }
} }
initListeners(): void { initListeners(): void {
this.addCertificateButton.addEventListener('click', () => { this.addCertificateButton.addEventListener('click', async () => {
this.addHandler(); await this.addHandler();
}); });
this.serverUrl.addEventListener('keypress', event => { this.serverUrl.addEventListener('keypress', async event => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
this.addHandler(); await this.addHandler();
} }
}); });
} }

View File

@@ -30,6 +30,7 @@ function updateOverlayIcon(messageCount: number, mainWindow: electron.BrowserWin
if (!mainWindow.isFocused()) { if (!mainWindow.isFocused()) {
mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage')); mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
} }
if (messageCount === 0) { if (messageCount === 0) {
mainWindow.setOverlayIcon(null, ''); mainWindow.setOverlayIcon(null, '');
} else { } else {

View File

@@ -1,10 +1,18 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import escape from 'escape-html';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
interface BaseSectionProps {
$element: HTMLElement;
disabled?: boolean;
value: boolean;
clickHandler: () => void;
}
export default class BaseSection extends BaseComponent { export default class BaseSection extends BaseComponent {
// TODO: TypeScript - Here props should be object type generateSettingOption(props: BaseSectionProps): void {
generateSettingOption(props: any): void {
const {$element, disabled, value, clickHandler} = props; const {$element, disabled, value, clickHandler} = props;
$element.innerHTML = ''; $element.innerHTML = '';
@@ -17,7 +25,7 @@ export default class BaseSection extends BaseComponent {
} }
} }
generateOptionTemplate(settingOption: boolean, disabled: boolean): string { generateOptionTemplate(settingOption: boolean, disabled?: boolean): string {
const label = disabled ? '<label class="disallowed" title="Setting locked by system administrator."/>' : '<label/>'; const label = disabled ? '<label class="disallowed" title="Setting locked by system administrator."/>' : '<label/>';
if (settingOption) { if (settingOption) {
return ` return `
@@ -28,7 +36,8 @@ export default class BaseSection extends BaseComponent {
</div> </div>
</div> </div>
`; `;
} else { }
return ` return `
<div class="action"> <div class="action">
<div class="switch"> <div class="switch">
@@ -38,6 +47,17 @@ export default class BaseSection extends BaseComponent {
</div> </div>
`; `;
} }
/* A method that in future can be used to create dropdown menus using <select> <option> tags.
it needs an object which has ``key: value`` pairs and will return a string that can be appended to HTML
*/
generateSelectTemplate(options: {[key: string]: string}, className?: string, idName?: string): string {
let select = `<select class="${escape(className)}" id="${escape(idName)}">\n`;
Object.keys(options).forEach(key => {
select += `<option name="${escape(key)}" value="${escape(key)}">${escape(options[key])}</option>\n`;
});
select += '</select>';
return select;
} }
reloadApp(): void { reloadApp(): void {

View File

@@ -1,21 +1,25 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import BaseSection from './base-section';
import * as DomainUtil from '../../utils/domain-util'; import * as DomainUtil from '../../utils/domain-util';
import ServerInfoForm from './server-info-form';
import AddCertificate from './add-certificate';
import FindAccounts from './find-accounts';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
import AddCertificate from './add-certificate';
import BaseSection from './base-section';
import FindAccounts from './find-accounts';
import ServerInfoForm from './server-info-form';
interface ConnectedOrgSectionProps {
$root: Element;
}
export default class ConnectedOrgSection extends BaseSection { export default class ConnectedOrgSection extends BaseSection {
// TODO: TypeScript - Here props should be object type props: ConnectedOrgSectionProps;
props: any;
$serverInfoContainer: Element | null; $serverInfoContainer: Element | null;
$existingServers: Element | null; $existingServers: Element | null;
$newOrgButton: HTMLButtonElement | null; $newOrgButton: HTMLButtonElement | null;
$addCertificateContainer: Element | null; $addCertificateContainer: Element | null;
$findAccountsContainer: Element | null; $findAccountsContainer: Element | null;
constructor(props: any) { constructor(props: ConnectedOrgSectionProps) {
super(); super();
this.props = props; this.props = props;
} }

View File

@@ -1,16 +1,17 @@
'use-strict';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import * as LinkUtil from '../../utils/link-util'; import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
interface FindAccountsProps {
$root: Element;
}
export default class FindAccounts extends BaseComponent { export default class FindAccounts extends BaseComponent {
// TODO: TypeScript - Here props should be object type props: FindAccountsProps;
props: any;
$findAccounts: Element | null; $findAccounts: Element | null;
$findAccountsButton: Element | null; $findAccountsButton: Element | null;
$serverUrlField: HTMLInputElement | null; $serverUrlField: HTMLInputElement | null;
constructor(props: any) { constructor(props: FindAccountsProps) {
super(); super();
this.props = props; this.props = props;
} }
@@ -37,19 +38,21 @@ export default class FindAccounts extends BaseComponent {
this.initListeners(); this.initListeners();
} }
findAccounts(url: string): void { async findAccounts(url: string): Promise<void> {
if (!url) { if (!url) {
return; return;
} }
if (!url.startsWith('http')) { if (!url.startsWith('http')) {
url = 'https://' + url; url = 'https://' + url;
} }
LinkUtil.openBrowser(new URL('/accounts/find', url));
await LinkUtil.openBrowser(new URL('/accounts/find', url));
} }
initListeners(): void { initListeners(): void {
this.$findAccountsButton.addEventListener('click', () => { this.$findAccountsButton.addEventListener('click', async () => {
this.findAccounts(this.$serverUrlField.value); await this.findAccounts(this.$serverUrlField.value);
}); });
this.$serverUrlField.addEventListener('click', () => { this.$serverUrlField.addEventListener('click', () => {
@@ -58,9 +61,9 @@ export default class FindAccounts extends BaseComponent {
} }
}); });
this.$serverUrlField.addEventListener('keypress', event => { this.$serverUrlField.addEventListener('keypress', async event => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
this.findAccounts(this.$serverUrlField.value); await this.findAccounts(this.$serverUrlField.value);
} }
}); });

View File

@@ -1,20 +1,27 @@
import {ipcRenderer, remote, OpenDialogOptions} from 'electron'; import {ipcRenderer, remote, OpenDialogOptions} from 'electron';
import path from 'path'; import path from 'path';
import Tagify from '@yaireo/tagify';
import fs from 'fs-extra'; import fs from 'fs-extra';
import ISO6391 from 'iso-639-1';
const { app, dialog } = remote; import supportedLocales from '../../../../translations/supported-locales.json';
const currentBrowserWindow = remote.getCurrentWindow();
import BaseSection from './base-section';
import * as ConfigUtil from '../../utils/config-util'; import * as ConfigUtil from '../../utils/config-util';
import * as EnterpriseUtil from '../../utils/enterprise-util'; import * as EnterpriseUtil from '../../utils/enterprise-util';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
import BaseSection from './base-section';
const {app, dialog, session} = remote;
const currentBrowserWindow = remote.getCurrentWindow();
interface GeneralSectionProps {
$root: Element;
}
export default class GeneralSection extends BaseSection { export default class GeneralSection extends BaseSection {
// TODO: TypeScript - Here props should be object type props: GeneralSectionProps;
props: any; constructor(props: GeneralSectionProps) {
constructor(props: any) {
super(); super();
this.props = props; this.props = props;
} }
@@ -89,13 +96,23 @@ export default class GeneralSection extends BaseSection {
<div class="setting-description">${t.__('Enable spellchecker (requires restart)')}</div> <div class="setting-description">${t.__('Enable spellchecker (requires restart)')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="spellcheck-langs" style= "display:${process.platform === 'darwin' ? 'none' : ''}"></div>
<div class="setting-row" id="note"></div>
</div> </div>
<div class="title">${t.__('Advanced')}</div> <div class="title">${t.__('Advanced')}</div>
<div class="settings-card"> <div class="settings-card">
<div class="setting-row" id="enable-error-reporting"> <div class="setting-row" id="enable-error-reporting">
<div class="setting-description">${t.__('Enable error reporting (requires restart)')}</div> <div class="setting-description">${t.__('Enable error reporting (requires restart)')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="app-language">
<div class="setting-description">${t.__('App language (requires restart)')}</div>
<div id="lang-div" class="lang-div"></div>
</div>
<div class="setting-row" id="add-custom-css"> <div class="setting-row" id="add-custom-css">
<div class="setting-description"> <div class="setting-description">
${t.__('Add custom CSS')} ${t.__('Add custom CSS')}
@@ -104,7 +121,7 @@ export default class GeneralSection extends BaseSection {
</div> </div>
<div class="setting-row" id="remove-custom-css"> <div class="setting-row" id="remove-custom-css">
<div class="setting-description"> <div class="setting-description">
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div> <div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigString('customCSS', '')}</div>
</div> </div>
<div class="action red" id="css-delete-action"> <div class="action red" id="css-delete-action">
<i class="material-icons">indeterminate_check_box</i> <i class="material-icons">indeterminate_check_box</i>
@@ -119,7 +136,7 @@ export default class GeneralSection extends BaseSection {
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-description"> <div class="setting-description">
<div class="download-folder-path">${ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`)}</div> <div class="download-folder-path">${ConfigUtil.getConfigString('downloadsPath', app.getPath('downloads'))}</div>
</div> </div>
</div> </div>
<div class="setting-row" id="prompt-download"> <div class="setting-row" id="prompt-download">
@@ -127,12 +144,12 @@ export default class GeneralSection extends BaseSection {
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
</div> </div>
<div class="title">${t.__('Reset Application Data')}</div> <div class="title">${t.__('Factory Reset Data')}</div>
<div class="settings-card"> <div class="settings-card">
<div class="setting-row" id="resetdata-option"> <div class="setting-row" id="factory-reset-option">
<div class="setting-description">${t.__('This will delete all application data including all added accounts and preferences')} <div class="setting-description">${t.__('Reset the application, thus deleting all the connected organizations, accounts, and certificates.')}
</div> </div>
<button class="reset-data-button red w-150">${t.__('Reset App Data')}</button> <button class="factory-reset-button red w-150">${t.__('Factory Reset')}</button>
</div> </div>
</div> </div>
</div> </div>
@@ -148,7 +165,7 @@ export default class GeneralSection extends BaseSection {
this.betaUpdateOption(); this.betaUpdateOption();
this.updateSidebarOption(); this.updateSidebarOption();
this.updateStartAtLoginOption(); this.updateStartAtLoginOption();
this.updateResetDataOption(); this.factoryReset();
this.showDesktopNotification(); this.showDesktopNotification();
this.enableSpellchecker(); this.enableSpellchecker();
this.minimizeOnStart(); this.minimizeOnStart();
@@ -159,6 +176,8 @@ export default class GeneralSection extends BaseSection {
this.updateQuitOnCloseOption(); this.updateQuitOnCloseOption();
this.updatePromptDownloadOption(); this.updatePromptDownloadOption();
this.enableErrorReporting(); this.enableErrorReporting();
this.setLocale();
this.initSpellChecker();
// Platform specific settings // Platform specific settings
@@ -166,6 +185,7 @@ export default class GeneralSection extends BaseSection {
if (process.platform === 'win32') { if (process.platform === 'win32') {
this.updateFlashTaskbar(); this.updateFlashTaskbar();
} }
// Dock bounce on macOS // Dock bounce on macOS
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
this.updateDockBouncing(); this.updateDockBouncing();
@@ -252,6 +272,7 @@ export default class GeneralSection extends BaseSection {
ConfigUtil.setConfigItem('betaUpdate', false); ConfigUtil.setConfigItem('betaUpdate', false);
this.betaUpdateOption(); this.betaUpdateOption();
} }
this.autoUpdateOption(); this.autoUpdateOption();
} }
}); });
@@ -342,6 +363,10 @@ export default class GeneralSection extends BaseSection {
const newValue = !ConfigUtil.getConfigItem('enableSpellchecker'); const newValue = !ConfigUtil.getConfigItem('enableSpellchecker');
ConfigUtil.setConfigItem('enableSpellchecker', newValue); ConfigUtil.setConfigItem('enableSpellchecker', newValue);
this.enableSpellchecker(); this.enableSpellchecker();
const spellcheckerLanguageInput: HTMLElement = document.querySelector('#spellcheck-langs');
const spellcheckerNote: HTMLElement = document.querySelector('#note');
spellcheckerLanguageInput.style.display = spellcheckerLanguageInput.style.display === 'none' ? '' : 'none';
spellcheckerNote.style.display = spellcheckerNote.style.display === 'none' ? '' : 'none';
} }
}); });
} }
@@ -358,23 +383,6 @@ export default class GeneralSection extends BaseSection {
}); });
} }
async clearAppDataDialog(): Promise<void> {
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.name);
const { response } = await dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
message: 'Are you sure',
detail: clearAppDataMessage
});
if (response === 0) {
fs.remove(getAppPath);
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000);
}
}
async customCssDialog(): Promise<void> { async customCssDialog(): Promise<void> {
const showDialogOptions: OpenDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: 'Select file', title: 'Select file',
@@ -389,10 +397,20 @@ export default class GeneralSection extends BaseSection {
} }
} }
updateResetDataOption(): void { setLocale(): void {
const resetDataButton = document.querySelector('#resetdata-option .reset-data-button'); const langDiv: HTMLSelectElement = document.querySelector('.lang-div');
resetDataButton.addEventListener('click', () => { const langList = this.generateSelectTemplate(supportedLocales, 'lang-menu');
this.clearAppDataDialog(); langDiv.innerHTML += langList;
// `langMenu` is the select-option dropdown menu formed after executing the previous command
const langMenu: HTMLSelectElement = document.querySelector('.lang-menu');
// The next three lines set the selected language visible on the dropdown button
let language = ConfigUtil.getConfigItem('appLanguage');
language = language && langMenu.options.namedItem(language) ? language : 'en';
langMenu.options.namedItem(language).selected = true;
langMenu.addEventListener('change', (event: Event) => {
ConfigUtil.setConfigItem('appLanguage', (event.target as HTMLSelectElement).value);
}); });
} }
@@ -410,8 +428,8 @@ export default class GeneralSection extends BaseSection {
addCustomCSS(): void { addCustomCSS(): void {
const customCSSButton = document.querySelector('#add-custom-css .custom-css-button'); const customCSSButton = document.querySelector('#add-custom-css .custom-css-button');
customCSSButton.addEventListener('click', () => { customCSSButton.addEventListener('click', async () => {
this.customCssDialog(); await this.customCssDialog();
}); });
} }
@@ -446,8 +464,8 @@ export default class GeneralSection extends BaseSection {
downloadFolder(): void { downloadFolder(): void {
const downloadFolder = document.querySelector('#download-folder .download-folder-button'); const downloadFolder = document.querySelector('#download-folder .download-folder-button');
downloadFolder.addEventListener('click', () => { downloadFolder.addEventListener('click', async () => {
this.downloadFolderDialog(); await this.downloadFolderDialog();
}); });
} }
@@ -462,4 +480,103 @@ export default class GeneralSection extends BaseSection {
} }
}); });
} }
async factoryResetSettings(): Promise<void> {
const clearAppDataMessage = 'When the application restarts, it will be as if you have just downloaded Zulip app.';
const getAppPath = path.join(app.getPath('appData'), app.name);
const {response} = await dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
message: 'Are you sure?',
detail: clearAppDataMessage
});
if (response === 0) {
await fs.remove(getAppPath);
setTimeout(() => ipcRenderer.send('clear-app-settings'), 1000);
}
}
factoryReset(): void {
const factoryResetButton = document.querySelector('#factory-reset-option .factory-reset-button');
factoryResetButton.addEventListener('click', async () => {
await this.factoryResetSettings();
});
}
initSpellChecker(): void {
// The elctron API is a no-op on macOS and macOS default spellchecker is used.
if (process.platform === 'darwin') {
const note: HTMLElement = document.querySelector('#note');
note.append(t.__('On macOS, the OS spellchecker is used.'));
note.append(document.createElement('br'));
note.append(t.__('Change the language from System Preferences → Keyboard → Text → Spelling.'));
} else {
const note: HTMLElement = document.querySelector('#note');
note.append(t.__('You can select a maximum of 3 languages for spellchecking.'));
const spellDiv: HTMLElement = document.querySelector('#spellcheck-langs');
spellDiv.innerHTML += `
<div class="setting-description">${t.__('Spellchecker Languages')}</div>
<input name='spellcheck' placeholder='Enter Languages'>`;
const availableLanguages = session.fromPartition('persist:webviewsession').availableSpellCheckerLanguages;
let languagePairs: Map<string, string> = new Map();
availableLanguages.forEach((l: string) => {
if (ISO6391.validate(l)) {
languagePairs.set(ISO6391.getName(l), l);
}
});
// Manually set names for languages not available in ISO6391
languagePairs.set('English (AU)', 'en-AU');
languagePairs.set('English (CA)', 'en-CA');
languagePairs.set('English (GB)', 'en-GB');
languagePairs.set('English (US)', 'en-US');
languagePairs.set('Spanish (Latin America)', 'es-419');
languagePairs.set('Spanish (Argentina)', 'es-AR');
languagePairs.set('Spanish (Mexico)', 'es-MX');
languagePairs.set('Spanish (US)', 'es-US');
languagePairs.set('Portuguese (Brazil)', 'pt-BR');
languagePairs.set('Portuguese (Portugal)', 'pt-PT');
languagePairs.set('Serbo-Croatian', 'sh');
languagePairs = new Map([...languagePairs].sort((a, b) => ((a[0] < b[0]) ? -1 : 1)));
const tagField: HTMLElement = document.querySelector('input[name=spellcheck]');
const tagify = new Tagify(tagField, {
whitelist: [...languagePairs.keys()],
enforceWhitelist: true,
maxTags: 3,
dropdown: {
enabled: 0,
maxItems: Infinity,
closeOnSelect: false,
highlightFirst: true
}
});
const configuredLanguages: string[] = ConfigUtil.getConfigItem('spellcheckerLanguages').map((code: string) => [...languagePairs].filter(pair => (pair[1] === code))[0][0]);
tagify.addTags(configuredLanguages);
tagField.addEventListener('change', event => {
if ((event.target as HTMLInputElement).value.length === 0) {
ConfigUtil.setConfigItem('spellcheckerLanguages', []);
ipcRenderer.send('set-spellcheck-langs');
} else {
const spellLangs: string[] = [...JSON.parse((event.target as HTMLInputElement).value).values()].map(elt => languagePairs.get(elt.value));
ConfigUtil.setConfigItem('spellcheckerLanguages', spellLangs);
ipcRenderer.send('set-spellcheck-langs');
}
});
}
// Do not display the spellchecker input and note if it is disabled
if (!ConfigUtil.getConfigItem('enableSpellchecker')) {
const spellcheckerLanguageInput: HTMLElement = document.querySelector('#spellcheck-langs');
const spellcheckerNote: HTMLElement = document.querySelector('#note');
spellcheckerLanguageInput.style.display = 'none';
spellcheckerNote.style.display = 'none';
}
}
} }

View File

@@ -1,12 +1,16 @@
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
interface PreferenceNavProps {
$root: Element;
onItemSelected: (navItem: string) => void;
}
export default class PreferenceNav extends BaseComponent { export default class PreferenceNav extends BaseComponent {
// TODO: TypeScript - Here props should be object type props: PreferenceNavProps;
props: any;
navItems: string[]; navItems: string[];
$el: Element; $el: Element;
constructor(props: any) { constructor(props: PreferenceNavProps) {
super(); super();
this.props = props; this.props = props;
this.navItems = ['General', 'Network', 'AddServer', 'Organizations', 'Shortcuts']; this.navItems = ['General', 'Network', 'AddServer', 'Organizations', 'Shortcuts'];

View File

@@ -1,18 +1,22 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import BaseSection from './base-section';
import * as ConfigUtil from '../../utils/config-util'; import * as ConfigUtil from '../../utils/config-util';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
import BaseSection from './base-section';
interface NetworkSectionProps {
$root: Element;
}
export default class NetworkSection extends BaseSection { export default class NetworkSection extends BaseSection {
// TODO: TypeScript - Here props should be object type props: NetworkSectionProps;
props: any;
$proxyPAC: HTMLInputElement; $proxyPAC: HTMLInputElement;
$proxyRules: HTMLInputElement; $proxyRules: HTMLInputElement;
$proxyBypass: HTMLInputElement; $proxyBypass: HTMLInputElement;
$proxySaveAction: Element; $proxySaveAction: Element;
$manualProxyBlock: Element; $manualProxyBlock: Element;
constructor(props: any) { constructor(props: NetworkSectionProps) {
super(); super();
this.props = props; this.props = props;
} }
@@ -102,11 +106,13 @@ export default class NetworkSection extends BaseSection {
ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue); ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue);
this.toggleManualProxySettings(!manualProxyValue); this.toggleManualProxySettings(!manualProxyValue);
} }
if (!newValue) { if (!newValue) {
// Remove proxy system proxy settings // Remove proxy system proxy settings
ConfigUtil.setConfigItem('proxyRules', ''); ConfigUtil.setConfigItem('proxyRules', '');
ipcRenderer.send('forward-message', 'reload-proxy', false); ipcRenderer.send('forward-message', 'reload-proxy', false);
} }
ConfigUtil.setConfigItem('useSystemProxy', newValue); ConfigUtil.setConfigItem('useSystemProxy', newValue);
this.updateProxyOption(); this.updateProxyOption();
} }
@@ -121,6 +127,7 @@ export default class NetworkSection extends BaseSection {
if (systemProxyValue && newValue) { if (systemProxyValue && newValue) {
ConfigUtil.setConfigItem('useSystemProxy', !systemProxyValue); ConfigUtil.setConfigItem('useSystemProxy', !systemProxyValue);
} }
ConfigUtil.setConfigItem('proxyRules', ''); ConfigUtil.setConfigItem('proxyRules', '');
ConfigUtil.setConfigItem('useManualProxy', newValue); ConfigUtil.setConfigItem('useManualProxy', newValue);
// Reload app only when turning manual proxy off, hence !newValue // Reload app only when turning manual proxy off, hence !newValue

View File

@@ -1,17 +1,23 @@
import { ipcRenderer } from 'electron'; import {ipcRenderer, remote} from 'electron';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util'; import * as DomainUtil from '../../utils/domain-util';
import * as LinkUtil from '../../utils/link-util'; import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
const {dialog} = remote;
interface NewServerFormProps {
$root: Element;
onChange: () => void;
}
export default class NewServerForm extends BaseComponent { export default class NewServerForm extends BaseComponent {
// TODO: TypeScript - Here props should be object type props: NewServerFormProps;
props: any;
$newServerForm: Element; $newServerForm: Element;
$saveServerButton: HTMLButtonElement; $saveServerButton: HTMLButtonElement;
$newServerUrl: HTMLInputElement; $newServerUrl: HTMLInputElement;
constructor(props: any) { constructor(props: NewServerFormProps) {
super(); super();
this.props = props; this.props = props;
} }
@@ -62,20 +68,25 @@ export default class NewServerForm extends BaseComponent {
let serverConf; let serverConf;
try { try {
serverConf = await DomainUtil.checkDomain(this.$newServerUrl.value); serverConf = await DomainUtil.checkDomain(this.$newServerUrl.value);
} catch (errorMessage) { } catch (error) {
this.$saveServerButton.innerHTML = 'Connect'; this.$saveServerButton.innerHTML = 'Connect';
alert(errorMessage); await dialog.showMessageBox({
type: 'error',
message: error.toString(),
buttons: ['OK']
});
return; return;
} }
await DomainUtil.addDomain(serverConf); await DomainUtil.addDomain(serverConf);
this.props.onChange(this.props.index); this.props.onChange();
} }
openCreateNewOrgExternalLink(): void { openCreateNewOrgExternalLink(): void {
const link = 'https://zulipchat.com/new/'; const link = 'https://zulip.com/new/';
const externalCreateNewOrgElement = document.querySelector('#open-create-org-link'); const externalCreateNewOrgElement = document.querySelector('#open-create-org-link');
externalCreateNewOrgElement.addEventListener('click', () => { externalCreateNewOrgElement.addEventListener('click', async () => {
LinkUtil.openBrowser(new URL(link)); await LinkUtil.openBrowser(new URL(link));
}); });
} }
@@ -85,15 +96,15 @@ export default class NewServerForm extends BaseComponent {
} }
initActions(): void { initActions(): void {
this.$saveServerButton.addEventListener('click', () => { this.$saveServerButton.addEventListener('click', async () => {
this.submitFormHandler(); await this.submitFormHandler();
}); });
this.$newServerUrl.addEventListener('keypress', event => { this.$newServerUrl.addEventListener('keypress', async event => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
this.submitFormHandler(); await this.submitFormHandler();
} }
}); });
// open create new org link in default browser // Open create new org link in default browser
this.openCreateNewOrgExternalLink(); this.openCreateNewOrgExternalLink();
this.networkSettingsLink(); this.networkSettingsLink();
} }

View File

@@ -1,11 +1,13 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import Nav from './nav'; import type {DNDSettings} from '../../utils/dnd-util';
import ServersSection from './servers-section';
import GeneralSection from './general-section';
import NetworkSection from './network-section';
import ConnectedOrgSection from './connected-org-section'; import ConnectedOrgSection from './connected-org-section';
import GeneralSection from './general-section';
import Nav from './nav';
import NetworkSection from './network-section';
import ServersSection from './servers-section';
import ShortcutsSection from './shortcuts-section'; import ShortcutsSection from './shortcuts-section';
type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection; type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection;
@@ -37,6 +39,7 @@ export default class PreferenceView extends BaseComponent {
if (hasTag) { if (hasTag) {
nav = hasTag.slice(1); nav = hasTag.slice(1);
} }
this.handleNavigation(nav); this.handleNavigation(nav);
} }
@@ -49,32 +52,38 @@ export default class PreferenceView extends BaseComponent {
}); });
break; break;
} }
case 'General': { case 'General': {
this.section = new GeneralSection({ this.section = new GeneralSection({
$root: this.$settingsContainer $root: this.$settingsContainer
}); });
break; break;
} }
case 'Organizations': { case 'Organizations': {
this.section = new ConnectedOrgSection({ this.section = new ConnectedOrgSection({
$root: this.$settingsContainer $root: this.$settingsContainer
}); });
break; break;
} }
case 'Network': { case 'Network': {
this.section = new NetworkSection({ this.section = new NetworkSection({
$root: this.$settingsContainer $root: this.$settingsContainer
}); });
break; break;
} }
case 'Shortcuts': { case 'Shortcuts': {
this.section = new ShortcutsSection({ this.section = new ShortcutsSection({
$root: this.$settingsContainer $root: this.$settingsContainer
}); });
break; break;
} }
default: break; default: break;
} }
this.section.init(); this.section.init();
window.location.hash = `#${navItem}`; window.location.hash = `#${navItem}`;
} }
@@ -105,7 +114,7 @@ export default class PreferenceView extends BaseComponent {
this.handleToggle('tray-option', state); this.handleToggle('tray-option', state);
}); });
ipcRenderer.on('toggle-dnd', (_event: Event, _state: boolean, newSettings: any) => { ipcRenderer.on('toggle-dnd', (_event: Event, _state: boolean, newSettings: DNDSettings) => {
this.handleToggle('show-notification-option', newSettings.showNotification); this.handleToggle('show-notification-option', newSettings.showNotification);
this.handleToggle('silent-option', newSettings.silent); this.handleToggle('silent-option', newSettings.silent);

View File

@@ -1,21 +1,27 @@
import {remote, ipcRenderer} from 'electron'; import {remote, ipcRenderer} from 'electron';
import * as Messages from '../../../../resources/messages';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util'; import * as DomainUtil from '../../utils/domain-util';
import * as Messages from '../../../../resources/messages';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
const {dialog} = remote; const {dialog} = remote;
interface ServerInfoFormProps {
$root: Element;
server: DomainUtil.ServerConf;
index: number;
onChange: () => void;
}
export default class ServerInfoForm extends BaseComponent { export default class ServerInfoForm extends BaseComponent {
// TODO: TypeScript - Here props should be object type props: ServerInfoFormProps;
props: any;
$serverInfoForm: Element; $serverInfoForm: Element;
$serverInfoAlias: Element; $serverInfoAlias: Element;
$serverIcon: Element; $serverIcon: Element;
$deleteServerButton: Element; $deleteServerButton: Element;
$openServerButton: Element; $openServerButton: Element;
constructor(props: any) { constructor(props: ServerInfoFormProps) {
super(); super();
this.props = props; this.props = props;
} }

View File

@@ -1,12 +1,16 @@
import BaseSection from './base-section';
import NewServerForm from './new-server-form';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
import BaseSection from './base-section';
import NewServerForm from './new-server-form';
interface ServersSectionProps {
$root: Element;
}
export default class ServersSection extends BaseSection { export default class ServersSection extends BaseSection {
// TODO: TypeScript - Here props should be object type props: ServersSectionProps;
props: any;
$newServerContainer: Element; $newServerContainer: Element;
constructor(props: any) { constructor(props: ServersSectionProps) {
super(); super();
this.props = props; this.props = props;
} }

View File

@@ -1,11 +1,15 @@
import BaseSection from './base-section';
import * as LinkUtil from '../../utils/link-util'; import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
import BaseSection from './base-section';
interface ShortcutsSectionProps {
$root: Element;
}
export default class ShortcutsSection extends BaseSection { export default class ShortcutsSection extends BaseSection {
// TODO: TypeScript - Here props should be object type props: ShortcutsSectionProps;
props: any; constructor(props: ShortcutsSectionProps) {
constructor(props: any) {
super(); super();
this.props = props; this.props = props;
} }
@@ -327,10 +331,10 @@ export default class ShortcutsSection extends BaseSection {
} }
openHotkeysExternalLink(): void { openHotkeysExternalLink(): void {
const link = 'https://zulipchat.com/help/keyboard-shortcuts'; const link = 'https://zulip.com/help/keyboard-shortcuts';
const externalCreateNewOrgElement = document.querySelector('#open-hotkeys-link'); const externalCreateNewOrgElement = document.querySelector('#open-hotkeys-link');
externalCreateNewOrgElement.addEventListener('click', () => { externalCreateNewOrgElement.addEventListener('click', async () => {
LinkUtil.openBrowser(new URL(link)); await LinkUtil.openBrowser(new URL(link));
}); });
} }

View File

@@ -1,21 +1,22 @@
import {contextBridge, ipcRenderer, webFrame} from 'electron'; import {contextBridge, ipcRenderer, webFrame} from 'electron';
import fs from 'fs'; import fs from 'fs';
import * as SetupSpellChecker from './spellchecker';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import electron_bridge from './electron-bridge';
import {loadBots} from './notification/helpers';
import * as NetworkError from './pages/network'; import * as NetworkError from './pages/network';
// eslint-disable-next-line import/no-unassigned-import
import './notification';
// Prevent drag and drop event in main process which prevents remote code executaion // Prevent drag and drop event in main process which prevents remote code executaion
// eslint-disable-next-line import/no-unassigned-import // eslint-disable-next-line import/no-unassigned-import
import './shared/preventdrag'; import './shared/preventdrag';
import electron_bridge from './electron-bridge';
contextBridge.exposeInMainWorld('raw_electron_bridge', electron_bridge); contextBridge.exposeInMainWorld('raw_electron_bridge', electron_bridge);
electron_bridge.once('zulip-loaded', async () => {
await loadBots();
});
ipcRenderer.on('logout', () => { ipcRenderer.on('logout', () => {
// Create the menu for the below // Create the menu for the below
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle'); const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
@@ -48,19 +49,14 @@ ipcRenderer.on('show-notification-settings', () => {
const notificationItem: NodeListOf<HTMLElement> = document.querySelectorAll('.normal-settings-list li div'); const notificationItem: NodeListOf<HTMLElement> = document.querySelectorAll('.normal-settings-list li div');
// wait until the notification dom element shows up // Wait until the notification dom element shows up
setTimeout(() => { setTimeout(() => {
notificationItem[2].click(); notificationItem[2].click();
}, 100); }, 100);
}); });
electron_bridge.once('zulip-loaded', ({ serverLanguage }) => { electron_bridge.once('zulip-loaded', () => {
// Get the default language of the server // Redirect users to network troubleshooting page
if (serverLanguage) {
// Init spellchecker
SetupSpellChecker.init(serverLanguage);
}
// redirect users to network troubleshooting page
const getRestartButton = document.querySelector('.restart_get_events_button'); const getRestartButton = document.querySelector('.restart_get_events_button');
if (getRestartButton) { if (getRestartButton) {
getRestartButton.addEventListener('click', () => { getRestartButton.addEventListener('click', () => {
@@ -69,22 +65,17 @@ electron_bridge.once('zulip-loaded', ({ serverLanguage }) => {
} }
}); });
// Clean up spellchecker events after you navigate away from this page;
// otherwise, you may experience errors
window.addEventListener('beforeunload', (): void => {
SetupSpellChecker.unsubscribeSpellChecker();
});
window.addEventListener('load', (event: any): void => { window.addEventListener('load', (event: any): void => {
if (!event.target.URL.includes('app/renderer/network.html')) { if (!event.target.URL.includes('app/renderer/network.html')) {
return; return;
} }
const $reconnectButton = document.querySelector('#reconnect'); const $reconnectButton = document.querySelector('#reconnect');
const $settingsButton = document.querySelector('#settings'); const $settingsButton = document.querySelector('#settings');
NetworkError.init($reconnectButton, $settingsButton); NetworkError.init($reconnectButton, $settingsButton);
}); });
// electron's globalShortcut can cause unexpected results // Electron's globalShortcut can cause unexpected results
// so adding the reload shortcut in the old-school way // so adding the reload shortcut in the old-school way
// Zoom from numpad keys is not supported by electron, so adding it through listeners. // Zoom from numpad keys is not supported by electron, so adding it through listeners.
document.addEventListener('keydown', event => { document.addEventListener('keydown', event => {
@@ -105,6 +96,7 @@ ipcRenderer.on('set-active', () => {
if (isDev) { if (isDev) {
console.log('active'); console.log('active');
} }
electron_bridge.idle_on_system = false; electron_bridge.idle_on_system = false;
electron_bridge.last_active_on_system = Date.now(); electron_bridge.last_active_on_system = Date.now();
}); });
@@ -114,9 +106,10 @@ ipcRenderer.on('set-idle', () => {
if (isDev) { if (isDev) {
console.log('idle'); console.log('idle');
} }
electron_bridge.idle_on_system = true; electron_bridge.idle_on_system = true;
}); });
webFrame.executeJavaScript( (async () => webFrame.executeJavaScript(
fs.readFileSync(require.resolve('./injected'), 'utf8') fs.readFileSync(require.resolve('./injected'), 'utf8')
); ))();

View File

@@ -1,59 +0,0 @@
import type { Subject } from 'rxjs';
import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker';
import * as ConfigUtil from './utils/config-util';
import Logger from './utils/logger-util';
declare module 'electron-spellchecker' {
interface SpellCheckHandler {
currentSpellcheckerChanged: Subject<true>;
currentSpellcheckerLanguage: string;
}
}
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
let spellCheckHandler: SpellCheckHandler;
let contextMenuListener: ContextMenuListener;
export function init(serverLanguage: string): void {
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
enableSpellChecker();
}
enableContextMenu(serverLanguage);
}
function enableSpellChecker(): void {
try {
spellCheckHandler = new SpellCheckHandler();
} catch (err) {
logger.error(err);
}
}
function enableContextMenu(serverLanguage: string): void {
if (spellCheckHandler) {
spellCheckHandler.attachToInput();
spellCheckHandler.switchLanguage(serverLanguage);
spellCheckHandler.currentSpellcheckerChanged.subscribe(() => {
spellCheckHandler.switchLanguage(spellCheckHandler.currentSpellcheckerLanguage);
});
}
const contextMenuBuilder = new ContextMenuBuilder(spellCheckHandler);
contextMenuListener = new ContextMenuListener(info => {
contextMenuBuilder.showPopupMenu(info);
});
}
export function unsubscribeSpellChecker(): void {
if (spellCheckHandler) {
spellCheckHandler.unsubscribe();
}
if (contextMenuListener) {
contextMenuListener.unsubscribe();
}
}

View File

@@ -1,16 +1,13 @@
import {ipcRenderer, remote, WebviewTag, NativeImage} from 'electron'; import {ipcRenderer, remote, WebviewTag, NativeImage} from 'electron';
import path from 'path'; import path from 'path';
import * as ConfigUtil from './utils/config-util'; import * as ConfigUtil from './utils/config-util';
const { Tray, Menu, nativeImage, BrowserWindow, nativeTheme } = remote; const {Tray, Menu, nativeImage, BrowserWindow} = remote;
let tray: Electron.Tray; let tray: Electron.Tray;
// get the theme on macOS const ICON_DIR = '../../resources/tray';
const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
const ICON_DIR = process.platform === 'darwin' ? `../../resources/tray/${theme}` : '../../resources/tray';
const TRAY_SUFFIX = 'tray'; const TRAY_SUFFIX = 'tray';
@@ -20,7 +17,8 @@ const iconPath = (): string => {
if (process.platform === 'linux') { if (process.platform === 'linux') {
return APP_ICON + 'linux.png'; return APP_ICON + 'linux.png';
} }
return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'osx.png');
return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'macOSTemplate.png');
}; };
let unread = 0; let unread = 0;
@@ -92,6 +90,7 @@ const renderCanvas = function (arg: number): HTMLCanvasElement {
return canvas; return canvas;
}; };
/** /**
* Renders the tray icon as a native image * Renders the tray icon as a native image
* @param arg: Unread count * @param arg: Unread count
@@ -168,6 +167,7 @@ ipcRenderer.on('tray', (_event: Event, arg: number): void => {
if (!tray) { if (!tray) {
return; return;
} }
// We don't want to create tray from unread messages on macOS since it already has dock badges. // We don't want to create tray from unread messages on macOS since it already has dock badges.
if (process.platform === 'linux' || process.platform === 'win32') { if (process.platform === 'linux' || process.platform === 'win32') {
if (arg === 0) { if (arg === 0) {
@@ -178,7 +178,7 @@ ipcRenderer.on('tray', (_event: Event, arg: number): void => {
unread = arg; unread = arg;
const image = renderNativeImage(arg); const image = renderNativeImage(arg);
tray.setImage(image); tray.setImage(image);
tray.setToolTip(arg + ' unread messages'); tray.setToolTip(`${arg} unread messages`);
} }
} }
}); });
@@ -191,6 +191,7 @@ function toggleTray(): void {
if (tray.isDestroyed()) { if (tray.isDestroyed()) {
tray = null; tray = null;
} }
ConfigUtil.setConfigItem('trayIcon', false); ConfigUtil.setConfigItem('trayIcon', false);
} else { } else {
state = true; state = true;
@@ -198,13 +199,15 @@ function toggleTray(): void {
if (process.platform === 'linux' || process.platform === 'win32') { if (process.platform === 'linux' || process.platform === 'win32') {
const image = renderNativeImage(unread); const image = renderNativeImage(unread);
tray.setImage(image); tray.setImage(image);
tray.setToolTip(unread + ' unread messages'); tray.setToolTip(`${unread} unread messages`);
} }
ConfigUtil.setConfigItem('trayIcon', true); ConfigUtil.setConfigItem('trayIcon', true);
} }
const selector = 'webview:not([class*=disabled])'; const selector = 'webview:not([class*=disabled])';
const webview: WebviewTag = document.querySelector(selector); const webview: WebviewTag = document.querySelector(selector);
const webContents = webview.getWebContents(); const webContents = remote.webContents.fromId(webview.getWebContentsId());
webContents.send('toggletray', state); webContents.send('toggletray', state);
} }

View File

@@ -1,12 +1,14 @@
import { remote } from 'electron'; import electron from 'electron';
import { JsonDB } from 'node-json-db';
import { initSetUp } from './default-util';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {JsonDB} from 'node-json-db';
import {initSetUp} from './default-util';
import Logger from './logger-util'; import Logger from './logger-util';
const { app, dialog } = remote; const {app, dialog} =
process.type === 'renderer' ? electron.remote : electron;
initSetUp(); initSetUp();
@@ -21,14 +23,9 @@ let db: JsonDB;
reloadDB(); reloadDB();
export function getCertificate(server: string, defaultValue: any = null): any { export function getCertificate(server: string): string | undefined {
reloadDB(); reloadDB();
const value = db.getData('/')[server]; return db.getData('/')[server];
if (value === undefined) {
return defaultValue;
} else {
return value;
}
} }
// Function to copy the certificate to userData folder // Function to copy the certificate to userData folder
@@ -38,14 +35,15 @@ export function copyCertificate(_server: string, location: string, fileName: str
try { try {
fs.copyFileSync(location, filePath); fs.copyFileSync(location, filePath);
copied = true; copied = true;
} catch (err) { } catch (error) {
dialog.showErrorBox( dialog.showErrorBox(
'Error saving certificate', 'Error saving certificate',
'We encountered error while saving the certificate.' 'We encountered error while saving the certificate.'
); );
logger.error('Error while copying the certificate to certificates folder.'); logger.error('Error while copying the certificate to certificates folder.');
logger.error(err); logger.error(error);
} }
return copied; return copied;
} }
@@ -65,7 +63,7 @@ function reloadDB(): void {
try { try {
const file = fs.readFileSync(settingsJsonPath, 'utf8'); const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file); JSON.parse(file);
} catch (err) { } catch (error) {
if (fs.existsSync(settingsJsonPath)) { if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath); fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox( dialog.showErrorBox(
@@ -73,8 +71,9 @@ function reloadDB(): void {
'We encountered error while saving the certificate.' 'We encountered error while saving the certificate.'
); );
logger.error('Error while JSON parsing certificates.json: '); logger.error('Error while JSON parsing certificates.json: ');
logger.error(err); logger.error(error);
} }
} }
db = new JsonDB(settingsJsonPath, true, true); db = new JsonDB(settingsJsonPath, true, true);
} }

View File

@@ -1,4 +1,4 @@
// unescape already encoded/escaped strings // Unescape already encoded/escaped strings
export function decodeString(stringInput: string): string { export function decodeString(stringInput: string): string {
const parser = new DOMParser(); const parser = new DOMParser();
const dom = parser.parseFromString( const dom = parser.parseFromString(

View File

@@ -1,10 +1,11 @@
import { JsonDB } from 'node-json-db'; import electron from 'electron';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import electron from 'electron';
import Logger from './logger-util'; import {JsonDB} from 'node-json-db';
import * as EnterpriseUtil from './enterprise-util'; import * as EnterpriseUtil from './enterprise-util';
import Logger from './logger-util';
const logger = new Logger({ const logger = new Logger({
file: 'config-util.log', file: 'config-util.log',
@@ -28,39 +29,52 @@ let db: JsonDB;
reloadDB(); reloadDB();
export function getConfigItem(key: string, defaultValue: any = null): any { export function getConfigItem(key: string, defaultValue: unknown = null): any {
try { try {
db.reload(); db.reload();
} catch (err) { } catch (error) {
logger.error('Error while reloading settings.json: '); logger.error('Error while reloading settings.json: ');
logger.error(err); logger.error(error);
} }
const value = db.getData('/')[key]; const value = db.getData('/')[key];
if (value === undefined) { if (value === undefined) {
setConfigItem(key, defaultValue); setConfigItem(key, defaultValue);
return defaultValue; return defaultValue;
} else { }
return value; return value;
} }
export function getConfigString(key: string, defaultValue: string): string {
const value = getConfigItem(key, defaultValue);
if (typeof value === 'string') {
return value;
}
setConfigItem(key, defaultValue);
return defaultValue;
} }
// This function returns whether a key exists in the configuration file (settings.json) // This function returns whether a key exists in the configuration file (settings.json)
export function isConfigItemExists(key: string): boolean { export function isConfigItemExists(key: string): boolean {
try { try {
db.reload(); db.reload();
} catch (err) { } catch (error) {
logger.error('Error while reloading settings.json: '); logger.error('Error while reloading settings.json: ');
logger.error(err); logger.error(error);
} }
const value = db.getData('/')[key]; const value = db.getData('/')[key];
return (value !== undefined); return (value !== undefined);
} }
export function setConfigItem(key: string, value: any, override? : boolean): void { export function setConfigItem(key: string, value: unknown, override? : boolean): void {
if (EnterpriseUtil.configItemExists(key) && !override) { if (EnterpriseUtil.configItemExists(key) && !override) {
// if item is in global config and we're not trying to override // If item is in global config and we're not trying to override
return; return;
} }
db.push(`/${key}`, value, true); db.push(`/${key}`, value, true);
db.save(); db.save();
} }
@@ -75,7 +89,7 @@ function reloadDB(): void {
try { try {
const file = fs.readFileSync(settingsJsonPath, 'utf8'); const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file); JSON.parse(file);
} catch (err) { } catch (error) {
if (fs.existsSync(settingsJsonPath)) { if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath); fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox( dialog.showErrorBox(
@@ -83,9 +97,10 @@ function reloadDB(): void {
'We encountered an error while saving the settings.' 'We encountered an error while saving the settings.'
); );
logger.error('Error while JSON parsing settings.json: '); logger.error('Error while JSON parsing settings.json: ');
logger.error(err); logger.error(error);
logger.reportSentry(err); logger.reportSentry(error);
} }
} }
db = new JsonDB(settingsJsonPath, true, true); db = new JsonDB(settingsJsonPath, true, true);
} }

View File

@@ -14,7 +14,7 @@ const logDir = `${zulipDir}/Logs/`;
const certificatesDir = `${zulipDir}/certificates/`; const certificatesDir = `${zulipDir}/certificates/`;
const configDir = `${zulipDir}/config/`; const configDir = `${zulipDir}/config/`;
export const initSetUp = (): void => { export const initSetUp = (): void => {
// if it is the first time the app is running // If it is the first time the app is running
// create zulip dir in userData folder to // create zulip dir in userData folder to
// avoid errors // avoid errors
if (!setupCompleted) { if (!setupCompleted) {
@@ -63,7 +63,7 @@ export const initSetUp = (): void => {
fs.unlinkSync(data.path); fs.unlinkSync(data.path);
} }
}); });
// window-state.json is only deleted not moved, as the electron-window-state // `window-state.json` is only deleted not moved, as the electron-window-state
// package will recreate the file in the config folder. // package will recreate the file in the config folder.
if (fs.existsSync(windowStateJson)) { if (fs.existsSync(windowStateJson)) {
fs.unlinkSync(windowStateJson); fs.unlinkSync(windowStateJson);

View File

@@ -1,28 +1,28 @@
import * as ConfigUtil from './config-util'; import * as ConfigUtil from './config-util';
// TODO: TypeScript - add to Setting interface type SettingName = 'showNotification' | 'silent' | 'flashTaskbarOnMessage';
// the list of settings since we have fixed amount of them
// We want to do this by creating a new module that exports export interface DNDSettings {
// this interface showNotification?: boolean;
interface Setting { silent?: boolean;
[key: string]: any; flashTaskbarOnMessage?: boolean;
} }
interface Toggle { interface Toggle {
dnd: boolean; dnd: boolean;
newSettings: Setting; newSettings: DNDSettings;
} }
export function toggle(): Toggle { export function toggle(): Toggle {
const dnd = !ConfigUtil.getConfigItem('dnd', false); const dnd = !ConfigUtil.getConfigItem('dnd', false);
const dndSettingList = ['showNotification', 'silent']; const dndSettingList: SettingName[] = ['showNotification', 'silent'];
if (process.platform === 'win32') { if (process.platform === 'win32') {
dndSettingList.push('flashTaskbarOnMessage'); dndSettingList.push('flashTaskbarOnMessage');
} }
let newSettings: Setting; let newSettings: DNDSettings;
if (dnd) { if (dnd) {
const oldSettings: Setting = {}; const oldSettings: DNDSettings = {};
newSettings = {}; newSettings = {};
// Iterate through the dndSettingList. // Iterate through the dndSettingList.

View File

@@ -1,23 +1,20 @@
import { JsonDB } from 'node-json-db'; import {remote, ipcRenderer} from 'electron';
import escape from 'escape-html';
import request from 'request';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import Logger from './logger-util';
import { ipcRenderer, remote } from 'electron';
import * as RequestUtil from './request-util'; import {JsonDB} from 'node-json-db';
import * as EnterpriseUtil from './enterprise-util';
import * as Messages from '../../../resources/messages'; import * as Messages from '../../../resources/messages';
import * as EnterpriseUtil from './enterprise-util';
import Logger from './logger-util';
const {app, dialog} = remote; const {app, dialog} = remote;
interface ServerConf { export interface ServerConf {
url: string; url: string;
alias?: string; alias?: string;
icon?: string; icon?: string;
ignoreCerts?: boolean;
} }
const logger = new Logger({ const logger = new Logger({
@@ -27,25 +24,27 @@ const logger = new Logger({
const defaultIconUrl = '../renderer/img/icon.png'; const defaultIconUrl = '../renderer/img/icon.png';
export let db: JsonDB; let db: JsonDB;
reloadDB(); reloadDB();
// Migrate from old schema // Migrate from old schema
if (db.getData('/').domain) { if (db.getData('/').domain) {
addDomain({ (async () => {
await addDomain({
alias: 'Zulip', alias: 'Zulip',
url: db.getData('/domain') url: db.getData('/domain')
}); });
db.delete('/domain'); db.delete('/domain');
})();
} }
export function getDomains(): ServerConf[] { export function getDomains(): ServerConf[] {
reloadDB(); reloadDB();
if (db.getData('/').domains === undefined) { if (db.getData('/').domains === undefined) {
return []; return [];
} else {
return db.getData('/domains');
} }
return db.getData('/domains');
} }
export function getDomain(index: number): ServerConf { export function getDomain(index: number): ServerConf {
@@ -53,25 +52,14 @@ export function getDomain(index: number): ServerConf {
return db.getData(`/domains[${index}]`); return db.getData(`/domains[${index}]`);
} }
export function shouldIgnoreCerts(url: string): boolean { export function updateDomain(index: number, server: ServerConf): void {
const domains = getDomains();
for (const domain of domains) {
if (domain.url === url) {
return domain.ignoreCerts;
}
}
return null;
}
function updateDomain(index: number, server: ServerConf): void {
reloadDB(); reloadDB();
db.push(`/domains[${index}]`, server, true); db.push(`/domains[${index}]`, server, true);
} }
export async function addDomain(server: ServerConf): Promise<void> { export async function addDomain(server: ServerConf): Promise<void> {
const { ignoreCerts } = server;
if (server.icon) { if (server.icon) {
const localIconUrl = await saveServerIcon(server, ignoreCerts); const localIconUrl = await saveServerIcon(server);
server.icon = localIconUrl; server.icon = localIconUrl;
db.push('/domains[]', server, true); db.push('/domains[]', server, true);
reloadDB(); reloadDB();
@@ -91,6 +79,7 @@ export function removeDomain(index: number): boolean {
if (EnterpriseUtil.isPresetOrg(getDomain(index).url)) { if (EnterpriseUtil.isPresetOrg(getDomain(index).url)) {
return false; return false;
} }
db.delete(`/domains[${index}]`); db.delete(`/domains[${index}]`);
reloadDB(); reloadDB();
return true; return true;
@@ -102,44 +91,28 @@ export function duplicateDomain(domain: string): boolean {
return getDomains().some(server => server.url === domain); return getDomains().some(server => server.url === domain);
} }
async function checkCertError(domain: string, serverConf: ServerConf, error: string, silent: boolean): Promise<ServerConf> { async function checkCertError(domain: string, serverConf: ServerConf, error: any, silent: boolean): Promise<ServerConf> {
if (silent) { if (silent) {
// since getting server settings has already failed // Since getting server settings has already failed
return serverConf; return serverConf;
} else { }
// Report error to sentry to get idea of possible certificate errors // Report error to sentry to get idea of possible certificate errors
// users get when adding the servers // users get when adding the servers
logger.reportSentry(new Error(error).toString()); logger.reportSentry(error);
const certErrorMessage = Messages.certErrorMessage(domain, error); const certErrorMessage = Messages.certErrorMessage(domain, error);
const certErrorDetail = Messages.certErrorDetail(); const certErrorDetail = Messages.certErrorDetail();
const { response } = await dialog.showMessageBox({ await dialog.showMessageBox({
type: 'warning', type: 'error',
buttons: ['Yes', 'No'], buttons: ['OK'],
defaultId: 1,
message: certErrorMessage, message: certErrorMessage,
detail: certErrorDetail detail: certErrorDetail
}); });
if (response === 0) {
// set ignoreCerts parameter to true in case user responds with yes
serverConf.ignoreCerts = true;
try {
return await getServerSettings(domain, serverConf.ignoreCerts);
} catch (_) {
if (error === Messages.noOrgsError(domain)) {
throw new Error(error);
}
return serverConf;
}
} else {
throw new Error('Untrusted certificate.'); throw new Error('Untrusted certificate.');
} }
}
}
// ignoreCerts parameter helps in fetching server icon and export async function checkDomain(domain: string, silent = false): Promise<ServerConf> {
// other server details when user chooses to ignore certificate warnings
export async function checkDomain(domain: string, ignoreCerts = false, silent = false): Promise<ServerConf> {
if (!silent && duplicateDomain(domain)) { if (!silent && duplicateDomain(domain)) {
// Do not check duplicate in silent mode // Do not check duplicate in silent mode
throw new Error('This server has been added.'); throw new Error('This server has been added.');
@@ -150,118 +123,58 @@ export async function checkDomain(domain: string, ignoreCerts = false, silent =
const serverConf = { const serverConf = {
icon: defaultIconUrl, icon: defaultIconUrl,
url: domain, url: domain,
alias: domain, alias: domain
ignoreCerts
}; };
try { try {
return await getServerSettings(domain, serverConf.ignoreCerts); return await getServerSettings(domain);
} catch (err) { } catch (error_) {
// Make sure that error is an error or string not undefined // Make sure that error is an error or string not undefined
// so validation does not throw error. // so validation does not throw error.
const error = err || ''; const error = error_ || '';
const certsError = error.toString().includes('certificate'); const certsError = error.toString().includes('certificate');
if (certsError) { if (certsError) {
const result = await checkCertError(domain, serverConf, error, silent); const result = await checkCertError(domain, serverConf, error, silent);
return result; return result;
} else { }
throw new Error(Messages.invalidZulipServerError(domain)); throw new Error(Messages.invalidZulipServerError(domain));
} }
} }
async function getServerSettings(domain: string): Promise<ServerConf> {
return ipcRenderer.invoke('get-server-settings', domain);
} }
async function getServerSettings(domain: string, ignoreCerts = false): Promise<ServerConf> { export async function saveServerIcon(server: ServerConf): Promise<string> {
const serverSettingsOptions = { return ipcRenderer.invoke('save-server-icon', server.icon);
url: domain + '/api/v1/server_settings',
...RequestUtil.requestOptions(domain, ignoreCerts)
};
return new Promise((resolve, reject) => {
request(serverSettingsOptions, (error: string, response: any) => {
if (!error && response.statusCode === 200) {
const data = JSON.parse(response.body);
if (Object.prototype.hasOwnProperty.call(data, 'realm_icon') && data.realm_icon) {
resolve({
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
url: data.realm_uri,
alias: escape(data.realm_name),
ignoreCerts
});
} else {
reject(Messages.noOrgsError(domain));
}
} else {
reject(response);
}
});
});
}
export async function saveServerIcon(server: ServerConf, ignoreCerts = false): Promise<string> {
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 = generateFilePath(url);
const file = fs.createWriteStream(filePath);
try {
request(serverIconOptions).on('response', (response: any) => {
response.on('error', (err: string) => {
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: string) => {
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
});
} catch (err) {
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
}
});
} }
export async function updateSavedServer(url: string, index: number): Promise<void> { export async function updateSavedServer(url: string, index: number): Promise<void> {
// Does not promise successful update // Does not promise successful update
const oldIcon = getDomain(index).icon; const oldIcon = getDomain(index).icon;
const { ignoreCerts } = getDomain(index);
try { try {
const newServerConf = await checkDomain(url, ignoreCerts, true); const newServerConf = await checkDomain(url, true);
const localIconUrl = await saveServerIcon(newServerConf, ignoreCerts); const localIconUrl = await saveServerIcon(newServerConf);
if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') { if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') {
newServerConf.icon = localIconUrl; newServerConf.icon = localIconUrl;
updateDomain(index, newServerConf); updateDomain(index, newServerConf);
reloadDB(); reloadDB();
} }
} catch (err) { } catch (error) {
ipcRenderer.send('forward-message', 'show-network-error', index); logger.log('Could not update server icon.');
logger.log(error);
logger.reportSentry(error);
} }
} }
export function reloadDB(): void { function reloadDB(): void {
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json'); const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
try { try {
const file = fs.readFileSync(domainJsonPath, 'utf8'); const file = fs.readFileSync(domainJsonPath, 'utf8');
JSON.parse(file); JSON.parse(file);
} catch (err) { } catch (error) {
if (fs.existsSync(domainJsonPath)) { if (fs.existsSync(domainJsonPath)) {
fs.unlinkSync(domainJsonPath); fs.unlinkSync(domainJsonPath);
dialog.showErrorBox( dialog.showErrorBox(
@@ -270,38 +183,22 @@ export function reloadDB(): void {
'you may have to re-add your previous organizations back.' 'you may have to re-add your previous organizations back.'
); );
logger.error('Error while JSON parsing domain.json: '); logger.error('Error while JSON parsing domain.json: ');
logger.error(err); logger.error(error);
logger.reportSentry(err); logger.reportSentry(error);
} }
} }
db = new JsonDB(domainJsonPath, true, true); db = new JsonDB(domainJsonPath, true, true);
} }
function generateFilePath(url: string): string {
const dir = `${app.getPath('userData')}/server-icons`;
const extension = path.extname(url).split('?')[0];
let hash = 5381;
let { length } = url;
while (length) {
hash = (hash * 33) ^ url.charCodeAt(--length);
}
// Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return `${dir}/${hash >>> 0}${extension}`;
}
export function formatUrl(domain: string): string { export function formatUrl(domain: string): string {
if (domain.startsWith('http://') || domain.startsWith('https://')) { if (domain.startsWith('http://') || domain.startsWith('https://')) {
return domain; return domain;
} }
if (domain.startsWith('localhost:')) { if (domain.startsWith('localhost:')) {
return `http://${domain}`; return `http://${domain}`;
} }
return `https://${domain}`; return `https://${domain}`;
} }

View File

@@ -8,9 +8,9 @@ const logger = new Logger({
timestamp: true timestamp: true
}); });
// todo: replace enterpriseSettings type with an interface once settings are final // TODO: replace enterpriseSettings type with an interface once settings are final
export let enterpriseSettings: any; let enterpriseSettings: {[key: string]: unknown};
export let configFile: boolean; let configFile: boolean;
reloadDB(); reloadDB();
@@ -26,23 +26,29 @@ function reloadDB(): void {
try { try {
const file = fs.readFileSync(enterpriseFile, 'utf8'); const file = fs.readFileSync(enterpriseFile, 'utf8');
enterpriseSettings = JSON.parse(file); enterpriseSettings = JSON.parse(file);
} catch (err) { } catch (error) {
logger.log('Error while JSON parsing global_config.json: '); logger.log('Error while JSON parsing global_config.json: ');
logger.log(err); logger.log(error);
} }
} else { } else {
configFile = false; configFile = false;
} }
} }
export function getConfigItem(key: string, defaultValue?: any): any { export function hasConfigFile(): boolean {
return configFile;
}
export function getConfigItem(key: string, defaultValue?: unknown): any {
reloadDB(); reloadDB();
if (!configFile) { if (!configFile) {
return defaultValue; return defaultValue;
} }
if (defaultValue === undefined) { if (defaultValue === undefined) {
defaultValue = null; defaultValue = null;
} }
return configItemExists(key) ? enterpriseSettings[key] : defaultValue; return configItemExists(key) ? enterpriseSettings[key] : defaultValue;
} }
@@ -51,6 +57,7 @@ export function configItemExists(key: string): boolean {
if (!configFile) { if (!configFile) {
return false; return false;
} }
return (enterpriseSettings[key] !== undefined); return (enterpriseSettings[key] !== undefined);
} }
@@ -58,11 +65,17 @@ export function isPresetOrg(url: string): boolean {
if (!configFile || !configItemExists('presetOrganizations')) { if (!configFile || !configItemExists('presetOrganizations')) {
return false; return false;
} }
const presetOrgs = enterpriseSettings.presetOrganizations; const presetOrgs = enterpriseSettings.presetOrganizations;
if (!Array.isArray(presetOrgs)) {
throw new TypeError('Expected array for presetOrgs');
}
for (const org of presetOrgs) { for (const org of presetOrgs) {
if (url.includes(org)) { if (url.includes(org)) {
return true; return true;
} }
} }
return false; return false;
} }

View File

@@ -1,16 +1,17 @@
import {shell} from 'electron'; import {shell} from 'electron';
import escape from 'escape-html';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import escape from 'escape-html';
export function isUploadsUrl(server: string, url: URL): boolean { export function isUploadsUrl(server: string, url: URL): boolean {
return url.origin === server && url.pathname.startsWith('/user_uploads/'); return url.origin === server && url.pathname.startsWith('/user_uploads/');
} }
export function openBrowser(url: URL): void { export async function openBrowser(url: URL): Promise<void> {
if (['http:', 'https:', 'mailto:'].includes(url.protocol)) { if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
shell.openExternal(url.href); await shell.openExternal(url.href);
} else { } else {
// For security, indirect links to non-whitelisted protocols // For security, indirect links to non-whitelisted protocols
// through a real web browser via a local HTML file. // through a real web browser via a local HTML file.
@@ -36,7 +37,7 @@ export function openBrowser(url: URL): void {
</body> </body>
</html> </html>
`); `);
shell.openItem(file); await shell.openPath(file);
setTimeout(() => { setTimeout(() => {
fs.unlinkSync(file); fs.unlinkSync(file);
fs.rmdirSync(dir); fs.rmdirSync(dir);

View File

@@ -1,8 +1,9 @@
import { JsonDB } from 'node-json-db'; import electron from 'electron';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import electron from 'electron';
import {JsonDB} from 'node-json-db';
import Logger from './logger-util'; import Logger from './logger-util';
const remote = const remote =
@@ -20,18 +21,18 @@ let db: JsonDB;
reloadDB(); reloadDB();
export function getUpdateItem(key: string, defaultValue: any = null): any { export function getUpdateItem(key: string, defaultValue: unknown = null): any {
reloadDB(); reloadDB();
const value = db.getData('/')[key]; const value = db.getData('/')[key];
if (value === undefined) { if (value === undefined) {
setUpdateItem(key, defaultValue); setUpdateItem(key, defaultValue);
return defaultValue; return defaultValue;
} else {
return value;
}
} }
export function setUpdateItem(key: string, value: any): void { return value;
}
export function setUpdateItem(key: string, value: unknown): void {
db.push(`/${key}`, value, true); db.push(`/${key}`, value, true);
reloadDB(); reloadDB();
} }
@@ -46,7 +47,7 @@ function reloadDB(): void {
try { try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8'); const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
JSON.parse(file); JSON.parse(file);
} catch (err) { } catch (error) {
if (fs.existsSync(linuxUpdateJsonPath)) { if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath); fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox( dialog.showErrorBox(
@@ -54,8 +55,9 @@ function reloadDB(): void {
'We encountered an error while saving the update notifications.' 'We encountered an error while saving the update notifications.'
); );
logger.error('Error while JSON parsing updates.json: '); logger.error('Error while JSON parsing updates.json: ');
logger.error(err); logger.error(error);
} }
} }
db = new JsonDB(linuxUpdateJsonPath, true, true); db = new JsonDB(linuxUpdateJsonPath, true, true);
} }

View File

@@ -1,16 +1,12 @@
import { Console as NodeConsole } from 'console'; // eslint-disable-line node/prefer-global/console import {Console} from 'console'; // eslint-disable-line node/prefer-global/console
import { initSetUp } from './default-util'; import electron from 'electron';
import { sentryInit, captureException } from './sentry-util';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import electron from 'electron';
// this interface adds [key: string]: any so import {initSetUp} from './default-util';
// we can do console[type] later on in the code import {sentryInit, captureException} from './sentry-util';
interface PatchedConsole extends Console {
[key: string]: any;
}
interface LoggerOptions { interface LoggerOptions {
timestamp?: true | (() => string); timestamp?: true | (() => string);
@@ -31,7 +27,7 @@ if (process.type === 'renderer') {
// For main process, sentryInit() is handled in index.js // For main process, sentryInit() is handled in index.js
const {ipcRenderer} = electron; const {ipcRenderer} = electron;
ipcRenderer.send('error-reporting'); ipcRenderer.send('error-reporting');
ipcRenderer.on('error-reporting-val', (_event: any, errorReporting: boolean) => { ipcRenderer.on('error-reporting-val', (_event: Event, errorReporting: boolean) => {
reportErrors = errorReporting; reportErrors = errorReporting;
if (reportErrors) { if (reportErrors) {
sentryInit(); sentryInit();
@@ -41,15 +37,23 @@ if (process.type === 'renderer') {
app = electron.app; app = electron.app;
} }
const browserConsole: PatchedConsole = console;
const logDir = `${app.getPath('userData')}/Logs`; const logDir = `${app.getPath('userData')}/Logs`;
type Level = 'log' | 'debug' | 'info' | 'warn' | 'error';
const levels: Level[] = ['log', 'debug', 'info', 'warn', 'error'];
type LogMethod = (...args: unknown[]) => void;
export default class Logger { export default class Logger {
nodeConsole: PatchedConsole; log: LogMethod;
debug: LogMethod;
info: LogMethod;
warn: LogMethod;
error: LogMethod;
nodeConsole: Console;
timestamp?: () => string; timestamp?: () => string;
level: boolean; level: boolean;
logInDevMode: boolean; logInDevMode: boolean;
[key: string]: any;
constructor(options: LoggerOptions = {}) { constructor(options: LoggerOptions = {}) {
let { let {
@@ -72,7 +76,7 @@ export default class Logger {
} }
const fileStream = fs.createWriteStream(file, {flags: 'a'}); const fileStream = fs.createWriteStream(file, {flags: 'a'});
const nodeConsole = new NodeConsole(fileStream); const nodeConsole = new Console(fileStream);
this.nodeConsole = nodeConsole; this.nodeConsole = nodeConsole;
this.timestamp = timestamp; this.timestamp = timestamp;
@@ -81,42 +85,38 @@ export default class Logger {
this.setUpConsole(); this.setUpConsole();
} }
_log(type: string, ...args: any[]): void { _log(type: Level, ...args: unknown[]): void {
const { const {
nodeConsole, timestamp, level, logInDevMode nodeConsole, timestamp, level, logInDevMode
} = this; } = this;
let nodeConsoleLog;
/* eslint-disable no-fallthrough */
switch (true) { switch (true) {
case typeof timestamp === 'function': case typeof timestamp === 'function':
args.unshift(timestamp() + ' |\t'); args.unshift(timestamp() + ' |\t');
// Fall through
case (level): case (level):
args.unshift(type.toUpperCase() + ' |'); args.unshift(type.toUpperCase() + ' |');
// Fall through
case isDev || logInDevMode: case isDev || logInDevMode:
nodeConsoleLog = nodeConsole[type] || nodeConsole.log; nodeConsole[type](...args);
nodeConsoleLog.apply(null, args); // eslint-disable-line prefer-spread break;
default: break; default:
} }
/* eslint-enable no-fallthrough */
browserConsole[type].apply(null, args); console[type](...args);
} }
setUpConsole(): void { setUpConsole(): void {
for (const type of Object.keys(browserConsole)) { for (const type of levels) {
this.setupConsoleMethod(type); this.setupConsoleMethod(type);
} }
} }
setupConsoleMethod(type: string): void { setupConsoleMethod(type: Level): void {
this[type] = (...args: any[]) => { this[type] = (...args: unknown[]) => this._log(type, ...args);
const log = this._log.bind(this, type, ...args);
log();
};
} }
getTimestamp(): string { getTimestamp(): string {
@@ -127,7 +127,7 @@ export default class Logger {
return timestamp; return timestamp;
} }
reportSentry(err: string): void { reportSentry(err: unknown): void {
if (reportErrors) { if (reportErrors) {
captureException(err); captureException(err);
} }
@@ -138,6 +138,7 @@ export default class Logger {
if (err) { if (err) {
throw err; throw err;
} }
const MAX_LOG_FILE_LINES = 500; const MAX_LOG_FILE_LINES = 500;
const logs = data.split(os.EOL); const logs = data.split(os.EOL);
const logLength = logs.length - 1; const logLength = logs.length - 1;

View File

@@ -5,45 +5,6 @@ export interface ProxyRule {
port?: number; port?: number;
} }
// Return proxy to be used for a particular uri, to be used for request
export function getProxy(_uri: string): ProxyRule | void {
let uri;
try {
uri = new URL(_uri);
} catch (err) {
return;
}
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: any = {};
if (uri.protocol === 'http:') {
proxyRules.forEach((proxy: string) => {
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: string) => {
if (proxy.includes('https=')) {
proxyRule.hostname = proxy.split('https=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('https=')[1].trim().split(':')[1];
}
});
return proxyRule;
}
}
// TODO: Refactor to async function // TODO: Refactor to async function
export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Promise<void> { export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Promise<void> {
const page = mainWindow.webContents; const page = mainWindow.webContents;
@@ -55,12 +16,13 @@ export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Pr
const proxy = await ses.resolveProxy('http://' + resolveProxyUrl); const proxy = await ses.resolveProxy('http://' + resolveProxyUrl);
let httpString = ''; let httpString = '';
if (proxy !== 'DIRECT') { if (proxy !== 'DIRECT') {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY // 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 // for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY') || proxy.includes('HTTPS')) { if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
httpString = 'http=' + proxy.split('PROXY')[1] + ';'; httpString = 'http=' + proxy.split('PROXY')[1] + ';';
} }
} }
return httpString; return httpString;
})(); })();
// Check HTTPS Proxy // Check HTTPS Proxy
@@ -68,12 +30,13 @@ export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Pr
const proxy = await ses.resolveProxy('https://' + resolveProxyUrl); const proxy = await ses.resolveProxy('https://' + resolveProxyUrl);
let httpsString = ''; let httpsString = '';
if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) { if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY // 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 // for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY') || proxy.includes('HTTPS')) { if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
httpsString += 'https=' + proxy.split('PROXY')[1] + ';'; httpsString += 'https=' + proxy.split('PROXY')[1] + ';';
} }
} }
return httpsString; return httpsString;
})(); })();
@@ -86,6 +49,7 @@ export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Pr
ftpString += 'ftp=' + proxy.split('PROXY')[1] + ';'; ftpString += 'ftp=' + proxy.split('PROXY')[1] + ';';
} }
} }
return ftpString; return ftpString;
})(); })();
@@ -102,6 +66,7 @@ export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Pr
socksString += 'socks=' + proxy.split('PROXY')[1] + ';'; socksString += 'socks=' + proxy.split('PROXY')[1] + ';';
} }
} }
return socksString; return socksString;
})(); })();

View File

@@ -1,11 +1,10 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import type WebView from '../components/webview';
import backoff from 'backoff'; import backoff from 'backoff';
import request from 'request';
import type WebView from '../components/webview';
import Logger from './logger-util'; import Logger from './logger-util';
import * as RequestUtil from './request-util';
import * as DomainUtil from './domain-util';
const logger = new Logger({ const logger = new Logger({
file: 'domain-util.log', file: 'domain-util.log',
@@ -33,27 +32,7 @@ export default class ReconnectUtil {
} }
async isOnline(): Promise<boolean> { async isOnline(): Promise<boolean> {
return new Promise(resolve => { return ipcRenderer.invoke('is-online', this.url);
try {
const ignoreCerts = DomainUtil.shouldIgnoreCerts(this.url);
if (ignoreCerts === null) {
return;
}
request(
{
url: `${this.url}/static/favicon.ico`,
...RequestUtil.requestOptions(this.url, ignoreCerts)
},
(error: Error, response: any) => {
const isValidResponse =
!error && response.statusCode >= 200 && response.statusCode < 400;
resolve(isValidResponse);
}
);
} catch (err) {
logger.log(err);
}
});
} }
pollInternetAndReload(): void { pollInternetAndReload(): void {
@@ -71,11 +50,13 @@ export default class ReconnectUtil {
if (this.alreadyReloaded) { if (this.alreadyReloaded) {
return true; return true;
} }
if (await this.isOnline()) { if (await this.isOnline()) {
ipcRenderer.send('forward-message', 'reload-viewer'); ipcRenderer.send('forward-message', 'reload-viewer');
logger.log('You\'re back online.'); logger.log('You\'re back online.');
return true; return true;
} }
logger.log('There is no internet connection, try checking network cables, modem and router.'); logger.log('There is no internet connection, try checking network cables, modem and router.');
const errorMessageHolder = document.querySelector('#description'); const errorMessageHolder = document.querySelector('#description');
if (errorMessageHolder) { if (errorMessageHolder) {
@@ -83,6 +64,7 @@ export default class ReconnectUtil {
<div>Your internet connection doesn't seem to work properly!</div> <div>Your internet connection doesn't seem to work properly!</div>
<div>Verify that it works and then click try again.</div>`; <div>Verify that it works and then click try again.</div>`;
} }
return false; return false;
} }
} }

View File

@@ -1,70 +0,0 @@
import { remote } from 'electron';
import fs from 'fs';
import path from 'path';
import * as ConfigUtil from './config-util';
import Logger from './logger-util';
import * as ProxyUtil from './proxy-util';
import * as CertificateUtil from './certificate-util';
import * as SystemUtil from './system-util';
const { app } = remote;
const logger = new Logger({
file: 'request-util.log',
timestamp: true
});
interface RequestUtilResponse {
ca: string;
proxy: string | void | ProxyUtil.ProxyRule;
ecdhCurve: 'auto';
headers: { 'User-Agent': string };
rejectUnauthorized: boolean;
}
// ignoreCerts parameter helps in fetching server icon and
// other server details when user chooses to ignore certificate warnings
export function requestOptions(domain: string, ignoreCerts: boolean): RequestUtilResponse {
domain = formatUrl(domain);
const certificate = CertificateUtil.getCertificate(
encodeURIComponent(domain)
);
let certificateFile = null;
if (certificate?.includes('/')) {
// certificate saved using old app version
certificateFile = certificate;
} else if (certificate) {
certificateFile = path.join(`${app.getPath('userData')}/certificates`, certificate);
}
let certificateLocation = '';
if (certificate) {
// To handle case where certificate has been moved from the location in certificates.json
try {
certificateLocation = fs.readFileSync(certificateFile, 'utf8');
} 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
};
}
function formatUrl(domain: string): string {
const hasPrefix = domain.startsWith('http', 0);
if (hasPrefix) {
return domain;
} else {
return domain.includes('localhost:') ? `http://${domain}` : `https://${domain}`;
}
}

View File

@@ -1,5 +1,4 @@
import {init} from '@sentry/electron'; import {init} from '@sentry/electron';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
export const sentryInit = (): void => { export const sentryInit = (): void => {
@@ -10,7 +9,7 @@ export const sentryInit = (): void => {
// This error mainly comes from the console logs. // This error mainly comes from the console logs.
// This is a temp solution until Sentry supports disabling 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'] 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 /// sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second
}); });
} }
}; };

View File

@@ -1,9 +1,5 @@
import { remote } from 'electron'; import {ipcRenderer} from 'electron';
import os from 'os'; import os from 'os';
import * as ConfigUtil from './config-util';
const { app } = remote;
export const connectivityERR: string[] = [ export const connectivityERR: string[] = [
'ERR_INTERNET_DISCONNECTED', 'ERR_INTERNET_DISCONNECTED',
@@ -14,32 +10,29 @@ export const connectivityERR: string[] = [
'ERR_NETWORK_CHANGED' 'ERR_NETWORK_CHANGED'
]; ];
let userAgent: string | null = null; const userAgent = ipcRenderer.sendSync('fetch-user-agent');
export function getOS(): string { export function getOS(): string {
const platform = os.platform(); const platform = os.platform();
if (platform === 'darwin') { if (platform === 'darwin') {
return 'Mac'; return 'Mac';
} else if (platform === 'linux') { }
if (platform === 'linux') {
return 'Linux'; return 'Linux';
} else if (platform === 'win32') { }
if (parseFloat(os.release()) < 6.2) {
if (platform === 'win32') {
if (Number.parseFloat(os.release()) < 6.2) {
return 'Windows 7'; return 'Windows 7';
} else { }
return 'Windows 10'; return 'Windows 10';
} }
} else {
return ''; return '';
} }
}
export function setUserAgent(webViewUserAgent: string): void { export function getUserAgent(): string {
userAgent = `ZulipElectron/${app.getVersion()} ${webViewUserAgent}`;
}
export function getUserAgent(): string | null {
if (!userAgent) {
setUserAgent(ConfigUtil.getConfigItem('userAgent', null));
}
return userAgent; return userAgent;
} }

View File

@@ -1,20 +1,18 @@
import path from 'path'; import path from 'path';
import electron from 'electron';
import i18n from 'i18n'; import i18n from 'i18n';
let app: Electron.App = null; import * as ConfigUtil from './config-util';
/* To make the util runnable in both main and renderer process */
if (process.type === 'renderer') {
app = electron.remote.app;
} else {
app = electron.app;
}
i18n.configure({ i18n.configure({
directory: path.join(__dirname, '../../../translations/') directory: path.join(__dirname, '../../../translations/'),
updateFiles: false
}); });
/* Fetches the current appLocale from settings.json */
const appLocale = ConfigUtil.getConfigItem('appLanguage');
/* If no locale present in the json, en is set default */
export function __(phrase: string): string { export function __(phrase: string): string {
return i18n.__({ phrase, locale: app.getLocale() ? app.getLocale() : 'en' }); return i18n.__({phrase, locale: appLocale ? appLocale : 'en'});
} }

View File

@@ -52,7 +52,7 @@
</div> </div>
<div id="feedback-modal"> <div id="feedback-modal">
<send-feedback></send-feedback> <send-feedback show-cancel-button="show"></send-feedback>
</div> </div>
</body> </body>

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<title>Zulip - Settings</title> <title>Zulip - Settings</title>
<link rel="stylesheet" href="css/preference.css" type="text/css" media="screen"> <link rel="stylesheet" href="css/preference.css" type="text/css" media="screen">
<link id="tagify-css" rel="stylesheet" href="data:text/css,">
</head> </head>
<body> <body>
<div id="content"> <div id="content">
@@ -13,6 +14,7 @@
</div> </div>
</body> </body>
<script> <script>
document.querySelector('#tagify-css').href = require.resolve('@yaireo/tagify/dist/tagify.css');
require('./js/pages/preference/preference.js'); require('./js/pages/preference/preference.js');
require('./js/shared/preventdrag.js') require('./js/shared/preventdrag.js')
</script> </script>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -19,14 +19,12 @@ export function noOrgsError(domain: string): string {
} }
export function certErrorMessage(domain: string, error: string): string { export function certErrorMessage(domain: string, error: string): string {
return `Do you trust certificate from ${domain}? \n ${error}`; return `Certificate error for ${domain}\n${error}`;
} }
export function certErrorDetail(): string { export function certErrorDetail(): string {
return `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. return `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. \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.`;
} }
export function enterpriseOrgError(length: number, domains: string[]): DialogBoxError { export function enterpriseOrgError(length: number, domains: string[]): DialogBoxError {
@@ -34,6 +32,7 @@ export function enterpriseOrgError(length: number, domains: string[]): DialogBox
for (const domain of domains) { for (const domain of domains) {
domainList += `${domain}\n`; domainList += `${domain}\n`;
} }
return { return {
title: `Could not add the following ${length === 1 ? 'organization' : 'organizations'}`, title: `Could not add the following ${length === 1 ? 'organization' : 'organizations'}`,
content: `${domainList}\nPlease contact your system administrator.` content: `${domainList}\nPlease contact your system administrator.`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 737 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 13 KiB

134
app/translations/ar.json Normal file
View File

@@ -0,0 +1,134 @@
{
"About Zulip": "حول \"زوليب\"",
"Actual Size": "الحجم الفعلي",
"Add Custom Certificates": "إضافة رخصة معدلة",
"Add Organization": "إضافة منظمة",
"Add a Zulip organization": "إضافة منظمة \"زوليب\"",
"Add custom CSS": "إصافة CSS معدلة",
"Advanced": "متقدم",
"All the connected organizations will appear here": "جميع المنظمات المتصلة تعرض هنا",
"Always start minimized": "دائماً إبدأ بالقليل",
"App Updates": "تحديث التطبيق",
"Appearance": "المظهر",
"Application Shortcuts": "إختصارات التطبيق",
"Are you sure you want to disconnect this organization?": "هل أنت متأكد من فصل هذة المنظمة؟",
"Auto hide Menu bar": "أخف القائمة تلقائياً",
"Auto hide menu bar (Press Alt key to display)": "أخف القائمة تلقائياً (إضغط Alt لعرض القائمة)",
"Back": "رجوع",
"Bounce dock on new private message": "أخرج المنصة في حال رسالة خاصة جديدة",
"Certificate file": "ملف الشهادة",
"Change": "تغيير",
"Check for Updates": "التحقق من التحديثات",
"Close": "اغلاق",
"Connect": "اتصال",
"Connect to another organization": "التوصيل مع منظمة أخرى",
"Connected organizations": "المنظمات المتصلة",
"Copy": "نسخ",
"Copy Zulip URL": "نسخ رابط زوليب",
"Create a new organization": "Create a new organization",
"Cut": "قص",
"Default download location": "موقع التحميل الافتراضي",
"Delete": "حذف",
"Desktop App Settings": "إعدادت تطبيق سطح المكتب",
"Desktop Notifications": "إشعارات سطح المكتب",
"Desktop Settings": "إعدادات سطح المكتب",
"Disconnect": "قطع الاتصال",
"Download App Logs": "تنزيل سجلات التطبيق",
"Edit": "تعديل",
"Edit Shortcuts": "تعديل الاختصارات",
"Enable auto updates": "تفعيل التحديثات التلقائية",
"Enable error reporting (requires restart)": "تفعيل تقارير الأخطاء (يتطلب إعادة التشغيل)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Factory Reset": "إعادة ضبط المصنع",
"File": "ملف",
"Find accounts": "Find accounts",
"Find accounts by email": "Find accounts by email",
"Flash taskbar on new message": "Flash taskbar on new message",
"Forward": "Forward",
"Functionality": "Functionality",
"General": "General",
"Get beta updates": "Get beta updates",
"Hard Reload": "Hard Reload",
"Help": "Help",
"Help Center": "Help Center",
"History": "History",
"History Shortcuts": "History Shortcuts",
"Keyboard Shortcuts": "Keyboard Shortcuts",
"Log Out": "Log Out",
"Log Out of Organization": "Log Out of Organization",
"Manual proxy configuration": "Manual proxy configuration",
"Minimize": "Minimize",
"Mute all sounds from Zulip": "Mute all sounds from Zulip",
"NO": "NO",
"Network": "Network",
"OR": "OR",
"Organization URL": "Organization URL",
"Organizations": "Organizations",
"Paste": "Paste",
"Paste and Match Style": "Paste and Match Style",
"Proxy": "Proxy",
"Proxy bypass rules": "Proxy bypass rules",
"Proxy rules": "Proxy rules",
"Quit": "Quit",
"Quit Zulip": "Quit Zulip",
"Redo": "Redo",
"Release Notes": "Release Notes",
"Reload": "Reload",
"Report an Issue": "Report an Issue",
"Save": "Save",
"Select All": "Select All",
"Settings": "Settings",
"Shortcuts": "Shortcuts",
"Show App Logs": "Show App Logs",
"Show app icon in system tray": "Show app icon in system tray",
"Show app unread badge": "Show app unread badge",
"Show desktop notifications": "Show desktop notifications",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Show sidebar": "Show sidebar",
"Start app at login": "Start app at login",
"Switch to Next Organization": "Switch to Next Organization",
"Switch to Previous Organization": "Switch to Previous Organization",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Tip": "Tip",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle Do Not Disturb": "Toggle Do Not Disturb",
"Toggle Full Screen": "Toggle Full Screen",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon",
"Tools": "Tools",
"Undo": "Undo",
"Upload": "Upload",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"View": "View",
"View Shortcuts": "View Shortcuts",
"Window": "Window",
"Window Shortcuts": "Window Shortcuts",
"YES": "YES",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Zulip Help": "Zulip Help",
"keyboard shortcuts": "keyboard shortcuts",
"script": "script",
"Quit when the window is closed": "Quit when the window is closed",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"AddServer": "AddServer",
"App language (requires restart)": "App language (requires restart)",
"Factory Reset Data": "Factory Reset Data",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Copy Link": "Copy Link",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"No Suggestion Found": "No Suggestion Found",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Spellchecker Languages": "Spellchecker Languages",
"Add to Dictionary": "Add to Dictionary",
"Look Up": "Look Up"
}

View File

@@ -1,5 +1,5 @@
{ {
"About Zulip": "За Zulip", "About Zulip": "Относно Zulip",
"Actual Size": "Действителен размер", "Actual Size": "Действителен размер",
"Add Custom Certificates": "Добавяне на персонализирани сертификати", "Add Custom Certificates": "Добавяне на персонализирани сертификати",
"Add Organization": "Добавяне на организация", "Add Organization": "Добавяне на организация",
@@ -75,9 +75,6 @@
"Release Notes": "Бележки към изданието", "Release Notes": "Бележки към изданието",
"Reload": "Презареди", "Reload": "Презареди",
"Report an Issue": "Подаване на сигнал за проблем", "Report an Issue": "Подаване на сигнал за проблем",
"Reset App Data": "Нулиране на данни за приложения",
"Reset App Settings": "Нулиране на настройките на приложението",
"Reset Application Data": "Нулиране на данните на приложението",
"Save": "Запази", "Save": "Запази",
"Select All": "Избери всички", "Select All": "Избери всички",
"Settings": "Настройки", "Settings": "Настройки",
@@ -113,5 +110,25 @@
"Zoom Out": "Отдалечавам", "Zoom Out": "Отдалечавам",
"Zulip Help": "Помощ за Zulip", "Zulip Help": "Помощ за Zulip",
"keyboard shortcuts": "комбинация от клавиши", "keyboard shortcuts": "комбинация от клавиши",
"script": "писменост" "script": "писменост",
"Quit when the window is closed": "Quit when the window is closed",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"AddServer": "AddServer",
"App language (requires restart)": "App language (requires restart)",
"Factory Reset Data": "Factory Reset Data",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Copy Link": "Copy Link",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"No Suggestion Found": "No Suggestion Found",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Spellchecker Languages": "Spellchecker Languages",
"Add to Dictionary": "Add to Dictionary",
"Look Up": "Look Up"
} }

134
app/translations/ca.json Normal file
View File

@@ -0,0 +1,134 @@
{
"About Zulip": "Quant a Zulip",
"Actual Size": "Actual Size",
"Add Custom Certificates": "Add Custom Certificates",
"Add Organization": "Add Organization",
"Add a Zulip organization": "Add a Zulip organization",
"Add custom CSS": "Add custom CSS",
"Advanced": "Advanced",
"All the connected organizations will appear here": "All the connected organizations will appear here",
"Always start minimized": "Always start minimized",
"App Updates": "App Updates",
"Appearance": "Appearance",
"Application Shortcuts": "Application Shortcuts",
"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?",
"Auto hide Menu bar": "Auto hide Menu bar",
"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)",
"Back": "Back",
"Bounce dock on new private message": "Bounce dock on new private message",
"Certificate file": "Certificate file",
"Change": "Change",
"Check for Updates": "Check for Updates",
"Close": "Tancar",
"Connect": "Connect",
"Connect to another organization": "Connect to another organization",
"Connected organizations": "Connected organizations",
"Copy": "Copia",
"Copy Zulip URL": "Copy Zulip URL",
"Create a new organization": "Crea una nova organització",
"Cut": "Cut",
"Default download location": "Default download location",
"Delete": "Elimina",
"Desktop App Settings": "Configuració de l'aplicació d'escriptori",
"Desktop Notifications": "Desktop Notifications",
"Desktop Settings": "Configuració d'escriptori",
"Disconnect": "Disconnect",
"Download App Logs": "Download App Logs",
"Edit": "Edita",
"Edit Shortcuts": "Edit Shortcuts",
"Enable auto updates": "Enable auto updates",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Factory Reset": "Factory Reset",
"File": "Fitxer",
"Find accounts": "Find accounts",
"Find accounts by email": "Find accounts by email",
"Flash taskbar on new message": "Flash taskbar on new message",
"Forward": "Forward",
"Functionality": "Functionality",
"General": "General",
"Get beta updates": "Get beta updates",
"Hard Reload": "Hard Reload",
"Help": "Help",
"Help Center": "Centre d'ajuda",
"History": "Historial",
"History Shortcuts": "Dreceres d'historial",
"Keyboard Shortcuts": "Keyboard Shortcuts",
"Log Out": "Log Out",
"Log Out of Organization": "Log Out of Organization",
"Manual proxy configuration": "Manual proxy configuration",
"Minimize": "Minimize",
"Mute all sounds from Zulip": "Silencia tots els sons de Zulip",
"NO": "NO",
"Network": "Network",
"OR": "OR",
"Organization URL": "URL d'organització",
"Organizations": "Organizations",
"Paste": "Paste",
"Paste and Match Style": "Paste and Match Style",
"Proxy": "Proxy",
"Proxy bypass rules": "Proxy bypass rules",
"Proxy rules": "Proxy rules",
"Quit": "Quit",
"Quit Zulip": "Quit Zulip",
"Redo": "Redo",
"Release Notes": "Release Notes",
"Reload": "Reload",
"Report an Issue": "Report an Issue",
"Save": "Guardar",
"Select All": "Select All",
"Settings": "Configuració",
"Shortcuts": "Shortcuts",
"Show App Logs": "Show App Logs",
"Show app icon in system tray": "Show app icon in system tray",
"Show app unread badge": "Mostrar una marca en la icona si hi ha missatges no llegits",
"Show desktop notifications": "Show desktop notifications",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Show sidebar": "Show sidebar",
"Start app at login": "Start app at login",
"Switch to Next Organization": "Switch to Next Organization",
"Switch to Previous Organization": "Switch to Previous Organization",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Tip": "Tip",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle Do Not Disturb": "Toggle Do Not Disturb",
"Toggle Full Screen": "Toggle Full Screen",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon",
"Tools": "Tools",
"Undo": "Undo",
"Upload": "Pujada",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"View": "View",
"View Shortcuts": "View Shortcuts",
"Window": "Window",
"Window Shortcuts": "Window Shortcuts",
"YES": "YES",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Zulip Help": "Zulip Help",
"keyboard shortcuts": "keyboard shortcuts",
"script": "script",
"Quit when the window is closed": "Quit when the window is closed",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"AddServer": "AddServer",
"App language (requires restart)": "App language (requires restart)",
"Factory Reset Data": "Factory Reset Data",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Copy Link": "Copy Link",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"No Suggestion Found": "No Suggestion Found",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Spellchecker Languages": "Spellchecker Languages",
"Add to Dictionary": "Add to Dictionary",
"Look Up": "Look Up"
}

View File

@@ -1,117 +1,134 @@
{ {
"About Zulip": "O uživateli Zulip", "About Zulip": "O Zulipu",
"Actual Size": "Aktuální velikost", "Actual Size": "Skutečná velikost",
"Add Custom Certificates": "Přidat vlastní certifikáty", "Add Custom Certificates": "Přidat vlastní certifikáty",
"Add Organization": "Přidat organizaci", "Add Organization": "Přidat organizaci",
"Add a Zulip organization": "Přidejte organizaci Zulip", "Add a Zulip organization": "Přidat organizaci Zulip",
"Add custom CSS": "Přidat vlastní CSS", "Add custom CSS": "Přidat vlastní CSS",
"Advanced": "Pokročilý", "Advanced": "Rozšířené",
"All the connected organizations will appear here": "Zde se zobrazí všechny připojené organizace", "All the connected organizations will appear here": "Všechny připojené organizace se objeví zde",
"Always start minimized": "Vždy začněte minimalizovat", "Always start minimized": "Vždy spouštět minimalizova",
"App Updates": "Aktualizace aplikací", "App Updates": "Aktualizace aplikace",
"Appearance": "Vzhled", "Appearance": "Vzhled",
"Application Shortcuts": "Zkratky aplikací", "Application Shortcuts": "Zkratky programu",
"Are you sure you want to disconnect this organization?": "Opravdu chcete tuto organizaci odpojit?", "Are you sure you want to disconnect this organization?": "Opravdu chcete odpojit tuto organizaci?",
"Auto hide Menu bar": "Auto hide Menu bar", "Auto hide Menu bar": "Automaticky skrývat menu",
"Auto hide menu bar (Press Alt key to display)": "Automaticky skrýt lištu menu (stiskněte klávesu Alt pro zobrazení)", "Auto hide menu bar (Press Alt key to display)": "Automaticky skrývat menu (pro zobrazení stiskněte klávesu Alt)",
"Back": "Zadní", "Back": "Zpět",
"Bounce dock on new private message": "Bounce dock na nové soukromé zprávy", "Bounce dock on new private message": "Poskakování ikony v docku po obdržení nové soukromé zprávy",
"Certificate file": "Soubor certifikátu", "Certificate file": "Soubor s certifikátem",
"Change": "Změna", "Change": "Změnit",
"Check for Updates": "Kontrola aktualizací", "Check for Updates": "Zkontrolovat aktualizace",
"Close": "Zavřít", "Close": "Zavřít",
"Connect": "Připojit", "Connect": "Připojit se",
"Connect to another organization": "Připojte se k jiné organizaci", "Connect to another organization": "Připojit se k jiné organizaci",
"Connected organizations": "Připojené organizace", "Connected organizations": "Připojené organizace",
"Copy": "kopírovat", "Copy": "Kopírovat",
"Copy Zulip URL": "Kopírovat adresu URL aplikace Zulip", "Copy Zulip URL": "Kopírovat adresu (URL) Zulipu",
"Create a new organization": "Vytvořit novou organizaci", "Create a new organization": "Vytvořit novou organizaci",
"Cut": "Střih", "Cut": "Vyjmout",
"Default download location": "Výchozí umístění stahování", "Default download location": "Výchozí umístění stahování",
"Delete": "Smazat", "Delete": "Smazat",
"Desktop App Settings": "Nastavení aplikace Desktop", "Desktop App Settings": "Nastavení desktopové aplikace",
"Desktop Notifications": "Oznámení o pracovní ploše", "Desktop Notifications": "Oznámení na ploše",
"Desktop Settings": "Nastavení plochy", "Desktop Settings": "Nastavení plochy",
"Disconnect": "Odpojit", "Disconnect": "Odpojit",
"Download App Logs": "Stáhnout App Logs", "Download App Logs": "Stáhnout záznamy programu",
"Edit": "Upravit", "Edit": "Upravit",
"Edit Shortcuts": "Upravit zkratky", "Edit Shortcuts": "Upravit zkratky",
"Enable auto updates": "Povolit automatické aktualizace", "Enable auto updates": "Povolit automatické aktualizace",
"Enable error reporting (requires restart)": "Povolit hlášení chyb (vyžaduje restart)", "Enable error reporting (requires restart)": "Povolit hlášení chyb (vyžaduje opětovné spuštění programu)",
"Enable spellchecker (requires restart)": "Povolit kontrolu pravopisu (vyžaduje restartování)", "Enable spellchecker (requires restart)": "Povolit kontrolu pravopisu (vyžaduje opětovné spuštění programu)",
"Factory Reset": "Tovární nastavení", "Factory Reset": "Obnovení do továrního nastavení",
"File": "Soubor", "File": "Soubor",
"Find accounts": "Najít účty", "Find accounts": "Najít účty",
"Find accounts by email": "Najít účty e-mailem", "Find accounts by email": "Najít účty podle adresy elektronické pošty",
"Flash taskbar on new message": "Hlavní panel Flash na nové zprávě", "Flash taskbar on new message": "Blikání ikony v hlavním panelu při obdržení nové zprávy",
"Forward": "Vpřed", "Forward": "Vpřed",
"Functionality": "Funkčnost", "Functionality": "Funkce",
"General": "Všeobecné", "General": "Obecné",
"Get beta updates": "Získejte aktualizace beta", "Get beta updates": "Dostávat beta aktualizace",
"Hard Reload": "Hard Reload", "Hard Reload": "Tvrdé znovunahrání",
"Help": "Pomoc", "Help": "Nápověda",
"Help Center": "Centrum nápovědy", "Help Center": "Centrum nápovědy",
"History": "Dějiny", "History": "Historie",
"History Shortcuts": "Zkratky historie", "History Shortcuts": "Zkratky pro historii",
"Keyboard Shortcuts": "Klávesové zkratky", "Keyboard Shortcuts": "Klávesové zkratky",
"Log Out": "Odhlásit se", "Log Out": "Odhlásit se",
"Log Out of Organization": "Odhlásit se z organizace", "Log Out of Organization": "Odhlásit se z organizace",
"Manual proxy configuration": "Ruční konfigurace proxy", "Manual proxy configuration": "Ruční nastavení proxy",
"Minimize": "Minimalizovat", "Minimize": "Minimalizovat",
"Mute all sounds from Zulip": "Vypněte všechny zvuky ze Zulipu", "Mute all sounds from Zulip": "Ztlumit všechny zvuky ze Zulipu",
"NO": "NE", "NO": "NE",
"Network": "Síť", "Network": "Síť",
"OR": "NEBO", "OR": "NEBO",
"Organization URL": "URL organizace", "Organization URL": "Adresa organizace",
"Organizations": "Organizace", "Organizations": "Organizace",
"Paste": "Vložit", "Paste": "Vložit",
"Paste and Match Style": "Vložit a shodit styl", "Paste and Match Style": "Vložit a ponechat styl",
"Proxy": "Proxy", "Proxy": "Proxy",
"Proxy bypass rules": "Proxy bypass pravidla", "Proxy bypass rules": "Pravidla pro obejití Proxy",
"Proxy rules": "Proxy pravidla", "Proxy rules": "Pravidla Proxy",
"Quit": "Přestat", "Quit": "Ukončit",
"Quit Zulip": "Ukončete Zulip", "Quit Zulip": "Ukončit Zulip",
"Redo": "Předělat", "Redo": "Znovu",
"Release Notes": "Poznámky k vydání", "Release Notes": "Poznámky k této verzi",
"Reload": "Znovu načíst", "Reload": "Nahrát znovu",
"Report an Issue": "Nahlásit problém", "Report an Issue": "Nahlásit problém",
"Reset App Data": "Resetovat data aplikace",
"Reset App Settings": "Obnovit nastavení aplikace",
"Reset Application Data": "Resetovat data aplikace",
"Save": "Uložit", "Save": "Uložit",
"Select All": "Vybrat vše", "Select All": "Vybrat vše",
"Settings": "Nastavení", "Settings": "Nastavení",
"Shortcuts": "Zkratky", "Shortcuts": "Zkratky",
"Show App Logs": "Zobrazit protokoly aplikací", "Show App Logs": "Zobrazit záznamy programu",
"Show app icon in system tray": "Zobrazit ikonu aplikace v systémové liště", "Show app icon in system tray": "Zobrazovat ikonu programu v oznamovací oblasti panelu",
"Show app unread badge": "Zobrazit nepřečtený odznak aplikace", "Show app unread badge": "Zobrazovat u ikony aplikace symbol nepřečteno",
"Show desktop notifications": "Zobrazit oznámení na ploše", "Show desktop notifications": "Zobrazovat oznámení na ploše",
"Show downloaded files in file manager": "Zobrazit stažené soubory ve správci souborů", "Show downloaded files in file manager": "Zobrazit stažené soubory ve správci souborů",
"Show sidebar": "Zobrazit postranní panel", "Show sidebar": "Zobrazovat postranní panel",
"Start app at login": "Spuštění aplikace při přihlášení", "Start app at login": "Spustit program při přihlášení",
"Switch to Next Organization": "Přepnout na další organizaci", "Switch to Next Organization": "Přepnout na další organizaci",
"Switch to Previous Organization": "Přepněte na předchozí organizaci", "Switch to Previous Organization": "Přepnout na předchozí organizaci",
"These desktop app shortcuts extend the Zulip webapp's": "Tyto klávesové zkratky pro aplikaci na ploše rozšiřují aplikaci Zulip webapp", "These desktop app shortcuts extend the Zulip webapp's": "Tyto zkratky rozšiřují webovou aplikaci Zulipu",
"This will delete all application data including all added accounts and preferences": "Tím odstraníte všechna data aplikace včetně všech přidaných účtů a předvoleb", "This will delete all application data including all added accounts and preferences": "Toto smaže všechna data programu včetně všech přidaných účtů a nastavení",
"Tip": "Tip", "Tip": "Tip",
"Toggle DevTools for Active Tab": "Přepnout DevTools pro Active Tab", "Toggle DevTools for Active Tab": "Přepnout vývojářské nástroje pro aktivní kartu",
"Toggle DevTools for Zulip App": "Přepnout DevTools pro Zulip App", "Toggle DevTools for Zulip App": "Přepnout vývojářské nástroje pro program Zulip",
"Toggle Do Not Disturb": "Přepnout Do Nerušit", "Toggle Do Not Disturb": "Přepnout mód Nerušit",
"Toggle Full Screen": "Přepnout na celou obrazovku", "Toggle Full Screen": "Přepnout na celou obrazovku",
"Toggle Sidebar": "Přepnout postranní panel", "Toggle Sidebar": "Přepnout zobrazení postranního panelu",
"Toggle Tray Icon": "Přepnout ikonu zásobníku", "Toggle Tray Icon": "Přepnout ikonu v oznamovací oblasti panelu",
"Tools": "Nástroje", "Tools": "Nástroje",
"Undo": "vrátit", "Undo": "Zpět",
"Upload": "nahrát", "Upload": "Nahrát",
"Use system proxy settings (requires restart)": "Použít nastavení proxy serveru (vyžaduje restart)", "Use system proxy settings (requires restart)": "Použít systémová nastavení proxy (vyžaduje opětovné spuštění programu)",
"View": "Pohled", "View": "Zobrazení",
"View Shortcuts": "Zobrazit zástupce", "View Shortcuts": "Zobrazit zkratky",
"Window": "Okno", "Window": "Okno",
"Window Shortcuts": "Klávesové zkratky", "Window Shortcuts": "Zkratky pro okno",
"YES": "ANO", "YES": "ANO",
"Zoom In": "Přiblížit", "Zoom In": "Přiblížit",
"Zoom Out": "Oddálit", "Zoom Out": "Oddálit",
"Zulip Help": "Nápověda Zulip", "Zulip Help": "Nápověda Zulipu",
"keyboard shortcuts": "klávesové zkratky", "keyboard shortcuts": "klávesové zkratky",
"script": "skript" "script": "skript",
"Quit when the window is closed": "Ukončit, když je okno zavřeno",
"Ask where to save files before downloading": "Před stažením se zeptat kam uložit soubory",
"Services": "Služby",
"Hide": "Skrýt",
"Hide Others": "Skrýt jiné",
"Unhide": "Zobrazit",
"AddServer": "Přidat server",
"App language (requires restart)": "Jazyk programu (vyžaduje opětovné spuštění programu)",
"Factory Reset Data": "Obnovení dat do továrního nastavení",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Obnovit program do výchozího nastavení. čili smazat všechny připojené organizace, účty a certifikáty.",
"On macOS, the OS spellchecker is used.": "Na macOS se používá kontrola pravopisu OS.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Změnit jazyk v Nastavení systému → Klávesnice → Text → Kontrola pravopisu.",
"Copy Link": "Kopírovat odkaz",
"Copy Image": "Kopírovat obrázek",
"Copy Image URL": "Kopírovat adresu (URL) obrázku",
"No Suggestion Found": "Nenalezen žádný návrh",
"You can select a maximum of 3 languages for spellchecking.": "Pro kontrolu pravopisu můžete vybrat nejvíce 3 jazyky.",
"Spellchecker Languages": "Kontrola pravopisu jazyků",
"Add to Dictionary": "Přidat do slovníku",
"Look Up": "Vyhledat"
} }

134
app/translations/da.json Normal file
View File

@@ -0,0 +1,134 @@
{
"About Zulip": "Om Zulip",
"Actual Size": "Faktisk størrelse",
"Add Custom Certificates": "Tilføj egne certifikater",
"Add Organization": "Opret organisation",
"Add a Zulip organization": "Opret en Zulip organisation",
"Add custom CSS": "Tilføj egen CSS",
"Advanced": "Avanceret",
"All the connected organizations will appear here": "Alle de tilsluttede organisationer vil blive vist her",
"Always start minimized": "Start altid minimeret",
"App Updates": "App-opdateringer",
"Appearance": "Udseende",
"Application Shortcuts": "Genveje",
"Are you sure you want to disconnect this organization?": "Er du sikker på du vil frakoble denne organisation? ",
"Auto hide Menu bar": "Skjul menu automatisk",
"Auto hide menu bar (Press Alt key to display)": "Skjul menu automatisk (tryk på Alt-tasten for at vise)",
"Back": "Tilbage",
"Bounce dock on new private message": "Bounce dock on new private message",
"Certificate file": "Certifikat-fil",
"Change": "Skift",
"Check for Updates": "Tjek for opdateringer",
"Close": "Luk",
"Connect": "Tilslut",
"Connect to another organization": "Forbind til en anden organisation",
"Connected organizations": "Tilsluttede organisationer",
"Copy": "Kopiér",
"Copy Zulip URL": "Kopiér Zulip URL",
"Create a new organization": "Opret ny organisation",
"Cut": "Klip",
"Default download location": "Default download placering",
"Delete": "Slet",
"Desktop App Settings": "Desktop app-indstillinger",
"Desktop Notifications": "Desktop-notifikationer",
"Desktop Settings": "Desktop-indstillinger",
"Disconnect": "Frakobl",
"Download App Logs": "Download app-logfiler",
"Edit": "Redigér",
"Edit Shortcuts": "Redigér genveje",
"Enable auto updates": "Aktivér auto-opdateringer",
"Enable error reporting (requires restart)": "Aktivér fejlrapportering (kræver genstart)",
"Enable spellchecker (requires restart)": "Aktivér stavekontrol (kræver genstart)",
"Factory Reset": "Nulstil til fabriksindstillinger",
"File": "Fil",
"Find accounts": "Find konti",
"Find accounts by email": "Find accounts by email",
"Flash taskbar on new message": "Flash taskbar on new message",
"Forward": "Forward",
"Functionality": "Funktionalitet",
"General": "Generelt",
"Get beta updates": "Få beta opdateringer",
"Hard Reload": "Hård reload",
"Help": "Hjælp",
"Help Center": "Hjælpecenter",
"History": "Historik",
"History Shortcuts": "Historik genveje",
"Keyboard Shortcuts": "Tastatur genveje",
"Log Out": "Log ud",
"Log Out of Organization": "Log ud af organisation",
"Manual proxy configuration": "Manuel proxy opsætning",
"Minimize": "Minimér",
"Mute all sounds from Zulip": "Mute all sounds from Zulip",
"NO": "NEJ",
"Network": "Netværk",
"OR": "ELLER",
"Organization URL": "Organisation URL",
"Organizations": "Organisationer",
"Paste": "Indsæt",
"Paste and Match Style": "Indsæt med samme formattering",
"Proxy": "Proxy",
"Proxy bypass rules": "Proxy bypass regler",
"Proxy rules": "Proxy regler",
"Quit": "Luk",
"Quit Zulip": "Luk Zulip",
"Redo": "Redo",
"Release Notes": "Release Notes",
"Reload": "Reload",
"Report an Issue": "Report an Issue",
"Save": "Save",
"Select All": "Select All",
"Settings": "Settings",
"Shortcuts": "Shortcuts",
"Show App Logs": "Show App Logs",
"Show app icon in system tray": "Show app icon in system tray",
"Show app unread badge": "Show app unread badge",
"Show desktop notifications": "Show desktop notifications",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Show sidebar": "Show sidebar",
"Start app at login": "Start app at login",
"Switch to Next Organization": "Switch to Next Organization",
"Switch to Previous Organization": "Switch to Previous Organization",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Tip": "Tip",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle Do Not Disturb": "Toggle Do Not Disturb",
"Toggle Full Screen": "Toggle Full Screen",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon",
"Tools": "Tools",
"Undo": "Undo",
"Upload": "Upload",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"View": "View",
"View Shortcuts": "View Shortcuts",
"Window": "Window",
"Window Shortcuts": "Window Shortcuts",
"YES": "YES",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Zulip Help": "Zulip Help",
"keyboard shortcuts": "keyboard shortcuts",
"script": "script",
"Quit when the window is closed": "Quit when the window is closed",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"AddServer": "AddServer",
"App language (requires restart)": "App language (requires restart)",
"Factory Reset Data": "Factory Reset Data",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Copy Link": "Copy Link",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"No Suggestion Found": "No Suggestion Found",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Spellchecker Languages": "Spellchecker Languages",
"Add to Dictionary": "Add to Dictionary",
"Look Up": "Look Up"
}

View File

@@ -1,117 +1,134 @@
{ {
"About Zulip": "Über Zulip", "About Zulip": "Über Zulip",
"Actual Size": "Tatsächliche Größe", "Actual Size": "Tatsächliche Größe",
"Add Custom Certificates": "Benutzerdefinierte Zertifikate hinzufügen", "Add Custom Certificates": "Eigene Zertifikate hinzufügen",
"Add Organization": "Organisation hinzufügen", "Add Organization": "Organisation hinzufügen",
"Add a Zulip organization": "Fügen Sie eine Zulip-Organisation hinzu", "Add a Zulip organization": "Zulip-Organisation hinzufügen",
"Add custom CSS": "Fügen Sie benutzerdefiniertes CSS hinzu", "Add custom CSS": "Eigenes CSS hinzufügen",
"Advanced": "Fortgeschritten", "Advanced": "Erweitert",
"All the connected organizations will appear here": "Alle verbundenen Organisationen werden hier angezeigt", "All the connected organizations will appear here": "Alle verbundenen Organisationen erscheinen hier",
"Always start minimized": "Immer minimiert anfangen", "Always start minimized": "Immer minimiert öffnen",
"App Updates": "App-Updates", "App Updates": "App Updates",
"Appearance": "Aussehen", "Appearance": "Erscheinungsbild",
"Application Shortcuts": "Anwendungsverknüpfungen", "Application Shortcuts": "App-Verknüpfungen",
"Are you sure you want to disconnect this organization?": "Möchten Sie diese Organisation wirklich trennen?", "Are you sure you want to disconnect this organization?": "Bist du dir sicher, dass du die Verbindung zur Organisation trennen möchtest?",
"Auto hide Menu bar": "Menüleiste automatisch ausblenden", "Auto hide Menu bar": "Menü automatisch verstecken",
"Auto hide menu bar (Press Alt key to display)": "Menüleiste automatisch ausblenden (Drücken Sie die Alt-Taste, um anzuzeigen)", "Auto hide menu bar (Press Alt key to display)": "Menü automatisch verstecken (zum Anzeigen die Alt-Taste drücken)",
"Back": "Zurück", "Back": "Zurück",
"Bounce dock on new private message": "Bounce-Dock für neue private Nachricht", "Bounce dock on new private message": "Im Dock hüpfen, wenn neue private Nachrichten eingehen",
"Certificate file": "Zertifikatsdatei", "Certificate file": "Zertifikatsdatei",
"Change": "Veränderung", "Change": "Ändern",
"Check for Updates": "Auf Updates prüfen", "Check for Updates": "Auf Updates prüfen",
"Close": "Schließen", "Close": "Schließen",
"Connect": "Verbinden", "Connect": "Verbinden",
"Connect to another organization": "Stellen Sie eine Verbindung zu einer anderen Organisation her", "Connect to another organization": "Mit einer anderen Organisation verbinden",
"Connected organizations": "Verbundene Organisationen", "Connected organizations": "Verbundene Organisationen",
"Copy": "Kopieren", "Copy": "Kopieren",
"Copy Zulip URL": "Zulip-URL kopieren", "Copy Zulip URL": "Zulip-URL kopieren",
"Create a new organization": "Erstellen Sie eine neue Organisation", "Create a new organization": "Eine neue Organisation erstellen",
"Cut": "Schnitt", "Cut": "Ausschneiden",
"Default download location": "Standard-Download-Speicherort", "Default download location": "Voreingestelltes Ziel für Downloads",
"Delete": "Löschen", "Delete": "Löschen",
"Desktop App Settings": "Desktop App-Einstellungen", "Desktop App Settings": "Einstellungen für Desktop-App",
"Desktop Notifications": "Desktop-Benachrichtigungen", "Desktop Notifications": "Desktopbenachrichtigungen",
"Desktop Settings": "Desktop-Einstellungen", "Desktop Settings": "Desktop-Einstellungen",
"Disconnect": "Trennen", "Disconnect": "Verbindung trennen",
"Download App Logs": "App-Protokolle herunterladen", "Download App Logs": "Logdateien der App herunterladen",
"Edit": "Bearbeiten", "Edit": "Bearbeiten",
"Edit Shortcuts": "Verknüpfungen bearbeiten", "Edit Shortcuts": "Tastenkürzel bearbeiten",
"Enable auto updates": "Automatische Updates aktivieren", "Enable auto updates": "Automatisch aktualisieren",
"Enable error reporting (requires restart)": "Fehlerberichterstattung aktivieren (Neustart erforderlich)", "Enable error reporting (requires restart)": "Fehlerberichte aktivieren (erfordert Neustart)",
"Enable spellchecker (requires restart)": "Rechtschreibprüfung aktivieren (Neustart erforderlich)", "Enable spellchecker (requires restart)": "Rechtschreibprüfung aktivieren (erfordert Neustart)",
"Factory Reset": "Werkseinstellungen zurückgesetzt", "Factory Reset": "Alle Einstellungen auf Standardwerte zurücksetzen",
"File": "Datei", "File": "Datei",
"Find accounts": "Konten suchen", "Find accounts": "Accounts finden",
"Find accounts by email": "Finden Sie Konten per E-Mail", "Find accounts by email": "Accounts anhand E-Mail-Adresse finden",
"Flash taskbar on new message": "Flash-Taskleiste bei neuer Nachricht", "Flash taskbar on new message": "Farbliche Hervorhebung in Taskbar bei neuen Nachrichten",
"Forward": "Nach vorne", "Forward": "Weiter",
"Functionality": "Funktionalität", "Functionality": "Funktionalität",
"General": "Allgemeines", "General": "Allgemein",
"Get beta updates": "Erhalten Sie Beta-Updates", "Get beta updates": "Auf Betaversionen aktualisieren",
"Hard Reload": "Festes Nachladen", "Hard Reload": "Komplett neu laden",
"Help": "Hilfe", "Help": "Hilfe",
"Help Center": "Hilfezentrum", "Help Center": "Hilfe-Zentrum",
"History": "Geschichte", "History": "Verlauf",
"History Shortcuts": "Verlaufsverknüpfungen", "History Shortcuts": "Kurzbefehle für Verlauf",
"Keyboard Shortcuts": "Tastatürkürzel", "Keyboard Shortcuts": "Tastenkürzel",
"Log Out": "Ausloggen", "Log Out": "Abmelden",
"Log Out of Organization": "Aus Organisation ausloggen", "Log Out of Organization": "Von Organisation abmelden",
"Manual proxy configuration": "Manuelle Proxy-Konfiguration", "Manual proxy configuration": "Manuelle Proxy-Konfiguration",
"Minimize": "Minimieren", "Minimize": "Minimieren",
"Mute all sounds from Zulip": "Schaltet alle Sounds von Zulip stumm", "Mute all sounds from Zulip": "Alle Zulip-Klänge stummschalten",
"NO": "NEIN", "NO": "NEIN",
"Network": "Netzwerk", "Network": "Netzwerk",
"OR": "ODER", "OR": "ODER",
"Organization URL": "Organisations-URL", "Organization URL": "URL der Organisation",
"Organizations": "Organisationen", "Organizations": "Organisationen",
"Paste": "Einfügen", "Paste": "Einfügen",
"Paste and Match Style": "Einfügen und Format anpassen", "Paste and Match Style": "Ohne Formatierung einfügen",
"Proxy": "Proxy", "Proxy": "Proxy",
"Proxy bypass rules": "Proxy-Umgehungsregeln", "Proxy bypass rules": "Proxy-Ausnahmen",
"Proxy rules": "Proxy-Regeln", "Proxy rules": "Proxy-Regeln",
"Quit": "Verlassen", "Quit": "Beenden",
"Quit Zulip": "Beenden Sie Zulip", "Quit Zulip": "Zulip beenden",
"Redo": "Redo", "Redo": "Wiederholen",
"Release Notes": "Versionshinweise", "Release Notes": "Hinweise zur Versionsfreigabe",
"Reload": "Neu laden", "Reload": "Neu laden",
"Report an Issue": "Ein Problem melden", "Report an Issue": "Ein Problem melden",
"Reset App Data": "App-Daten zurücksetzen", "Save": "Speichern",
"Reset App Settings": "App-Einstellungen zurücksetzen", "Select All": "Alles auswählen",
"Reset Application Data": "Anwendungsdaten zurücksetzen", "Settings": "Einstellungen",
"Save": "sparen", "Shortcuts": "Kurzbefehle",
"Select All": "Wählen Sie Alle", "Show App Logs": "Logdateien der App anzeigen",
"Settings": "die Einstellungen", "Show app icon in system tray": "App-Icon in Systemleiste anzeigen",
"Shortcuts": "Verknüpfungen", "Show app unread badge": "Anzahl ungelesener Nachrichten in App-Icon einblenden",
"Show App Logs": "App-Protokolle anzeigen", "Show desktop notifications": "Desktopbenachrichtigungen anzeigen",
"Show app icon in system tray": "App-Symbol in der Taskleiste anzeigen", "Show downloaded files in file manager": "Heruntergeladene Dateien in Dateimanager anzeigen",
"Show app unread badge": "Zeige App ungelesenen Badge",
"Show desktop notifications": "Desktop-Benachrichtigungen anzeigen",
"Show downloaded files in file manager": "Heruntergeladene Dateien im Dateimanager anzeigen",
"Show sidebar": "Seitenleiste anzeigen", "Show sidebar": "Seitenleiste anzeigen",
"Start app at login": "App beim Login starten", "Start app at login": "App beim Login automatisch starten",
"Switch to Next Organization": "Wechseln Sie zur nächsten Organisation", "Switch to Next Organization": "Zur nächsten Organisation wechseln",
"Switch to Previous Organization": "Zur vorherigen Organisation wechseln", "Switch to Previous Organization": "Zur vorhergehenden Organisation wechseln",
"These desktop app shortcuts extend the Zulip webapp's": "Diese Desktop-App-Verknüpfungen erweitern die Zulip-Webanwendungen", "These desktop app shortcuts extend the Zulip webapp's": "Dies sind zusätzliche Kurzbefehle in der Desktop-App gegenüber der Web-App",
"This will delete all application data including all added accounts and preferences": "Dadurch werden alle Anwendungsdaten einschließlich aller hinzugefügten Konten und Einstellungen gelöscht", "This will delete all application data including all added accounts and preferences": "Hiermit werden alle Anwendungsdaten einschließlich aller Accounts und Einstellungen gelöscht",
"Tip": "Spitze", "Tip": "Tipp",
"Toggle DevTools for Active Tab": "DevTools für aktive Registerkarte umschalten", "Toggle DevTools for Active Tab": "Entwickler-Tools für aktiven Tab umschalten",
"Toggle DevTools for Zulip App": "DevTools für Zulip App umschalten", "Toggle DevTools for Zulip App": "Entwickler-Tools für Zulip-App umschalten",
"Toggle Do Not Disturb": "Nicht stören umschalten", "Toggle Do Not Disturb": "Nicht-Stören-Modus umschalten",
"Toggle Full Screen": "Vollbild umschalten", "Toggle Full Screen": "Vollbildschirm umschalten",
"Toggle Sidebar": "Seitenleiste umschalten", "Toggle Sidebar": "Seitenleiste umschalten",
"Toggle Tray Icon": "Taskleistensymbol umschalten", "Toggle Tray Icon": "Tray-Icon umschalten",
"Tools": "Werkzeuge", "Tools": "Extras",
"Undo": "Rückgängig machen", "Undo": "Rückgängig",
"Upload": "Hochladen", "Upload": "Hochladen",
"Use system proxy settings (requires restart)": "System-Proxy-Einstellungen verwenden (Neustart erforderlich)", "Use system proxy settings (requires restart)": "Systemweite Proxy-Einstellungen verwenden (erfordert Neustart)",
"View": "Aussicht", "View": "Ansicht",
"View Shortcuts": "Verknüpfungen anzeigen", "View Shortcuts": "Tastenkürzel anzeigen",
"Window": "Fenster", "Window": "Fenster",
"Window Shortcuts": "Fensterverknüpfungen", "Window Shortcuts": "Kurzbefehle für Fenster",
"YES": "JA", "YES": "JA",
"Zoom In": "Hineinzoomen", "Zoom In": "Vergrößern",
"Zoom Out": "Rauszoomen", "Zoom Out": "Verkleinern",
"Zulip Help": "Zulip-Hilfe", "Zulip Help": "Hilfe zu Zulip",
"keyboard shortcuts": "Tastatürkürzel", "keyboard shortcuts": "Tastenkürzel",
"script": "Skript" "script": "Skript",
"Quit when the window is closed": "Beim Schließen des Fensters beenden",
"Ask where to save files before downloading": "Fragen, wo heruntergeladene Dateien gespeichert werden sollen",
"Services": "Dienste",
"Hide": "Verbergen",
"Hide Others": "Andere verbergen",
"Unhide": "Nicht mehr verbergen",
"AddServer": "ServerHinzufügen",
"App language (requires restart)": "Sprache der App (benötigt Neustart)",
"Factory Reset Data": "Auf Werkseinstellung zurücksetzen",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Die App wird zurückgesetzt, somit werden alle verbundenen Organisationen, Konten und Zertifikate gelöscht.",
"On macOS, the OS spellchecker is used.": "In macOS wird die OS Rechtschreibprüfung verwendet.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Ändere die Spracheinstellung über Systemeinstellungen → Tastatur → Text → Rechtschreibung.",
"Copy Link": "Link kopieren",
"Copy Image": "Bild kopieren",
"Copy Image URL": "Bild-URL kopieren",
"No Suggestion Found": "Keine Vorschläge gefunden",
"You can select a maximum of 3 languages for spellchecking.": "Du kannst höchstens 3 Sprachen für die Rechtschreibprüfung auswählen.",
"Spellchecker Languages": "Sprachen für die Rechtschreibprüfung",
"Add to Dictionary": "Zum Wörterbuch hinzufügen",
"Look Up": "Nachschlagen"
} }

View File

@@ -1,119 +1,134 @@
{ {
"File": "File", "About Zulip": "About Zulip",
"Actual Size": "Actual Size",
"Add Custom Certificates": "Add Custom Certificates",
"Add Organization": "Add Organization", "Add Organization": "Add Organization",
"Toggle Do Not Disturb": "Toggle Do Not Disturb", "Add a Zulip organization": "Add a Zulip organization",
"Desktop Settings": "Desktop Settings", "Add custom CSS": "Add custom CSS",
"Keyboard Shortcuts": "Keyboard Shortcuts", "Advanced": "Advanced",
"Copy Zulip URL": "Copy Zulip URL", "All the connected organizations will appear here": "All the connected organizations will appear here",
"Log Out of Organization": "Log Out of Organization", "Always start minimized": "Always start minimized",
"Minimize": "Minimize", "App Updates": "App Updates",
"Appearance": "Appearance",
"Application Shortcuts": "Application Shortcuts",
"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?",
"Auto hide Menu bar": "Auto hide Menu bar",
"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)",
"Back": "Back",
"Bounce dock on new private message": "Bounce dock on new private message",
"Certificate file": "Certificate file",
"Change": "Change",
"Check for Updates": "Check for Updates",
"Close": "Close", "Close": "Close",
"Quit": "Quit", "Connect": "Connect",
"Edit": "Edit", "Connect to another organization": "Connect to another organization",
"Undo": "Undo", "Connected organizations": "Connected organizations",
"Redo": "Redo",
"Cut": "Cut",
"Copy": "Copy", "Copy": "Copy",
"Copy Zulip URL": "Copy Zulip URL",
"Create a new organization": "Create a new organization",
"Cut": "Cut",
"Default download location": "Default download location",
"Delete": "Delete",
"Desktop App Settings": "Desktop App Settings",
"Desktop Notifications": "Desktop Notifications",
"Desktop Settings": "Desktop Settings",
"Disconnect": "Disconnect",
"Download App Logs": "Download App Logs",
"Edit": "Edit",
"Edit Shortcuts": "Edit Shortcuts",
"Enable auto updates": "Enable auto updates",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Factory Reset": "Factory Reset",
"File": "File",
"Find accounts": "Find accounts",
"Find accounts by email": "Find accounts by email",
"Flash taskbar on new message": "Flash taskbar on new message",
"Forward": "Forward",
"Functionality": "Functionality",
"General": "General",
"Get beta updates": "Get beta updates",
"Hard Reload": "Hard Reload",
"Help": "Help",
"Help Center": "Help Center",
"History": "History",
"History Shortcuts": "History Shortcuts",
"Keyboard Shortcuts": "Keyboard Shortcuts",
"Log Out": "Log Out",
"Log Out of Organization": "Log Out of Organization",
"Manual proxy configuration": "Manual proxy configuration",
"Minimize": "Minimize",
"Mute all sounds from Zulip": "Mute all sounds from Zulip",
"NO": "NO",
"Network": "Network",
"OR": "OR",
"Organization URL": "Organization URL",
"Organizations": "Organizations",
"Paste": "Paste", "Paste": "Paste",
"Paste and Match Style": "Paste and Match Style", "Paste and Match Style": "Paste and Match Style",
"Select All": "Select All", "Proxy": "Proxy",
"View": "View", "Proxy bypass rules": "Proxy bypass rules",
"Reload": "Reload", "Proxy rules": "Proxy rules",
"Hard Reload": "Hard Reload", "Quit": "Quit",
"Toggle Full Screen": "Toggle Full Screen", "Quit Zulip": "Quit Zulip",
"Zoom In": "Zoom In", "Redo": "Redo",
"Zoom Out": "Zoom Out",
"Actual Size": "Actual Size",
"Toggle Tray Icon": "Toggle Tray Icon",
"Toggle Sidebar": "Toggle Sidebar",
"Auto hide Menu bar": "Auto hide Menu bar",
"History": "History",
"Back": "Back",
"Forward": "Forward",
"Window": "Window",
"Tools": "Tools",
"Check for Updates": "Check for Updates",
"Release Notes": "Release Notes", "Release Notes": "Release Notes",
"Factory Reset": "Factory Reset", "Reload": "Reload",
"Download App Logs": "Download App Logs",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Help": "Help",
"About Zulip": "About Zulip",
"Help Center": "Help Center",
"Report an Issue": "Report an Issue", "Report an Issue": "Report an Issue",
"Save": "Save",
"Select All": "Select All",
"Settings": "Settings",
"Shortcuts": "Shortcuts",
"Show App Logs": "Show App Logs",
"Show app icon in system tray": "Show app icon in system tray",
"Show app unread badge": "Show app unread badge",
"Show desktop notifications": "Show desktop notifications",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Show sidebar": "Show sidebar",
"Start app at login": "Start app at login",
"Switch to Next Organization": "Switch to Next Organization", "Switch to Next Organization": "Switch to Next Organization",
"Switch to Previous Organization": "Switch to Previous Organization", "Switch to Previous Organization": "Switch to Previous Organization",
"General": "General",
"Network": "Network",
"AddServer": "AddServer",
"Organizations": "Organizations",
"Shortcuts": "Shortcuts",
"Appearance": "Appearance",
"Show app icon in system tray": "Show app icon in system tray",
"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)",
"Show sidebar": "Show sidebar",
"Show app unread badge": "Show app unread badge",
"Bounce dock on new private message": "Bounce dock on new private message",
"Flash taskbar on new message": "Flash taskbar on new message",
"Desktop Notifications": "Desktop Notifications",
"Show desktop notifications": "Show desktop notifications",
"Mute all sounds from Zulip": "Mute all sounds from Zulip",
"App Updates": "App Updates",
"Enable auto updates": "Enable auto updates",
"Get beta updates": "Get beta updates",
"Functionality": "Functionality",
"Start app at login": "Start app at login",
"Always start minimized": "Always start minimized",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Advanced": "Advanced",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Add custom CSS": "Add custom CSS",
"Upload": "Upload",
"Delete": "Delete",
"Default download location": "Default download location",
"Change": "Change",
"Reset Application Data": "Reset Application Data",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Reset App Data": "Reset App Data",
"Proxy": "Proxy",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"Manual proxy configuration": "Manual proxy configuration",
"script": "script",
"Proxy rules": "Proxy rules",
"Proxy bypass rules": "Proxy bypass rules",
"Save": "Save",
"Connected organizations": "Connected organizations",
"All the connected orgnizations will appear here.": "All the connected orgnizations will appear here.",
"Connect to another organization": "Connect to another organization",
"Add Custom Certificates": "Add Custom Certificates",
"Find accounts by email": "Find accounts by email",
"All the connected orgnizations will appear here": "All the connected orgnizations will appear here",
"Disconnect": "Disconnect",
"Organization URL": "Organization URL",
"Certificate file": "Certificate file",
"Find accounts": "Find accounts",
"Tip": "Tip",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's", "These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"keyboard shortcuts": "keyboard shortcuts", "This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Application Shortcuts": "Application Shortcuts", "Tip": "Tip",
"Settings": "Settings", "Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Log Out": "Log Out", "Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Quit Zulip": "Quit Zulip", "Toggle Do Not Disturb": "Toggle Do Not Disturb",
"Edit Shortcuts": "Edit Shortcuts", "Toggle Full Screen": "Toggle Full Screen",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon",
"Tools": "Tools",
"Undo": "Undo",
"Upload": "Upload",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"View": "View",
"View Shortcuts": "View Shortcuts", "View Shortcuts": "View Shortcuts",
"History Shortcuts": "History Shortcuts", "Window": "Window",
"Window Shortcuts": "Window Shortcuts", "Window Shortcuts": "Window Shortcuts",
"Quit when the window is closed": "Quit when the window is closed",
"Force social login in app instead of browser": "Force social login in app instead of browser",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Add a Zulip organization": "Add a Zulip organization",
"Connect": "Connect",
"OR": "OR",
"Create a new organization": "Create a new organization",
"Network and Proxy Settings": "Network and Proxy Settings",
"YES": "YES", "YES": "YES",
"NO": "NO", "Zoom In": "Zoom In",
"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?" "Zoom Out": "Zoom Out",
"Zulip Help": "Zulip Help",
"keyboard shortcuts": "keyboard shortcuts",
"script": "script",
"Quit when the window is closed": "Quit when the window is closed",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"AddServer": "AddServer",
"App language (requires restart)": "App language (requires restart)",
"Factory Reset Data": "Factory Reset Data",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Copy Link": "Copy Link",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"No Suggestion Found": "No Suggestion Found",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Spellchecker Languages": "Spellchecker Languages",
"Add to Dictionary": "Add to Dictionary",
"Look Up": "Look Up"
} }

View File

@@ -1,122 +1,134 @@
{ {
"About Zulip": "About Zulip",
"Actual Size": "Actual Size",
"Add Custom Certificates": "Add Custom Certificates",
"Add Organization": "Add Organization", "Add Organization": "Add Organization",
"Toggle Do Not Disturb": "Toggle Do Not Disturb", "Add a Zulip organization": "Add a Zulip organization",
"Desktop Settings": "Desktop Settings", "Add custom CSS": "Add custom CSS",
"Keyboard Shortcuts": "Keyboard Shortcuts", "Advanced": "Advanced",
"All the connected organizations will appear here": "All the connected organizations will appear here",
"Always start minimized": "Always start minimized",
"App Updates": "App Updates",
"Appearance": "Appearance",
"Application Shortcuts": "Application Shortcuts",
"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?",
"Auto hide Menu bar": "Auto hide Menu bar",
"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)",
"Back": "Back",
"Bounce dock on new private message": "Bounce dock on new private message",
"Certificate file": "Certificate file",
"Change": "Change",
"Check for Updates": "Check for Updates",
"Close": "Close",
"Connect": "Connect",
"Connect to another organization": "Connect to another organization",
"Connected organizations": "Connected organizations",
"Copy": "Copy",
"Copy Zulip URL": "Copy Zulip URL", "Copy Zulip URL": "Copy Zulip URL",
"Create a new organization": "Create a new organization",
"Cut": "Cut",
"Default download location": "Default download location",
"Delete": "Delete",
"Desktop App Settings": "Desktop App Settings",
"Desktop Notifications": "Desktop Notifications",
"Desktop Settings": "Desktop Settings",
"Disconnect": "Disconnect",
"Download App Logs": "Download App Logs",
"Edit": "Edit",
"Edit Shortcuts": "Edit Shortcuts",
"Enable auto updates": "Enable auto updates",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Factory Reset": "Factory Reset",
"File": "File",
"Find accounts": "Find accounts",
"Find accounts by email": "Find accounts by email",
"Flash taskbar on new message": "Flash taskbar on new message",
"Forward": "Forward",
"Functionality": "Functionality",
"General": "General",
"Get beta updates": "Get beta updates",
"Hard Reload": "Hard Reload",
"Help": "Help",
"Help Center": "Help Center",
"History": "History",
"History Shortcuts": "History Shortcuts",
"Keyboard Shortcuts": "Keyboard Shortcuts",
"Log Out": "Log Out",
"Log Out of Organization": "Log Out of Organization", "Log Out of Organization": "Log Out of Organization",
"Manual proxy configuration": "Manual proxy configuration",
"Minimize": "Minimize",
"Mute all sounds from Zulip": "Mute all sounds from Zulip",
"NO": "NO",
"Network": "Network",
"OR": "OR",
"Organization URL": "Organization URL",
"Organizations": "Organizations",
"Paste": "Paste",
"Paste and Match Style": "Paste and Match Style",
"Proxy": "Proxy",
"Proxy bypass rules": "Proxy bypass rules",
"Proxy rules": "Proxy rules",
"Quit": "Quit",
"Quit Zulip": "Quit Zulip",
"Redo": "Redo",
"Release Notes": "Release Notes",
"Reload": "Reload",
"Report an Issue": "Report an Issue",
"Save": "Save",
"Select All": "Select All",
"Settings": "Settings",
"Shortcuts": "Shortcuts",
"Show App Logs": "Show App Logs",
"Show app icon in system tray": "Show app icon in system tray",
"Show app unread badge": "Show app unread badge",
"Show desktop notifications": "Show desktop notifications",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Show sidebar": "Show sidebar",
"Start app at login": "Start app at login",
"Switch to Next Organization": "Switch to Next Organization",
"Switch to Previous Organization": "Switch to Previous Organization",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Tip": "Tip",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle Do Not Disturb": "Toggle Do Not Disturb",
"Toggle Full Screen": "Toggle Full Screen",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon",
"Tools": "Tools",
"Undo": "Undo",
"Upload": "Upload",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"View": "View",
"View Shortcuts": "View Shortcuts",
"Window": "Window",
"Window Shortcuts": "Window Shortcuts",
"YES": "YES",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Zulip Help": "Zulip Help",
"keyboard shortcuts": "keyboard shortcuts",
"script": "script",
"Quit when the window is closed": "Quit when the window is closed",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Services": "Services", "Services": "Services",
"Hide": "Hide", "Hide": "Hide",
"Hide Others": "Hide Others", "Hide Others": "Hide Others",
"Unhide": "Unhide", "Unhide": "Unhide",
"Minimize": "Minimize",
"Close": "Close",
"Quit": "Quit",
"Edit": "Edit",
"Undo": "Undo",
"Redo": "Redo",
"Cut": "Cut",
"Copy": "Copy",
"Paste": "Paste",
"Paste and Match Style": "Paste and Match Style",
"Select All": "Select All",
"View": "View",
"Reload": "Reload",
"Hard Reload": "Hard Reload",
"Toggle Full Screen": "Toggle Full Screen",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Actual Size": "Actual Size",
"Toggle Tray Icon": "Toggle Tray Icon",
"Toggle Sidebar": "Toggle Sidebar",
"Auto hide Menu bar": "Auto hide Menu bar",
"History": "History",
"Back": "Back",
"Forward": "Forward",
"Window": "Window",
"Tools": "Tools",
"Check for Updates": "Check for Updates",
"Release Notes": "Release Notes",
"Factory Reset": "Factory Reset",
"Download App Logs": "Download App Logs",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Help": "Help",
"About Zulip": "About Zulip",
"Help Center": "Help Center",
"Report an Issue": "Report an Issue",
"Switch to Next Organization": "Switch to Next Organization",
"Switch to Previous Organization": "Switch to Previous Organization",
"General": "General",
"Network": "Network",
"AddServer": "AddServer", "AddServer": "AddServer",
"Organizations": "Organizations", "App language (requires restart)": "App language (requires restart)",
"Shortcuts": "Shortcuts", "Factory Reset Data": "Factory Reset Data",
"Settings": "Settings", "Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.",
"Add a Zulip organization": "Add a Zulip organization", "On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Organization URL": "Organization URL", "Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Connect": "Connect", "Copy Link": "Copy Link",
"OR": "OR", "Copy Image": "Copy Image",
"Create a new organization": "Create a new organization", "Copy Image URL": "Copy Image URL",
"Proxy": "Proxy", "No Suggestion Found": "No Suggestion Found",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)", "You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Manual proxy configuration": "Manual proxy configuration", "Spellchecker Languages": "Spellchecker Languages",
"script": "script", "Add to Dictionary": "Add to Dictionary",
"Proxy rules": "Proxy rules", "Look Up": "Look Up"
"Proxy bypass rules": "Proxy bypass rules",
"Save": "Save",
"Appearance": "Appearance",
"Show app icon in system tray": "Show app icon in system tray",
"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)",
"Show sidebar": "Show sidebar",
"Show app unread badge": "Show app unread badge",
"Bounce dock on new private message": "Bounce dock on new private message",
"Flash taskbar on new message": "Flash taskbar on new message",
"Desktop Notifications": "Desktop Notifications",
"Show desktop notifications": "Show desktop notifications",
"Mute all sounds from Zulip": "Mute all sounds from Zulip",
"App Updates": "App Updates",
"Enable auto updates": "Enable auto updates",
"Get beta updates": "Get beta updates",
"Functionality": "Functionality",
"Start app at login": "Start app at login",
"Always start minimized": "Always start minimized",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Advanced": "Advanced",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Add custom CSS": "Add custom CSS",
"Upload": "Upload",
"Delete": "Delete",
"Default download location": "Default download location",
"Change": "Change",
"Reset Application Data": "Reset Application Data",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Reset App Data": "Reset App Data",
"Connected organizations": "Connected organizations",
"All the connected orgnizations will appear here.": "All the connected orgnizations will appear here.",
"Connect to another organization": "Connect to another organization",
"Add Custom Certificates": "Add Custom Certificates",
"Find accounts by email": "Find accounts by email",
"All the connected orgnizations will appear here": "All the connected orgnizations will appear here",
"Disconnect": "Disconnect",
"Certificate file": "Certificate file",
"Find accounts": "Find accounts",
"Tip": "Tip",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"keyboard shortcuts": "keyboard shortcuts",
"Application Shortcuts": "Application Shortcuts",
"Reset App Settings": "Reset App Settings",
"Log Out": "Log Out",
"Hide Zulip": "Hide Zulip",
"Quit Zulip": "Quit Zulip",
"Edit Shortcuts": "Edit Shortcuts",
"Emoji & Symbols": "Emoji & Symbols",
"View Shortcuts": "View Shortcuts",
"Enter Full Screen": "Enter Full Screen",
"History Shortcuts": "History Shortcuts",
"Quit when the window is closed": "Quit when the window is closed",
"File": "File",
"Network and Proxy Settings": "Network and Proxy Settings",
"Ask where to save files before downloading": "Ask where to save files before downloading"
} }

134
app/translations/en_GB.json Normal file
View File

@@ -0,0 +1,134 @@
{
"About Zulip": "About Zulip",
"Actual Size": "Actual Size",
"Add Custom Certificates": "Add Custom Certificates",
"Add Organization": "Add Organisation",
"Add a Zulip organization": "Add a Zulip organisation",
"Add custom CSS": "Add custom CSS",
"Advanced": "Advanced",
"All the connected organizations will appear here": "All the connected organisations will appear here",
"Always start minimized": "Always start minimized",
"App Updates": "App Updates",
"Appearance": "Appearance",
"Application Shortcuts": "Application Shortcuts",
"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organisation?",
"Auto hide Menu bar": "Auto hide Menu bar",
"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)",
"Back": "Back",
"Bounce dock on new private message": "Bounce dock on new private message",
"Certificate file": "Certificate file",
"Change": "Change",
"Check for Updates": "Check for Updates",
"Close": "Close",
"Connect": "Connect",
"Connect to another organization": "Connect to another organisation",
"Connected organizations": "Connected organisations",
"Copy": "Copy",
"Copy Zulip URL": "Copy Zulip URL",
"Create a new organization": "Create a new organisation",
"Cut": "Cut",
"Default download location": "Default download location",
"Delete": "Delete",
"Desktop App Settings": "Desktop App Settings",
"Desktop Notifications": "Desktop Notifications",
"Desktop Settings": "Desktop Settings",
"Disconnect": "Disconnect",
"Download App Logs": "Download App Logs",
"Edit": "Edit",
"Edit Shortcuts": "Edit Shortcuts",
"Enable auto updates": "Enable auto updates",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Factory Reset": "Factory Reset",
"File": "File",
"Find accounts": "Find accounts",
"Find accounts by email": "Find accounts by email",
"Flash taskbar on new message": "Flash taskbar on new message",
"Forward": "Forward",
"Functionality": "Functionality",
"General": "General",
"Get beta updates": "Get beta updates",
"Hard Reload": "Hard Reload",
"Help": "Help",
"Help Center": "Help Center",
"History": "History",
"History Shortcuts": "History Shortcuts",
"Keyboard Shortcuts": "Keyboard Shortcuts",
"Log Out": "Log Out",
"Log Out of Organization": "Log Out of Organisation",
"Manual proxy configuration": "Manual proxy configuration",
"Minimize": "Minimize",
"Mute all sounds from Zulip": "Mute all sounds from Zulip",
"NO": "NO",
"Network": "Network",
"OR": "OR",
"Organization URL": "Organisation URL",
"Organizations": "Organisations",
"Paste": "Paste",
"Paste and Match Style": "Paste and Match Style",
"Proxy": "Proxy",
"Proxy bypass rules": "Proxy bypass rules",
"Proxy rules": "Proxy rules",
"Quit": "Quit",
"Quit Zulip": "Quit Zulip",
"Redo": "Redo",
"Release Notes": "Release Notes",
"Reload": "Reload",
"Report an Issue": "Report an Issue",
"Save": "Save",
"Select All": "Select All",
"Settings": "Settings",
"Shortcuts": "Shortcuts",
"Show App Logs": "Show App Logs",
"Show app icon in system tray": "Show app icon in system tray",
"Show app unread badge": "Show app unread badge",
"Show desktop notifications": "Show desktop notifications",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Show sidebar": "Show sidebar",
"Start app at login": "Start app at login",
"Switch to Next Organization": "Switch to Next Organisation",
"Switch to Previous Organization": "Switch to Previous Organisation",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Tip": "Tip",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle Do Not Disturb": "Toggle Do Not Disturb",
"Toggle Full Screen": "Toggle Full Screen",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon",
"Tools": "Tools",
"Undo": "Undo",
"Upload": "Upload",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"View": "View",
"View Shortcuts": "View Shortcuts",
"Window": "Window",
"Window Shortcuts": "Window Shortcuts",
"YES": "YES",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Zulip Help": "Zulip Help",
"keyboard shortcuts": "keyboard shortcuts",
"script": "script",
"Quit when the window is closed": "Quit when the window is closed",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"AddServer": "AddServer",
"App language (requires restart)": "App language (requires restart)",
"Factory Reset Data": "Factory Reset Data",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organisations, accounts, and certificates.",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Copy Link": "Copy Link",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"No Suggestion Found": "No Suggestion Found",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Spellchecker Languages": "Spellchecker Languages",
"Add to Dictionary": "Add to Dictionary",
"Look Up": "Look Up"
}

View File

@@ -1,117 +1,134 @@
{ {
"About Zulip": "Acerca de Zulip", "About Zulip": "Acerca de Zulip",
"Actual Size": "Tamaño real", "Actual Size": "Tamaño actual",
"Add Custom Certificates": "Añadir certificados personalizados", "Add Custom Certificates": "Añadir certificados personalizados",
"Add Organization": "Añadir organización", "Add Organization": "Añadir organización",
"Add a Zulip organization": "Añadir una organización Zulip", "Add a Zulip organization": "Añadir una organización de Zulip",
"Add custom CSS": "Añadir CSS personalizado", "Add custom CSS": "Añadir CSS personalizado",
"Advanced": "Avanzado", "Advanced": "Avanzado",
"All the connected organizations will appear here": "Todas las organizaciones conectadas aparecerán aquí.", "All the connected organizations will appear here": "Todas las organizaciones conectadas aparecerán aquí",
"Always start minimized": "Siempre empezar minimizado", "Always start minimized": "Iniciar siempre minimizado",
"App Updates": "Actualizaciones de la aplicación", "App Updates": "Actualizaciones de la aplicación",
"Appearance": "Apariencia", "Appearance": "Apariencia",
"Application Shortcuts": "Atajos de aplicación", "Application Shortcuts": "Atajos de la aplicación",
"Are you sure you want to disconnect this organization?": "¿Estás seguro de que quieres desconectar esta organización?", "Are you sure you want to disconnect this organization?": "Estas seguro de desconectar esta organización?",
"Auto hide Menu bar": "Ocultar automáticamente la barra de menú", "Auto hide Menu bar": "Ocultar la barra de menú automáticamente",
"Auto hide menu bar (Press Alt key to display)": "Ocultar automáticamente la barra de menú (presionar la tecla Alt para mostrar)", "Auto hide menu bar (Press Alt key to display)": "Ocultar la barra de menú automáticamente (mantén tecla Alt para mostrar)",
"Back": "Espalda", "Back": "Atrás",
"Bounce dock on new private message": "Bounce dock en nuevo mensaje privado", "Bounce dock on new private message": "Rebotar dock cuando se reciba un nuevo mensaje privado",
"Certificate file": "Archivo de certificado", "Certificate file": "Archivo de certificado",
"Change": "Cambio", "Change": "Cambiar",
"Check for Updates": "Buscar actualizaciones", "Check for Updates": "Comprobar actualizaciones",
"Close": "Cerrar", "Close": "Cerrar",
"Connect": "Conectar", "Connect": "Conectar",
"Connect to another organization": "Conectarse a otra organización", "Connect to another organization": "Conectar a otra organización",
"Connected organizations": "Organizaciones conectadas", "Connected organizations": "Organizaciones conectada",
"Copy": "Dupdo", "Copy": "Copiar",
"Copy Zulip URL": "Copiar URL de Zulip", "Copy Zulip URL": "Copiar URL de Zulip",
"Create a new organization": "Crear una nueva organización", "Create a new organization": "Crear una nueva organización",
"Cut": "Cortar", "Cut": "Cortar",
"Default download location": "Ubicación de descarga predeterminada", "Default download location": "Ubicación por defecto de descargas",
"Delete": "Borrar", "Delete": "Eliminar",
"Desktop App Settings": "Configuración de la aplicación de escritorio", "Desktop App Settings": "Ajustes de la aplicación de escritorio",
"Desktop Notifications": "Notificaciones de escritorio", "Desktop Notifications": "Notificaciones de escritorio",
"Desktop Settings": "Configuraciones de escritorio", "Desktop Settings": "Ajustes de escritorio",
"Disconnect": "Desconectar", "Disconnect": "Desconectar",
"Download App Logs": "Descargar App Logs", "Download App Logs": "Descargar registros de la aplicación",
"Edit": "Editar", "Edit": "Editar",
"Edit Shortcuts": "Editar accesos directos", "Edit Shortcuts": "Editar atajos",
"Enable auto updates": "Habilitar actualizaciones automáticas", "Enable auto updates": "Activar actualizaciones automáticas",
"Enable error reporting (requires restart)": "Habilitar informes de errores (requiere reinicio)", "Enable error reporting (requires restart)": "Activar reporte de fallos (necesita reinicio)",
"Enable spellchecker (requires restart)": "Habilitar el corrector ortográfico (requiere reinicio)", "Enable spellchecker (requires restart)": "Activar corrector ortográfico (necesita reinicio)",
"Factory Reset": "Restablecimiento de fábrica", "Factory Reset": "Reinicio de fábrica",
"File": "Expediente", "File": "Archivo",
"Find accounts": "Encontrar cuentas", "Find accounts": "Encontrar cuentas",
"Find accounts by email": "Encuentra cuentas por correo electrónico", "Find accounts by email": "Encontrar cuentas por correo electrónico",
"Flash taskbar on new message": "Flash barra de tareas en nuevo mensaje", "Flash taskbar on new message": "Hacer que la barra de tareas parpadee cuando se reciba un mensaje nuevo",
"Forward": "Adelante", "Forward": "Reenviar",
"Functionality": "Funcionalidad", "Functionality": "Funcionalidad",
"General": "General", "General": "General",
"Get beta updates": "Recibe actualizaciones beta", "Get beta updates": "Obtener actualizaciones beta",
"Hard Reload": "Recarga dura", "Hard Reload": "Recarga forzosa",
"Help": "Ayuda", "Help": "Ayuda",
"Help Center": "Centro de ayuda", "Help Center": "Centro de ayuda",
"History": "Historia", "History": "Historial",
"History Shortcuts": "Atajos de historia", "History Shortcuts": "Atajos del historial",
"Keyboard Shortcuts": "Atajos de teclado", "Keyboard Shortcuts": "Atajos de teclado",
"Log Out": "Cerrar sesión", "Log Out": "Cerrar sesión",
"Log Out of Organization": "Salir de la organización", "Log Out of Organization": "Cerrar sesión de organización",
"Manual proxy configuration": "Configuración de proxy manual", "Manual proxy configuration": "Configuración de proxy manual",
"Minimize": "Minimizar", "Minimize": "Minimizar",
"Mute all sounds from Zulip": "Silencia todos los sonidos de Zulip", "Mute all sounds from Zulip": "Silenciar todos los sonidos de Zulip",
"NO": "NO", "NO": "NO",
"Network": "Red", "Network": "Red",
"OR": "O", "OR": "O",
"Organization URL": "URL de la organización", "Organization URL": "URL de la organización",
"Organizations": "Organizaciones", "Organizations": "Organizaciones",
"Paste": "Pegar", "Paste": "Pegar",
"Paste and Match Style": "Pegar y combinar estilo", "Paste and Match Style": "Pegar y mantener estilo",
"Proxy": "Apoderado", "Proxy": "Proxy",
"Proxy bypass rules": "Reglas de omisión de proxy", "Proxy bypass rules": "Reglas para ignorar proxy",
"Proxy rules": "Reglas de proxy", "Proxy rules": "Reglas del proxy",
"Quit": "Dejar", "Quit": "Cerrar",
"Quit Zulip": "Dejar Zulip", "Quit Zulip": "Cerrar Zulip",
"Redo": "Rehacer", "Redo": "Rehacer",
"Release Notes": "Notas de lanzamiento", "Release Notes": "Notas de la versión",
"Reload": "Recargar", "Reload": "Recargar",
"Report an Issue": "Reportar un problema", "Report an Issue": "Informar de un error",
"Reset App Data": "Restablecer datos de la aplicación", "Save": "Guardar",
"Reset App Settings": "Restablecer la configuración de la aplicación",
"Reset Application Data": "Restablecer datos de aplicación",
"Save": "Salvar",
"Select All": "Seleccionar todo", "Select All": "Seleccionar todo",
"Settings": "Ajustes", "Settings": "Ajustes",
"Shortcuts": "Atajos", "Shortcuts": "Atajos de teclado",
"Show App Logs": "Mostrar registros de aplicaciones", "Show App Logs": "Mostrar registros de la aplicación",
"Show app icon in system tray": "Mostrar icono de la aplicación en la bandeja del sistema", "Show app icon in system tray": "Mostrar un icono de la aplicación en la bandeja del sistema",
"Show app unread badge": "Mostrar la aplicación de placa sin leer", "Show app unread badge": "Mostrar un globo en el icono si hay mensajes sin leer",
"Show desktop notifications": "Mostrar notificaciones de escritorio", "Show desktop notifications": "Mostrar notificaciones de escritorio",
"Show downloaded files in file manager": "Mostrar los archivos descargados en el administrador de archivos", "Show downloaded files in file manager": "Mostrar archivos descargados en el explorador",
"Show sidebar": "Mostrar barra lateral", "Show sidebar": "Mostrar barra lateral",
"Start app at login": "Iniciar la aplicación al iniciar sesión", "Start app at login": "Lanzar aplicación al inicio",
"Switch to Next Organization": "Cambiar a la siguiente organización", "Switch to Next Organization": "Cambiar a la siguiente organización",
"Switch to Previous Organization": "Cambiar a la organización anterior", "Switch to Previous Organization": "Cambiar a la anterior organización",
"These desktop app shortcuts extend the Zulip webapp's": "Estos accesos directos de aplicaciones de escritorio extienden las aplicaciones web de Zulip.", "These desktop app shortcuts extend the Zulip webapp's": "Estos atajos de la aplicación de escritorio extienden los ya existentes en Zulip",
"This will delete all application data including all added accounts and preferences": "Esto eliminará todos los datos de la aplicación, incluidas todas las cuentas y preferencias agregadas.", "This will delete all application data including all added accounts and preferences": "Esto borrará todos los datos de la aplicación, incluyendo cuentas añadidas y preferencia",
"Tip": "Propina", "Tip": "Consej",
"Toggle DevTools for Active Tab": "Alternar DevTools para Active Tab", "Toggle DevTools for Active Tab": "Activar/desactivar herramientas de desarrollador para la pestaña activa",
"Toggle DevTools for Zulip App": "Alternar DevTools para Zulip App", "Toggle DevTools for Zulip App": "Activar/desactivar herramientas de desarrollador para la aplicación de Zulip",
"Toggle Do Not Disturb": "Alternar No molestar", "Toggle Do Not Disturb": "Activar/desactivar no molestar",
"Toggle Full Screen": "Alternar pantalla completa", "Toggle Full Screen": "Activar/desactivar pantalla completa",
"Toggle Sidebar": "Alternar barra lateral", "Toggle Sidebar": "Activar/desactivar barra lateral",
"Toggle Tray Icon": "Icono de bandeja de palanca", "Toggle Tray Icon": "Activar/desactivar icono en bandeja del sistema",
"Tools": "Herramientas", "Tools": "Herramientas",
"Undo": "Deshacer", "Undo": "Deshacer",
"Upload": "Subir", "Upload": "Subir",
"Use system proxy settings (requires restart)": "Usar la configuración del proxy del sistema (requiere reinicio)", "Use system proxy settings (requires restart)": "Usar ajustes de proxy del sistema (necesita reinicio)",
"View": "Ver", "View": "Ver",
"View Shortcuts": "Ver accesos directos", "View Shortcuts": "Ver atajos",
"Window": "Ventana", "Window": "Ventana",
"Window Shortcuts": "Atajos de ventana", "Window Shortcuts": "Atajos de ventana",
"YES": "SÍ", "YES": "SÍ",
"Zoom In": "Acercarse", "Zoom In": "Aumentar zoom",
"Zoom Out": "Disminuir el zoom", "Zoom Out": "Reducir zoom",
"Zulip Help": "Ayuda de Zulip", "Zulip Help": "Ayuda sobre Zulip",
"keyboard shortcuts": "atajos de teclado", "keyboard shortcuts": "atajos de teclado",
"script": "guión" "script": "script",
"Quit when the window is closed": "Salir cuando la ventana se cierre",
"Ask where to save files before downloading": "Preguntar dónde guardar los archivos antes de descargar",
"Services": "Servicios",
"Hide": "Ocultar",
"Hide Others": "Ocultar otros",
"Unhide": "Dejar de ocultar",
"AddServer": "AddServer",
"App language (requires restart)": "Idioma de la aplicación (requiere reinicio)",
"Factory Reset Data": "Factory Reset Data",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reinicia la aplicación, borrando todas las organizaciones, cuentas y certificados conectados.",
"On macOS, the OS spellchecker is used.": "En macOS se utiliza la verificación ortográfica del sistema operativo.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Modifica el idioma en Preferencias del sistema → Teclado → Texto → Ortografía.",
"Copy Link": "Copiar enlace",
"Copy Image": "Copiar imagen",
"Copy Image URL": "Copiar URL de la imagen",
"No Suggestion Found": "No se encontró ninguna sugerencia",
"You can select a maximum of 3 languages for spellchecking.": "Puedes elegir un máximo de 3 idiomas para la verificación ortográfica.",
"Spellchecker Languages": "Idiomas de verificación ortográfica",
"Add to Dictionary": "Añadir al diccionario",
"Look Up": "Consultar"
} }

134
app/translations/fa.json Normal file
View File

@@ -0,0 +1,134 @@
{
"About Zulip": "درباره Zulip ",
"Actual Size": "اندازه واقعی",
"Add Custom Certificates": "اضافه کردن مجوز دلخواه",
"Add Organization": "اضافه کردن سازمان",
"Add a Zulip organization": "اضافه کردن سازمان Zulip",
"Add custom CSS": "اضافه کردن CSS دلخواه",
"Advanced": "پیشرفته",
"All the connected organizations will appear here": "همه سازمان‌های متصل شده اینجا نمایش داده می‌شوند",
"Always start minimized": "همواره به صورت کوچک شده اجرا شو",
"App Updates": "به‌روزرسانی‌های برنامه",
"Appearance": "شمایل",
"Application Shortcuts": "میانبرهای برنامه",
"Are you sure you want to disconnect this organization?": "آیا از قطع ارتباط از سازمان اطمینان دارید؟",
"Auto hide Menu bar": "مخفی‌سازی خودکار نوار منو",
"Auto hide menu bar (Press Alt key to display)": "مخفی‌سازی خودکار نوار منو (برای نمایش دکمه Alt را بزنید)",
"Back": "عقب",
"Bounce dock on new private message": "Bounce dock on new private message",
"Certificate file": "فایل مجوز",
"Change": "تغییر دادن",
"Check for Updates": "بررسی برای به‌روز‌رسانی",
"Close": "بستن",
"Connect": "اتصال",
"Connect to another organization": "اتصال به یک سازمان دیگر",
"Connected organizations": "سازمان‌های وصل شده",
"Copy": "رونوشت",
"Copy Zulip URL": "رونوشت از Zulip URL",
"Create a new organization": "ایجاد سازمان جدید",
"Cut": "بریدن",
"Default download location": "محل پیش‌فرض دانلود",
"Delete": "حذف",
"Desktop App Settings": "Desktop App Settings",
"Desktop Notifications": "اطلاع‌رسانی‌های دسکتاپ",
"Desktop Settings": "تنظیمات دسکتاپ",
"Disconnect": "Disconnect",
"Download App Logs": "Download App Logs",
"Edit": "ویرایش",
"Edit Shortcuts": "ویرایش میانبرها",
"Enable auto updates": "Enable auto updates",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Factory Reset": "Factory Reset",
"File": "فایل",
"Find accounts": "پیدا کردن حساب های کاربری ",
"Find accounts by email": "Find accounts by email",
"Flash taskbar on new message": "Flash taskbar on new message",
"Forward": "Forward",
"Functionality": "Functionality",
"General": "عمومی",
"Get beta updates": "Get beta updates",
"Hard Reload": "Hard Reload",
"Help": "کمک",
"Help Center": "مرکز کمک",
"History": "تاریخچه ",
"History Shortcuts": "تاریخچه میانبرها",
"Keyboard Shortcuts": "میانبرهای صفحه‌کلید",
"Log Out": "خروج",
"Log Out of Organization": "Log Out of Organization",
"Manual proxy configuration": "Manual proxy configuration",
"Minimize": "Minimize",
"Mute all sounds from Zulip": "Mute all sounds from Zulip",
"NO": "NO",
"Network": "Network",
"OR": "یا",
"Organization URL": "URL سازمان",
"Organizations": "Organizations",
"Paste": "Paste",
"Paste and Match Style": "Paste and Match Style",
"Proxy": "Proxy",
"Proxy bypass rules": "Proxy bypass rules",
"Proxy rules": "Proxy rules",
"Quit": "Quit",
"Quit Zulip": "Quit Zulip",
"Redo": "Redo",
"Release Notes": "Release Notes",
"Reload": "Reload",
"Report an Issue": "Report an Issue",
"Save": "ذخیره",
"Select All": "Select All",
"Settings": "تنظیمات",
"Shortcuts": "Shortcuts",
"Show App Logs": "Show App Logs",
"Show app icon in system tray": "Show app icon in system tray",
"Show app unread badge": "Show app unread badge",
"Show desktop notifications": "Show desktop notifications",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Show sidebar": "Show sidebar",
"Start app at login": "Start app at login",
"Switch to Next Organization": "Switch to Next Organization",
"Switch to Previous Organization": "Switch to Previous Organization",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Tip": "Tip",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle Do Not Disturb": "Toggle Do Not Disturb",
"Toggle Full Screen": "Toggle Full Screen",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon",
"Tools": "Tools",
"Undo": "Undo",
"Upload": "Upload",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"View": "View",
"View Shortcuts": "View Shortcuts",
"Window": "Window",
"Window Shortcuts": "Window Shortcuts",
"YES": "YES",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Zulip Help": "Zulip Help",
"keyboard shortcuts": "keyboard shortcuts",
"script": "script",
"Quit when the window is closed": "Quit when the window is closed",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"AddServer": "AddServer",
"App language (requires restart)": "App language (requires restart)",
"Factory Reset Data": "Factory Reset Data",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Copy Link": "Copy Link",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"No Suggestion Found": "No Suggestion Found",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Spellchecker Languages": "Spellchecker Languages",
"Add to Dictionary": "Add to Dictionary",
"Look Up": "Look Up"
}

Some files were not shown because too many files have changed in this diff Show More