Compare commits

...

99 Commits

Author SHA1 Message Date
Anders Kaseorg
c08bbf49ab release: New release v5.12.2.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-09-01 10:49:56 -07:00
Hosted Weblate
913eda8b1e translations: Update translations from Weblate. 2025-09-01 19:39:02 +02:00
Anders Kaseorg
52a3fa6bd1 release: New release v5.12.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-29 13:23:54 -07:00
Anders Kaseorg
c1f2ae5ef8 context-menu: Enable macOS Writing Tools.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-29 13:23:54 -07:00
Anders Kaseorg
301fe26d80 Upgrade dependencies, including Electron 37.4.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-29 12:41:45 -07:00
Hosted Weblate
92a2b4eae9 translations: Update translations from Weblate. 2025-08-29 19:41:14 +00:00
Anders Kaseorg
6e307570d0 Replace Transifex documentation and configuration with Weblate.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-29 12:35:20 -07:00
Anders Kaseorg
dc39c68389 Modernize APT configuration format.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-28 16:38:58 -07:00
Anders Kaseorg
73cdfa7249 how-to-install: Note that the Debian package configures APT.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-28 16:38:58 -07:00
Anders Kaseorg
d9e4b0a40b Update electron-builder configuration.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-28 16:23:57 -07:00
Anders Kaseorg
0c7ce62ce1 Update apt signing key with SHA-256 binding signatures.
Fixes #1437.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-12 20:37:06 -07:00
Anders Kaseorg
9dd5fd2aa5 Mark more strings for translation.
Fixes #1128 among many other things.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-06 14:04:43 -07:00
Anders Kaseorg
11e2635aa0 Correct node-json-db Errors.js imports.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-06 14:02:00 -07:00
Anders Kaseorg
b35cf13a77 tests: Move tests/package.json to its own folder.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-06 13:38:35 -07:00
Anders Kaseorg
814de8ad6a tests: Convert tests to TypeScript.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-06 13:29:46 -07:00
Anders Kaseorg
d9dbbf2359 tests: Switch from medium to p-fifo.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-06 13:29:46 -07:00
Anders Kaseorg
a9c9de2dee Convert i18next-parser configuration to TypeScript.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-06 13:29:46 -07:00
Anders Kaseorg
9b626950ae workflows: Use actions/setup-node.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-05 01:10:40 -07:00
Anders Kaseorg
45672432db Focus the webview for notification settings, keyboard shortcuts.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-05 00:57:36 -07:00
Anders Kaseorg
b5665abb3e Upgrade dependencies, including Electron 37.2.5.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-05 00:48:30 -07:00
Anders Kaseorg
5b30bb2a16 stylelint: Fix property-no-deprecated.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-05 00:40:03 -07:00
Anders Kaseorg
598aa6f4b9 webview: Adjust app.dock feature test for TypeScript friendliness.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-05 00:40:03 -07:00
Anders Kaseorg
2e7ed457f0 xo: Fix @typescript-eslint/prefer-nullish-coalescing.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-05 00:40:03 -07:00
Anders Kaseorg
bb3cad818b xo: Fix unicorn/prefer-string-raw.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-05 00:40:03 -07:00
Anders Kaseorg
e3d9308c21 xo: Fix unicorn/prefer-single-call.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-05 00:40:03 -07:00
Anders Kaseorg
098d35fc5c index: Avoid deprecated Buffer#slice.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-05 00:40:03 -07:00
Anders Kaseorg
eb849a7b3d Switch to "type": "module".
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-05 00:32:25 -07:00
Anders Kaseorg
ab3698f56c Switch i18next-scanner to i18next-parser.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-08-02 11:53:21 -07:00
Anders Kaseorg
0fdeb1fd17 translations: Update translations from Transifex.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-07-31 17:48:29 -07:00
Anders Kaseorg
d270d56309 xo: Prohibit main-only and renderer-only APIs in wrong processes.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-06-20 16:35:18 -07:00
Anders Kaseorg
2c5b1ad297 xo: Use eslint-import-resolver-typescript.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-06-20 16:35:18 -07:00
Anders Kaseorg
26b226c7ae Use .ts extensions for imports.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-06-20 16:35:18 -07:00
Anders Kaseorg
7f6699e235 tsconfig: Enable allowImportingTsExtensions.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-06-20 16:35:18 -07:00
Anders Kaseorg
339f0d19c7 xo: Configure import/no-extraneous-dependencies packageDir.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-06-20 16:35:18 -07:00
Anders Kaseorg
86882c0741 xo: Move configuration to xo.config.cjs.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-06-20 16:35:18 -07:00
Anders Kaseorg
cf5a691a36 Revert "enterprise: Quit app after showing error for invalid global config."
This reverts commit 51ff949d34.

It incorrectly uses a main-only API in app/common, which is shared
between the main and renderer processes.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-06-20 16:35:18 -07:00
Shubham Padia
51ff949d34 enterprise: Quit app after showing error for invalid global config.
Otherwise, the error will keep showing multiple times ultimately
leading to a non-working app after multiple errors.
2025-06-17 18:07:11 -07:00
Shubham Padia
e5680b12f4 enterprise: Show error dialogbox on invalid JSON.
Fixes #1404.

Co-authored-by: sammamama <samridhsame@gmail.com>
2025-06-17 18:07:11 -07:00
Shubham Padia
b42f9de27d preferences: Increase contrast of setting text and nav items.
The text in the settings panel was unnecessarily fade, making it hard to
read. Increasing the contrast makes it more readable. The setting text
color is the same as the heading of the settings. This commit also
changes the color of the nav items to be the same as `|` bar preceding
it.
https://chat.zulip.org/#narrow/channel/101-design/topic/zulip-desktop.20preferences.20contrast
2025-06-11 12:33:00 -07:00
Shubham Padia
201faa9449 settings: Make Do not disturb icon brigther when it's on.
It was not so obvious to users when they were in DND mode, making the
icon brigther when in DND mode hopes to address that.
https://chat.zulip.org/#narrow/channel/101-design/topic/zulip-desktop.20DND.20icon.20contrast
2025-06-11 12:32:11 -07:00
Shubham Padia
4125de4a60 css: Use hsl for action-button icon colors.
We only change this for `.action-button i` and `.action-button:hover i`
since we need to make the dnd icon lighter in comparison to these two.
These are the two that are absolutely necessary to change, we can also
change others to hsl color space after discussion on CZO.
2025-06-11 12:32:11 -07:00
Alex Vandiver
916fab7963 translations: Update translations from Transifex. 2025-05-20 18:59:55 +00:00
Anders Kaseorg
15902e51f6 release: New release v5.12.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-03-13 14:06:16 -07:00
Anders Kaseorg
19705bc90b Update macOS notarization configuration.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-03-13 14:06:12 -07:00
Anders Kaseorg
a9313f4756 Update Azure Trusted Signing configuration.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-03-13 14:06:12 -07:00
Anders Kaseorg
13b4d2037a Upgrade dependencies, including Electron 35.0.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-03-13 13:36:26 -07:00
Anders Kaseorg
ab63ec2a4a translations: Update translations from Transifex.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-03-12 17:22:53 -07:00
Anders Kaseorg
1de4f88c6c webview: Address deprecation of WebContents.goBack et al.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-03-12 17:15:03 -07:00
Anders Kaseorg
ab4381a6bf xo: Fix unicorn/prefer-global-this.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-03-12 17:15:03 -07:00
Anders Kaseorg
d409a0bf33 menu: Check focusedWindow type.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-03-12 17:15:03 -07:00
Anders Kaseorg
c40e05646e Update macOS icon with a native appearance.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-03-12 16:59:49 -07:00
Anders Kaseorg
13f3818c77 supported-locales: Fix for removal of el_GR and zh-Hant.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-12-11 14:44:47 -05:00
Tim Abbott
4a0e590921 github: Use zulip/zulip pull request template style.
Hopefully this will help improve the quality of pull requests to this
repository.
2024-12-11 11:22:04 -08:00
Shubham Padia
eb19b20da2 preference: Rename show app unread badge setting.
The original wording was found to be confusing, see
https://chat.zulip.org/#narrow/channel/16-desktop/topic/.22app.20unread.20badge.22/near/1993426
for more details.
2024-12-11 11:18:19 -08:00
Alex Vandiver
69cb509fe5 translations: Remove control characters from zh_TW translation string. 2024-12-11 11:28:13 -05:00
Alex Vandiver
123263e5bb translations: Remove zh-Hant translation.
The `zh_TW` translation is much more complete.
2024-12-11 11:24:47 -05:00
Alex Vandiver
a26a10849d translations: Remove empty el_GR duplicate translation. 2024-12-11 11:20:20 -05:00
Anders Kaseorg
da7e026550 Mark dialog strings for translation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-12-02 15:53:25 -08:00
Anders Kaseorg
c70f6df096 about: Remove inexplicable ‘cursor: pointer’.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-12-02 15:52:29 -08:00
Anders Kaseorg
ef0110f8e7 about: Mark strings for translation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-12-02 15:52:29 -08:00
Anders Kaseorg
b7a7ca3e5c renderer: Mark strings for translation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-12-02 15:52:29 -08:00
Anders Kaseorg
467e7b11c5 functional-tab: Split ‘name’ into ‘page’ and ‘label’.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-12-02 15:52:29 -08:00
Anders Kaseorg
105e7e93a1 translations: Add missing translatable strings with i18next-scanner.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-12-02 15:52:28 -08:00
Anders Kaseorg
a736f664c6 nav: Statically mark navigation items for translation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-12-02 15:52:06 -08:00
Anders Kaseorg
38c7695a99 release: New release v5.11.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-08-23 16:31:21 -07:00
Anders Kaseorg
b268fe9478 Sign Windows binaries with Azure Trusted Signing.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-08-23 16:22:19 -07:00
Anders Kaseorg
981a262836 xo: Remove obsolete scripts/notarize.js options.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-08-22 16:18:05 -07:00
Anders Kaseorg
527bb5ab2f Upgrade dependencies, including Electron 32.0.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-08-22 16:13:32 -07:00
Anders Kaseorg
e2947a0ce6 translations: Update translations from Transifex.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-08-22 16:13:27 -07:00
Anders Kaseorg
3b2c758e09 translations: Sort supported-locales by display name.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-08-22 16:13:27 -07:00
Anders Kaseorg
4867fc672a preference: Sort spellchecker language names with localeCompare.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-08-22 16:13:27 -07:00
Anders Kaseorg
f85f05d66b preference: Show spellchecker language names from Intl.DisplayNames.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-08-22 16:13:27 -07:00
Anders Kaseorg
39fd0e9877 tsconfig: Work around @sentry/electron regression.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-08-07 14:58:13 -07:00
Anders Kaseorg
f6ff112f0e stylelint: Fix declaration-block-no-shorthand-property-overrides.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-08-07 14:53:08 -07:00
Anders Kaseorg
6fcd1ef0d5 Remove rimraf.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-08-06 22:55:40 -07:00
Misha Brukman
92260b0f97 ci: Replace Travis CI badge with GitHub Actions.
Travis CI was replaced with GitHub Actions in this project.
2024-07-02 15:41:59 -07:00
Anders Kaseorg
c45c9537d1 release: New release v5.11.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-03-22 16:19:52 -07:00
Anders Kaseorg
0eb4c9236e Upgrade dependencies, including Electron 29.1.5.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-03-22 16:12:34 -07:00
Anders Kaseorg
47366b7617 xo: Fix unicorn/prevent-abbreviations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-03-22 16:07:32 -07:00
Anders Kaseorg
86e28f5b00 xo: Fix import/no-duplicates.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-03-22 15:33:00 -07:00
Anders Kaseorg
7072a41e01 Remove dialog for certificate errors on subresources.
Fixes #1119.  Closes #1277.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-03-22 14:56:23 -07:00
enesonus
79f6f13008 Allow hiding the window from full screen mode on macOS.
Fixes #1187.
2024-03-22 14:39:48 -07:00
Anders Kaseorg
70f0170f1d webview: Enable zooming with the mouse wheel.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-03-21 15:32:52 -07:00
Anders Kaseorg
bc75eba2bd webview: Use an exponential scale for zooming.
This matches the native Electron behavior.

Fixes part of #1360 by removing the separate zoomFactor state
variable.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-03-21 15:31:56 -07:00
Anders Kaseorg
af7272a439 release: New release v5.10.5.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-01-25 17:02:14 -08:00
Anders Kaseorg
9d08a13e64 Set a restrictive Content-Security-Policy for the app UI.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-01-25 15:39:05 -08:00
Anders Kaseorg
f98d6d7037 Upgrade dependencies, including Electron 28.2.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-01-25 14:05:37 -08:00
Anders Kaseorg
da1cad9dff autoupdater: Use a separate electron-log instance.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-01-23 16:09:20 -08:00
Anders Kaseorg
955a2eb6c7 Use process-specific electron-log modules.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-01-23 16:09:20 -08:00
Anders Kaseorg
1cf822a2b5 Use process-specific @sentry/electron modules.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-01-23 16:09:20 -08:00
Anders Kaseorg
b9baf140eb release: New release v5.10.4.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-01-08 17:12:08 -08:00
Anders Kaseorg
727c2335f6 electron-bridge: Fix unicorn/prefer-node-protocol.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-01-08 17:10:37 -08:00
Anders Kaseorg
e8173919f8 Upgrade dependencies, including Electron 28.1.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-01-08 17:10:37 -08:00
Anders Kaseorg
cf2f4fe9c9 Avoid deprecated ipcRenderer.sendTo.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2024-01-08 16:02:14 -08:00
Anders Kaseorg
47cdd5fa8b release: New release v5.10.3.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-29 23:38:26 -07:00
Anders Kaseorg
90e76fab6e Upgrade dependencies, including Electron 25.8.4.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-29 23:38:22 -07:00
Anders Kaseorg
193adb1901 Fix gatemaker TypeError with Electron 25.
This had been breaking our download notifications.  Fixes #1333.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-29 23:25:32 -07:00
Anders Kaseorg
b520e12492 release: New release v5.10.2.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-14 10:31:05 -07:00
Anders Kaseorg
ae642bc7ba Downgrade Electron from 26.2.1 to 25.8.1 to avoid renderer crash.
https://github.com/electron/electron/issues/39775

Fixes #1327.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-14 10:30:50 -07:00
165 changed files with 13482 additions and 8330 deletions

View File

@@ -6,6 +6,6 @@ charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[{*.css,*.html,*.js,*.json,*.ts}] [{*.cjs,*.css,*.html,*.js,*.json,*.ts}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

View File

@@ -1,18 +1,49 @@
--- <!-- Describe your pull request here.-->
<!-- Fixes: <!-- Issue link, or clear description.-->
Remove the fields that are not appropriate
Please include: <!-- If the PR makes UI changes, always include one or more still screenshots to demonstrate your changes. If it seems helpful, add a screen capture of the new functionality as well.
Tooling tips: https://zulip.readthedocs.io/en/latest/tutorials/screenshot-and-gif-software.html
--> -->
**What's this PR do?** **Screenshots and screen captures:**
**Any background context you want to provide?** **Platforms this PR was tested on:**
**Screenshots?**
**You have tested this PR on:**
- [ ] Windows - [ ] Windows
- [ ] Linux/Ubuntu
- [ ] macOS - [ ] macOS
- [ ] Linux (specify distro)
<details>
<summary>Self-review checklist</summary>
<!-- Prior to submitting a PR, follow our step-by-step guide to review your own code:
https://zulip.readthedocs.io/en/latest/contributing/code-reviewing.html#how-to-review-code -->
<!-- Once you create the PR, check off all the steps below that you have completed.
If any of these steps are not relevant or you have not completed, leave them unchecked.-->
- [ ] [Self-reviewed](https://zulip.readthedocs.io/en/latest/contributing/code-reviewing.html#how-to-review-code) the changes for clarity and maintainability
(variable names, code reuse, readability, etc.).
Communicate decisions, questions, and potential concerns.
- [ ] Explains differences from previous plans (e.g., issue description).
- [ ] Highlights technical choices and bugs encountered.
- [ ] Calls out remaining decisions and concerns.
- [ ] Automated tests verify logic where appropriate.
Individual commits are ready for review (see [commit discipline](https://zulip.readthedocs.io/en/latest/contributing/commit-discipline.html)).
- [ ] Each commit is a coherent idea.
- [ ] Commit message(s) explain reasoning and motivation for changes.
Completed manual review and testing of the following:
- [ ] Visual appearance of the changes.
- [ ] Responsiveness and internationalization.
- [ ] Strings and tooltips.
- [ ] End-to-end functionality of buttons, interactions and flows.
- [ ] Corner cases, error conditions, and easily imagined bugs.
</details>

View File

@@ -10,6 +10,9 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-node@v4
with:
node-version: lts/*
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: npm ci - run: npm ci
- run: npm test - run: npm test

3
.gitignore vendored
View File

@@ -4,9 +4,6 @@
# 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/
/dist-electron/ /dist-electron/

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
node-options=--experimental-strip-types

View File

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

View File

@@ -1,6 +1,6 @@
# Zulip Desktop Client # Zulip Desktop Client
[![Build Status](https://travis-ci.com/zulip/zulip-desktop.svg?branch=main)](https://travis-ci.com/github/zulip/zulip-desktop) [![Build Status](https://github.com/zulip/zulip-desktop/actions/workflows/node.js.yml/badge.svg)](https://github.com/zulip/zulip-desktop/actions/workflows/node.js.yml?query=branch%3Amain)
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/zulip/zulip-desktop?branch=main&svg=true)](https://ci.appveyor.com/project/zulip/zulip-desktop/branch/main) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/zulip/zulip-desktop?branch=main&svg=true)](https://ci.appveyor.com/project/zulip/zulip-desktop/branch/main)
[![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)
[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://chat.zulip.org) [![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://chat.zulip.org)

View File

@@ -37,6 +37,7 @@ export const configSchemata = {
useProxy: z.boolean(), useProxy: z.boolean(),
useSystemProxy: z.boolean(), useSystemProxy: z.boolean(),
}; };
export type ConfigSchemata = typeof configSchemata;
export const enterpriseConfigSchemata = { export const enterpriseConfigSchemata = {
...configSchemata, ...configSchemata,

View File

@@ -1,41 +1,47 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import * as Sentry from "@sentry/electron"; import * as Sentry from "@sentry/core";
import {JsonDB} from "node-json-db"; import {JsonDB} from "node-json-db";
import {DataError} from "node-json-db/dist/lib/Errors"; import {DataError} from "node-json-db/dist/lib/Errors.js";
import type {z} from "zod"; import type {z} from "zod";
import {app, dialog} from "zulip:remote"; import {app, dialog} from "zulip:remote";
import {configSchemata} from "./config-schemata.js"; import {type ConfigSchemata, configSchemata} from "./config-schemata.ts";
import * as EnterpriseUtil from "./enterprise-util.js"; import * as EnterpriseUtil from "./enterprise-util.ts";
import Logger from "./logger-util.js"; import Logger from "./logger-util.ts";
export type Config = { export type Config = {
[Key in keyof typeof configSchemata]: z.output<(typeof configSchemata)[Key]>; [Key in keyof ConfigSchemata]: z.output<ConfigSchemata[Key]>;
}; };
const logger = new Logger({ const logger = new Logger({
file: "config-util.log", file: "config-util.log",
}); });
let db: JsonDB; let database: JsonDB;
reloadDb(); reloadDatabase();
export function getConfigItem<Key extends keyof Config>( export function getConfigItem<Key extends keyof Config>(
key: Key, key: Key,
defaultValue: Config[Key], defaultValue: Config[Key],
): z.output<(typeof configSchemata)[Key]> { ): z.output<ConfigSchemata[Key]> {
try { try {
db.reload(); database.reload();
} catch (error: unknown) { } catch (error: unknown) {
logger.error("Error while reloading settings.json: "); logger.error("Error while reloading settings.json: ");
logger.error(error); logger.error(error);
} }
try { try {
return configSchemata[key].parse(db.getObject<unknown>(`/${key}`)); const typedSchemata: {
[Key in keyof Config]: z.ZodType<
z.output<ConfigSchemata[Key]>,
z.input<ConfigSchemata[Key]>
>;
} = configSchemata; // https://github.com/colinhacks/zod/issues/5154
return typedSchemata[key].parse(database.getObject<unknown>(`/${key}`));
} catch (error: unknown) { } catch (error: unknown) {
if (!(error instanceof DataError)) throw error; if (!(error instanceof DataError)) throw error;
setConfigItem(key, defaultValue); setConfigItem(key, defaultValue);
@@ -46,13 +52,13 @@ export function getConfigItem<Key extends keyof Config>(
// 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(); database.reload();
} catch (error: unknown) { } catch (error: unknown) {
logger.error("Error while reloading settings.json: "); logger.error("Error while reloading settings.json: ");
logger.error(error); logger.error(error);
} }
return db.exists(`/${key}`); return database.exists(`/${key}`);
} }
export function setConfigItem<Key extends keyof Config>( export function setConfigItem<Key extends keyof Config>(
@@ -66,16 +72,16 @@ export function setConfigItem<Key extends keyof Config>(
} }
configSchemata[key].parse(value); configSchemata[key].parse(value);
db.push(`/${key}`, value, true); database.push(`/${key}`, value, true);
db.save(); database.save();
} }
export function removeConfigItem(key: string): void { export function removeConfigItem(key: string): void {
db.delete(`/${key}`); database.delete(`/${key}`);
db.save(); database.save();
} }
function reloadDb(): void { function reloadDatabase(): void {
const settingsJsonPath = path.join( const settingsJsonPath = path.join(
app.getPath("userData"), app.getPath("userData"),
"/config/settings.json", "/config/settings.json",
@@ -96,5 +102,5 @@ function reloadDb(): void {
} }
} }
db = new JsonDB(settingsJsonPath, true, true); database = new JsonDB(settingsJsonPath, true, true);
} }

View File

@@ -4,30 +4,30 @@ import {app} from "zulip:remote";
let setupCompleted = false; let setupCompleted = false;
const zulipDir = app.getPath("userData"); const zulipDirectory = app.getPath("userData");
const logDir = `${zulipDir}/Logs/`; const logDirectory = `${zulipDirectory}/Logs/`;
const configDir = `${zulipDir}/config/`; const configDirectory = `${zulipDirectory}/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) {
if (!fs.existsSync(zulipDir)) { if (!fs.existsSync(zulipDirectory)) {
fs.mkdirSync(zulipDir); fs.mkdirSync(zulipDirectory);
} }
if (!fs.existsSync(logDir)) { if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDir); fs.mkdirSync(logDirectory);
} }
// Migrate config files from app data folder to config folder inside app // Migrate config files from app data folder to config folder inside app
// data folder. This will be done once when a user updates to the new version. // data folder. This will be done once when a user updates to the new version.
if (!fs.existsSync(configDir)) { if (!fs.existsSync(configDirectory)) {
fs.mkdirSync(configDir); fs.mkdirSync(configDirectory);
const domainJson = `${zulipDir}/domain.json`; const domainJson = `${zulipDirectory}/domain.json`;
const settingsJson = `${zulipDir}/settings.json`; const settingsJson = `${zulipDirectory}/settings.json`;
const updatesJson = `${zulipDir}/updates.json`; const updatesJson = `${zulipDirectory}/updates.json`;
const windowStateJson = `${zulipDir}/window-state.json`; const windowStateJson = `${zulipDirectory}/window-state.json`;
const configData = [ const configData = [
{ {
path: domainJson, path: domainJson,
@@ -44,7 +44,7 @@ export const initSetUp = (): void => {
]; ];
for (const data of configData) { for (const data of configData) {
if (fs.existsSync(data.path)) { if (fs.existsSync(data.path)) {
fs.copyFileSync(data.path, configDir + data.fileName); fs.copyFileSync(data.path, configDirectory + data.fileName);
fs.unlinkSync(data.path); fs.unlinkSync(data.path);
} }
} }

View File

@@ -2,8 +2,8 @@ import process from "node:process";
import type {z} from "zod"; import type {z} from "zod";
import type {dndSettingsSchemata} from "./config-schemata.js"; import type {dndSettingsSchemata} from "./config-schemata.ts";
import * as ConfigUtil from "./config-util.js"; import * as ConfigUtil from "./config-util.ts";
export type DndSettings = { export type DndSettings = {
[Key in keyof typeof dndSettingsSchemata]: z.output< [Key in keyof typeof dndSettingsSchemata]: z.output<

View File

@@ -3,9 +3,10 @@ import path from "node:path";
import process from "node:process"; import process from "node:process";
import {z} from "zod"; import {z} from "zod";
import {dialog} from "zulip:remote";
import {enterpriseConfigSchemata} from "./config-schemata.js"; import {enterpriseConfigSchemata} from "./config-schemata.ts";
import Logger from "./logger-util.js"; import Logger from "./logger-util.ts";
type EnterpriseConfig = { type EnterpriseConfig = {
[Key in keyof typeof enterpriseConfigSchemata]: z.output< [Key in keyof typeof enterpriseConfigSchemata]: z.output<
@@ -20,13 +21,12 @@ const logger = new Logger({
let enterpriseSettings: Partial<EnterpriseConfig>; let enterpriseSettings: Partial<EnterpriseConfig>;
let configFile: boolean; let configFile: boolean;
reloadDb(); reloadDatabase();
function reloadDb(): void { function reloadDatabase(): void {
let enterpriseFile = "/etc/zulip-desktop-config/global_config.json"; let enterpriseFile = "/etc/zulip-desktop-config/global_config.json";
if (process.platform === "win32") { if (process.platform === "win32") {
enterpriseFile = enterpriseFile = String.raw`C:\Program Files\Zulip-Desktop-Config\global_config.json`;
"C:\\Program Files\\Zulip-Desktop-Config\\global_config.json";
} }
enterpriseFile = path.resolve(enterpriseFile); enterpriseFile = path.resolve(enterpriseFile);
@@ -40,6 +40,10 @@ function reloadDb(): void {
.partial() .partial()
.parse(data); .parse(data);
} catch (error: unknown) { } catch (error: unknown) {
dialog.showErrorBox(
"Error loading global_config",
"We encountered an error while reading global_config.json, please make sure the file contains valid JSON.",
);
logger.log("Error while JSON parsing global_config.json: "); logger.log("Error while JSON parsing global_config.json: ");
logger.log(error); logger.log(error);
} }
@@ -56,7 +60,7 @@ export function getConfigItem<Key extends keyof EnterpriseConfig>(
key: Key, key: Key,
defaultValue: EnterpriseConfig[Key], defaultValue: EnterpriseConfig[Key],
): EnterpriseConfig[Key] { ): EnterpriseConfig[Key] {
reloadDb(); reloadDatabase();
if (!configFile) { if (!configFile) {
return defaultValue; return defaultValue;
} }
@@ -66,7 +70,7 @@ export function getConfigItem<Key extends keyof EnterpriseConfig>(
} }
export function configItemExists(key: keyof EnterpriseConfig): boolean { export function configItemExists(key: keyof EnterpriseConfig): boolean {
reloadDb(); reloadDatabase();
if (!configFile) { if (!configFile) {
return false; return false;
} }

View File

@@ -3,7 +3,8 @@ import fs from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import {html} from "./html.js"; import {Html, html} from "./html.ts";
import * as t from "./translation-util.ts";
export async function openBrowser(url: URL): Promise<void> { export async function openBrowser(url: URL): Promise<void> {
if (["http:", "https:", "mailto:"].includes(url.protocol)) { if (["http:", "https:", "mailto:"].includes(url.protocol)) {
@@ -11,8 +12,8 @@ export async function openBrowser(url: URL): Promise<void> {
} 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.
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "zulip-redirect-")); const directory = fs.mkdtempSync(path.join(os.tmpdir(), "zulip-redirect-"));
const file = path.join(dir, "redirect.html"); const file = path.join(directory, "redirect.html");
fs.writeFileSync( fs.writeFileSync(
file, file,
html` html`
@@ -21,7 +22,7 @@ export async function openBrowser(url: URL): Promise<void> {
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="Refresh" content="0; url=${url.href}" /> <meta http-equiv="Refresh" content="0; url=${url.href}" />
<title>Redirecting</title> <title>${t.__("Redirecting")}</title>
<style> <style>
html { html {
font-family: menu, "Helvetica Neue", sans-serif; font-family: menu, "Helvetica Neue", sans-serif;
@@ -29,7 +30,13 @@ export async function openBrowser(url: URL): Promise<void> {
</style> </style>
</head> </head>
<body> <body>
<p>Opening <a href="${url.href}">${url.href}</a>…</p> <p>
${new Html({
html: t.__("Opening {{{link}}}…", {
link: html`<a href="${url.href}">${url.href}</a>`.html,
}),
})}
</p>
</body> </body>
</html> </html>
`.html, `.html,
@@ -37,7 +44,7 @@ export async function openBrowser(url: URL): Promise<void> {
await shell.openPath(file); await shell.openPath(file);
setTimeout(() => { setTimeout(() => {
fs.unlinkSync(file); fs.unlinkSync(file);
fs.rmdirSync(dir); fs.rmdirSync(directory);
}, 15_000); }, 15_000);
} }
} }

View File

@@ -5,7 +5,7 @@ import process from "node:process";
import {app} from "zulip:remote"; import {app} from "zulip:remote";
import {initSetUp} from "./default-util.js"; import {initSetUp} from "./default-util.ts";
type LoggerOptions = { type LoggerOptions = {
file?: string; file?: string;
@@ -13,7 +13,7 @@ type LoggerOptions = {
initSetUp(); initSetUp();
const logDir = `${app.getPath("userData")}/Logs`; const logDirectory = `${app.getPath("userData")}/Logs`;
type Level = "log" | "debug" | "info" | "warn" | "error"; type Level = "log" | "debug" | "info" | "warn" | "error";
@@ -23,7 +23,7 @@ export default class Logger {
constructor(options: LoggerOptions = {}) { constructor(options: LoggerOptions = {}) {
let {file = "console.log"} = options; let {file = "console.log"} = options;
file = `${logDir}/${file}`; file = `${logDirectory}/${file}`;
// Trim log according to type of process // Trim log according to type of process
if (process.type === "renderer") { if (process.type === "renderer") {
@@ -38,31 +38,31 @@ export default class Logger {
this.nodeConsole = nodeConsole; this.nodeConsole = nodeConsole;
} }
_log(type: Level, ...args: unknown[]): void { _log(type: Level, ...arguments_: unknown[]): void {
args.unshift(this.getTimestamp() + " |\t"); arguments_.unshift(this.getTimestamp() + " |\t");
args.unshift(type.toUpperCase() + " |"); arguments_.unshift(type.toUpperCase() + " |");
this.nodeConsole[type](...args); this.nodeConsole[type](...arguments_);
console[type](...args); console[type](...arguments_);
} }
log(...args: unknown[]): void { log(...arguments_: unknown[]): void {
this._log("log", ...args); this._log("log", ...arguments_);
} }
debug(...args: unknown[]): void { debug(...arguments_: unknown[]): void {
this._log("debug", ...args); this._log("debug", ...arguments_);
} }
info(...args: unknown[]): void { info(...arguments_: unknown[]): void {
this._log("info", ...args); this._log("info", ...arguments_);
} }
warn(...args: unknown[]): void { warn(...arguments_: unknown[]): void {
this._log("warn", ...args); this._log("warn", ...arguments_);
} }
error(...args: unknown[]): void { error(...arguments_: unknown[]): void {
this._log("error", ...args); this._log("error", ...arguments_);
} }
getTimestamp(): string { getTimestamp(): string {

View File

@@ -1,3 +1,5 @@
import * as t from "./translation-util.ts";
type DialogBoxError = { type DialogBoxError = {
title: string; title: string;
content: string; content: string;
@@ -13,26 +15,24 @@ export function invalidZulipServerError(domain: string): string {
https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`; https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`;
} }
export function enterpriseOrgError( export function enterpriseOrgError(domains: string[]): DialogBoxError {
length: number,
domains: string[],
): DialogBoxError {
let domainList = ""; let domainList = "";
for (const domain of domains) { for (const domain of domains) {
domainList += `${domain}\n`; domainList += `${domain}\n`;
} }
return { return {
title: `Could not add the following ${ title: t.__mf(
length === 1 ? "organization" : "organizations" "{number, plural, one {Could not add # organization} other {Could not add # organizations}}",
}`, {number: domains.length},
content: `${domainList}\nPlease contact your system administrator.`, ),
content: `${domainList}\n${t.__("Please contact your system administrator.")}`,
}; };
} }
export function orgRemovalError(url: string): DialogBoxError { export function orgRemovalError(url: string): DialogBoxError {
return { return {
title: `Removing ${url} is a restricted operation.`, title: t.__("Removing {{{url}}} is a restricted operation.", {url}),
content: "Please contact your system administrator.", content: t.__("Please contact your system administrator."),
}; };
} }

View File

@@ -2,8 +2,8 @@ import path from "node:path";
import i18n from "i18n"; import i18n from "i18n";
import * as ConfigUtil from "./config-util.js"; import * as ConfigUtil from "./config-util.ts";
import {publicPath} from "./paths.js"; import {publicPath} from "./paths.ts";
i18n.configure({ i18n.configure({
directory: path.join(publicPath, "translations/"), directory: path.join(publicPath, "translations/"),
@@ -13,4 +13,4 @@ i18n.configure({
/* Fetches the current appLocale from settings.json */ /* Fetches the current appLocale from settings.json */
i18n.setLocale(ConfigUtil.getConfigItem("appLanguage", "en") ?? "en"); i18n.setLocale(ConfigUtil.getConfigItem("appLanguage", "en") ?? "en");
export {__} from "i18n"; export {__, __mf} from "i18n";

View File

@@ -1,5 +1,5 @@
import type {DndSettings} from "./dnd-util.js"; import type {DndSettings} from "./dnd-util.ts";
import type {MenuProps, ServerConf} from "./types.js"; import type {MenuProperties, ServerConfig} from "./types.ts";
export type MainMessage = { export type MainMessage = {
"clear-app-settings": () => void; "clear-app-settings": () => void;
@@ -21,12 +21,12 @@ export type MainMessage = {
toggleAutoLauncher: (AutoLaunchValue: boolean) => void; toggleAutoLauncher: (AutoLaunchValue: boolean) => void;
"unread-count": (unreadCount: number) => void; "unread-count": (unreadCount: number) => void;
"update-badge": (messageCount: number) => void; "update-badge": (messageCount: number) => void;
"update-menu": (props: MenuProps) => void; "update-menu": (properties: MenuProperties) => void;
"update-taskbar-icon": (data: string, text: string) => void; "update-taskbar-icon": (data: string, text: string) => void;
}; };
export type MainCall = { export type MainCall = {
"get-server-settings": (domain: string) => ServerConf; "get-server-settings": (domain: string) => ServerConfig;
"is-online": (url: string) => boolean; "is-online": (url: string) => boolean;
"poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined; "poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined;
"save-server-icon": (iconURL: string) => string | null; "save-server-icon": (iconURL: string) => string | null;
@@ -74,7 +74,7 @@ export type RendererMessage = {
"toggle-silent": (state: boolean) => void; "toggle-silent": (state: boolean) => void;
"toggle-tray": (state: boolean) => void; "toggle-tray": (state: boolean) => void;
toggletray: () => void; toggletray: () => void;
tray: (arg: number) => void; tray: (argument: number) => void;
"update-realm-icon": (serverURL: string, iconURL: string) => void; "update-realm-icon": (serverURL: string, iconURL: string) => void;
"update-realm-name": (serverURL: string, realmName: string) => void; "update-realm-name": (serverURL: string, realmName: string) => void;
"webview-reload": () => void; "webview-reload": () => void;

View File

@@ -1,17 +1,17 @@
export type MenuProps = { export type MenuProperties = {
tabs: TabData[]; tabs: TabData[];
activeTabIndex?: number; activeTabIndex?: number;
enableMenu?: boolean; enableMenu?: boolean;
}; };
export type NavItem = export type NavigationItem =
| "General" | "General"
| "Network" | "Network"
| "AddServer" | "AddServer"
| "Organizations" | "Organizations"
| "Shortcuts"; | "Shortcuts";
export type ServerConf = { export type ServerConfig = {
url: string; url: string;
alias: string; alias: string;
icon: string; icon: string;
@@ -20,9 +20,11 @@ export type ServerConf = {
}; };
export type TabRole = "server" | "function"; export type TabRole = "server" | "function";
export type TabPage = "Settings" | "About";
export type TabData = { export type TabData = {
role: TabRole; role: TabRole;
name: string; page?: TabPage;
label: string;
index: number; index: number;
}; };

View File

@@ -2,13 +2,17 @@ import {shell} from "electron/common";
import {app, dialog, session} from "electron/main"; import {app, dialog, session} from "electron/main";
import process from "node:process"; import process from "node:process";
import log from "electron-log"; import log from "electron-log/main";
import type {UpdateDownloadedEvent, UpdateInfo} from "electron-updater"; import {
import {autoUpdater} from "electron-updater"; type UpdateDownloadedEvent,
type UpdateInfo,
autoUpdater,
} from "electron-updater";
import * as ConfigUtil from "../common/config-util.js"; import * as ConfigUtil from "../common/config-util.ts";
import * as t from "../common/translation-util.ts";
import {linuxUpdateNotification} from "./linuxupdater.js"; // Required only in case of linux import {linuxUpdateNotification} from "./linuxupdater.ts"; // Required only in case of linux
let quitting = false; let quitting = false;
@@ -31,9 +35,10 @@ export async function appUpdater(updateFromMenu = false): Promise<void> {
let updateAvailable = false; let updateAvailable = false;
// Log what's happening // Log what's happening
log.transports.file.fileName = "updates.log"; const updateLogger = log.create({logId: "updates"});
log.transports.file.level = "info"; updateLogger.transports.file.fileName = "updates.log";
autoUpdater.logger = log; updateLogger.transports.file.level = "info";
autoUpdater.logger = updateLogger;
// Handle auto updates for beta/pre releases // Handle auto updates for beta/pre releases
const isBetaUpdate = ConfigUtil.getConfigItem("betaUpdate", false); const isBetaUpdate = ConfigUtil.getConfigItem("betaUpdate", false);
@@ -54,9 +59,13 @@ export async function appUpdater(updateFromMenu = false): Promise<void> {
} }
await dialog.showMessageBox({ await dialog.showMessageBox({
message: `A new version ${info.version}, of Zulip Desktop is available`, message: t.__(
detail: "A new version {{{version}}} of Zulip Desktop is available.",
{version: info.version},
),
detail: t.__(
"The update will be downloaded in the background. You will be notified when it is ready to be installed.", "The update will be downloaded in the background. You will be notified when it is ready to be installed.",
),
}); });
} }
}); });
@@ -68,8 +77,11 @@ export async function appUpdater(updateFromMenu = false): Promise<void> {
autoUpdater.removeAllListeners(); autoUpdater.removeAllListeners();
await dialog.showMessageBox({ await dialog.showMessageBox({
message: "No updates available", message: t.__("No updates available."),
detail: `You are running the latest version of Zulip Desktop.\nVersion: ${app.getVersion()}`, detail: t.__(
"You are running the latest version of Zulip Desktop.\nVersion: {{{version}}}",
{version: app.getVersion()},
),
}); });
} }
}); });
@@ -81,20 +93,20 @@ export async function appUpdater(updateFromMenu = false): Promise<void> {
autoUpdater.removeAllListeners(); autoUpdater.removeAllListeners();
const messageText = updateAvailable const messageText = updateAvailable
? "Unable to download the updates" ? t.__("Unable to download the update.")
: "Unable to check for updates"; : t.__("Unable to check for updates.");
const link = "https://zulip.com/apps/";
const {response} = await dialog.showMessageBox({ const {response} = await dialog.showMessageBox({
type: "error", type: "error",
buttons: ["Manual Download", "Cancel"], buttons: [t.__("Manual Download"), t.__("Cancel")],
message: messageText, message: messageText,
detail: `Error: ${error.message} detail: t.__(
"Error: {{{error}}}\n\nThe latest version of Zulip Desktop is available at:\n{{{link}}}\nCurrent version: {{{version}}}",
The latest version of Zulip Desktop is available at - {error: error.message, link, version: app.getVersion()},
https://zulip.com/apps/. ),
Current Version: ${app.getVersion()}`,
}); });
if (response === 0) { if (response === 0) {
await shell.openExternal("https://zulip.com/apps/"); await shell.openExternal(link);
} }
} }
}); });
@@ -104,10 +116,14 @@ Current Version: ${app.getVersion()}`,
// 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",
buttons: ["Install and Relaunch", "Install Later"], buttons: [t.__("Install and Relaunch"), t.__("Install Later")],
defaultId: 0, defaultId: 0,
message: `A new update ${event.version} has been downloaded`, message: t.__("A new update {{{version}}} has been downloaded.", {
detail: "It will be installed the next time you restart the application", version: event.version,
}),
detail: t.__(
"It will be installed the next time you restart the application.",
),
}); });
if (response === 0) { if (response === 0) {
quitting = true; quitting = true;

View File

@@ -1,11 +1,10 @@
import {nativeImage} from "electron/common"; import {nativeImage} from "electron/common";
import type {BrowserWindow} from "electron/main"; import {type BrowserWindow, app} from "electron/main";
import {app} from "electron/main";
import process from "node:process"; import process from "node:process";
import * as ConfigUtil from "../common/config-util.js"; import * as ConfigUtil from "../common/config-util.ts";
import {send} from "./typed-ipc-main.js"; import {send} from "./typed-ipc-main.ts";
function showBadgeCount(messageCount: number, mainWindow: BrowserWindow): void { function showBadgeCount(messageCount: number, mainWindow: BrowserWindow): void {
if (process.platform === "win32") { if (process.platform === "win32") {

View File

@@ -1,18 +1,19 @@
import type {Event} from "electron/common"; import {type Event, shell} from "electron/common";
import {shell} from "electron/common"; import {
import type { type HandlerDetails,
HandlerDetails, Notification,
SaveDialogOptions, type SaveDialogOptions,
WebContents, type WebContents,
app,
} from "electron/main"; } from "electron/main";
import {Notification, app} from "electron/main";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import * as ConfigUtil from "../common/config-util.js"; import * as ConfigUtil from "../common/config-util.ts";
import * as LinkUtil from "../common/link-util.js"; import * as LinkUtil from "../common/link-util.ts";
import * as t from "../common/translation-util.ts";
import {send} from "./typed-ipc-main.js"; import {send} from "./typed-ipc-main.ts";
function isUploadsUrl(server: string, url: URL): boolean { 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/");
@@ -125,8 +126,8 @@ export default function handleExternalLink(
downloadPath, downloadPath,
async completed(filePath: string, fileName: string) { async completed(filePath: string, fileName: string) {
const downloadNotification = new Notification({ const downloadNotification = new Notification({
title: "Download Complete", title: t.__("Download Complete"),
body: `Click to show ${fileName} in folder`, body: t.__("Click to show {{{fileName}}} in folder", {fileName}),
silent: true, // We'll play our own sound - ding.ogg silent: true, // We'll play our own sound - ding.ogg
}); });
downloadNotification.on("click", () => { downloadNotification.on("click", () => {
@@ -149,8 +150,8 @@ export default function handleExternalLink(
if (state !== "cancelled") { if (state !== "cancelled") {
if (ConfigUtil.getConfigItem("promptDownload", false)) { if (ConfigUtil.getConfigItem("promptDownload", false)) {
new Notification({ new Notification({
title: "Download Complete", title: t.__("Download Complete"),
body: "Download failed", body: t.__("Download failed"),
}).show(); }).show();
} else { } else {
contents.downloadURL(url.href); contents.downloadURL(url.href);

View File

@@ -1,7 +1,14 @@
import type {Event} from "electron/common";
import {clipboard} from "electron/common"; import {clipboard} from "electron/common";
import type {IpcMainEvent, WebContents} from "electron/main"; import {
import {BrowserWindow, app, dialog, powerMonitor, session} from "electron/main"; BrowserWindow,
type IpcMainEvent,
type WebContents,
app,
dialog,
powerMonitor,
session,
webContents,
} from "electron/main";
import {Buffer} from "node:buffer"; import {Buffer} from "node:buffer";
import crypto from "node:crypto"; import crypto from "node:crypto";
import path from "node:path"; import path from "node:path";
@@ -10,21 +17,22 @@ import process from "node:process";
import * as remoteMain from "@electron/remote/main"; import * as remoteMain from "@electron/remote/main";
import windowStateKeeper from "electron-window-state"; import windowStateKeeper from "electron-window-state";
import * as ConfigUtil from "../common/config-util.js"; import * as ConfigUtil from "../common/config-util.ts";
import {bundlePath, bundleUrl, publicPath} from "../common/paths.js"; import {bundlePath, bundleUrl, publicPath} from "../common/paths.ts";
import type {RendererMessage} from "../common/typed-ipc.js"; import * as t from "../common/translation-util.ts";
import type {MenuProps} from "../common/types.js"; import type {RendererMessage} from "../common/typed-ipc.ts";
import type {MenuProperties} from "../common/types.ts";
import {appUpdater, shouldQuitForUpdate} from "./autoupdater.js"; import {appUpdater, shouldQuitForUpdate} from "./autoupdater.ts";
import * as BadgeSettings from "./badge-settings.js"; import * as BadgeSettings from "./badge-settings.ts";
import handleExternalLink from "./handle-external-link.js"; import handleExternalLink from "./handle-external-link.ts";
import * as AppMenu from "./menu.js"; import * as AppMenu from "./menu.ts";
import {_getServerSettings, _isOnline, _saveServerIcon} from "./request.js"; import {_getServerSettings, _isOnline, _saveServerIcon} from "./request.ts";
import {sentryInit} from "./sentry.js"; import {sentryInit} from "./sentry.ts";
import {setAutoLaunch} from "./startup.js"; import {setAutoLaunch} from "./startup.ts";
import {ipcMain, send} from "./typed-ipc-main.js"; import {ipcMain, send} from "./typed-ipc-main.ts";
import "gatemaker/electron-setup"; // eslint-disable-line import/no-unassigned-import import "gatemaker/electron-setup.js"; // eslint-disable-line import-x/no-unassigned-import
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
const {GDK_BACKEND} = process.env; const {GDK_BACKEND} = process.env;
@@ -79,7 +87,7 @@ function createMainWindow(): BrowserWindow {
minWidth: 500, minWidth: 500,
minHeight: 400, minHeight: 400,
webPreferences: { webPreferences: {
preload: path.join(bundlePath, "renderer.js"), preload: path.join(bundlePath, "renderer.cjs"),
sandbox: false, sandbox: false,
webviewTag: true, webviewTag: true,
}, },
@@ -103,7 +111,14 @@ function createMainWindow(): BrowserWindow {
event.preventDefault(); event.preventDefault();
if (process.platform === "darwin") { if (process.platform === "darwin") {
app.hide(); if (win.isFullScreen()) {
win.setFullScreen(false);
win.once("leave-full-screen", () => {
app.hide();
});
} else {
app.hide();
}
} else { } else {
win.hide(); win.hide();
} }
@@ -224,9 +239,9 @@ function createMainWindow(): BrowserWindow {
try { try {
// Check that the data on the clipboard was encrypted to the key. // Check that the data on the clipboard was encrypted to the key.
const data = Buffer.from(clipboard.readText(), "hex"); const data = Buffer.from(clipboard.readText(), "hex");
const iv = data.slice(0, 12); const iv = data.subarray(0, 12);
const ciphertext = data.slice(12, -16); const ciphertext = data.subarray(12, -16);
const authTag = data.slice(-16); const authTag = data.subarray(-16);
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, { const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, {
authTagLength: 16, authTagLength: 16,
}); });
@@ -292,18 +307,25 @@ function createMainWindow(): BrowserWindow {
app.on( app.on(
"certificate-error", "certificate-error",
( (
event: Event, event,
webContents: WebContents, webContents,
urlString: string, urlString,
error: string, error,
certificate,
callback,
isMainFrame,
// eslint-disable-next-line max-params
) => { ) => {
const url = new URL(urlString); if (isMainFrame) {
dialog.showErrorBox( const url = new URL(urlString);
"Certificate error", dialog.showErrorBox(
`The server presented an invalid certificate for ${url.origin}: t.__("Certificate error"),
t.__(
${error}`, "The server presented an invalid certificate for {{{origin}}}:\n\n{{{error}}}",
); {origin: url.origin, error},
),
);
}
}, },
); );
@@ -389,11 +411,26 @@ ${error}`,
}, },
); );
ipcMain.on("update-menu", (_event, props: MenuProps) => { ipcMain.on(
AppMenu.setMenu(props); "forward-to",
if (props.activeTabIndex !== undefined) { <Channel extends keyof RendererMessage>(
const activeTab = props.tabs[props.activeTabIndex]; _event: IpcMainEvent,
mainWindow.setTitle(`Zulip - ${activeTab.name}`); webContentsId: number,
listener: Channel,
...parameters: Parameters<RendererMessage[Channel]>
) => {
const contents = webContents.fromId(webContentsId);
if (contents !== undefined) {
send(contents, listener, ...parameters);
}
},
);
ipcMain.on("update-menu", (_event, properties: MenuProperties) => {
AppMenu.setMenu(properties);
if (properties.activeTabIndex !== undefined) {
const activeTab = properties.tabs[properties.activeTabIndex];
mainWindow.setTitle(`Zulip - ${activeTab.label}`);
} }
}); });

View File

@@ -3,26 +3,27 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import {JsonDB} from "node-json-db"; import {JsonDB} from "node-json-db";
import {DataError} from "node-json-db/dist/lib/Errors"; import {DataError} from "node-json-db/dist/lib/Errors.js";
import Logger from "../common/logger-util.js"; import Logger from "../common/logger-util.ts";
import * as t from "../common/translation-util.ts";
const logger = new Logger({ const logger = new Logger({
file: "linux-update-util.log", file: "linux-update-util.log",
}); });
let db: JsonDB; let database: JsonDB;
reloadDb(); reloadDatabase();
export function getUpdateItem( export function getUpdateItem(
key: string, key: string,
defaultValue: true | null = null, defaultValue: true | null = null,
): true | null { ): true | null {
reloadDb(); reloadDatabase();
let value: unknown; let value: unknown;
try { try {
value = db.getObject<unknown>(`/${key}`); value = database.getObject<unknown>(`/${key}`);
} catch (error: unknown) { } catch (error: unknown) {
if (!(error instanceof DataError)) throw error; if (!(error instanceof DataError)) throw error;
} }
@@ -36,16 +37,16 @@ export function getUpdateItem(
} }
export function setUpdateItem(key: string, value: true | null): void { export function setUpdateItem(key: string, value: true | null): void {
db.push(`/${key}`, value, true); database.push(`/${key}`, value, true);
reloadDb(); reloadDatabase();
} }
export function removeUpdateItem(key: string): void { export function removeUpdateItem(key: string): void {
db.delete(`/${key}`); database.delete(`/${key}`);
reloadDb(); reloadDatabase();
} }
function reloadDb(): void { function reloadDatabase(): void {
const linuxUpdateJsonPath = path.join( const linuxUpdateJsonPath = path.join(
app.getPath("userData"), app.getPath("userData"),
"/config/updates.json", "/config/updates.json",
@@ -57,13 +58,13 @@ function reloadDb(): void {
if (fs.existsSync(linuxUpdateJsonPath)) { if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath); fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox( dialog.showErrorBox(
"Error saving update notifications.", t.__("Error saving update notifications"),
"We encountered an error while saving the update notifications.", t.__("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(error); logger.error(error);
} }
} }
db = new JsonDB(linuxUpdateJsonPath, true, true); database = new JsonDB(linuxUpdateJsonPath, true, true);
} }

View File

@@ -1,13 +1,13 @@
import type {Session} from "electron/main"; import {Notification, type Session, app} from "electron/main";
import {Notification, app} from "electron/main";
import * as semver from "semver"; import * as semver from "semver";
import {z} from "zod"; import {z} from "zod";
import * as ConfigUtil from "../common/config-util.js"; import * as ConfigUtil from "../common/config-util.ts";
import Logger from "../common/logger-util.js"; import Logger from "../common/logger-util.ts";
import * as t from "../common/translation-util.ts";
import * as LinuxUpdateUtil from "./linux-update-util.js"; import * as LinuxUpdateUtil from "./linux-update-util.ts";
const logger = new Logger({ const logger = new Logger({
file: "linux-update-util.log", file: "linux-update-util.log",
@@ -35,8 +35,11 @@ export async function linuxUpdateNotification(session: Session): Promise<void> {
const notified = LinuxUpdateUtil.getUpdateItem(latestVersion); const notified = LinuxUpdateUtil.getUpdateItem(latestVersion);
if (notified === null) { if (notified === null) {
new Notification({ new Notification({
title: "Zulip Update", title: t.__("Zulip Update"),
body: `A new version ${latestVersion} is available. Please update using your package manager.`, body: t.__(
"A new version {{{version}}} is available. Please update using your package manager.",
{version: latestVersion},
),
}).show(); }).show();
LinuxUpdateUtil.setUpdateItem(latestVersion, true); LinuxUpdateUtil.setUpdateItem(latestVersion, true);
} }

View File

@@ -1,18 +1,22 @@
import {shell} from "electron/common"; import {shell} from "electron/common";
import type {MenuItemConstructorOptions} from "electron/main"; import {
import {BrowserWindow, Menu, app} from "electron/main"; BrowserWindow,
Menu,
type MenuItemConstructorOptions,
app,
} from "electron/main";
import process from "node:process"; import process from "node:process";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import * as ConfigUtil from "../common/config-util.js"; import * as ConfigUtil from "../common/config-util.ts";
import * as DNDUtil from "../common/dnd-util.js"; import * as DNDUtil from "../common/dnd-util.ts";
import * as t from "../common/translation-util.js"; import * as t from "../common/translation-util.ts";
import type {RendererMessage} from "../common/typed-ipc.js"; import type {RendererMessage} from "../common/typed-ipc.ts";
import type {MenuProps, TabData} from "../common/types.js"; import type {MenuProperties, TabData} from "../common/types.ts";
import {appUpdater} from "./autoupdater.js"; import {appUpdater} from "./autoupdater.ts";
import {send} from "./typed-ipc-main.js"; import {send} from "./typed-ipc-main.ts";
const appName = app.name; const appName = app.name;
@@ -90,7 +94,7 @@ function getToolsSubmenu(): MenuItemConstructorOptions[] {
accelerator: accelerator:
process.platform === "darwin" ? "Alt+Command+I" : "Ctrl+Shift+I", process.platform === "darwin" ? "Alt+Command+I" : "Ctrl+Shift+I",
click(_item, focusedWindow) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow instanceof BrowserWindow) {
focusedWindow.webContents.openDevTools({mode: "undocked"}); focusedWindow.webContents.openDevTools({mode: "undocked"});
} }
}, },
@@ -218,7 +222,7 @@ function getViewSubmenu(): MenuItemConstructorOptions[] {
{ {
label: t.__("Toggle Tray Icon"), label: t.__("Toggle Tray Icon"),
click(_item, focusedWindow) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow instanceof BrowserWindow) {
send(focusedWindow.webContents, "toggletray"); send(focusedWindow.webContents, "toggletray");
} }
}, },
@@ -227,7 +231,7 @@ function getViewSubmenu(): MenuItemConstructorOptions[] {
label: t.__("Toggle Sidebar"), label: t.__("Toggle Sidebar"),
accelerator: "CommandOrControl+Shift+S", accelerator: "CommandOrControl+Shift+S",
click(_item, focusedWindow) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow instanceof BrowserWindow) {
const newValue = !ConfigUtil.getConfigItem("showSidebar", true); const newValue = !ConfigUtil.getConfigItem("showSidebar", true);
send(focusedWindow.webContents, "toggle-sidebar", newValue); send(focusedWindow.webContents, "toggle-sidebar", newValue);
ConfigUtil.setConfigItem("showSidebar", newValue); ConfigUtil.setConfigItem("showSidebar", newValue);
@@ -239,7 +243,7 @@ function getViewSubmenu(): MenuItemConstructorOptions[] {
checked: ConfigUtil.getConfigItem("autoHideMenubar", false), checked: ConfigUtil.getConfigItem("autoHideMenubar", false),
visible: process.platform !== "darwin", visible: process.platform !== "darwin",
click(_item, focusedWindow) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow instanceof BrowserWindow) {
const newValue = !ConfigUtil.getConfigItem("autoHideMenubar", false); const newValue = !ConfigUtil.getConfigItem("autoHideMenubar", false);
focusedWindow.autoHideMenuBar = newValue; focusedWindow.autoHideMenuBar = newValue;
focusedWindow.setMenuBarVisibility(!newValue); focusedWindow.setMenuBarVisibility(!newValue);
@@ -314,12 +318,12 @@ function getWindowSubmenu(
if (tab === undefined) continue; if (tab === undefined) continue;
// 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.role === "function" && tab.name === "Settings") { if (tab.role === "function" && tab.page === "Settings") {
continue; continue;
} }
initialSubmenu.push({ initialSubmenu.push({
label: tab.name, label: tab.label,
accelerator: accelerator:
tab.role === "function" ? "" : `${shortcutKey} + ${tab.index + 1}`, tab.role === "function" ? "" : `${shortcutKey} + ${tab.index + 1}`,
checked: tab.index === activeTabIndex, checked: tab.index === activeTabIndex,
@@ -368,8 +372,10 @@ function getWindowSubmenu(
return initialSubmenu; return initialSubmenu;
} }
function getDarwinTpl(props: MenuProps): MenuItemConstructorOptions[] { function getDarwinTpl(
const {tabs, activeTabIndex, enableMenu = false} = props; properties: MenuProperties,
): MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu = false} = properties;
return [ return [
{ {
@@ -533,8 +539,8 @@ function getDarwinTpl(props: MenuProps): MenuItemConstructorOptions[] {
]; ];
} }
function getOtherTpl(props: MenuProps): MenuItemConstructorOptions[] { function getOtherTpl(properties: MenuProperties): MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu = false} = props; const {tabs, activeTabIndex, enableMenu = false} = properties;
return [ return [
{ {
label: t.__("File"), label: t.__("File"),
@@ -683,7 +689,7 @@ function getOtherTpl(props: MenuProps): MenuItemConstructorOptions[] {
function sendAction<Channel extends keyof RendererMessage>( function sendAction<Channel extends keyof RendererMessage>(
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void { ): void {
const win = BrowserWindow.getAllWindows()[0]; const win = BrowserWindow.getAllWindows()[0];
@@ -691,7 +697,7 @@ function sendAction<Channel extends keyof RendererMessage>(
win.restore(); win.restore();
} }
send(win.webContents, channel, ...args); send(win.webContents, channel, ...arguments_);
} }
async function checkForUpdate(): Promise<void> { async function checkForUpdate(): Promise<void> {
@@ -714,9 +720,11 @@ function getPreviousServer(tabs: TabData[], activeTabIndex: number): number {
return activeTabIndex; return activeTabIndex;
} }
export function setMenu(props: MenuProps): void { export function setMenu(properties: MenuProperties): void {
const tpl = const tpl =
process.platform === "darwin" ? getDarwinTpl(props) : getOtherTpl(props); process.platform === "darwin"
? getDarwinTpl(properties)
: getOtherTpl(properties);
const menu = Menu.buildFromTemplate(tpl); const menu = Menu.buildFromTemplate(tpl);
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
} }

View File

@@ -1,17 +1,16 @@
import type {Session} from "electron/main"; import {type Session, app} from "electron/main";
import {app} from "electron/main";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import {Readable} from "node:stream"; import {Readable} from "node:stream";
import {pipeline} from "node:stream/promises"; import {pipeline} from "node:stream/promises";
import type {ReadableStream} from "node:stream/web"; import type {ReadableStream} from "node:stream/web";
import * as Sentry from "@sentry/electron"; import * as Sentry from "@sentry/electron/main";
import {z} from "zod"; import {z} from "zod";
import Logger from "../common/logger-util.js"; import Logger from "../common/logger-util.ts";
import * as Messages from "../common/messages.js"; import * as Messages from "../common/messages.ts";
import type {ServerConf} from "../common/types.js"; import type {ServerConfig} from "../common/types.ts";
/* Request: domain-util */ /* Request: domain-util */
@@ -20,7 +19,7 @@ const logger = new Logger({
}); });
const generateFilePath = (url: string): string => { const generateFilePath = (url: string): string => {
const dir = `${app.getPath("userData")}/server-icons`; const directory = `${app.getPath("userData")}/server-icons`;
const extension = path.extname(url).split("?")[0]; const extension = path.extname(url).split("?")[0];
let hash = 5381; let hash = 5381;
@@ -32,18 +31,18 @@ const generateFilePath = (url: string): string => {
} }
// Create 'server-icons' directory if not existed // Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) { if (!fs.existsSync(directory)) {
fs.mkdirSync(dir); fs.mkdirSync(directory);
} }
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
return `${dir}/${hash >>> 0}${extension}`; return `${directory}/${hash >>> 0}${extension}`;
}; };
export const _getServerSettings = async ( export const _getServerSettings = async (
domain: string, domain: string,
session: Session, session: Session,
): Promise<ServerConf> => { ): Promise<ServerConfig> => {
const response = await session.fetch(domain + "/api/v1/server_settings"); const response = await session.fetch(domain + "/api/v1/server_settings");
if (!response.ok) { if (!response.ok) {
throw new Error(Messages.invalidZulipServerError(domain)); throw new Error(Messages.invalidZulipServerError(domain));
@@ -60,7 +59,7 @@ export const _getServerSettings = async (
} = z } = z
.object({ .object({
realm_name: z.string(), realm_name: z.string(),
realm_uri: z.string().url(), realm_uri: z.url(),
realm_icon: z.string(), realm_icon: z.string(),
zulip_version: z.string().default("unknown"), zulip_version: z.string().default("unknown"),
zulip_feature_level: z.number().default(0), zulip_feature_level: z.number().default(0),

View File

@@ -1,8 +1,8 @@
import {app} from "electron/main"; import {app} from "electron/main";
import * as Sentry from "@sentry/electron/main"; // eslint-disable-line n/file-extension-in-import import * as Sentry from "@sentry/electron/main";
import {getConfigItem} from "../common/config-util.js"; import {getConfigItem} from "../common/config-util.ts";
export const sentryInit = (): void => { export const sentryInit = (): void => {
Sentry.init({ Sentry.init({

View File

@@ -3,7 +3,7 @@ import process from "node:process";
import AutoLaunch from "auto-launch"; import AutoLaunch from "auto-launch";
import * as ConfigUtil from "../common/config-util.js"; import * as ConfigUtil from "../common/config-util.ts";
export const setAutoLaunch = async ( export const setAutoLaunch = async (
AutoLaunchValue: boolean, AutoLaunchValue: boolean,

View File

@@ -1,9 +1,7 @@
import type {
IpcMainEvent,
IpcMainInvokeEvent,
WebContents,
} from "electron/main";
import { import {
type IpcMainEvent,
type IpcMainInvokeEvent,
type WebContents,
ipcMain as untypedIpcMain, // eslint-disable-line no-restricted-imports ipcMain as untypedIpcMain, // eslint-disable-line no-restricted-imports
} from "electron/main"; } from "electron/main";
@@ -14,14 +12,20 @@ import type {
} from "../common/typed-ipc.js"; } from "../common/typed-ipc.js";
type MainListener<Channel extends keyof MainMessage> = type MainListener<Channel extends keyof MainMessage> =
MainMessage[Channel] extends (...args: infer Args) => infer Return MainMessage[Channel] extends (...arguments_: infer Arguments) => infer Return
? (event: IpcMainEvent & {returnValue: Return}, ...args: Args) => void ? (
event: IpcMainEvent & {returnValue: Return},
...arguments_: Arguments
) => void
: never; : never;
type MainHandler<Channel extends keyof MainCall> = MainCall[Channel] extends ( type MainHandler<Channel extends keyof MainCall> = MainCall[Channel] extends (
...args: infer Args ...arguments_: infer Arguments
) => infer Return ) => infer Return
? (event: IpcMainInvokeEvent, ...args: Args) => Return | Promise<Return> ? (
event: IpcMainInvokeEvent,
...arguments_: Arguments
) => Return | Promise<Return>
: never; : never;
export const ipcMain: { export const ipcMain: {
@@ -30,7 +34,16 @@ export const ipcMain: {
listener: <Channel extends keyof RendererMessage>( listener: <Channel extends keyof RendererMessage>(
event: IpcMainEvent, event: IpcMainEvent,
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
) => void,
): void;
on(
channel: "forward-to",
listener: <Channel extends keyof RendererMessage>(
event: IpcMainEvent,
webContentsId: number,
channel: Channel,
...arguments_: Parameters<RendererMessage[Channel]>
) => void, ) => void,
): void; ): void;
on<Channel extends keyof MainMessage>( on<Channel extends keyof MainMessage>(
@@ -60,16 +73,16 @@ export const ipcMain: {
export function send<Channel extends keyof RendererMessage>( export function send<Channel extends keyof RendererMessage>(
contents: WebContents, contents: WebContents,
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void { ): void {
contents.send(channel, ...args); contents.send(channel, ...arguments_);
} }
export function sendToFrame<Channel extends keyof RendererMessage>( export function sendToFrame<Channel extends keyof RendererMessage>(
contents: WebContents, contents: WebContents,
frameId: number | [number, number], frameId: number | [number, number],
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void { ): void {
contents.sendToFrame(frameId, channel, ...args); contents.sendToFrame(frameId, channel, ...arguments_);
} }

View File

@@ -6,21 +6,4 @@
<div class="about" hidden> <div class="about" hidden>
<img class="logo" src="../resources/zulip.png" /> <img class="logo" src="../resources/zulip.png" />
<p class="detail" id="version"></p> <p class="detail" id="version"></p>
<div class="maintenance-info">
<p class="detail maintainer">
Maintained by
<a href="https://zulip.com" target="_blank" rel="noopener noreferrer"
>Zulip</a
>
</p>
<p class="detail license">
Available under the
<a
href="https://github.com/zulip/zulip-desktop/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
>Apache 2.0 License</a
>
</p>
</div>
</div> </div>

View File

@@ -47,7 +47,6 @@
} }
.maintenance-info { .maintenance-info {
cursor: pointer;
position: absolute; position: absolute;
width: 100%; width: 100%;
left: 0; left: 0;

View File

@@ -85,7 +85,7 @@ body {
line-height: 1; line-height: 1;
text-transform: none; text-transform: none;
letter-spacing: normal; letter-spacing: normal;
word-wrap: normal; overflow-wrap: normal;
white-space: nowrap; white-space: nowrap;
direction: ltr; direction: ltr;
@@ -114,12 +114,20 @@ body {
} }
.action-button i { .action-button i {
color: rgb(108 133 146 / 100%); color: hsl(200.53deg 14.96% 49.8%);
font-size: 28px; font-size: 28px;
} }
.action-button:hover i { .action-button:hover i {
color: rgb(152 169 179 / 100%); color: hsl(202.22deg 15.08% 64.9%);
}
.action-button > .dnd-on {
color: hsl(200.53deg 14.96% 85%);
}
.action-button:hover > .dnd-on {
color: hsl(202.22deg 15.08% 95%);
} }
.action-button.active { .action-button.active {

View File

@@ -11,7 +11,7 @@
background: rgb(239 239 239 / 100%); background: rgb(239 239 239 / 100%);
letter-spacing: -0.08px; letter-spacing: -0.08px;
line-height: 18px; line-height: 18px;
color: rgb(139 142 143 / 100%); color: rgb(34 44 49 / 100%);
/* Copied from https://github.com/yairEO/tagify/blob/v4.17.7/src/tagify.scss#L4-L8 */ /* Copied from https://github.com/yairEO/tagify/blob/v4.17.7/src/tagify.scss#L4-L8 */
--tagify-dd-color-primary: rgb(53 149 246); --tagify-dd-color-primary: rgb(53 149 246);
@@ -68,7 +68,7 @@ td:nth-child(odd) {
line-height: 1; line-height: 1;
text-transform: none; text-transform: none;
letter-spacing: normal; letter-spacing: normal;
word-wrap: normal; overflow-wrap: normal;
white-space: nowrap; white-space: nowrap;
direction: ltr; direction: ltr;
@@ -101,7 +101,7 @@ td:nth-child(odd) {
.nav { .nav {
padding: 7px 0; padding: 7px 0;
color: rgb(153 153 153 / 100%); color: rgb(70 78 90 / 100%);
cursor: pointer; cursor: pointer;
} }
@@ -578,7 +578,6 @@ input.toggle-round:checked + label::after {
text-align: center; text-align: center;
color: rgb(255 255 255 / 100%); color: rgb(255 255 255 / 100%);
background: rgb(78 191 172 / 100%); background: rgb(78 191 172 / 100%);
border-color: none;
border: none; border: none;
width: 98%; width: 98%;
height: 46px; height: 46px;

View File

@@ -1,4 +1,4 @@
import {ipcRenderer} from "./typed-ipc-renderer.js"; import {ipcRenderer} from "./typed-ipc-renderer.ts";
// This helper is exposed via electron_bridge for use in the social // This helper is exposed via electron_bridge for use in the social
// login flow. // login flow.
@@ -20,7 +20,7 @@ export type ClipboardDecrypter = {
pasted: Promise<string>; pasted: Promise<string>;
}; };
export class ClipboardDecrypterImpl implements ClipboardDecrypter { export class ClipboardDecrypterImplementation implements ClipboardDecrypter {
version: number; version: number;
key: Uint8Array; key: Uint8Array;
pasted: Promise<string>; pasted: Promise<string>;
@@ -33,10 +33,7 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter {
this.pasted = new Promise((resolve) => { this.pasted = new Promise((resolve) => {
let interval: NodeJS.Timeout | null = null; let interval: NodeJS.Timeout | null = null;
const startPolling = () => { const startPolling = () => {
if (interval === null) { interval ??= setInterval(poll, 1000);
interval = setInterval(poll, 1000);
}
void poll(); void poll();
}; };

View File

@@ -1,4 +1,4 @@
import type {Html} from "../../../common/html.js"; import type {Html} from "../../../common/html.ts";
export function generateNodeFromHtml(html: Html): Element { export function generateNodeFromHtml(html: Html): Element {
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");

View File

@@ -1,5 +1,4 @@
import type {Event} from "electron/common"; import {type Event, clipboard} from "electron/common";
import {clipboard} from "electron/common";
import type {WebContents} from "electron/main"; import type {WebContents} from "electron/main";
import type { import type {
ContextMenuParams, ContextMenuParams,
@@ -7,18 +6,18 @@ import type {
} from "electron/renderer"; } from "electron/renderer";
import process from "node:process"; import process from "node:process";
import {Menu} from "@electron/remote"; import {BrowserWindow, Menu} from "@electron/remote";
import * as t from "../../../common/translation-util.js"; import * as t from "../../../common/translation-util.ts";
export const contextMenu = ( export const contextMenu = (
webContents: WebContents, webContents: WebContents,
event: Event, event: Event,
props: ContextMenuParams, properties: ContextMenuParams,
) => { ) => {
const isText = props.selectionText !== ""; const isText = properties.selectionText !== "";
const isLink = props.linkURL !== ""; const isLink = properties.linkURL !== "";
const linkUrl = isLink ? new URL(props.linkURL) : undefined; const linkUrl = isLink ? new URL(properties.linkURL) : undefined;
const makeSuggestion = (suggestion: string) => ({ const makeSuggestion = (suggestion: string) => ({
label: suggestion, label: suggestion,
@@ -31,19 +30,21 @@ export const contextMenu = (
let menuTemplate: MenuItemConstructorOptions[] = [ let menuTemplate: MenuItemConstructorOptions[] = [
{ {
label: t.__("Add to Dictionary"), label: t.__("Add to Dictionary"),
visible: props.isEditable && isText && props.misspelledWord.length > 0, visible:
properties.isEditable && isText && properties.misspelledWord.length > 0,
click(_item) { click(_item) {
webContents.session.addWordToSpellCheckerDictionary( webContents.session.addWordToSpellCheckerDictionary(
props.misspelledWord, properties.misspelledWord,
); );
}, },
}, },
{ {
type: "separator", type: "separator",
visible: props.isEditable && isText && props.misspelledWord.length > 0, visible:
properties.isEditable && isText && properties.misspelledWord.length > 0,
}, },
{ {
label: `${t.__("Look Up")} "${props.selectionText}"`, label: `${t.__("Look Up")} "${properties.selectionText}"`,
visible: process.platform === "darwin" && isText, visible: process.platform === "darwin" && isText,
click(_item) { click(_item) {
webContents.showDefinitionForSelection(); webContents.showDefinitionForSelection();
@@ -56,7 +57,7 @@ export const contextMenu = (
{ {
label: t.__("Cut"), label: t.__("Cut"),
visible: isText, visible: isText,
enabled: props.isEditable, enabled: properties.isEditable,
accelerator: "CommandOrControl+X", accelerator: "CommandOrControl+X",
click(_item) { click(_item) {
webContents.cut(); webContents.cut();
@@ -65,7 +66,7 @@ export const contextMenu = (
{ {
label: t.__("Copy"), label: t.__("Copy"),
accelerator: "CommandOrControl+C", accelerator: "CommandOrControl+C",
enabled: props.editFlags.canCopy, enabled: properties.editFlags.canCopy,
click(_item) { click(_item) {
webContents.copy(); webContents.copy();
}, },
@@ -73,7 +74,7 @@ export const contextMenu = (
{ {
label: t.__("Paste"), // Bug: Paste replaces text label: t.__("Paste"), // Bug: Paste replaces text
accelerator: "CommandOrControl+V", accelerator: "CommandOrControl+V",
enabled: props.isEditable, enabled: properties.isEditable,
click() { click() {
webContents.paste(); webContents.paste();
}, },
@@ -89,44 +90,37 @@ export const contextMenu = (
visible: isLink, visible: isLink,
click(_item) { click(_item) {
clipboard.write({ clipboard.write({
bookmark: props.linkText, bookmark: properties.linkText,
text: text:
linkUrl?.protocol === "mailto:" ? linkUrl.pathname : props.linkURL, linkUrl?.protocol === "mailto:"
? linkUrl.pathname
: properties.linkURL,
}); });
}, },
}, },
{ {
label: t.__("Copy Image"), label: t.__("Copy Image"),
visible: props.mediaType === "image", visible: properties.mediaType === "image",
click(_item) { click(_item) {
webContents.copyImageAt(props.x, props.y); webContents.copyImageAt(properties.x, properties.y);
}, },
}, },
{ {
label: t.__("Copy Image URL"), label: t.__("Copy Image URL"),
visible: props.mediaType === "image", visible: properties.mediaType === "image",
click(_item) { click(_item) {
clipboard.write({ clipboard.write({
bookmark: props.srcURL, bookmark: properties.srcURL,
text: props.srcURL, text: properties.srcURL,
}); });
}, },
}, },
{
type: "separator",
visible: isLink || props.mediaType === "image",
},
{
label: t.__("Services"),
visible: process.platform === "darwin",
role: "services",
},
]; ];
if (props.misspelledWord) { if (properties.misspelledWord) {
if (props.dictionarySuggestions.length > 0) { if (properties.dictionarySuggestions.length > 0) {
const suggestions: MenuItemConstructorOptions[] = const suggestions: MenuItemConstructorOptions[] =
props.dictionarySuggestions.map((suggestion: string) => properties.dictionarySuggestions.map((suggestion: string) =>
makeSuggestion(suggestion), makeSuggestion(suggestion),
); );
menuTemplate = [...suggestions, ...menuTemplate]; menuTemplate = [...suggestions, ...menuTemplate];
@@ -146,5 +140,11 @@ export const contextMenu = (
(menuItem) => menuItem.visible ?? true, (menuItem) => menuItem.visible ?? true,
); );
const menu = Menu.buildFromTemplate(filteredMenuTemplate); const menu = Menu.buildFromTemplate(filteredMenuTemplate);
menu.popup(); menu.popup({
window: BrowserWindow.fromWebContents(webContents) ?? undefined,
frame: properties.frame ?? undefined,
x: properties.x,
y: properties.y,
sourceType: properties.menuSourceType,
});
}; };

View File

@@ -1,26 +1,26 @@
import type {Html} from "../../../common/html.js"; import {type Html, html} from "../../../common/html.ts";
import {html} from "../../../common/html.js"; import type {TabPage} from "../../../common/types.ts";
import {generateNodeFromHtml} from "./base.js"; import {generateNodeFromHtml} from "./base.ts";
import type {TabProps} from "./tab.js"; import Tab, {type TabProperties} from "./tab.ts";
import Tab from "./tab.js";
export type FunctionalTabProps = { export type FunctionalTabProperties = {
$view: Element; $view: Element;
} & TabProps; page: TabPage;
} & TabProperties;
export default class FunctionalTab extends Tab { export default class FunctionalTab extends Tab {
$view: Element; $view: Element;
$el: Element; $el: Element;
$closeButton?: Element; $closeButton?: Element;
constructor({$view, ...props}: FunctionalTabProps) { constructor({$view, ...properties}: FunctionalTabProperties) {
super(props); super(properties);
this.$view = $view; this.$view = $view;
this.$el = generateNodeFromHtml(this.templateHtml()); this.$el = generateNodeFromHtml(this.templateHtml());
if (this.props.name !== "Settings") { if (properties.page !== "Settings") {
this.props.$root.append(this.$el); this.properties.$root.append(this.$el);
this.$closeButton = this.$el.querySelector(".server-tab-badge")!; this.$closeButton = this.$el.querySelector(".server-tab-badge")!;
this.registerListeners(); this.registerListeners();
} }
@@ -43,12 +43,12 @@ export default class FunctionalTab extends Tab {
templateHtml(): Html { templateHtml(): Html {
return html` return html`
<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}"> <div class="tab functional-tab" data-tab-id="${this.properties.tabIndex}">
<div class="server-tab-badge close-button"> <div class="server-tab-badge close-button">
<i class="material-icons">close</i> <i class="material-icons">close</i>
</div> </div>
<div class="server-tab"> <div class="server-tab">
<i class="material-icons">${this.props.materialIcon}</i> <i class="material-icons">${this.properties.materialIcon}</i>
</div> </div>
</div> </div>
`; `;
@@ -66,7 +66,7 @@ export default class FunctionalTab extends Tab {
}); });
this.$closeButton?.addEventListener("click", (event) => { this.$closeButton?.addEventListener("click", (event) => {
this.props.onDestroy?.(); this.properties.onDestroy?.();
event.stopPropagation(); event.stopPropagation();
}); });
} }

View File

@@ -1,17 +1,15 @@
import process from "node:process"; import process from "node:process";
import type {Html} from "../../../common/html.js"; import {type Html, html} from "../../../common/html.ts";
import {html} from "../../../common/html.js"; import {ipcRenderer} from "../typed-ipc-renderer.ts";
import {ipcRenderer} from "../typed-ipc-renderer.js";
import {generateNodeFromHtml} from "./base.js"; import {generateNodeFromHtml} from "./base.ts";
import type {TabProps} from "./tab.js"; import Tab, {type TabProperties} from "./tab.ts";
import Tab from "./tab.js"; import type WebView from "./webview.ts";
import type WebView from "./webview.js";
export type ServerTabProps = { export type ServerTabProperties = {
webview: Promise<WebView>; webview: Promise<WebView>;
} & TabProps; } & TabProperties;
export default class ServerTab extends Tab { export default class ServerTab extends Tab {
webview: Promise<WebView>; webview: Promise<WebView>;
@@ -20,12 +18,12 @@ export default class ServerTab extends Tab {
$icon: HTMLImageElement; $icon: HTMLImageElement;
$badge: Element; $badge: Element;
constructor({webview, ...props}: ServerTabProps) { constructor({webview, ...properties}: ServerTabProperties) {
super(props); super(properties);
this.webview = webview; this.webview = webview;
this.$el = generateNodeFromHtml(this.templateHtml()); this.$el = generateNodeFromHtml(this.templateHtml());
this.props.$root.append(this.$el); this.properties.$root.append(this.$el);
this.registerListeners(); this.registerListeners();
this.$name = this.$el.querySelector(".server-tooltip")!; this.$name = this.$el.querySelector(".server-tooltip")!;
this.$icon = this.$el.querySelector(".server-icons")!; this.$icon = this.$el.querySelector(".server-icons")!;
@@ -49,26 +47,26 @@ export default class ServerTab extends Tab {
templateHtml(): Html { templateHtml(): Html {
return html` return html`
<div class="tab" data-tab-id="${this.props.tabIndex}"> <div class="tab" data-tab-id="${this.properties.tabIndex}">
<div class="server-tooltip" style="display:none"> <div class="server-tooltip" style="display:none">
${this.props.name} ${this.properties.label}
</div> </div>
<div class="server-tab-badge"></div> <div class="server-tab-badge"></div>
<div class="server-tab"> <div class="server-tab">
<img class="server-icons" src="${this.props.icon}" /> <img class="server-icons" src="${this.properties.icon}" />
</div> </div>
<div class="server-tab-shortcut">${this.generateShortcutText()}</div> <div class="server-tab-shortcut">${this.generateShortcutText()}</div>
</div> </div>
`; `;
} }
setName(name: string): void { setLabel(label: string): void {
this.props.name = name; this.properties.label = label;
this.$name.textContent = name; this.$name.textContent = label;
} }
setIcon(icon: string): void { setIcon(icon: string): void {
this.props.icon = icon; this.properties.icon = icon;
this.$icon.src = icon; this.$icon.src = icon;
} }
@@ -79,11 +77,11 @@ export default class ServerTab extends Tab {
generateShortcutText(): string { generateShortcutText(): string {
// Only provide shortcuts for server [0..9] // Only provide shortcuts for server [0..9]
if (this.props.index >= 9) { if (this.properties.index >= 9) {
return ""; return "";
} }
const shownIndex = this.props.index + 1; const shownIndex = this.properties.index + 1;
// Array index == Shown index - 1 // Array index == Shown index - 1
ipcRenderer.send("switch-server-tab", shownIndex - 1); ipcRenderer.send("switch-server-tab", shownIndex - 1);

View File

@@ -1,9 +1,10 @@
import type {TabRole} from "../../../common/types.js"; import type {TabPage, TabRole} from "../../../common/types.ts";
export type TabProps = { export type TabProperties = {
role: TabRole; role: TabRole;
page?: TabPage;
icon?: string; icon?: string;
name: string; label: string;
$root: Element; $root: Element;
onClick: () => void; onClick: () => void;
index: number; index: number;
@@ -17,17 +18,17 @@ export type TabProps = {
export default abstract class Tab { export default abstract class Tab {
abstract $el: Element; abstract $el: Element;
constructor(readonly props: TabProps) {} constructor(readonly properties: TabProperties) {}
registerListeners(): void { registerListeners(): void {
this.$el.addEventListener("click", this.props.onClick); this.$el.addEventListener("click", this.properties.onClick);
if (this.props.onHover !== undefined) { if (this.properties.onHover !== undefined) {
this.$el.addEventListener("mouseover", this.props.onHover); this.$el.addEventListener("mouseover", this.properties.onHover);
} }
if (this.props.onHoverOut !== undefined) { if (this.properties.onHoverOut !== undefined) {
this.$el.addEventListener("mouseout", this.props.onHoverOut); this.$el.addEventListener("mouseout", this.properties.onHoverOut);
} }
} }

View File

@@ -1,25 +1,24 @@
import type {WebContents} from "electron/main"; import type {WebContents} from "electron/main";
import fs from "node:fs"; import fs from "node:fs";
import process from "node:process";
import * as remote from "@electron/remote"; import * as remote from "@electron/remote";
import {app, dialog} from "@electron/remote"; import {app, dialog} from "@electron/remote";
import * as ConfigUtil from "../../../common/config-util.js"; import * as ConfigUtil from "../../../common/config-util.ts";
import type {Html} from "../../../common/html.js"; import {type Html, html} from "../../../common/html.ts";
import {html} from "../../../common/html.js"; import * as t from "../../../common/translation-util.ts";
import type {RendererMessage} from "../../../common/typed-ipc.js"; import type {RendererMessage} from "../../../common/typed-ipc.ts";
import type {TabRole} from "../../../common/types.js"; import type {TabRole} from "../../../common/types.ts";
import preloadCss from "../../css/preload.css?raw"; // eslint-disable-line n/file-extension-in-import import preloadCss from "../../css/preload.css?raw";
import {ipcRenderer} from "../typed-ipc-renderer.js"; import {ipcRenderer} from "../typed-ipc-renderer.ts";
import * as SystemUtil from "../utils/system-util.js"; import * as SystemUtil from "../utils/system-util.ts";
import {generateNodeFromHtml} from "./base.js"; import {generateNodeFromHtml} from "./base.ts";
import {contextMenu} from "./context-menu.js"; import {contextMenu} from "./context-menu.ts";
const shouldSilentWebview = ConfigUtil.getConfigItem("silent", false); const shouldSilentWebview = ConfigUtil.getConfigItem("silent", false);
type WebViewProps = { type WebViewProperties = {
$root: Element; $root: Element;
rootWebContents: WebContents; rootWebContents: WebContents;
index: number; index: number;
@@ -36,24 +35,24 @@ type WebViewProps = {
}; };
export default class WebView { export default class WebView {
static templateHtml(props: WebViewProps): Html { static templateHtml(properties: WebViewProperties): Html {
return html` return html`
<div class="webview-pane"> <div class="webview-pane">
<div <div
class="webview-unsupported" class="webview-unsupported"
${props.unsupportedMessage === undefined ? html`hidden` : html``} ${properties.unsupportedMessage === undefined ? html`hidden` : html``}
> >
<span class="webview-unsupported-message" <span class="webview-unsupported-message"
>${props.unsupportedMessage ?? ""}</span >${properties.unsupportedMessage ?? ""}</span
> >
<span class="webview-unsupported-dismiss">×</span> <span class="webview-unsupported-dismiss">×</span>
</div> </div>
<webview <webview
data-tab-id="${props.tabIndex}" data-tab-id="${properties.tabIndex}"
src="${props.url}" src="${properties.url}"
${props.preload === undefined ${properties.preload === undefined
? html`` ? html``
: html`preload="${props.preload}"`} : html`preload="${properties.preload}"`}
partition="persist:webviewsession" partition="persist:webviewsession"
allowpopups allowpopups
> >
@@ -62,11 +61,11 @@ export default class WebView {
`; `;
} }
static async create(props: WebViewProps): Promise<WebView> { static async create(properties: WebViewProperties): Promise<WebView> {
const $pane = generateNodeFromHtml( const $pane = generateNodeFromHtml(
WebView.templateHtml(props), WebView.templateHtml(properties),
) as HTMLElement; ) as HTMLElement;
props.$root.append($pane); properties.$root.append($pane);
const $webview: HTMLElement = $pane.querySelector(":scope > webview")!; const $webview: HTMLElement = $pane.querySelector(":scope > webview")!;
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@@ -90,22 +89,21 @@ export default class WebView {
} }
const selector = `webview[data-tab-id="${CSS.escape( const selector = `webview[data-tab-id="${CSS.escape(
`${props.tabIndex}`, `${properties.tabIndex}`,
)}"]`; )}"]`;
const webContentsId: unknown = const webContentsId: unknown =
await props.rootWebContents.executeJavaScript( await properties.rootWebContents.executeJavaScript(
`(${getWebContentsIdFunction.toString()})(${JSON.stringify(selector)})`, `(${getWebContentsIdFunction.toString()})(${JSON.stringify(selector)})`,
); );
if (typeof webContentsId !== "number") { if (typeof webContentsId !== "number") {
throw new TypeError("Failed to get WebContents ID"); throw new TypeError("Failed to get WebContents ID");
} }
return new WebView(props, $pane, $webview, webContentsId); return new WebView(properties, $pane, $webview, webContentsId);
} }
badgeCount = 0; badgeCount = 0;
loading = true; loading = true;
private zoomFactor = 1;
private customCss: string | false | null; private customCss: string | false | null;
private readonly $webviewsContainer: DOMTokenList; private readonly $webviewsContainer: DOMTokenList;
private readonly $unsupported: HTMLElement; private readonly $unsupported: HTMLElement;
@@ -114,7 +112,7 @@ export default class WebView {
private unsupportedDismissed = false; private unsupportedDismissed = false;
private constructor( private constructor(
readonly props: WebViewProps, readonly properties: WebViewProperties,
private readonly $pane: HTMLElement, private readonly $pane: HTMLElement,
private readonly $webview: HTMLElement, private readonly $webview: HTMLElement,
readonly webContentsId: number, readonly webContentsId: number,
@@ -144,6 +142,7 @@ export default class WebView {
showNotificationSettings(): void { showNotificationSettings(): void {
this.send("show-notification-settings"); this.send("show-notification-settings");
this.focus();
} }
focus(): void { focus(): void {
@@ -161,18 +160,15 @@ export default class WebView {
} }
zoomIn(): void { zoomIn(): void {
this.zoomFactor += 0.1; this.getWebContents().zoomLevel += 0.5;
this.getWebContents().setZoomFactor(this.zoomFactor);
} }
zoomOut(): void { zoomOut(): void {
this.zoomFactor -= 0.1; this.getWebContents().zoomLevel -= 0.5;
this.getWebContents().setZoomFactor(this.zoomFactor);
} }
zoomActualSize(): void { zoomActualSize(): void {
this.zoomFactor = 1; this.getWebContents().zoomLevel = 0;
this.getWebContents().setZoomFactor(this.zoomFactor);
} }
logOut(): void { logOut(): void {
@@ -181,6 +177,7 @@ export default class WebView {
showKeyboardShortcuts(): void { showKeyboardShortcuts(): void {
this.send("show-keyboard-shortcuts"); this.send("show-keyboard-shortcuts");
this.focus();
} }
openDevTools(): void { openDevTools(): void {
@@ -188,8 +185,8 @@ export default class WebView {
} }
back(): void { back(): void {
if (this.getWebContents().canGoBack()) { if (this.getWebContents().navigationHistory.canGoBack()) {
this.getWebContents().goBack(); this.getWebContents().navigationHistory.goBack();
this.focus(); this.focus();
} }
} }
@@ -198,12 +195,15 @@ export default class WebView {
const $backButton = document.querySelector( const $backButton = document.querySelector(
"#actions-container #back-action", "#actions-container #back-action",
)!; )!;
$backButton.classList.toggle("disable", !this.getWebContents().canGoBack()); $backButton.classList.toggle(
"disable",
!this.getWebContents().navigationHistory.canGoBack(),
);
} }
forward(): void { forward(): void {
if (this.getWebContents().canGoForward()) { if (this.getWebContents().navigationHistory.canGoForward()) {
this.getWebContents().goForward(); this.getWebContents().navigationHistory.goForward();
} }
} }
@@ -212,7 +212,7 @@ export default class WebView {
// Shows the loading indicator till the webview is reloaded // Shows the loading indicator till the webview is reloaded
this.$webviewsContainer.remove("loaded"); this.$webviewsContainer.remove("loaded");
this.loading = true; this.loading = true;
this.props.switchLoading(true, this.props.url); this.properties.switchLoading(true, this.properties.url);
this.getWebContents().reload(); this.getWebContents().reload();
} }
@@ -224,9 +224,9 @@ export default class WebView {
send<Channel extends keyof RendererMessage>( send<Channel extends keyof RendererMessage>(
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void { ): void {
ipcRenderer.sendTo(this.webContentsId, channel, ...args); ipcRenderer.send("forward-to", this.webContentsId, channel, ...arguments_);
} }
private registerListeners(): void { private registerListeners(): void {
@@ -238,7 +238,7 @@ export default class WebView {
webContents.on("page-title-updated", (_event, title) => { webContents.on("page-title-updated", (_event, title) => {
this.badgeCount = this.getBadgeCount(title); this.badgeCount = this.getBadgeCount(title);
this.props.onTitleChange(); this.properties.onTitleChange();
}); });
this.$webview.addEventListener("did-navigate-in-page", () => { this.$webview.addEventListener("did-navigate-in-page", () => {
@@ -252,10 +252,7 @@ export default class WebView {
webContents.on("page-favicon-updated", (_event, favicons) => { webContents.on("page-favicon-updated", (_event, favicons) => {
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like // This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
// https://chat.zulip.org/static/images/favicon/favicon-pms.png // https://chat.zulip.org/static/images/favicon/favicon-pms.png
if ( if (favicons[0].indexOf("favicon-pms") > 0 && app.dock !== undefined) {
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
@@ -271,7 +268,7 @@ export default class WebView {
this.$webview.addEventListener("dom-ready", () => { this.$webview.addEventListener("dom-ready", () => {
this.loading = false; this.loading = false;
this.props.switchLoading(false, this.props.url); this.properties.switchLoading(false, this.properties.url);
this.show(); this.show();
}); });
@@ -280,24 +277,29 @@ export default class WebView {
SystemUtil.connectivityError.includes(errorDescription); SystemUtil.connectivityError.includes(errorDescription);
if (hasConnectivityError) { if (hasConnectivityError) {
console.error("error", errorDescription); console.error("error", errorDescription);
if (!this.props.url.includes("network.html")) { if (!this.properties.url.includes("network.html")) {
this.props.onNetworkError(this.props.index); this.properties.onNetworkError(this.properties.index);
} }
} }
}); });
this.$webview.addEventListener("did-start-loading", () => { this.$webview.addEventListener("did-start-loading", () => {
this.props.switchLoading(true, this.props.url); this.properties.switchLoading(true, this.properties.url);
}); });
this.$webview.addEventListener("did-stop-loading", () => { this.$webview.addEventListener("did-stop-loading", () => {
this.props.switchLoading(false, this.props.url); this.properties.switchLoading(false, this.properties.url);
}); });
this.$unsupportedDismiss.addEventListener("click", () => { this.$unsupportedDismiss.addEventListener("click", () => {
this.unsupportedDismissed = true; this.unsupportedDismissed = true;
this.$unsupported.hidden = true; this.$unsupported.hidden = true;
}); });
webContents.on("zoom-changed", (event, zoomDirection) => {
if (zoomDirection === "in") this.zoomIn();
else if (zoomDirection === "out") this.zoomOut();
});
} }
private getBadgeCount(title: string): number { private getBadgeCount(title: string): number {
@@ -307,7 +309,7 @@ export default class WebView {
private show(): void { private show(): void {
// Do not show WebView if another tab was selected and this tab should be in background. // Do not show WebView if another tab was selected and this tab should be in background.
if (!this.props.isActive()) { if (!this.properties.isActive()) {
return; return;
} }
@@ -316,7 +318,7 @@ export default class WebView {
this.$pane.classList.add("active"); this.$pane.classList.add("active");
this.focus(); this.focus();
this.props.onTitleChange(); this.properties.onTitleChange();
// Injecting preload css in webview to override some css rules // Injecting preload css in webview to override some css rules
(async () => this.getWebContents().insertCSS(preloadCss))(); (async () => this.getWebContents().insertCSS(preloadCss))();
@@ -328,8 +330,8 @@ export default class WebView {
this.customCss = null; this.customCss = null;
ConfigUtil.setConfigItem("customCSS", null); ConfigUtil.setConfigItem("customCSS", null);
const errorMessage = "The custom css previously set is deleted!"; const errorMessage = t.__("The custom CSS previously set is deleted.");
dialog.showErrorBox("custom css file deleted!", errorMessage); dialog.showErrorBox(t.__("Custom CSS file deleted"), errorMessage);
return; return;
} }

View File

@@ -1,16 +1,17 @@
import {EventEmitter} from "events"; // eslint-disable-line unicorn/prefer-node-protocol import {EventEmitter} from "node:events";
import type {ClipboardDecrypter} from "./clipboard-decrypter.js"; import {
import {ClipboardDecrypterImpl} from "./clipboard-decrypter.js"; type ClipboardDecrypter,
import type {NotificationData} from "./notification/index.js"; ClipboardDecrypterImplementation,
import {newNotification} from "./notification/index.js"; } from "./clipboard-decrypter.ts";
import {ipcRenderer} from "./typed-ipc-renderer.js"; import {type NotificationData, newNotification} from "./notification/index.ts";
import {ipcRenderer} from "./typed-ipc-renderer.ts";
type ListenerType = (...args: any[]) => void; type ListenerType = (...arguments_: any[]) => void;
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
export type ElectronBridge = { export type ElectronBridge = {
send_event: (eventName: string | symbol, ...args: unknown[]) => boolean; send_event: (eventName: string | symbol, ...arguments_: unknown[]) => boolean;
on_event: (eventName: string, listener: ListenerType) => void; on_event: (eventName: string, listener: ListenerType) => void;
new_notification: ( new_notification: (
title: string, title: string,
@@ -35,8 +36,8 @@ export const bridgeEvents = new EventEmitter(); // eslint-disable-line unicorn/p
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
const electron_bridge: ElectronBridge = { const electron_bridge: ElectronBridge = {
send_event: (eventName: string | symbol, ...args: unknown[]): boolean => send_event: (eventName: string | symbol, ...arguments_: unknown[]): boolean =>
bridgeEvents.emit(eventName, ...args), bridgeEvents.emit(eventName, ...arguments_),
on_event(eventName: string, listener: ListenerType): void { on_event(eventName: string, listener: ListenerType): void {
bridgeEvents.on(eventName, listener); bridgeEvents.on(eventName, listener);
@@ -60,7 +61,7 @@ const electron_bridge: ElectronBridge = {
}, },
decrypt_clipboard: (version: number): ClipboardDecrypter => decrypt_clipboard: (version: number): ClipboardDecrypter =>
new ClipboardDecrypterImpl(version), new ClipboardDecrypterImplementation(version),
}; };
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */

View File

@@ -1,3 +1,5 @@
import "./zod-config.ts"; // eslint-disable-line import-x/no-unassigned-import
import {clipboard} from "electron/common"; import {clipboard} from "electron/common";
import path from "node:path"; import path from "node:path";
import process from "node:process"; import process from "node:process";
@@ -5,29 +7,36 @@ import url from "node:url";
import {Menu, app, dialog, session} from "@electron/remote"; import {Menu, app, dialog, session} from "@electron/remote";
import * as remote from "@electron/remote"; import * as remote from "@electron/remote";
import * as Sentry from "@sentry/electron"; import * as Sentry from "@sentry/electron/renderer";
import type {Config} from "../../common/config-util.js"; import type {Config} from "../../common/config-util.ts";
import * as ConfigUtil from "../../common/config-util.js"; import * as ConfigUtil from "../../common/config-util.ts";
import * as DNDUtil from "../../common/dnd-util.js"; import * as DNDUtil from "../../common/dnd-util.ts";
import type {DndSettings} from "../../common/dnd-util.js"; import type {DndSettings} from "../../common/dnd-util.ts";
import * as EnterpriseUtil from "../../common/enterprise-util.js"; import * as EnterpriseUtil from "../../common/enterprise-util.ts";
import * as LinkUtil from "../../common/link-util.js"; import {html} from "../../common/html.ts";
import Logger from "../../common/logger-util.js"; import * as LinkUtil from "../../common/link-util.ts";
import * as Messages from "../../common/messages.js"; import Logger from "../../common/logger-util.ts";
import {bundlePath, bundleUrl} from "../../common/paths.js"; import * as Messages from "../../common/messages.ts";
import type {NavItem, ServerConf, TabData} from "../../common/types.js"; import {bundlePath, bundleUrl} from "../../common/paths.ts";
import * as t from "../../common/translation-util.ts";
import type {
NavigationItem,
ServerConfig,
TabData,
TabPage,
} from "../../common/types.js";
import defaultIcon from "../img/icon.png"; import defaultIcon from "../img/icon.png";
import FunctionalTab from "./components/functional-tab.js"; import FunctionalTab from "./components/functional-tab.ts";
import ServerTab from "./components/server-tab.js"; import ServerTab from "./components/server-tab.ts";
import WebView from "./components/webview.js"; import WebView from "./components/webview.ts";
import {AboutView} from "./pages/about.js"; import {AboutView} from "./pages/about.ts";
import {PreferenceView} from "./pages/preference/preference.js"; import {PreferenceView} from "./pages/preference/preference.ts";
import {initializeTray} from "./tray.js"; import {initializeTray} from "./tray.ts";
import {ipcRenderer} from "./typed-ipc-renderer.js"; import {ipcRenderer} from "./typed-ipc-renderer.ts";
import * as DomainUtil from "./utils/domain-util.js"; import * as DomainUtil from "./utils/domain-util.ts";
import ReconnectUtil from "./utils/reconnect-util.js"; import ReconnectUtil from "./utils/reconnect-util.ts";
Sentry.init({}); Sentry.init({});
@@ -73,11 +82,10 @@ export class ServerManagerView {
$dndTooltip: HTMLElement; $dndTooltip: HTMLElement;
$sidebar: Element; $sidebar: Element;
$fullscreenPopup: Element; $fullscreenPopup: Element;
$fullscreenEscapeKey: string;
loading: Set<string>; loading: Set<string>;
activeTabIndex: number; activeTabIndex: number;
tabs: ServerOrFunctionalTab[]; tabs: ServerOrFunctionalTab[];
functionalTabs: Map<string, number>; functionalTabs: Map<TabPage, number>;
tabIndex: number; tabIndex: number;
presetOrgs: string[]; presetOrgs: string[];
preferenceView?: PreferenceView; preferenceView?: PreferenceView;
@@ -114,8 +122,10 @@ export class ServerManagerView {
this.$sidebar = document.querySelector("#sidebar")!; this.$sidebar = document.querySelector("#sidebar")!;
this.$fullscreenPopup = document.querySelector("#fullscreen-popup")!; this.$fullscreenPopup = document.querySelector("#fullscreen-popup")!;
this.$fullscreenEscapeKey = process.platform === "darwin" ? "^⌘F" : "F11"; this.$fullscreenPopup.textContent = t.__(
this.$fullscreenPopup.textContent = `Press ${this.$fullscreenEscapeKey} to exit full screen`; "Press {{{exitKey}}} to exit full screen",
{exitKey: process.platform === "darwin" ? "^⌘F" : "F11"},
);
this.loading = new Set(); this.loading = new Set();
this.activeTabIndex = -1; this.activeTabIndex = -1;
@@ -156,12 +166,12 @@ export class ServerManagerView {
ConfigUtil.getConfigItem("useSystemProxy", false) ConfigUtil.getConfigItem("useSystemProxy", false)
? {mode: "system"} ? {mode: "system"}
: ConfigUtil.getConfigItem("useManualProxy", false) : ConfigUtil.getConfigItem("useManualProxy", false)
? { ? {
pacScript: ConfigUtil.getConfigItem("proxyPAC", ""), pacScript: ConfigUtil.getConfigItem("proxyPAC", ""),
proxyRules: ConfigUtil.getConfigItem("proxyRules", ""), proxyRules: ConfigUtil.getConfigItem("proxyRules", ""),
proxyBypassRules: ConfigUtil.getConfigItem("proxyBypass", ""), proxyBypassRules: ConfigUtil.getConfigItem("proxyBypass", ""),
} }
: {mode: "direct"}, : {mode: "direct"},
); );
} }
@@ -248,13 +258,16 @@ export class ServerManagerView {
// 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 serverConfig = await DomainUtil.checkDomain(domain);
await DomainUtil.addDomain(serverConf); await DomainUtil.addDomain(serverConfig);
return true; return true;
} catch (error: unknown) { } catch (error: unknown) {
logger.error(error); logger.error(error);
logger.error( logger.error(
`Could not add ${domain}. Please contact your system administrator.`, t.__(
"Could not add {{{domain}}}. Please contact your system administrator.",
{domain},
),
); );
return false; return false;
} }
@@ -283,12 +296,9 @@ export class ServerManagerView {
// 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",
buttons: ["Yes", "Later"], buttons: [t.__("Yes"), t.__("Later")],
defaultId: 0, defaultId: 0,
message: message: t.__("New servers added. Reload app now?"),
"New server" +
(domainsAdded.length > 1 ? "s" : "") +
" added. Reload app now?",
}); });
if (response === 0) { if (response === 0) {
ipcRenderer.send("reload-full-app"); ipcRenderer.send("reload-full-app");
@@ -307,10 +317,7 @@ export class ServerManagerView {
failedDomains.push(org); failedDomains.push(org);
} }
const {title, content} = Messages.enterpriseOrgError( const {title, content} = Messages.enterpriseOrgError(failedDomains);
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
@@ -325,11 +332,14 @@ export class ServerManagerView {
for (const [i, server] of servers.entries()) { for (const [i, server] of servers.entries()) {
const tab = this.initServer(server, i); const tab = this.initServer(server, i);
(async () => { (async () => {
const serverConf = await DomainUtil.updateSavedServer(server.url, i); const serverConfig = await DomainUtil.updateSavedServer(
tab.setName(serverConf.alias); server.url,
tab.setIcon(DomainUtil.iconAsUrl(serverConf.icon)); i,
);
tab.setLabel(serverConfig.alias);
tab.setIcon(DomainUtil.iconAsUrl(serverConfig.icon));
(await tab.webview).setUnsupportedMessage( (await tab.webview).setUnsupportedMessage(
DomainUtil.getUnsupportedMessage(serverConf), DomainUtil.getUnsupportedMessage(serverConfig),
); );
})(); })();
} }
@@ -364,12 +374,12 @@ export class ServerManagerView {
} }
} }
initServer(server: ServerConf, index: number): ServerTab { initServer(server: ServerConfig, index: number): ServerTab {
const tabIndex = this.getTabIndex(); const tabIndex = this.getTabIndex();
const tab = new ServerTab({ const tab = new ServerTab({
role: "server", role: "server",
icon: DomainUtil.iconAsUrl(server.icon), icon: DomainUtil.iconAsUrl(server.icon),
name: server.alias, label: server.alias,
$root: this.$tabsContainer, $root: this.$tabsContainer,
onClick: this.activateLastTab.bind(this, index), onClick: this.activateLastTab.bind(this, index),
index, index,
@@ -398,14 +408,14 @@ export class ServerManagerView {
const tab = this.tabs[this.activeTabIndex]; const tab = this.tabs[this.activeTabIndex];
this.showLoading( this.showLoading(
tab instanceof ServerTab && tab instanceof ServerTab &&
this.loading.has((await tab.webview).props.url), this.loading.has((await tab.webview).properties.url),
); );
}, },
onNetworkError: async (index: number) => { onNetworkError: async (index: number) => {
await this.openNetworkTroubleshooting(index); await this.openNetworkTroubleshooting(index);
}, },
onTitleChange: this.updateBadge.bind(this), onTitleChange: this.updateBadge.bind(this),
preload: url.pathToFileURL(path.join(bundlePath, "preload.js")).href, preload: url.pathToFileURL(path.join(bundlePath, "preload.cjs")).href,
unsupportedMessage: DomainUtil.getUnsupportedMessage(server), unsupportedMessage: DomainUtil.getUnsupportedMessage(server),
}), }),
}); });
@@ -481,7 +491,7 @@ export class ServerManagerView {
async getCurrentActiveServer(): Promise<string> { async getCurrentActiveServer(): Promise<string> {
const tab = this.tabs[this.activeTabIndex]; const tab = this.tabs[this.activeTabIndex];
return tab instanceof ServerTab ? (await tab.webview).props.url : ""; return tab instanceof ServerTab ? (await tab.webview).properties.url : "";
} }
displayInitialCharLogo($img: HTMLImageElement, index: number): void { displayInitialCharLogo($img: HTMLImageElement, index: number): void {
@@ -504,8 +514,7 @@ export class ServerManagerView {
} }
$altIcon.textContent = realmName.charAt(0) || "Z"; $altIcon.textContent = realmName.charAt(0) || "Z";
$altIcon.classList.add("server-icon"); $altIcon.classList.add("server-icon", "alt-icon");
$altIcon.classList.add("alt-icon");
$img.remove(); $img.remove();
$parent.append($altIcon); $parent.append($altIcon);
@@ -550,36 +559,38 @@ export class ServerManagerView {
this.$serverIconTooltip[index].style.display = "none"; this.$serverIconTooltip[index].style.display = "none";
} }
async openFunctionalTab(tabProps: { async openFunctionalTab(tabProperties: {
name: string; label: string;
page: TabPage;
materialIcon: string; materialIcon: string;
makeView: () => Promise<Element>; makeView: () => Promise<Element>;
destroyView: () => void; destroyView: () => void;
}): Promise<void> { }): Promise<void> {
if (this.functionalTabs.has(tabProps.name)) { if (this.functionalTabs.has(tabProperties.page)) {
await this.activateTab(this.functionalTabs.get(tabProps.name)!); await this.activateTab(this.functionalTabs.get(tabProperties.page)!);
return; return;
} }
const index = this.tabs.length; const index = this.tabs.length;
this.functionalTabs.set(tabProps.name, index); this.functionalTabs.set(tabProperties.page, index);
const tabIndex = this.getTabIndex(); const tabIndex = this.getTabIndex();
const $view = await tabProps.makeView(); const $view = await tabProperties.makeView();
this.$webviewsContainer.append($view); this.$webviewsContainer.append($view);
this.tabs.push( this.tabs.push(
new FunctionalTab({ new FunctionalTab({
role: "function", role: "function",
materialIcon: tabProps.materialIcon, materialIcon: tabProperties.materialIcon,
name: tabProps.name, label: tabProperties.label,
page: tabProperties.page,
$root: this.$tabsContainer, $root: this.$tabsContainer,
index, index,
tabIndex, tabIndex,
onClick: this.activateTab.bind(this, index), onClick: this.activateTab.bind(this, index),
onDestroy: async () => { onDestroy: async () => {
await this.destroyTab(tabProps.name, index); await this.destroyFunctionalTab(tabProperties.page, index);
tabProps.destroyView(); tabProperties.destroyView();
}, },
$view, $view,
}), }),
@@ -589,12 +600,15 @@ export 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");
await this.activateTab(this.functionalTabs.get(tabProps.name)!); await this.activateTab(this.functionalTabs.get(tabProperties.page)!);
} }
async openSettings(nav: NavItem = "General"): Promise<void> { async openSettings(
navigationItem: NavigationItem = "General",
): Promise<void> {
await this.openFunctionalTab({ await this.openFunctionalTab({
name: "Settings", page: "Settings",
label: t.__("Settings"),
materialIcon: "settings", materialIcon: "settings",
makeView: async () => { makeView: async () => {
this.preferenceView = await PreferenceView.create(); this.preferenceView = await PreferenceView.create();
@@ -607,13 +621,14 @@ export class ServerManagerView {
}, },
}); });
this.$settingsButton.classList.add("active"); this.$settingsButton.classList.add("active");
this.preferenceView!.handleNavigation(nav); this.preferenceView!.handleNavigation(navigationItem);
} }
async openAbout(): Promise<void> { async openAbout(): Promise<void> {
let aboutView: AboutView; let aboutView: AboutView;
await this.openFunctionalTab({ await this.openFunctionalTab({
name: "About", page: "About",
label: t.__("About"),
materialIcon: "sentiment_very_satisfied", materialIcon: "sentiment_very_satisfied",
async makeView() { async makeView() {
aboutView = await AboutView.create(); aboutView = await AboutView.create();
@@ -646,13 +661,14 @@ export class ServerManagerView {
// 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 properties.webview
// properties in ServerTab causes the app to crash. // properties in ServerTab causes the app to crash.
get tabsForIpc(): TabData[] { get tabsForIpc(): TabData[] {
return this.tabs.map((tab) => ({ return this.tabs.map((tab) => ({
role: tab.props.role, role: tab.properties.role,
name: tab.props.name, page: tab.properties.page,
index: tab.props.index, label: tab.properties.label,
index: tab.properties.index,
})); }));
} }
@@ -670,8 +686,8 @@ export class ServerManagerView {
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 ( if (
this.tabs[this.activeTabIndex].props.role === "function" && this.tabs[this.activeTabIndex].properties.role === "function" &&
this.tabs[this.activeTabIndex].props.name === "Settings" this.tabs[this.activeTabIndex].properties.page === "Settings"
) { ) {
this.$settingsButton.classList.remove("active"); this.$settingsButton.classList.remove("active");
} }
@@ -695,7 +711,7 @@ export class ServerManagerView {
this.showLoading( this.showLoading(
tab instanceof ServerTab && tab instanceof ServerTab &&
this.loading.has((await tab.webview).props.url), this.loading.has((await tab.webview).properties.url),
); );
ipcRenderer.send("update-menu", { ipcRenderer.send("update-menu", {
@@ -704,7 +720,7 @@ export class ServerManagerView {
tabs: this.tabsForIpc, tabs: this.tabsForIpc,
activeTabIndex: this.activeTabIndex, activeTabIndex: this.activeTabIndex,
// Following flag controls whether a menu item should be enabled or not // Following flag controls whether a menu item should be enabled or not
enableMenu: tab.props.role === "server", enableMenu: tab.properties.role === "server",
}); });
} }
@@ -713,7 +729,7 @@ export class ServerManagerView {
this.$loadingIndicator.classList.toggle("hidden", !loading); this.$loadingIndicator.classList.toggle("hidden", !loading);
} }
async destroyTab(name: string, index: number): Promise<void> { async destroyFunctionalTab(page: TabPage, index: number): Promise<void> {
const tab = this.tabs[index]; const tab = this.tabs[index];
if (tab instanceof ServerTab && (await tab.webview).loading) { if (tab instanceof ServerTab && (await tab.webview).loading) {
return; return;
@@ -721,8 +737,8 @@ export class ServerManagerView {
await tab.destroy(); await tab.destroy();
delete this.tabs[index]; delete this.tabs[index]; // eslint-disable-line @typescript-eslint/no-array-delete
this.functionalTabs.delete(name); this.functionalTabs.delete(page);
// 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) {
@@ -746,7 +762,7 @@ export class ServerManagerView {
async reloadView(): Promise<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].properties.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
@@ -782,11 +798,17 @@ export class ServerManagerView {
// Toggles the dnd button icon. // Toggles the dnd button icon.
toggleDndButton(alert: boolean): void { toggleDndButton(alert: boolean): void {
this.$dndTooltip.textContent = this.$dndTooltip.textContent = alert
(alert ? "Disable" : "Enable") + " Do Not Disturb"; ? t.__("Disable Do Not Disturb")
this.$dndButton.querySelector("i")!.textContent = alert : t.__("Enable Do Not Disturb");
? "notifications_off" const $dndIcon = this.$dndButton.querySelector("i")!;
: "notifications"; $dndIcon.textContent = alert ? "notifications_off" : "notifications";
if (alert) {
$dndIcon.classList.add("dnd-on");
} else {
$dndIcon.classList.remove("dnd-on");
}
} }
async isLoggedIn(tabIndex: number): Promise<boolean> { async isLoggedIn(tabIndex: number): Promise<boolean> {
@@ -802,13 +824,15 @@ export class ServerManagerView {
event.preventDefault(); event.preventDefault();
const template = [ const template = [
{ {
label: "Disconnect organization", label: t.__("Disconnect organization"),
async click() { async click() {
const {response} = await dialog.showMessageBox({ const {response} = await dialog.showMessageBox({
type: "warning", type: "warning",
buttons: ["YES", "NO"], buttons: [t.__("Yes"), t.__("No")],
defaultId: 0, defaultId: 0,
message: "Are you sure you want to disconnect this organization?", message: t.__(
"Are you sure you want to disconnect this organization?",
),
}); });
if (response === 0) { if (response === 0) {
if (DomainUtil.removeDomain(index)) { if (DomainUtil.removeDomain(index)) {
@@ -823,7 +847,7 @@ export class ServerManagerView {
}, },
}, },
{ {
label: "Notification settings", label: t.__("Notification settings"),
enabled: await this.isLoggedIn(index), enabled: await this.isLoggedIn(index),
click: async () => { click: async () => {
// Switch to tab whose icon was right-clicked // Switch to tab whose icon was right-clicked
@@ -834,7 +858,7 @@ export class ServerManagerView {
}, },
}, },
{ {
label: "Copy Zulip URL", label: t.__("Copy Zulip URL"),
click() { click() {
clipboard.writeText(DomainUtil.getDomain(index).url); clipboard.writeText(DomainUtil.getDomain(index).url);
}, },
@@ -946,7 +970,7 @@ export class ServerManagerView {
const webview = await tab.webview; const webview = await tab.webview;
return ( return (
webview.webContentsId === webContentsId && webview.webContentsId === webContentsId &&
webview.props.hasPermission?.(origin, permission) webview.properties.hasPermission?.(origin, permission)
); );
}), }),
) )
@@ -993,8 +1017,8 @@ export class ServerManagerView {
await this.loadProxy(); await this.loadProxy();
if (showAlert) { if (showAlert) {
await dialog.showMessageBox({ await dialog.showMessageBox({
message: "Proxy settings saved!", message: t.__("Proxy settings saved."),
buttons: ["OK"], buttons: [t.__("OK")],
}); });
ipcRenderer.send("reload-full-app"); ipcRenderer.send("reload-full-app");
} }
@@ -1044,7 +1068,7 @@ export class ServerManagerView {
for (const [index, domain] of DomainUtil.getDomains().entries()) { for (const [index, domain] of DomainUtil.getDomains().entries()) {
if (domain.url === serverURL) { if (domain.url === serverURL) {
const tab = this.tabs[index]; const tab = this.tabs[index];
if (tab instanceof ServerTab) tab.setName(realmName); if (tab instanceof ServerTab) tab.setLabel(realmName);
domain.alias = realmName; domain.alias = realmName;
DomainUtil.updateDomain(index, domain); DomainUtil.updateDomain(index, domain);
// Update the realm name also on the Window menu // Update the realm name also on the Window menu
@@ -1092,7 +1116,7 @@ export class ServerManagerView {
(await tab.webview).webContentsId === webviewId (await tab.webview).webContentsId === webviewId
) { ) {
const concurrentTab: HTMLButtonElement = document.querySelector( const concurrentTab: HTMLButtonElement = document.querySelector(
`div[data-tab-id="${CSS.escape(`${tab.props.tabIndex}`)}"]`, `div[data-tab-id="${CSS.escape(`${tab.properties.tabIndex}`)}"]`,
)!; )!;
concurrentTab.click(); concurrentTab.click();
} }
@@ -1107,22 +1131,22 @@ export class ServerManagerView {
canvas.height = 128; canvas.height = 128;
canvas.width = 128; canvas.width = 128;
canvas.style.letterSpacing = "-5px"; canvas.style.letterSpacing = "-5px";
const ctx = canvas.getContext("2d")!; const context = canvas.getContext("2d")!;
ctx.fillStyle = "#f42020"; context.fillStyle = "#f42020";
ctx.beginPath(); context.beginPath();
ctx.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI); context.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI);
ctx.fill(); context.fill();
ctx.textAlign = "center"; context.textAlign = "center";
ctx.fillStyle = "white"; context.fillStyle = "white";
if (messageCount > 99) { if (messageCount > 99) {
ctx.font = "65px Helvetica"; context.font = "65px Helvetica";
ctx.fillText("99+", 64, 85); context.fillText("99+", 64, 85);
} else if (messageCount < 10) { } else if (messageCount < 10) {
ctx.font = "90px Helvetica"; context.font = "90px Helvetica";
ctx.fillText(String(Math.min(99, messageCount)), 64, 96); context.fillText(String(Math.min(99, messageCount)), 64, 96);
} else { } else {
ctx.font = "85px Helvetica"; context.font = "85px Helvetica";
ctx.fillText(String(Math.min(99, messageCount)), 64, 90); context.fillText(String(Math.min(99, messageCount)), 64, 90);
} }
return canvas; return canvas;
@@ -1170,6 +1194,62 @@ export class ServerManagerView {
} }
window.addEventListener("load", async () => { window.addEventListener("load", async () => {
document.body.innerHTML = html`
<div id="content">
<div class="popup">
<span class="popuptext hidden" id="fullscreen-popup"></span>
</div>
<div id="sidebar" class="toggle-sidebar">
<div id="view-controls-container">
<div id="tabs-container"></div>
<div id="add-tab" class="tab functional-tab">
<div class="server-tab" id="add-action">
<i class="material-icons">add</i>
</div>
<span id="add-server-tooltip" style="display: none"
>${t.__("Add Organization")}</span
>
</div>
</div>
<div id="actions-container">
<div class="action-button" id="dnd-action">
<i class="material-icons md-48">notifications</i>
<span id="dnd-tooltip" style="display: none"
>${t.__("Do Not Disturb")}</span
>
</div>
<div class="action-button hidden" id="reload-action">
<i class="material-icons md-48">refresh</i>
<span id="reload-tooltip" style="display: none"
>${t.__("Reload")}</span
>
</div>
<div class="action-button disable" id="loading-action">
<i class="refresh material-icons md-48">loop</i>
<span id="loading-tooltip" style="display: none"
>${t.__("Loading")}</span
>
</div>
<div class="action-button disable" id="back-action">
<i class="material-icons md-48">arrow_back</i>
<span id="back-tooltip" style="display: none"
>${t.__("Go Back")}</span
>
</div>
<div class="action-button" id="settings-action">
<i class="material-icons md-48">settings</i>
<span id="setting-tooltip" style="display: none"
>${t.__("Settings")}</span
>
</div>
</div>
</div>
<div id="main-container">
<div id="webviews-container"></div>
</div>
</div>
`.html;
const serverManagerView = new ServerManagerView(); const serverManagerView = new ServerManagerView();
await serverManagerView.init(); await serverManagerView.init();
}); });

View File

@@ -1,4 +1,4 @@
import {ipcRenderer} from "../typed-ipc-renderer.js"; import {ipcRenderer} from "../typed-ipc-renderer.ts";
export type NotificationData = { export type NotificationData = {
close: () => void; close: () => void;
@@ -18,10 +18,10 @@ export function newNotification(
): NotificationData { ): NotificationData {
const notification = new Notification(title, {...options, silent: true}); const notification = new Notification(title, {...options, silent: true});
for (const type of ["click", "close", "error", "show"]) { for (const type of ["click", "close", "error", "show"]) {
notification.addEventListener(type, (ev) => { notification.addEventListener(type, (event) => {
if (type === "click") ipcRenderer.send("focus-this-webview"); if (type === "click") ipcRenderer.send("focus-this-webview");
if (!dispatch(type, ev)) { if (!dispatch(type, event)) {
ev.preventDefault(); event.preventDefault();
} }
}); });
} }

View File

@@ -1,6 +1,9 @@
import {app} from "@electron/remote"; import {app} from "@electron/remote";
import {bundleUrl} from "../../../common/paths.js"; import {Html, html} from "../../../common/html.ts";
import {bundleUrl} from "../../../common/paths.ts";
import * as t from "../../../common/translation-util.ts";
import {generateNodeFromHtml} from "../components/base.ts";
export class AboutView { export class AboutView {
static async create(): Promise<AboutView> { static async create(): Promise<AboutView> {
@@ -16,6 +19,32 @@ export class AboutView {
const $shadow = this.$view.attachShadow({mode: "open"}); const $shadow = this.$view.attachShadow({mode: "open"});
$shadow.innerHTML = templateHtml; $shadow.innerHTML = templateHtml;
$shadow.querySelector("#version")!.textContent = `v${app.getVersion()}`; $shadow.querySelector("#version")!.textContent = `v${app.getVersion()}`;
const maintenanceInfoHtml = html`
<div class="maintenance-info">
<p class="detail maintainer">
${new Html({
html: t.__("Maintained by {{{link}}}Zulip{{{endLink}}}", {
link: '<a href="https://zulip.com" target="_blank" rel="noopener noreferrer">',
endLink: "</a>",
}),
})}
</p>
<p class="detail license">
${new Html({
html: t.__(
"Available under the {{{link}}}Apache 2.0 License{{{endLink}}}",
{
link: '<a href="https://github.com/zulip/zulip-desktop/blob/main/LICENSE" target="_blank" rel="noopener noreferrer">',
endLink: "</a>",
},
),
})}
</p>
</div>
`;
$shadow
.querySelector(".about")!
.append(generateNodeFromHtml(maintenanceInfoHtml));
} }
destroy() { destroy() {

View File

@@ -1,4 +1,4 @@
import {ipcRenderer} from "../typed-ipc-renderer.js"; import {ipcRenderer} from "../typed-ipc-renderer.ts";
export function init( export function init(
$reconnectButton: Element, $reconnectButton: Element,

View File

@@ -1,17 +1,17 @@
import type {Html} from "../../../../common/html.js"; import {type Html, html} from "../../../../common/html.ts";
import {html} from "../../../../common/html.js"; import * as t from "../../../../common/translation-util.ts";
import {generateNodeFromHtml} from "../../components/base.js"; import {generateNodeFromHtml} from "../../components/base.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.js"; import {ipcRenderer} from "../../typed-ipc-renderer.ts";
type BaseSectionProps = { type BaseSectionProperties = {
$element: HTMLElement; $element: HTMLElement;
disabled?: boolean; disabled?: boolean;
value: boolean; value: boolean;
clickHandler: () => void; clickHandler: () => void;
}; };
export function generateSettingOption(props: BaseSectionProps): void { export function generateSettingOption(properties: BaseSectionProperties): void {
const {$element, disabled, value, clickHandler} = props; const {$element, disabled, value, clickHandler} = properties;
$element.textContent = ""; $element.textContent = "";
@@ -30,10 +30,9 @@ export function generateOptionHtml(
disabled?: boolean, disabled?: boolean,
): Html { ): Html {
const labelHtml = disabled const labelHtml = disabled
? // eslint-disable-next-line unicorn/template-indent ? html`<label
html`<label
class="disallowed" class="disallowed"
title="Setting locked by system administrator." title="${t.__("Setting locked by system administrator.")}"
></label>` ></label>`
: html`<label></label>`; : html`<label></label>`;
if (settingOption) { if (settingOption) {

View File

@@ -1,19 +1,19 @@
import {html} from "../../../../common/html.js"; import {html} from "../../../../common/html.ts";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.js"; import {ipcRenderer} from "../../typed-ipc-renderer.ts";
import * as DomainUtil from "../../utils/domain-util.js"; import * as DomainUtil from "../../utils/domain-util.ts";
import {reloadApp} from "./base-section.js"; import {reloadApp} from "./base-section.ts";
import {initFindAccounts} from "./find-accounts.js"; import {initFindAccounts} from "./find-accounts.ts";
import {initServerInfoForm} from "./server-info-form.js"; import {initServerInfoForm} from "./server-info-form.ts";
type ConnectedOrgSectionProps = { type ConnectedOrgSectionProperties = {
$root: Element; $root: Element;
}; };
export function initConnectedOrgSection({ export function initConnectedOrgSection({
$root, $root,
}: ConnectedOrgSectionProps): void { }: ConnectedOrgSectionProperties): void {
$root.textContent = ""; $root.textContent = "";
const servers = DomainUtil.getDomains(); const servers = DomainUtil.getDomains();

View File

@@ -1,9 +1,9 @@
import {html} from "../../../../common/html.js"; import {html} from "../../../../common/html.ts";
import * as LinkUtil from "../../../../common/link-util.js"; import * as LinkUtil from "../../../../common/link-util.ts";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.ts";
import {generateNodeFromHtml} from "../../components/base.js"; import {generateNodeFromHtml} from "../../components/base.ts";
type FindAccountsProps = { type FindAccountsProperties = {
$root: Element; $root: Element;
}; };
@@ -19,7 +19,7 @@ async function findAccounts(url: string): Promise<void> {
await LinkUtil.openBrowser(new URL("/accounts/find", url)); await LinkUtil.openBrowser(new URL("/accounts/find", url));
} }
export function initFindAccounts(props: FindAccountsProps): void { export function initFindAccounts(properties: FindAccountsProperties): void {
const $findAccounts = generateNodeFromHtml(html` const $findAccounts = generateNodeFromHtml(html`
<div class="settings-card certificate-card"> <div class="settings-card certificate-card">
<div class="certificate-input"> <div class="certificate-input">
@@ -33,7 +33,7 @@ export function initFindAccounts(props: FindAccountsProps): void {
</div> </div>
</div> </div>
`); `);
props.$root.append($findAccounts); properties.$root.append($findAccounts);
const $findAccountsButton = $findAccounts.querySelector( const $findAccountsButton = $findAccounts.querySelector(
"#find-accounts-button", "#find-accounts-button",
)!; )!;

View File

@@ -6,25 +6,24 @@ import process from "node:process";
import * as remote from "@electron/remote"; import * as remote from "@electron/remote";
import {app, dialog, session} from "@electron/remote"; import {app, dialog, session} from "@electron/remote";
import Tagify from "@yaireo/tagify"; import Tagify from "@yaireo/tagify";
import ISO6391 from "iso-639-1";
import {z} from "zod"; import {z} from "zod";
import supportedLocales from "../../../../../public/translations/supported-locales.json"; import supportedLocales from "../../../../../public/translations/supported-locales.json";
import * as ConfigUtil from "../../../../common/config-util.js"; import * as ConfigUtil from "../../../../common/config-util.ts";
import * as EnterpriseUtil from "../../../../common/enterprise-util.js"; import * as EnterpriseUtil from "../../../../common/enterprise-util.ts";
import {html} from "../../../../common/html.js"; import {html} from "../../../../common/html.ts";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.js"; import {ipcRenderer} from "../../typed-ipc-renderer.ts";
import {generateSelectHtml, generateSettingOption} from "./base-section.js"; import {generateSelectHtml, generateSettingOption} from "./base-section.ts";
const currentBrowserWindow = remote.getCurrentWindow(); const currentBrowserWindow = remote.getCurrentWindow();
type GeneralSectionProps = { type GeneralSectionProperties = {
$root: Element; $root: Element;
}; };
export function initGeneralSection({$root}: GeneralSectionProps): void { export function initGeneralSection({$root}: GeneralSectionProperties): void {
$root.innerHTML = html` $root.innerHTML = html`
<div class="settings-pane"> <div class="settings-pane">
<div class="title">${t.__("Appearance")}</div> <div class="title">${t.__("Appearance")}</div>
@@ -57,7 +56,7 @@ export function initGeneralSection({$root}: GeneralSectionProps): void {
</div> </div>
<div class="setting-row" id="badge-option"> <div class="setting-row" id="badge-option">
<div class="setting-description"> <div class="setting-description">
${t.__("Show app unread badge")} ${t.__("Show unread count badge on app icon")}
</div> </div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
@@ -356,7 +355,8 @@ export function initGeneralSection({$root}: GeneralSectionProps): void {
const newValue = !ConfigUtil.getConfigItem("silent", true); const newValue = !ConfigUtil.getConfigItem("silent", true);
ConfigUtil.setConfigItem("silent", newValue); ConfigUtil.setConfigItem("silent", newValue);
updateSilentOption(); updateSilentOption();
ipcRenderer.sendTo( ipcRenderer.send(
"forward-to",
currentBrowserWindow.webContents.id, currentBrowserWindow.webContents.id,
"toggle-silent", "toggle-silent",
newValue, newValue,
@@ -455,9 +455,9 @@ export function initGeneralSection({$root}: GeneralSectionProps): void {
async function customCssDialog(): Promise<void> { async function customCssDialog(): Promise<void> {
const showDialogOptions: OpenDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: "Select file", title: t.__("Select file"),
properties: ["openFile"], properties: ["openFile"],
filters: [{name: "CSS file", extensions: ["css"]}], filters: [{name: t.__("CSS file"), extensions: ["css"]}],
}; };
const {filePaths, canceled} = const {filePaths, canceled} =
@@ -524,7 +524,7 @@ export function initGeneralSection({$root}: GeneralSectionProps): void {
async function downloadFolderDialog(): Promise<void> { async function downloadFolderDialog(): Promise<void> {
const showDialogOptions: OpenDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: "Select Download Location", title: t.__("Select Download Location"),
properties: ["openDirectory"], properties: ["openDirectory"],
}; };
@@ -561,15 +561,16 @@ export function initGeneralSection({$root}: GeneralSectionProps): void {
} }
async function factoryResetSettings(): Promise<void> { async function factoryResetSettings(): Promise<void> {
const clearAppDataMessage = const clearAppDataMessage = t.__(
"When the application restarts, it will be as if you have just downloaded Zulip app."; "When the application restarts, it will be as if you have just downloaded the Zulip app.",
);
const getAppPath = path.join(app.getPath("appData"), app.name); const getAppPath = path.join(app.getPath("appData"), app.name);
const {response} = await dialog.showMessageBox({ const {response} = await dialog.showMessageBox({
type: "warning", type: "warning",
buttons: ["YES", "NO"], buttons: [t.__("Yes"), t.__("No")],
defaultId: 0, defaultId: 0,
message: "Are you sure?", message: t.__("Are you sure?"),
detail: clearAppDataMessage, detail: clearAppDataMessage,
}); });
if (response === 0) { if (response === 0) {
@@ -609,7 +610,7 @@ export function initGeneralSection({$root}: GeneralSectionProps): void {
spellDiv.innerHTML += html` spellDiv.innerHTML += html`
<div class="setting-description">${t.__("Spellchecker Languages")}</div> <div class="setting-description">${t.__("Spellchecker Languages")}</div>
<div id="spellcheck-langs-value"> <div id="spellcheck-langs-value">
<input name="spellcheck" placeholder="Enter Languages" /> <input name="spellcheck" placeholder="${t.__("Enter Languages")}" />
</div> </div>
`.html; `.html;
@@ -618,26 +619,23 @@ export function initGeneralSection({$root}: GeneralSectionProps): void {
).availableSpellCheckerLanguages; ).availableSpellCheckerLanguages;
let languagePairs = new Map<string, string>(); let languagePairs = new Map<string, string>();
for (const l of availableLanguages) { for (const l of availableLanguages) {
if (ISO6391.validate(l)) { const locale = new Intl.Locale(l.replaceAll("_", "-"));
languagePairs.set(ISO6391.getName(l), l); let displayName = new Intl.DisplayNames([locale], {
} type: "language",
}).of(locale.language);
if (displayName === undefined) continue;
displayName = displayName.replace(/^./u, (firstChar) =>
firstChar.toLocaleUpperCase(locale),
);
if (locale.script !== undefined)
displayName += ` (${new Intl.DisplayNames([locale], {type: "script"}).of(locale.script)})`;
if (locale.region !== undefined)
displayName += ` (${new Intl.DisplayNames([locale], {type: "region"}).of(locale.region)})`;
languagePairs.set(displayName, 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 = new Map(
[...languagePairs].sort((a, b) => (a[0] < b[0] ? -1 : 1)), [...languagePairs].sort((a, b) => a[0].localeCompare(b[1])),
); );
const tagField: HTMLInputElement = $root.querySelector( const tagField: HTMLInputElement = $root.querySelector(

View File

@@ -1,74 +1,74 @@
import type {Html} from "../../../../common/html.js"; import {type Html, html} from "../../../../common/html.ts";
import {html} from "../../../../common/html.js"; import * as t from "../../../../common/translation-util.ts";
import * as t from "../../../../common/translation-util.js"; import type {NavigationItem} from "../../../../common/types.ts";
import type {NavItem} from "../../../../common/types.js"; import {generateNodeFromHtml} from "../../components/base.ts";
import {generateNodeFromHtml} from "../../components/base.js";
type PreferenceNavProps = { type PreferenceNavigationProperties = {
$root: Element; $root: Element;
onItemSelected: (navItem: NavItem) => void; onItemSelected: (navigationItem: NavigationItem) => void;
}; };
export default class PreferenceNav { export default class PreferenceNavigation {
navItems: NavItem[]; navigationItems: Array<{navigationItem: NavigationItem; label: string}>;
$el: Element; $el: Element;
constructor(private readonly props: PreferenceNavProps) { constructor(private readonly properties: PreferenceNavigationProperties) {
this.navItems = [ this.navigationItems = [
"General", {navigationItem: "General", label: t.__("General")},
"Network", {navigationItem: "Network", label: t.__("Network")},
"AddServer", {navigationItem: "AddServer", label: t.__("Add Organization")},
"Organizations", {navigationItem: "Organizations", label: t.__("Organizations")},
"Shortcuts", {navigationItem: "Shortcuts", label: t.__("Shortcuts")},
]; ];
this.$el = generateNodeFromHtml(this.templateHtml()); this.$el = generateNodeFromHtml(this.templateHtml());
this.props.$root.append(this.$el); this.properties.$root.append(this.$el);
this.registerListeners(); this.registerListeners();
} }
templateHtml(): Html { templateHtml(): Html {
const navItemsHtml = html``.join( const navigationItemsHtml = html``.join(
this.navItems.map( this.navigationItems.map(
(navItem) => html` ({navigationItem, label}) =>
<div class="nav" id="nav-${navItem}">${t.__(navItem)}</div> html`<div class="nav" id="nav-${navigationItem}">${label}</div>`,
`,
), ),
); );
return html` return html`
<div> <div>
<div id="settings-header">${t.__("Settings")}</div> <div id="settings-header">${t.__("Settings")}</div>
<div id="nav-container">${navItemsHtml}</div> <div id="nav-container">${navigationItemsHtml}</div>
</div> </div>
`; `;
} }
registerListeners(): void { registerListeners(): void {
for (const navItem of this.navItems) { for (const {navigationItem} of this.navigationItems) {
const $item = this.$el.querySelector(`#nav-${CSS.escape(navItem)}`)!; const $item = this.$el.querySelector(
`#nav-${CSS.escape(navigationItem)}`,
)!;
$item.addEventListener("click", () => { $item.addEventListener("click", () => {
this.props.onItemSelected(navItem); this.properties.onItemSelected(navigationItem);
}); });
} }
} }
select(navItemToSelect: NavItem): void { select(navigationItemToSelect: NavigationItem): void {
for (const navItem of this.navItems) { for (const {navigationItem} of this.navigationItems) {
if (navItem === navItemToSelect) { if (navigationItem === navigationItemToSelect) {
this.activate(navItem); this.activate(navigationItem);
} else { } else {
this.deactivate(navItem); this.deactivate(navigationItem);
} }
} }
} }
activate(navItem: NavItem): void { activate(navigationItem: NavigationItem): void {
const $item = this.$el.querySelector(`#nav-${CSS.escape(navItem)}`)!; const $item = this.$el.querySelector(`#nav-${CSS.escape(navigationItem)}`)!;
$item.classList.add("active"); $item.classList.add("active");
} }
deactivate(navItem: NavItem): void { deactivate(navigationItem: NavigationItem): void {
const $item = this.$el.querySelector(`#nav-${CSS.escape(navItem)}`)!; const $item = this.$el.querySelector(`#nav-${CSS.escape(navigationItem)}`)!;
$item.classList.remove("active"); $item.classList.remove("active");
} }
} }

View File

@@ -1,15 +1,15 @@
import * as ConfigUtil from "../../../../common/config-util.js"; import * as ConfigUtil from "../../../../common/config-util.ts";
import {html} from "../../../../common/html.js"; import {html} from "../../../../common/html.ts";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.js"; import {ipcRenderer} from "../../typed-ipc-renderer.ts";
import {generateSettingOption} from "./base-section.js"; import {generateSettingOption} from "./base-section.ts";
type NetworkSectionProps = { type NetworkSectionProperties = {
$root: Element; $root: Element;
}; };
export function initNetworkSection({$root}: NetworkSectionProps): void { export function initNetworkSection({$root}: NetworkSectionProperties): void {
$root.innerHTML = html` $root.innerHTML = html`
<div class="settings-pane"> <div class="settings-pane">
<div class="title">${t.__("Proxy")}</div> <div class="title">${t.__("Proxy")}</div>
@@ -28,7 +28,7 @@ export function initNetworkSection({$root}: NetworkSectionProps): void {
</div> </div>
<div class="manual-proxy-block"> <div class="manual-proxy-block">
<div class="setting-row" id="proxy-pac-option"> <div class="setting-row" id="proxy-pac-option">
<span class="setting-input-key">PAC ${t.__("script")}</span> <span class="setting-input-key">${t.__("PAC script")}</span>
<input <input
class="setting-input-value" class="setting-input-value"
placeholder="e.g. foobar.com/pacfile.js" placeholder="e.g. foobar.com/pacfile.js"

View File

@@ -1,18 +1,21 @@
import {dialog} from "@electron/remote"; import {dialog} from "@electron/remote";
import {html} from "../../../../common/html.js"; import {html} from "../../../../common/html.ts";
import * as LinkUtil from "../../../../common/link-util.js"; import * as LinkUtil from "../../../../common/link-util.ts";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.ts";
import {generateNodeFromHtml} from "../../components/base.js"; import {generateNodeFromHtml} from "../../components/base.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.js"; import {ipcRenderer} from "../../typed-ipc-renderer.ts";
import * as DomainUtil from "../../utils/domain-util.js"; import * as DomainUtil from "../../utils/domain-util.ts";
type NewServerFormProps = { type NewServerFormProperties = {
$root: Element; $root: Element;
onChange: () => void; onChange: () => void;
}; };
export function initNewServerForm({$root, onChange}: NewServerFormProps): void { export function initNewServerForm({
$root,
onChange,
}: NewServerFormProperties): void {
const $newServerForm = generateNodeFromHtml(html` const $newServerForm = generateNodeFromHtml(html`
<div class="server-input-container"> <div class="server-input-container">
<div class="title">${t.__("Organization URL")}</div> <div class="title">${t.__("Organization URL")}</div>
@@ -20,7 +23,9 @@ export function initNewServerForm({$root, onChange}: NewServerFormProps): void {
<input <input
class="setting-input-value" class="setting-input-value"
autofocus autofocus
placeholder="your-organization.zulipchat.com or zulip.your-organization.com" placeholder="${t.__(
"your-organization.zulipchat.com or zulip.your-organization.com",
)}"
/> />
</div> </div>
<div class="server-center"> <div class="server-center">
@@ -57,24 +62,24 @@ export function initNewServerForm({$root, onChange}: NewServerFormProps): void {
)!; )!;
async function submitFormHandler(): Promise<void> { async function submitFormHandler(): Promise<void> {
$saveServerButton.textContent = "Connecting..."; $saveServerButton.textContent = t.__("Connecting…");
let serverConf; let serverConfig;
try { try {
serverConf = await DomainUtil.checkDomain($newServerUrl.value.trim()); serverConfig = await DomainUtil.checkDomain($newServerUrl.value.trim());
} catch (error: unknown) { } catch (error: unknown) {
$saveServerButton.textContent = "Connect"; $saveServerButton.textContent = t.__("Connect");
await dialog.showMessageBox({ await dialog.showMessageBox({
type: "error", type: "error",
message: message:
error instanceof Error error instanceof Error
? `${error.name}: ${error.message}` ? `${error.name}: ${error.message}`
: "Unknown error", : t.__("Unknown error"),
buttons: ["OK"], buttons: [t.__("OK")],
}); });
return; return;
} }
await DomainUtil.addDomain(serverConf); await DomainUtil.addDomain(serverConfig);
onChange(); onChange();
} }

View File

@@ -1,17 +1,17 @@
import type {IpcRendererEvent} from "electron/renderer"; import type {IpcRendererEvent} from "electron/renderer";
import process from "node:process"; import process from "node:process";
import type {DndSettings} from "../../../../common/dnd-util.js"; import type {DndSettings} from "../../../../common/dnd-util.ts";
import {bundleUrl} from "../../../../common/paths.js"; import {bundleUrl} from "../../../../common/paths.ts";
import type {NavItem} from "../../../../common/types.js"; import type {NavigationItem} from "../../../../common/types.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.js"; import {ipcRenderer} from "../../typed-ipc-renderer.ts";
import {initConnectedOrgSection} from "./connected-org-section.js"; import {initConnectedOrgSection} from "./connected-org-section.ts";
import {initGeneralSection} from "./general-section.js"; import {initGeneralSection} from "./general-section.ts";
import Nav from "./nav.js"; import Nav from "./nav.ts";
import {initNetworkSection} from "./network-section.js"; import {initNetworkSection} from "./network-section.ts";
import {initServersSection} from "./servers-section.js"; import {initServersSection} from "./servers-section.ts";
import {initShortcutsSection} from "./shortcuts-section.js"; import {initShortcutsSection} from "./shortcuts-section.ts";
export class PreferenceView { export class PreferenceView {
static async create(): Promise<PreferenceView> { static async create(): Promise<PreferenceView> {
@@ -26,7 +26,7 @@ export class PreferenceView {
private readonly $shadow: ShadowRoot; private readonly $shadow: ShadowRoot;
private readonly $settingsContainer: Element; private readonly $settingsContainer: Element;
private readonly nav: Nav; private readonly nav: Nav;
private navItem: NavItem = "General"; private navigationItem: NavigationItem = "General";
private constructor(templateHtml: string) { private constructor(templateHtml: string) {
this.$view = document.createElement("div"); this.$view = document.createElement("div");
@@ -47,13 +47,13 @@ export class PreferenceView {
ipcRenderer.on("toggle-autohide-menubar", this.handleToggleMenubar); ipcRenderer.on("toggle-autohide-menubar", this.handleToggleMenubar);
ipcRenderer.on("toggle-dnd", this.handleToggleDnd); ipcRenderer.on("toggle-dnd", this.handleToggleDnd);
this.handleNavigation(this.navItem); this.handleNavigation(this.navigationItem);
} }
handleNavigation = (navItem: NavItem): void => { handleNavigation = (navigationItem: NavigationItem): void => {
this.navItem = navItem; this.navigationItem = navigationItem;
this.nav.select(navItem); this.nav.select(navigationItem);
switch (navItem) { switch (navigationItem) {
case "AddServer": { case "AddServer": {
initServersSection({ initServersSection({
$root: this.$settingsContainer, $root: this.$settingsContainer,
@@ -88,13 +88,9 @@ export class PreferenceView {
}); });
break; break;
} }
default: {
((n: never) => n)(navItem);
}
} }
window.location.hash = `#${navItem}`; location.hash = `#${navigationItem}`;
}; };
handleToggleTray(state: boolean) { handleToggleTray(state: boolean) {

View File

@@ -1,37 +1,37 @@
import {dialog} from "@electron/remote"; import {dialog} from "@electron/remote";
import {html} from "../../../../common/html.js"; import {html} from "../../../../common/html.ts";
import * as Messages from "../../../../common/messages.js"; import * as Messages from "../../../../common/messages.ts";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.ts";
import type {ServerConf} from "../../../../common/types.js"; import type {ServerConfig} from "../../../../common/types.ts";
import {generateNodeFromHtml} from "../../components/base.js"; import {generateNodeFromHtml} from "../../components/base.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.js"; import {ipcRenderer} from "../../typed-ipc-renderer.ts";
import * as DomainUtil from "../../utils/domain-util.js"; import * as DomainUtil from "../../utils/domain-util.ts";
type ServerInfoFormProps = { type ServerInfoFormProperties = {
$root: Element; $root: Element;
server: ServerConf; server: ServerConfig;
index: number; index: number;
onChange: () => void; onChange: () => void;
}; };
export function initServerInfoForm(props: ServerInfoFormProps): void { export function initServerInfoForm(properties: ServerInfoFormProperties): void {
const $serverInfoForm = generateNodeFromHtml(html` const $serverInfoForm = generateNodeFromHtml(html`
<div class="settings-card"> <div class="settings-card">
<div class="server-info-left"> <div class="server-info-left">
<img <img
class="server-info-icon" class="server-info-icon"
src="${DomainUtil.iconAsUrl(props.server.icon)}" src="${DomainUtil.iconAsUrl(properties.server.icon)}"
/> />
<div class="server-info-row"> <div class="server-info-row">
<span class="server-info-alias">${props.server.alias}</span> <span class="server-info-alias">${properties.server.alias}</span>
<i class="material-icons open-tab-button">open_in_new</i> <i class="material-icons open-tab-button">open_in_new</i>
</div> </div>
</div> </div>
<div class="server-info-right"> <div class="server-info-right">
<div class="server-info-row server-url"> <div class="server-info-row server-url">
<span class="server-url-info" title="${props.server.url}" <span class="server-url-info" title="${properties.server.url}"
>${props.server.url}</span >${properties.server.url}</span
> >
</div> </div>
<div class="server-info-row"> <div class="server-info-row">
@@ -48,21 +48,21 @@ export function initServerInfoForm(props: ServerInfoFormProps): void {
".server-delete-action", ".server-delete-action",
)!; )!;
const $openServerButton = $serverInfoForm.querySelector(".open-tab-button")!; const $openServerButton = $serverInfoForm.querySelector(".open-tab-button")!;
props.$root.append($serverInfoForm); properties.$root.append($serverInfoForm);
$deleteServerButton.addEventListener("click", async () => { $deleteServerButton.addEventListener("click", async () => {
const {response} = await dialog.showMessageBox({ const {response} = await dialog.showMessageBox({
type: "warning", type: "warning",
buttons: [t.__("YES"), t.__("NO")], buttons: [t.__("Yes"), t.__("No")],
defaultId: 0, defaultId: 0,
message: t.__("Are you sure you want to disconnect this organization?"), message: t.__("Are you sure you want to disconnect this organization?"),
}); });
if (response === 0) { if (response === 0) {
if (DomainUtil.removeDomain(props.index)) { if (DomainUtil.removeDomain(properties.index)) {
ipcRenderer.send("reload-full-app"); ipcRenderer.send("reload-full-app");
} else { } else {
const {title, content} = Messages.orgRemovalError( const {title, content} = Messages.orgRemovalError(
DomainUtil.getDomain(props.index).url, DomainUtil.getDomain(properties.index).url,
); );
dialog.showErrorBox(title, content); dialog.showErrorBox(title, content);
} }
@@ -70,14 +70,14 @@ export function initServerInfoForm(props: ServerInfoFormProps): void {
}); });
$openServerButton.addEventListener("click", () => { $openServerButton.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", props.index); ipcRenderer.send("forward-message", "switch-server-tab", properties.index);
}); });
$serverInfoAlias.addEventListener("click", () => { $serverInfoAlias.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", props.index); ipcRenderer.send("forward-message", "switch-server-tab", properties.index);
}); });
$serverIcon.addEventListener("click", () => { $serverIcon.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", props.index); ipcRenderer.send("forward-message", "switch-server-tab", properties.index);
}); });
} }

View File

@@ -1,14 +1,14 @@
import {html} from "../../../../common/html.js"; import {html} from "../../../../common/html.ts";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.ts";
import {reloadApp} from "./base-section.js"; import {reloadApp} from "./base-section.ts";
import {initNewServerForm} from "./new-server-form.js"; import {initNewServerForm} from "./new-server-form.ts";
type ServersSectionProps = { type ServersSectionProperties = {
$root: Element; $root: Element;
}; };
export function initServersSection({$root}: ServersSectionProps): void { export function initServersSection({$root}: ServersSectionProperties): void {
$root.innerHTML = html` $root.innerHTML = html`
<div class="add-server-modal"> <div class="add-server-modal">
<div class="modal-container"> <div class="modal-container">

View File

@@ -1,15 +1,17 @@
import process from "node:process"; import process from "node:process";
import {html} from "../../../../common/html.js"; import {html} from "../../../../common/html.ts";
import * as LinkUtil from "../../../../common/link-util.js"; import * as LinkUtil from "../../../../common/link-util.ts";
import * as t from "../../../../common/translation-util.js"; import * as t from "../../../../common/translation-util.ts";
type ShortcutsSectionProps = { type ShortcutsSectionProperties = {
$root: Element; $root: Element;
}; };
// eslint-disable-next-line complexity // eslint-disable-next-line complexity
export function initShortcutsSection({$root}: ShortcutsSectionProps): void { export function initShortcutsSection({
$root,
}: ShortcutsSectionProperties): void {
const cmdOrCtrl = process.platform === "darwin" ? "⌘" : "Ctrl"; const cmdOrCtrl = process.platform === "darwin" ? "⌘" : "Ctrl";
$root.innerHTML = html` $root.innerHTML = html`

View File

@@ -1,8 +1,8 @@
import {contextBridge} from "electron/renderer"; import {contextBridge} from "electron/renderer";
import electron_bridge, {bridgeEvents} from "./electron-bridge.js"; import electron_bridge, {bridgeEvents} from "./electron-bridge.ts";
import * as NetworkError from "./pages/network.js"; import * as NetworkError from "./pages/network.ts";
import {ipcRenderer} from "./typed-ipc-renderer.js"; import {ipcRenderer} from "./typed-ipc-renderer.ts";
contextBridge.exposeInMainWorld("electron_bridge", electron_bridge); contextBridge.exposeInMainWorld("electron_bridge", electron_bridge);

View File

@@ -1,17 +1,17 @@
import type {NativeImage} from "electron/common"; import {type NativeImage, nativeImage} from "electron/common";
import {nativeImage} from "electron/common";
import type {Tray as ElectronTray} from "electron/main"; import type {Tray as ElectronTray} from "electron/main";
import path from "node:path"; import path from "node:path";
import process from "node:process"; import process from "node:process";
import {BrowserWindow, Menu, Tray} from "@electron/remote"; import {BrowserWindow, Menu, Tray} from "@electron/remote";
import * as ConfigUtil from "../../common/config-util.js"; import * as ConfigUtil from "../../common/config-util.ts";
import {publicPath} from "../../common/paths.js"; import {publicPath} from "../../common/paths.ts";
import type {RendererMessage} from "../../common/typed-ipc.js"; import * as t from "../../common/translation-util.ts";
import type {RendererMessage} from "../../common/typed-ipc.ts";
import type {ServerManagerView} from "./main.js"; import type {ServerManagerView} from "./main.ts";
import {ipcRenderer} from "./typed-ipc-renderer.js"; import {ipcRenderer} from "./typed-ipc-renderer.ts";
let tray: ElectronTray | null = null; let tray: ElectronTray | null = null;
@@ -64,8 +64,8 @@ const config = {
thick: process.platform === "win32", thick: process.platform === "win32",
}; };
const renderCanvas = function (arg: number): HTMLCanvasElement { const renderCanvas = function (argument: number): HTMLCanvasElement {
config.unreadCount = arg; config.unreadCount = argument;
const size = config.size * config.pixelRatio; const size = config.size * config.pixelRatio;
const padding = size * 0.05; const padding = size * 0.05;
@@ -79,30 +79,34 @@ const renderCanvas = function (arg: number): HTMLCanvasElement {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = size; canvas.width = size;
canvas.height = size; canvas.height = size;
const ctx = canvas.getContext("2d")!; const context = canvas.getContext("2d")!;
// Circle // Circle
// If (!config.thick || config.thick && hasCount) { // If (!config.thick || config.thick && hasCount) {
ctx.beginPath(); context.beginPath();
ctx.arc(center, center, size / 2 - padding, 0, 2 * Math.PI, false); context.arc(center, center, size / 2 - padding, 0, 2 * Math.PI, false);
ctx.fillStyle = backgroundColor; context.fillStyle = backgroundColor;
ctx.fill(); context.fill();
ctx.lineWidth = size / (config.thick ? 10 : 20); context.lineWidth = size / (config.thick ? 10 : 20);
ctx.strokeStyle = backgroundColor; context.strokeStyle = backgroundColor;
ctx.stroke(); context.stroke();
// Count or Icon // Count or Icon
if (hasCount) { if (hasCount) {
ctx.fillStyle = color; context.fillStyle = color;
ctx.textAlign = "center"; context.textAlign = "center";
if (config.unreadCount > 99) { if (config.unreadCount > 99) {
ctx.font = `${config.thick ? "bold " : ""}${size * 0.4}px Helvetica`; context.font = `${config.thick ? "bold " : ""}${size * 0.4}px Helvetica`;
ctx.fillText("99+", center, center + size * 0.15); context.fillText("99+", center, center + size * 0.15);
} else if (config.unreadCount < 10) { } else if (config.unreadCount < 10) {
ctx.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`; context.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), center, center + size * 0.2); context.fillText(String(config.unreadCount), center, center + size * 0.2);
} else { } else {
ctx.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`; context.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), center, center + size * 0.15); context.fillText(
String(config.unreadCount),
center,
center + size * 0.15,
);
} }
} }
@@ -114,12 +118,12 @@ const renderCanvas = function (arg: number): HTMLCanvasElement {
* @param arg: Unread count * @param arg: Unread count
* @return the native image * @return the native image
*/ */
const renderNativeImage = function (arg: number): NativeImage { const renderNativeImage = function (argument: number): NativeImage {
if (process.platform === "win32") { if (process.platform === "win32") {
return nativeImage.createFromPath(winUnreadTrayIconPath()); return nativeImage.createFromPath(winUnreadTrayIconPath());
} }
const canvas = renderCanvas(arg); const canvas = renderCanvas(argument);
const pngData = nativeImage const pngData = nativeImage
.createFromDataURL(canvas.toDataURL("image/png")) .createFromDataURL(canvas.toDataURL("image/png"))
.toPNG(); .toPNG();
@@ -130,7 +134,7 @@ const renderNativeImage = function (arg: number): NativeImage {
function sendAction<Channel extends keyof RendererMessage>( function sendAction<Channel extends keyof RendererMessage>(
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void { ): void {
const win = BrowserWindow.getAllWindows()[0]; const win = BrowserWindow.getAllWindows()[0];
@@ -138,19 +142,19 @@ function sendAction<Channel extends keyof RendererMessage>(
win.restore(); win.restore();
} }
ipcRenderer.sendTo(win.webContents.id, channel, ...args); ipcRenderer.send("forward-to", win.webContents.id, channel, ...arguments_);
} }
const createTray = function (): void { const createTray = function (): void {
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
label: "Zulip", label: t.__("Zulip"),
click() { click() {
ipcRenderer.send("focus-app"); ipcRenderer.send("focus-app");
}, },
}, },
{ {
label: "Settings", label: t.__("Settings"),
click() { click() {
ipcRenderer.send("focus-app"); ipcRenderer.send("focus-app");
sendAction("open-settings"); sendAction("open-settings");
@@ -160,7 +164,7 @@ const createTray = function (): void {
type: "separator", type: "separator",
}, },
{ {
label: "Quit", label: t.__("Quit"),
click() { click() {
ipcRenderer.send("quit-app"); ipcRenderer.send("quit-app");
}, },
@@ -189,22 +193,27 @@ export function initializeTray(serverManagerView: ServerManagerView) {
} }
}); });
ipcRenderer.on("tray", (_event, arg: number): void => { ipcRenderer.on("tray", (_event, argument: 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 (argument === 0) {
unread = arg; unread = argument;
tray.setImage(iconPath()); tray.setImage(iconPath());
tray.setToolTip("No unread messages"); tray.setToolTip(t.__("No unread messages"));
} else { } else {
unread = arg; unread = argument;
const image = renderNativeImage(arg); const image = renderNativeImage(argument);
tray.setImage(image); tray.setImage(image);
tray.setToolTip(`${arg} unread messages`); tray.setToolTip(
t.__mf(
"{number, plural, one {# unread message} other {# unread messages}}",
{number: `${argument}`},
),
);
} }
} }
}); });

View File

@@ -1,5 +1,5 @@
import type {IpcRendererEvent} from "electron/renderer";
import { import {
type IpcRendererEvent,
ipcRenderer as untypedIpcRenderer, // eslint-disable-line no-restricted-imports ipcRenderer as untypedIpcRenderer, // eslint-disable-line no-restricted-imports
} from "electron/renderer"; } from "electron/renderer";
@@ -10,8 +10,8 @@ import type {
} from "../../common/typed-ipc.js"; } from "../../common/typed-ipc.js";
type RendererListener<Channel extends keyof RendererMessage> = type RendererListener<Channel extends keyof RendererMessage> =
RendererMessage[Channel] extends (...args: infer Args) => void RendererMessage[Channel] extends (...arguments_: infer Arguments) => void
? (event: IpcRendererEvent, ...args: Args) => void ? (event: IpcRendererEvent, ...arguments_: Arguments) => void
: never; : never;
export const ipcRenderer: { export const ipcRenderer: {
@@ -35,19 +35,25 @@ export const ipcRenderer: {
send<Channel extends keyof RendererMessage>( send<Channel extends keyof RendererMessage>(
channel: "forward-message", channel: "forward-message",
rendererChannel: Channel, rendererChannel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void;
send<Channel extends keyof RendererMessage>(
channel: "forward-to",
webContentsId: number,
rendererChannel: Channel,
...arguments_: Parameters<RendererMessage[Channel]>
): void; ): void;
send<Channel extends keyof MainMessage>( send<Channel extends keyof MainMessage>(
channel: Channel, channel: Channel,
...args: Parameters<MainMessage[Channel]> ...arguments_: Parameters<MainMessage[Channel]>
): void; ): void;
invoke<Channel extends keyof MainCall>( invoke<Channel extends keyof MainCall>(
channel: Channel, channel: Channel,
...args: Parameters<MainCall[Channel]> ...arguments_: Parameters<MainCall[Channel]>
): Promise<ReturnType<MainCall[Channel]>>; ): Promise<ReturnType<MainCall[Channel]>>;
sendSync<Channel extends keyof MainMessage>( sendSync<Channel extends keyof MainMessage>(
channel: Channel, channel: Channel,
...args: Parameters<MainMessage[Channel]> ...arguments_: Parameters<MainMessage[Channel]>
): ReturnType<MainMessage[Channel]>; ): ReturnType<MainMessage[Channel]>;
postMessage<Channel extends keyof MainMessage>( postMessage<Channel extends keyof MainMessage>(
channel: Channel, channel: Channel,
@@ -56,13 +62,8 @@ export const ipcRenderer: {
: never, : never,
transfer?: MessagePort[], transfer?: MessagePort[],
): void; ): void;
sendTo<Channel extends keyof RendererMessage>(
webContentsId: number,
channel: Channel,
...args: Parameters<RendererMessage[Channel]>
): void;
sendToHost<Channel extends keyof RendererMessage>( sendToHost<Channel extends keyof RendererMessage>(
channel: Channel, channel: Channel,
...args: Parameters<RendererMessage[Channel]> ...arguments_: Parameters<RendererMessage[Channel]>
): void; ): void;
} = untypedIpcRenderer; } = untypedIpcRenderer;

View File

@@ -2,18 +2,18 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import {app, dialog} from "@electron/remote"; import {app, dialog} from "@electron/remote";
import * as Sentry from "@sentry/electron"; import * as Sentry from "@sentry/electron/renderer";
import {JsonDB} from "node-json-db"; import {JsonDB} from "node-json-db";
import {DataError} from "node-json-db/dist/lib/Errors"; import {DataError} from "node-json-db/dist/lib/Errors.js";
import {z} from "zod"; import {z} from "zod";
import * as EnterpriseUtil from "../../../common/enterprise-util.js"; import * as EnterpriseUtil from "../../../common/enterprise-util.ts";
import Logger from "../../../common/logger-util.js"; import Logger from "../../../common/logger-util.ts";
import * as Messages from "../../../common/messages.js"; import * as Messages from "../../../common/messages.ts";
import * as t from "../../../common/translation-util.js"; import * as t from "../../../common/translation-util.ts";
import type {ServerConf} from "../../../common/types.js"; import type {ServerConfig} from "../../../common/types.ts";
import defaultIcon from "../../img/icon.png"; import defaultIcon from "../../img/icon.png";
import {ipcRenderer} from "../typed-ipc-renderer.js"; import {ipcRenderer} from "../typed-ipc-renderer.ts";
const logger = new Logger({ const logger = new Logger({
file: "domain-util.log", file: "domain-util.log",
@@ -23,53 +23,57 @@ const logger = new Logger({
// missing icon; it does not change with the actual icon location. // missing icon; it does not change with the actual icon location.
export const defaultIconSentinel = "../renderer/img/icon.png"; export const defaultIconSentinel = "../renderer/img/icon.png";
const serverConfSchema = z.object({ const serverConfigSchema = z.object({
url: z.string().url(), url: z.url(),
alias: z.string(), alias: z.string(),
icon: z.string(), icon: z.string(),
zulipVersion: z.string().default("unknown"), zulipVersion: z.string().default("unknown"),
zulipFeatureLevel: z.number().default(0), zulipFeatureLevel: z.number().default(0),
}); });
let db!: JsonDB; let database!: JsonDB;
reloadDb(); reloadDatabase();
// Migrate from old schema // Migrate from old schema
try { try {
const oldDomain = db.getObject<unknown>("/domain"); const oldDomain = database.getObject<unknown>("/domain");
if (typeof oldDomain === "string") { if (typeof oldDomain === "string") {
(async () => { (async () => {
await addDomain({ await addDomain({
alias: "Zulip", alias: "Zulip",
url: oldDomain, url: oldDomain,
}); });
db.delete("/domain"); database.delete("/domain");
})(); })();
} }
} catch (error: unknown) { } catch (error: unknown) {
if (!(error instanceof DataError)) throw error; if (!(error instanceof DataError)) throw error;
} }
export function getDomains(): ServerConf[] { export function getDomains(): ServerConfig[] {
reloadDb(); reloadDatabase();
try { try {
return serverConfSchema.array().parse(db.getObject<unknown>("/domains")); return serverConfigSchema
.array()
.parse(database.getObject<unknown>("/domains"));
} catch (error: unknown) { } catch (error: unknown) {
if (!(error instanceof DataError)) throw error; if (!(error instanceof DataError)) throw error;
return []; return [];
} }
} }
export function getDomain(index: number): ServerConf { export function getDomain(index: number): ServerConfig {
reloadDb(); reloadDatabase();
return serverConfSchema.parse(db.getObject<unknown>(`/domains[${index}]`)); return serverConfigSchema.parse(
database.getObject<unknown>(`/domains[${index}]`),
);
} }
export function updateDomain(index: number, server: ServerConf): void { export function updateDomain(index: number, server: ServerConfig): void {
reloadDb(); reloadDatabase();
serverConfSchema.parse(server); serverConfigSchema.parse(server);
db.push(`/domains[${index}]`, server, true); database.push(`/domains[${index}]`, server, true);
} }
export async function addDomain(server: { export async function addDomain(server: {
@@ -80,20 +84,20 @@ export async function addDomain(server: {
if (server.icon) { if (server.icon) {
const localIconUrl = await saveServerIcon(server.icon); const localIconUrl = await saveServerIcon(server.icon);
server.icon = localIconUrl; server.icon = localIconUrl;
serverConfSchema.parse(server); serverConfigSchema.parse(server);
db.push("/domains[]", server, true); database.push("/domains[]", server, true);
reloadDb(); reloadDatabase();
} else { } else {
server.icon = defaultIconSentinel; server.icon = defaultIconSentinel;
serverConfSchema.parse(server); serverConfigSchema.parse(server);
db.push("/domains[]", server, true); database.push("/domains[]", server, true);
reloadDb(); reloadDatabase();
} }
} }
export function removeDomains(): void { export function removeDomains(): void {
db.delete("/domains"); database.delete("/domains");
reloadDb(); reloadDatabase();
} }
export function removeDomain(index: number): boolean { export function removeDomain(index: number): boolean {
@@ -101,8 +105,8 @@ export function removeDomain(index: number): boolean {
return false; return false;
} }
db.delete(`/domains[${index}]`); database.delete(`/domains[${index}]`);
reloadDb(); reloadDatabase();
return true; return true;
} }
@@ -115,7 +119,7 @@ export function duplicateDomain(domain: string): boolean {
export async function checkDomain( export async function checkDomain(
domain: string, domain: string,
silent = false, silent = false,
): Promise<ServerConf> { ): Promise<ServerConfig> {
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.");
@@ -130,7 +134,7 @@ export async function checkDomain(
} }
} }
async function getServerSettings(domain: string): Promise<ServerConf> { async function getServerSettings(domain: string): Promise<ServerConfig> {
return ipcRenderer.invoke("get-server-settings", domain); return ipcRenderer.invoke("get-server-settings", domain);
} }
@@ -144,29 +148,29 @@ export async function saveServerIcon(iconURL: string): Promise<string> {
export async function updateSavedServer( export async function updateSavedServer(
url: string, url: string,
index: number, index: number,
): Promise<ServerConf> { ): Promise<ServerConfig> {
// Does not promise successful update // Does not promise successful update
const serverConf = getDomain(index); const serverConfig = getDomain(index);
const oldIcon = serverConf.icon; const oldIcon = serverConfig.icon;
try { try {
const newServerConf = await checkDomain(url, true); const newServerConfig = await checkDomain(url, true);
const localIconUrl = await saveServerIcon(newServerConf.icon); const localIconUrl = await saveServerIcon(newServerConfig.icon);
if (!oldIcon || localIconUrl !== defaultIconSentinel) { if (!oldIcon || localIconUrl !== defaultIconSentinel) {
newServerConf.icon = localIconUrl; newServerConfig.icon = localIconUrl;
updateDomain(index, newServerConf); updateDomain(index, newServerConfig);
reloadDb(); reloadDatabase();
} }
return newServerConf; return newServerConfig;
} catch (error: unknown) { } catch (error: unknown) {
logger.log("Could not update server icon."); logger.log("Could not update server icon.");
logger.log(error); logger.log(error);
Sentry.captureException(error); Sentry.captureException(error);
return serverConf; return serverConfig;
} }
} }
function reloadDb(): void { function reloadDatabase(): void {
const domainJsonPath = path.join( const domainJsonPath = path.join(
app.getPath("userData"), app.getPath("userData"),
"config/domain.json", "config/domain.json",
@@ -178,9 +182,10 @@ function reloadDb(): void {
if (fs.existsSync(domainJsonPath)) { if (fs.existsSync(domainJsonPath)) {
fs.unlinkSync(domainJsonPath); fs.unlinkSync(domainJsonPath);
dialog.showErrorBox( dialog.showErrorBox(
"Error saving new organization", t.__("Error saving new organization"),
"There seems to be error while saving new organization, " + t.__(
"you may have to re-add your previous organizations back.", "There was an error while saving the new organization. You may have to add your previous organizations again.",
),
); );
logger.error("Error while JSON parsing domain.json: "); logger.error("Error while JSON parsing domain.json: ");
logger.error(error); logger.error(error);
@@ -188,7 +193,7 @@ function reloadDb(): void {
} }
} }
db = new JsonDB(domainJsonPath, true, true); database = new JsonDB(domainJsonPath, true, true);
} }
export function formatUrl(domain: string): string { export function formatUrl(domain: string): string {
@@ -203,7 +208,9 @@ export function formatUrl(domain: string): string {
return `https://${domain}`; return `https://${domain}`;
} }
export function getUnsupportedMessage(server: ServerConf): string | undefined { export function getUnsupportedMessage(
server: ServerConfig,
): string | undefined {
if (server.zulipFeatureLevel < 65 /* Zulip Server 4.0 */) { if (server.zulipFeatureLevel < 65 /* Zulip Server 4.0 */) {
const realm = new URL(server.url).hostname; const realm = new URL(server.url).hostname;
return t.__( return t.__(

View File

@@ -1,9 +1,10 @@
import * as backoff from "backoff"; import * as backoff from "backoff";
import {html} from "../../../common/html.js"; import {html} from "../../../common/html.ts";
import Logger from "../../../common/logger-util.js"; import Logger from "../../../common/logger-util.ts";
import type WebView from "../components/webview.js"; import * as t from "../../../common/translation-util.ts";
import {ipcRenderer} from "../typed-ipc-renderer.js"; import type WebView from "../components/webview.ts";
import {ipcRenderer} from "../typed-ipc-renderer.ts";
const logger = new Logger({ const logger = new Logger({
file: "domain-util.log", file: "domain-util.log",
@@ -15,7 +16,7 @@ export default class ReconnectUtil {
fibonacciBackoff: backoff.Backoff; fibonacciBackoff: backoff.Backoff;
constructor(webview: WebView) { constructor(webview: WebView) {
this.url = webview.props.url; this.url = webview.properties.url;
this.alreadyReloaded = false; this.alreadyReloaded = false;
this.fibonacciBackoff = backoff.fibonacci({ this.fibonacciBackoff = backoff.fibonacci({
initialDelay: 5000, initialDelay: 5000,
@@ -55,8 +56,10 @@ export default class ReconnectUtil {
const errorMessageHolder = document.querySelector("#description"); const errorMessageHolder = document.querySelector("#description");
if (errorMessageHolder) { if (errorMessageHolder) {
errorMessageHolder.innerHTML = html` errorMessageHolder.innerHTML = html`
<div>Your internet connection doesn't seem to work properly!</div> <div>
<div>Verify that it works and then click try again.</div> ${t.__("Your internet connection doesn't seem to work properly!")}
</div>
<div>${t.__("Verify that it works and then click Reconnect.")}</div>
`.html; `.html;
} }

View File

@@ -1,4 +1,4 @@
import {ipcRenderer} from "../typed-ipc-renderer.js"; import {ipcRenderer} from "../typed-ipc-renderer.ts";
export const connectivityError: string[] = [ export const connectivityError: string[] = [
"ERR_INTERNET_DISCONNECTED", "ERR_INTERNET_DISCONNECTED",

View File

@@ -0,0 +1,6 @@
import * as z from "zod";
// In an Electron preload script, Content-Security-Policy only takes effect
// after the page has loaded, which breaks Zod's detection of whether eval is
// allowed.
z.config({jitless: true});

View File

@@ -2,55 +2,15 @@
<html lang="en" class="responsive desktop"> <html lang="en" class="responsive desktop">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; connect-src 'self'; font-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'"
/>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<title>Zulip</title> <title>Zulip</title>
<link rel="stylesheet" href="css/fonts.css" /> <link rel="stylesheet" href="css/fonts.css" />
<link rel="stylesheet" href="css/main.css" /> <link rel="stylesheet" href="css/main.css" />
</head> </head>
<body> <body></body>
<div id="content">
<div class="popup">
<span class="popuptext hidden" id="fullscreen-popup"></span>
</div>
<div id="sidebar" class="toggle-sidebar">
<div id="view-controls-container">
<div id="tabs-container"></div>
<div id="add-tab" class="tab functional-tab">
<div class="server-tab" id="add-action">
<i class="material-icons">add</i>
</div>
<span id="add-server-tooltip" style="display: none"
>Add organization</span
>
</div>
</div>
<div id="actions-container">
<div class="action-button" id="dnd-action">
<i class="material-icons md-48">notifications</i>
<span id="dnd-tooltip" style="display: none">Do Not Disturb</span>
</div>
<div class="action-button hidden" id="reload-action">
<i class="material-icons md-48">refresh</i>
<span id="reload-tooltip" style="display: none">Reload</span>
</div>
<div class="action-button disable" id="loading-action">
<i class="refresh material-icons md-48">loop</i>
<span id="loading-tooltip" style="display: none">Loading</span>
</div>
<div class="action-button disable" id="back-action">
<i class="material-icons md-48">arrow_back</i>
<span id="back-tooltip" style="display: none">Go Back</span>
</div>
<div class="action-button" id="settings-action">
<i class="material-icons md-48">settings</i>
<span id="setting-tooltip" style="display: none">Settings</span>
</div>
</div>
</div>
<div id="main-container">
<div id="webviews-container"></div>
</div>
</div>
</body>
</html> </html>

View File

@@ -2,6 +2,10 @@
<html lang="en" class="responsive desktop"> <html lang="en" class="responsive desktop">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; connect-src 'self'; img-src 'self'; script-src 'self'; style-src 'self'"
/>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<title>Zulip - Network Troubleshooting</title> <title>Zulip - Network Troubleshooting</title>
<link <link

BIN
build/icon-macos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

1
build/icon-macos.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-100 -100 1024 1024"><defs><linearGradient id="a" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#50adff"/><stop offset="1" stop-color="#7877fc"/></linearGradient><mask id="b"><rect x="-100" y="-100" width="1024" height="1024" fill="#fff"/><path d="M566.66 527.25c0 33.03-24.23 60.05-53.84 60.05H260.29c-29.61 0-53.84-27.02-53.84-60.05 0-20.22 9.09-38.2 22.93-49.09l134.37-120c2.5-2.14 5.74 1.31 3.94 4.19l-49.29 98.69c-1.38 2.76.41 6.16 3.25 6.16h191.18c29.61 0 53.83 27.03 53.83 60.05zm0-281.39c0 20.22-9.09 38.2-22.93 49.09l-134.37 120c-2.5 2.14-5.74-1.31-3.94-4.19l49.29-98.69c1.38-2.76-.41-6.16-3.25-6.16H260.29c-29.61 0-53.84-27.02-53.84-60.05s24.23-60.05 53.84-60.05h252.54c29.61 0 53.83 27.02 53.83 60.05z" transform="scale(1.0658112582781456)" fill="#000"/></mask><path id="c" d="M824 257c0-64 0-104-14-141A173 173 0 00708 14C671 0 631 0 567 0H257C193 0 153 0 116 14A173 173 0 0014 116C0 153 0 193 0 257V567c0 64 0 104 14 141A173 173 0 00116 810c37 14 77 14 141 14H567c64 0 104 0 141-14A173 173 0 00810 708c14-37 14-77 14-141Z"/><filter id="d"><feGaussianBlur in="SourceGraphic" stdDeviation="10"/></filter><filter id="e"><feGaussianBlur in="SourceGraphic" stdDeviation="5"/></filter></defs><use href="#c" transform="translate(0 10)" fill-opacity="0.3" filter="url(#d)"/><rect x="120" y="120" width="584" height="584" fill="#fff" /><g filter="url(#e)"><rect x="120" y="120" width="584" height="584" fill="#32497f" mask="url(#b)" transform="translate(0 5)"/></g><use href="#c" mask="url(#b)" fill="url(#a)"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 797 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -2,6 +2,97 @@
All notable changes to the Zulip desktop app are documented in this file. All notable changes to the Zulip desktop app are documented in this file.
### v5.12.2 --2025-09-01
**Fixes**:
- Corrected broken translations in Chinese (simplified), Finnish, German, Greek, and Tamil that crashed the app.
### v5.12.1 --2025-08-29
**Enhancements**:
- Enabled macOS Writing Tools in the context menu.
- Marked untranslated strings for translation.
- Updated translations.
**Dependencies**:
- Upgraded all dependencies, including Electron 37.4.0.
### v5.12.0 --2025-03-13
**Enhancements**:
- Updated the icon in macOS with a native appearance.
- Marked untranslated strings for translation.
- Updated translations.
**Dependencies**:
- Upgraded all dependencies, including Electron 35.0.1.
**Removed features**:
- Removed support for macOS 10.15 and earlier, which reached end-of-life in 2022 and is [no longer supported](https://www.electronjs.org/blog/electron-33-0#removed-macos-1015-support) by Electron.
### v5.11.1 --2024-08-23
**Enhancements**:
- Updated translations.
**Dependencies**:
- Upgraded all dependencies, including Electron 32.0.1.
### v5.11.0 --2024-03-22
**Fixes**:
- Removed the popup dialog for certificate errors when loading subresources such as images.
- Allowed hiding the window from full screen mode on macOS.
**Enhancements**:
- Enabled zooming with Ctrl+mouse wheel on Linux and Windows.
**Dependencies**:
- Upgraded all dependencies, including Electron 29.1.5.
### v5.10.5 --2024-01-25
**Dependencies**:
- Upgraded all dependencies, including Electron 28.2.0.
**Enhancements**:
- Improved security hardening by setting a Content-Security-Policy for the app UI.
### v5.10.4 --2024-01-09
**Dependencies**:
- Upgraded all dependencies, including Electron 28.1.1.
### v5.10.3 --2023-09-30
**Fixes**:
- Fixed an error in the third-party `gatemaker` library that broke the display of notifications for completed downloads.
**Dependencies**:
- Upgraded all dependencies, including Electron 25.8.4.
### v5.10.2 --2023-09-14
**Dependencies**:
- Downgraded Electron to 25.8.1 to avoid a renderer process crash on Linux.
### v5.10.1 --2023-09-13 ### v5.10.1 --2023-09-13
**Dependencies**: **Dependencies**:
@@ -1061,7 +1152,6 @@ Minor improvements
**Fixes**: **Fixes**:
- Fixed : - Fixed :
- Auto-updates - Auto-updates
- Spellchecker - Spellchecker
- Zooming functionality - Zooming functionality

View File

@@ -8,55 +8,13 @@ appropriate translation for a given string ("message") used in the UI.
To manage the set of UI messages and translations for them, and To manage the set of UI messages and translations for them, and
provide a nice workflow for people to contribute translations, we use provide a nice workflow for people to contribute translations, we use
(along with the rest of the Zulip project) a service called Transifex. (along with the rest of the Zulip project) a service called Weblate.
## Maintainers: syncing to/from Transifex
### Setup
You'll want Transifex's CLI client, `tx`.
- Install in your homedir with `easy_install transifex-client` or `pip3 install --user transifex-client`.
Or you can use your Zulip dev server virtualenv, which has it.
- Configure a `.transifexrc` with your API token. See [upstream
instructions](https://docs.transifex.com/client/client-configuration#transifexrc).
This can go either in your homedir, or in your working tree to make
the configuration apply only locally; it's already ignored in our
`.gitignore`.
- You'll need to be added [as a "maintainer"][tx-zulip-maintainers] to
the Zulip project on Transifex. (Upstream [recommends
this][tx-docs-maintainers] as the set of permissions on a Transifex
project needed for interacting with it as a developer.)
[tx-zulip-maintainers]: https://www.transifex.com/zulip/zulip/settings/maintainers/
[tx-docs-maintainers]: https://docs.transifex.com/teams/understanding-user-roles#project-maintainers
### Uploading strings to translate
Run `tx push -s`.
This uploads from `public/translations/en.json` to the
set of strings Transifex shows for contributors to translate.
(See `.tx/config` for how that's configured.)
### Downloading translated strings
Run `tools/tx-pull`.
This writes to files `public/translations/<lang>.json`.
(See `.tx/config` for how that's configured.)
Then look at the following sections to see if further updates are
needed to take full advantage of the new or updated translations.
### Updating the languages supported in the code ### Updating the languages supported in the code
Sometimes when downloading translated strings we get a file for a new Sometimes when downloading translated strings we get a file for a new
language. This happens when we've opened up a new language for people language. This happens when we've opened up a new language for people
to contribute translations into in the Zulip project on Transifex, to contribute translations into in the Zulip project on Weblate,
which we do when someone expresses interest in contributing them. which we do when someone expresses interest in contributing them.
The locales for supported languages are stored in `public/translations/supported-locales.json` The locales for supported languages are stored in `public/translations/supported-locales.json`

View File

@@ -41,32 +41,10 @@
1. Download [Zulip-x.x.x-amd64.deb][lr] 1. Download [Zulip-x.x.x-amd64.deb][lr]
2. Double click and install, or run `dpkg -i Zulip-x.x.x-amd64.deb` in the terminal 2. Double click and install, or run `dpkg -i Zulip-x.x.x-amd64.deb` in the terminal
3. Start the app with your app launcher or by running `zulip` in a terminal 3. Start the app with your app launcher or by running `zulip` in a terminal
4. Done! The app will NOT update automatically, but you can still check for updates 4. Done! You can update the app [using APT](https://documentation.ubuntu.com/server/how-to/software/package-management/#upgrading-packages).
**Other distros (Fedora, CentOS, Arch Linux etc)** : **Other distros (Fedora, CentOS, Arch Linux etc)** :
1. Download Zulip-x.x.x-x86_64.AppImage[LR] 1. Download Zulip-x.x.x-x86_64.AppImage[LR]
2. Make it executable using chmod a+x Zulip-x.x.x-x86_64.AppImage 2. Make it executable using chmod a+x Zulip-x.x.x-x86_64.AppImage
3. Start the app with your app launcher 3. Start the app with your app launcher
**You can also use `apt-get` (recommended)**:
- First download our signing key to make sure the deb you download is correct:
```bash
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv 69AD12704E71A4803DCA3A682424BE5AE9BD10D9
```
- Add the repo to your apt source list :
```bash
echo "deb https://download.zulip.com/desktop/apt stable main" |
sudo tee -a /etc/apt/sources.list.d/zulip.list
```
- Now install the client :
```bash
sudo apt-get update
sudo apt-get install zulip
```

18
i18next-parser.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import type {UserConfig} from "i18next-parser";
const config: UserConfig = {
createOldCatalogs: false,
defaultValue: (locale, namespace, key, value) =>
locale === "en" ? key! : value!,
indentation: "\t" as unknown as number,
input: ["app/**/*.ts"],
keySeparator: false,
lexers: {
ts: [{lexer: "JavascriptLexer", functions: ["t.__", "t.__mf"]}],
},
locales: ["en"],
namespaceSeparator: false,
output: "public/translations/$LOCALE.json",
sort: (a, b) => (a < b ? -1 : a > b ? 1 : 0),
};
export default config;

13998
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
{ {
"name": "zulip", "name": "zulip",
"productName": "Zulip", "productName": "Zulip",
"version": "5.10.1", "version": "5.12.2",
"main": "./dist-electron", "main": "./dist-electron/index.cjs",
"description": "Zulip Desktop App", "description": "Zulip Desktop App",
"license": "Apache-2.0", "license": "Apache-2.0",
"copyright": "Kandra Labs, Inc.", "copyright": "Kandra Labs, Inc.",
@@ -17,8 +17,9 @@
"bugs": { "bugs": {
"url": "https://github.com/zulip/zulip-desktop/issues" "url": "https://github.com/zulip/zulip-desktop/issues"
}, },
"type": "module",
"engines": { "engines": {
"node": ">=16.13.2" "node": ">=18"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@@ -28,9 +29,9 @@
"lint-css": "stylelint \"app/**/*.css\"", "lint-css": "stylelint \"app/**/*.css\"",
"lint-html": "htmlhint \"app/**/*.html\"", "lint-html": "htmlhint \"app/**/*.html\"",
"lint-js": "xo", "lint-js": "xo",
"prettier-non-js": "prettier --check --log-level=warn . \"!**/*.{js,ts}\"", "prettier-non-js": "prettier --check --log-level=warn . \"!**/*.{cjs,js,ts}\"",
"test": "tsc && npm run lint-html && npm run lint-css && npm run lint-js && npm run prettier-non-js", "test": "tsc && npm run lint-html && npm run lint-css && npm run lint-js && npm run prettier-non-js",
"test-e2e": "vite build && tape \"tests/**/*.js\"", "test-e2e": "vite build && tape \"tests/**/*.ts\"",
"pack": "vite build && electron-builder --dir", "pack": "vite build && electron-builder --dir",
"dist": "vite build && electron-builder", "dist": "vite build && electron-builder",
"mas": "vite build && electron-builder --mac mas" "mas": "vite build && electron-builder --mac mas"
@@ -50,6 +51,7 @@
"copyright": "©2020 Kandra Labs, Inc.", "copyright": "©2020 Kandra Labs, Inc.",
"mac": { "mac": {
"category": "public.app-category.productivity", "category": "public.app-category.productivity",
"icon": "build/icon-macos.png",
"target": [ "target": [
{ {
"target": "default", "target": "default",
@@ -67,14 +69,11 @@
} }
], ],
"darkModeSupport": true, "darkModeSupport": true,
"artifactName": "${productName}-${version}-${arch}.${ext}", "artifactName": "${productName}-${version}-${arch}.${ext}"
"notarize": {
"teamId": "66KHCWMEYB"
}
}, },
"linux": { "linux": {
"category": "Chat;GNOME;GTK;Network;InstantMessaging", "category": "Chat;GNOME;GTK;Network;InstantMessaging",
"icon": "build/icon.icns", "icon": "build/zulip.png",
"description": "Zulip Desktop Client for Linux", "description": "Zulip Desktop Client for Linux",
"target": [ "target": [
"deb", "deb",
@@ -90,8 +89,8 @@
"synopsis": "Zulip Desktop App", "synopsis": "Zulip Desktop App",
"afterInstall": "./packaging/deb-after-install.sh", "afterInstall": "./packaging/deb-after-install.sh",
"fpm": [ "fpm": [
"./packaging/deb-apt.list=/etc/apt/sources.list.d/zulip-desktop.list", "./packaging/deb-apt.sources=/etc/apt/sources.list.d/zulip-desktop.sources",
"./packaging/deb-apt.asc=/etc/apt/trusted.gpg.d/zulip-desktop.asc", "./packaging/deb-apt.asc=/usr/share/keyrings/zulip-desktop.asc",
"./packaging/deb-release-upgrades.cfg=/etc/update-manager/release-upgrades.d/zulip-desktop.cfg" "./packaging/deb-release-upgrades.cfg=/etc/update-manager/release-upgrades.d/zulip-desktop.cfg"
] ]
}, },
@@ -120,7 +119,14 @@
} }
], ],
"icon": "build/icon.ico", "icon": "build/icon.ico",
"publisherName": "Kandra Labs, Inc." "azureSignOptions": {
"endpoint": "https://eus.codesigning.azure.net/",
"codeSigningAccountName": "kandralabs",
"certificateProfileName": "kandralabs",
"publisherName": "Kandra Labs, Inc.",
"timestampRfc3161": "http://timestamp.acs.microsoft.com",
"timestampDigest": "SHA256"
}
}, },
"msi": { "msi": {
"artifactName": "${productName}-${version}-${arch}.${ext}" "artifactName": "${productName}-${version}-${arch}.${ext}"
@@ -143,178 +149,57 @@
"InstantMessaging" "InstantMessaging"
], ],
"dependencies": { "dependencies": {
"gatemaker": "^1.0.0" "gatemaker": "https://github.com/andersk/gatemaker/archive/d31890ae1cb293faabcb1e4e465c673458f6eed2.tar.gz"
}, },
"devDependencies": { "devDependencies": {
"@electron/remote": "^2.0.8", "@electron/remote": "^2.0.8",
"@sentry/electron": "^4.1.2", "@sentry/core": "^10.1.0",
"@sentry/electron": "^6.1.0",
"@types/adm-zip": "^0.5.0", "@types/adm-zip": "^0.5.0",
"@types/auto-launch": "^5.0.2", "@types/auto-launch": "^5.0.2",
"@types/backoff": "^2.5.2", "@types/backoff": "^2.5.2",
"@types/i18n": "^0.13.1", "@types/i18n": "^0.13.1",
"@types/node": "^18.16.5", "@types/node": "^22.13.10",
"@types/p-fifo": "^1.0.2",
"@types/requestidlecallback": "^0.3.4", "@types/requestidlecallback": "^0.3.4",
"@types/semver": "^7.5.8",
"@types/tape": "^5.8.1",
"@types/yaireo__tagify": "^4.3.2", "@types/yaireo__tagify": "^4.3.2",
"@yaireo/tagify": "^4.5.0", "@yaireo/tagify": "^4.5.0",
"adm-zip": "^0.5.5", "adm-zip": "^0.5.5",
"auto-launch": "^5.0.5", "auto-launch": "^5.0.5",
"backoff": "^2.5.0", "backoff": "^2.5.0",
"electron": "^26.2.1", "electron": "^37.2.5",
"electron-builder": "^24.6.4", "electron-builder": "^26.0.12",
"electron-log": "^4.3.5", "electron-log": "^5.0.3",
"electron-updater": "^6.1.4", "electron-updater": "^6.3.4",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"escape-goat": "^4.0.0", "escape-goat": "^4.0.0",
"eslint-import-resolver-typescript": "^4.4.4",
"htmlhint": "^1.1.2", "htmlhint": "^1.1.2",
"i18n": "^0.15.1", "i18n": "^0.15.1",
"iso-639-1": "^3.1.0", "i18next-parser": "^9.3.0",
"medium": "^1.2.0",
"node-json-db": "^1.3.0", "node-json-db": "^1.3.0",
"playwright-core": "^1.30.0-alpha-jan-3-2023", "p-fifo": "^1.0.0",
"playwright-core": "^1.41.0-alpha-jan-9-2024",
"pre-commit": "^1.2.2", "pre-commit": "^1.2.2",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"rimraf": "^5.0.0",
"semver": "^7.3.5", "semver": "^7.3.5",
"stylelint": "^15.6.1", "stylelint": "^16.1.0",
"stylelint-config-standard": "^34.0.0", "stylelint-config-standard": "^39.0.0",
"tape": "^5.2.2", "tape": "^5.2.2",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vite": "^4.1.1", "vite": "^5.0.11",
"vite-plugin-electron": "^0.14.1", "vite-plugin-electron": "^0.28.0",
"xo": "^0.56.0", "xo": "^1.2.1",
"zod": "^3.5.1" "zod": "^4.1.5"
},
"overrides": {
"@types/pg": "^8.15.1"
}, },
"prettier": { "prettier": {
"bracketSpacing": false, "bracketSpacing": false,
"singleQuote": false, "singleQuote": false,
"trailingComma": "all" "trailingComma": "all"
},
"xo": {
"prettier": true,
"rules": {
"@typescript-eslint/no-dynamic-delete": "off",
"arrow-body-style": "error",
"import/no-restricted-paths": [
"error",
{
"zones": [
{
"target": "./app/common",
"from": "./app",
"except": [
"./common"
]
},
{
"target": "./app/main",
"from": "./app",
"except": [
"./common",
"./main"
]
},
{
"target": "./app/renderer",
"from": "./app",
"except": [
"./common",
"./renderer",
"./resources"
]
}
]
}
],
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always"
}
],
"import/unambiguous": "error",
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "electron",
"message": "Use electron/main, electron/renderer, or electron/common."
},
{
"name": "electron/main",
"importNames": [
"ipcMain"
],
"message": "Use typed-ipc-main."
},
{
"name": "electron/renderer",
"importNames": [
"ipcRenderer"
],
"message": "Use typed-ipc-renderer."
}
]
}
],
"no-warning-comments": "off",
"sort-imports": [
"error",
{
"ignoreDeclarationSort": true
}
],
"strict": "error",
"unicorn/prefer-module": "off",
"unicorn/prefer-top-level-await": "off"
},
"envs": [
"node",
"browser"
],
"overrides": [
{
"files": [
"**/*.ts"
],
"rules": {
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/consistent-type-imports": [
"error",
{
"disallowTypeAnnotations": false
}
],
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"caughtErrors": "all"
}
],
"unicorn/no-await-expression-member": "off"
}
},
{
"files": [
"scripts/notarize.js",
"tests/**/*.js"
],
"parserOptions": {
"sourceType": "script"
}
},
{
"files": [
"**/*.d.ts"
],
"rules": {
"import/unambiguous": "off"
}
}
]
} }
} }

View File

@@ -11,3 +11,6 @@ update-desktop-database /usr/share/applications || true
# Clean up configuration for old Bintray repository # Clean up configuration for old Bintray repository
rm -f /etc/apt/zulip.list rm -f /etc/apt/zulip.list
# Clean up legacy APT configuration
rm -f /etc/apt/sources.list.d/zulip-desktop.list /etc/apt/trusted.gpg.d/zulip-desktop.asc

View File

@@ -7,24 +7,28 @@ LoJVvA7uJHcsNaQVWQF4RP0MaI4TLyjHZAJlpthQfbmq0AbZMEjDu8Th5G9KTsqE
WRyFoAj/SWwKQK2U4xpnA6jEraMcvsYYQMrCXlG+MOV7zVknLrH5tfk7JlmWB4DV WRyFoAj/SWwKQK2U4xpnA6jEraMcvsYYQMrCXlG+MOV7zVknLrH5tfk7JlmWB4DV
cs+QP5Z/UrVu+YpTpaoJoZV6LlEU1kNGjtq9ABEBAAG0TVp1bGlwIEFQVCBSZXBv cs+QP5Z/UrVu+YpTpaoJoZV6LlEU1kNGjtq9ABEBAAG0TVp1bGlwIEFQVCBSZXBv
c2l0b3J5IFNpZ25pbmcgS2V5IEJpbnRyYXkgKFByb2R1Y3Rpb24pIDxzdXBwb3J0 c2l0b3J5IFNpZ25pbmcgS2V5IEJpbnRyYXkgKFByb2R1Y3Rpb24pIDxzdXBwb3J0
QHp1bGlwY2hhdC5jb20+iQE4BBMBAgAiBQJZnc70AhsDBgsJCAcDAgYVCAIJCgsE QHp1bGlwY2hhdC5jb20+iQGSBBMBCACGBYJonATwBAsJCAcJECQkvlrpvRDZRxQA
FgIDAQIeAQIXgAAKCRAkJL5a6b0Q2Vg1CADJzrH0mbwKi5GiHo5+iX5/WuUkSA8S AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ5to4d/e2Ts692fK
lI7FWzkbnPD0sfxJBwBNhZnAALQUvCybHxoU8VZ5ZbU1vbU+EG7pUMzENZLgEhoC y5pjZ8XOKTBvXckVdjBe8cSiLIkvAxUICgQWAgMBAheAAhsDAh4BFiEEaa0ScE5x
MDl1j8uCSahjjO+bk8qHhgM1FUKpoGec2wKfPKpcz1P+/bLTRKe7aqilkPSYOjeV pIA9yjpoJCS+Wum9ENkAAP7XCACjGUAzUgOAbf1BJTbbR1Np4BNy31++93TNj+/3
u8JI713zRL0nHd9vYZDoN2HR30J5sqgjRHtK5okNhiFG+pF3HFATG7nbNOa/tv+q gYPbNwSJBb99yZfI6J4KwT1WepIXRx2Ikx0ChxEU5oOjEcPoM8Xslg3/vTV76dcJ
ZvhbI/5S8P5VKPSK/1lmMh0UFyNIbPg6MvWiqnfy7DAvOZGJpawkiN2B0XhNZKZR CYtQdvIvLUBKN7MkDp6+H1LVu9AnzMYoAF8HiKk6NZNI2LjMMv1znYwod2Pp3EL7
KKXvFk3qvFpNTCUrH77MlPgjn+oRbE9SYm0phj0o2jQi/s1s2r75tk/ZuQENBFmd q/TPwiaOuNVDlaRSCsmbWYNPWLXAna7PU/yZ7FYwaCAKeC079+5rY59RvA/3oOmG
zvQBCACv7VNQ6x3hfaRl8YF8bbrWXN2ZWxEa353p4QryHODsa7wHtsoNR3P30TIL nUAcADyuMaNkPPnkYW5adNfCHWEPUrIUJxyJ+yVf/E/mHoUKkqYhOs60WFPXpgpX
yafjjcV8P6dzyDw6TpfRqqQDKLY6FtznT2HdceQSffGTXB4CRV7KURBqh81PX/Jo cnYYw8E/1kXM+kAfWIOi7dGlCFiWLyQF0wjwn/sehBXZy8yquQENBFmdzvQBCACv
dz0NwkNrd0NWqkk6BnLX6U5tGuYiqC3vLpjOHmVQezJ41xpf85ElJ2nBW0rEcmfk 7VNQ6x3hfaRl8YF8bbrWXN2ZWxEa353p4QryHODsa7wHtsoNR3P30TILyafjjcV8
fwQthJU7BbqWKd6nbt2G+xWkCVoN6q+CWLXtK0laHMKBGQnoiQpldotsKM8UnDeQ P6dzyDw6TpfRqqQDKLY6FtznT2HdceQSffGTXB4CRV7KURBqh81PX/Jodz0NwkNr
XPqrEi28ksjVW8tBStCkLwV2hCxk49zdTvRjrhBTQ1Ff/kenuEwqbSERiKfA7I8o d0NWqkk6BnLX6U5tGuYiqC3vLpjOHmVQezJ41xpf85ElJ2nBW0rEcmfkfwQthJU7
mlqulSiJ6rYdDnGjNcoRgnHb50hTABEBAAGJAR8EGAECAAkFAlmdzvQCGwwACgkQ BbqWKd6nbt2G+xWkCVoN6q+CWLXtK0laHMKBGQnoiQpldotsKM8UnDeQXPqrEi28
JCS+Wum9ENnsOQgApQ2+4azOXprYQXj1ImamD30pmvvKD06Z7oDzappFpEXzRSJK ksjVW8tBStCkLwV2hCxk49zdTvRjrhBTQ1Ff/kenuEwqbSERiKfA7I8omlqulSiJ
tMfNaowG7YrXujydrpqaOgv4kFzaAJizWGbmOKXTwQJnavGC1JC4Lijx0s3CLtms 6rYdDnGjNcoRgnHb50hTABEBAAGJAX4EGAEIAHIFgmicBPAJECQkvlrpvRDZRxQA
OY3EC2GMNTp2rACuxZQ+26lBuPG8Nd+rNnP8DSzdQROQD2EITplqR1Rc0FLHGspu AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ2yYQ1NoS1Il7WjP
rL0JsVTuWS3qSpR3nlmwuLjVgIs5KEaOVEa4pkH9QwyAFDsprF0uZP8xAAs8WrVr HCfqbeXJc9dm9yLgL46FmSMjScRXAhsMFiEEaa0ScE5xpIA9yjpoJCS+Wum9ENkA
Isg3zs7YUcAtu/i6C2jPuMsHjGfKStkYW/4+wONIynhoFjqeYrR0CiZ9lvVa3tJk AMOECACo0hRteH+CWZDLKaufkxQvfqd0/zq+uGJ2VYOrIUkuuaA0YBe+uGaoFwgT
BCeqaQFskx1HhgWBT9Qqc73+i45udWUsa3issg== hxVs0UiOpMOzSyl+zC+7ShQu9t/jIm5sTmvHsgzmO11w4b1Td7Ow8dgAnAXKcbmA
=YJGK O1yaMi1C40YUI1zHRt0xkrnTJB57q+8Hclum59UXiSIgU5bKVeJhsX4LVpxi67Qg
vIHgg6pL+kDzObjRuBw+8Qx/Cugf4W35IGLD6BGzLjZM98YhbaX52sFvuHj+8gAs
xFOefLGRjZNdcp3IViTcVeR41Y9mA1Pjtlvthqrq70yra+EWjR7hUFxE9/BWjb18
fQZRjlB5JKC69SdOMa5C2UTSWNbA
=5JdK
-----END PGP PUBLIC KEY BLOCK----- -----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1 +0,0 @@
deb https://download.zulip.com/desktop/apt stable main

View File

@@ -0,0 +1,5 @@
Types: deb
URIs: https://download.zulip.com/desktop/apt/
Suites: stable
Components: main
Signed-By: /usr/share/keyrings/zulip-desktop.asc

Binary file not shown.

View File

@@ -4,17 +4,17 @@ These are _generated_ files (\*) that contain translations of the strings in
the app. the app.
You can help translate Zulip Desktop into your language! We do our You can help translate Zulip Desktop into your language! We do our
translations in Transifex, which is a nice web app for collaborating on translations in Weblate, which is a nice web app for collaborating on
translations; a maintainer then syncs those translations into this repo. translations; a maintainer then syncs those translations into this repo.
To help out, [join the Zulip project on To help out, [join the Zulip project on
Transifex](https://www.transifex.com/zulip/zulip/) and enter translations Weblate](https://hosted.weblate.org/projects/zulip/) and enter translations
there. More details in the [Zulip contributor docs](https://zulip.readthedocs.io/en/latest/translating/translating.html#translators-workflow). there. More details in the [Zulip contributor docs](https://zulip.readthedocs.io/en/latest/translating/translating.html#translators-workflow).
Within that Transifex project, if you'd like to focus on Zulip Desktop, look Within that Weblate project, if you'd like to focus on Zulip Desktop, look
at `desktop.json`. The other resources there are for the Zulip web/mobile at the **Desktop** component. The other components are for the Zulip web/mobile
app, where translations are also very welcome. app, where translations are also very welcome.
(\*) One file is an exception: `en.json` is manually maintained as a (\*) One file is an exception: `en.json` is maintained by `i18next-parser` as a
list of (English) messages in the source code, and is used when we upload to list of (English) messages in the source code, and is used by Weblate as
Transifex a list of strings to be translated. It doesn't contain any a list of strings to be translated. It doesn't contain any
translations. translations.

View File

@@ -1,12 +1,10 @@
{ {
"About Zulip": "حول \"زوليب\"",
"Actual Size": "الحجم الفعلي", "Actual Size": "الحجم الفعلي",
"Add Organization": "إضافة منظمة", "Add Organization": "إضافة منظمة",
"Add a Zulip organization": "إضافة منظمة \"زوليب\"",
"Add custom CSS": "إضافة CSS معدلة", "Add custom CSS": "إضافة CSS معدلة",
"AddServer": "AddServer", "Add to Dictionary": "Add to Dictionary",
"Advanced": "متقدم", "Advanced": "متقدم",
"All the connected organizations will appear here.": "All the connected organizations will appear here.", "All the connected organizations will appear here.": "جميع المنظمات المتصلة ستظهر هنا",
"Always start minimized": "دائماً إبدأ بالقليل", "Always start minimized": "دائماً إبدأ بالقليل",
"App Updates": "تحديثات التطبيق", "App Updates": "تحديثات التطبيق",
"App language (requires restart)": "App language (requires restart)", "App language (requires restart)": "App language (requires restart)",
@@ -18,6 +16,7 @@
"Auto hide menu bar (Press Alt key to display)": "أخف القائمة تلقائياً (إضغط Alt لعرض القائمة)", "Auto hide menu bar (Press Alt key to display)": "أخف القائمة تلقائياً (إضغط Alt لعرض القائمة)",
"Back": "رجوع", "Back": "رجوع",
"Bounce dock on new private message": "أخرج المنصة في حال رسالة خاصة جديدة", "Bounce dock on new private message": "أخرج المنصة في حال رسالة خاصة جديدة",
"Cancel": "إلغاء",
"Change": "تغيير", "Change": "تغيير",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", "Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Check for Updates": "التحقق من التحديثات", "Check for Updates": "التحقق من التحديثات",
@@ -26,6 +25,9 @@
"Connect to another organization": "التوصيل مع منظمة أخرى", "Connect to another organization": "التوصيل مع منظمة أخرى",
"Connected organizations": "المنظمات المتصلة", "Connected organizations": "المنظمات المتصلة",
"Copy": "نسخ", "Copy": "نسخ",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"Copy Link": "Copy Link",
"Copy Zulip URL": "نسخ رابط زوليب", "Copy Zulip URL": "نسخ رابط زوليب",
"Create a new organization": "إنشاء منظمة جديدة", "Create a new organization": "إنشاء منظمة جديدة",
"Cut": "قص", "Cut": "قص",
@@ -37,11 +39,11 @@
"Download App Logs": "تنزيل سجلات التطبيق", "Download App Logs": "تنزيل سجلات التطبيق",
"Edit": "تعديل", "Edit": "تعديل",
"Edit Shortcuts": "تعديل الاختصارات", "Edit Shortcuts": "تعديل الاختصارات",
"Emoji & Symbols": "Emoji & Symbols", "Emoji & Symbols": "الإيموجي و الرموز",
"Enable auto updates": "تفعيل التحديثات التلقائية", "Enable auto updates": "تفعيل التحديثات التلقائية",
"Enable error reporting (requires restart)": "تفعيل تقارير الأخطاء (يتطلب إعادة التشغيل)", "Enable error reporting (requires restart)": "تفعيل تقارير الأخطاء (يتطلب إعادة التشغيل)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)", "Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Enter Full Screen": "Enter Full Screen", "Enter Full Screen": "اعرض الشاشة كاملة",
"Factory Reset": "إعادة ضبط المصنع", "Factory Reset": "إعادة ضبط المصنع",
"Factory Reset Data": "Factory Reset Data", "Factory Reset Data": "Factory Reset Data",
"File": "ملف", "File": "ملف",
@@ -57,18 +59,20 @@
"Help Center": "Help Center", "Help Center": "Help Center",
"Hide": "Hide", "Hide": "Hide",
"Hide Others": "Hide Others", "Hide Others": "Hide Others",
"Hide Zulip": "Hide Zulip", "Hide Zulip": "أخفي زوليب",
"History": "History", "History": "History",
"History Shortcuts": "History Shortcuts", "History Shortcuts": "History Shortcuts",
"Keyboard Shortcuts": "Keyboard Shortcuts", "Keyboard Shortcuts": "Keyboard Shortcuts",
"Log Out": "Log Out", "Log Out": "Log Out",
"Log Out of Organization": "Log Out of Organization", "Log Out of Organization": "Log Out of Organization",
"Look Up": "Look Up",
"Manual proxy configuration": "Manual proxy configuration", "Manual proxy configuration": "Manual proxy configuration",
"Minimize": "Minimize", "Minimize": "Minimize",
"Mute all sounds from Zulip": "Mute all sounds from Zulip", "Mute all sounds from Zulip": "Mute all sounds from Zulip",
"NO": "NO",
"Network": "Network", "Network": "Network",
"Network and Proxy Settings": "Network and Proxy Settings", "Network and Proxy Settings": "الشبكة و إعدادات البروكسي",
"No Suggestion Found": "No Suggestion Found",
"OK": "حسنًا",
"OR": "OR", "OR": "OR",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", "On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Organization URL": "Organization URL", "Organization URL": "Organization URL",
@@ -85,15 +89,14 @@
"Release Notes": "Release Notes", "Release Notes": "Release Notes",
"Reload": "Reload", "Reload": "Reload",
"Report an Issue": "Report an Issue", "Report an Issue": "Report an Issue",
"Reset App Settings": "Reset App Settings", "Reset App Settings": "أعد ضبط إعدادات التطبيق",
"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.", "Reset the application, thus deleting all the connected organizations and accounts.": "إعادة ضبط التطبيق, و بالتالي مسح جميع المنظمات المتصلة و الحسابات",
"Save": "Save", "Save": "Save",
"Select All": "Select All", "Select All": "Select All",
"Services": "Services", "Services": "Services",
"Settings": "Settings", "Settings": "Settings",
"Shortcuts": "Shortcuts", "Shortcuts": "Shortcuts",
"Show app icon in system tray": "Show app icon in system tray", "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 desktop notifications": "Show desktop notifications",
"Show sidebar": "Show sidebar", "Show sidebar": "Show sidebar",
"Spellchecker Languages": "Spellchecker Languages", "Spellchecker Languages": "Spellchecker Languages",
@@ -117,11 +120,9 @@
"View Shortcuts": "View Shortcuts", "View Shortcuts": "View Shortcuts",
"Window": "Window", "Window": "Window",
"Window Shortcuts": "Window Shortcuts", "Window Shortcuts": "Window Shortcuts",
"YES": "YES",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", "You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Zoom In": "Zoom In", "Zoom In": "Zoom In",
"Zoom Out": "Zoom Out", "Zoom Out": "Zoom Out",
"keyboard shortcuts": "keyboard shortcuts", "keyboard shortcuts": "keyboard shortcuts",
"script": "script", "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} يقوم بتشغيل نسخة قديمة من خادم زوليب {{{version}}}. قد لا يعمل بشكل كامل مع هذا التطبيق"
"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app."
} }

View File

@@ -4,7 +4,7 @@
"Add Organization": "Дадаць арганізацыю", "Add Organization": "Дадаць арганізацыю",
"Add a Zulip organization": "Дадаць арганізацыю Zulip", "Add a Zulip organization": "Дадаць арганізацыю Zulip",
"Add custom CSS": "Дадаць свой CSS", "Add custom CSS": "Дадаць свой CSS",
"AddServer": "Дадаць сэрвер", "Add to Dictionary": "Дадаць у слоўнік",
"Advanced": "Пашыраныя", "Advanced": "Пашыраныя",
"All the connected organizations will appear here.": "Тут з'явяцца ўсе звязаныя арганізацыі.", "All the connected organizations will appear here.": "Тут з'явяцца ўсе звязаныя арганізацыі.",
"Always start minimized": "Заўсёды адкрываць згорнутым", "Always start minimized": "Заўсёды адкрываць згорнутым",
@@ -18,6 +18,7 @@
"Auto hide menu bar (Press Alt key to display)": "Аўтаматычна хаваць радок меню (для выявы націсніце клавішу Alt)", "Auto hide menu bar (Press Alt key to display)": "Аўтаматычна хаваць радок меню (для выявы націсніце клавішу Alt)",
"Back": "Назад", "Back": "Назад",
"Bounce dock on new private message": "Подпрыгваючы dock пры новым асабістым паведамленні", "Bounce dock on new private message": "Подпрыгваючы dock пры новым асабістым паведамленні",
"Cancel": "Скасаваць",
"Change": "Змяніць", "Change": "Змяніць",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Змяніце мову ў: Сістэмныя налады → Клавіятура → Тэкст → Правапіс.", "Change the language from System Preferences → Keyboard → Text → Spelling.": "Змяніце мову ў: Сістэмныя налады → Клавіятура → Тэкст → Правапіс.",
"Check for Updates": "Праверыць наяўнасць абнаўленняў", "Check for Updates": "Праверыць наяўнасць абнаўленняў",
@@ -26,6 +27,9 @@
"Connect to another organization": "Падлучыць да іншай арганізацыі", "Connect to another organization": "Падлучыць да іншай арганізацыі",
"Connected organizations": "Падлучаныя арганізацыі", "Connected organizations": "Падлучаныя арганізацыі",
"Copy": "Капіяваць", "Copy": "Капіяваць",
"Copy Image": "Капіяваць відарыс",
"Copy Image URL": "Капіяваць URL відарысу",
"Copy Link": "Капіяваць спасылку",
"Copy Zulip URL": "Капіяваць Zulip URL", "Copy Zulip URL": "Капіяваць Zulip URL",
"Create a new organization": "Стварыць новую арганізацыю", "Create a new organization": "Стварыць новую арганізацыю",
"Cut": "Выразаць", "Cut": "Выразаць",
@@ -47,7 +51,7 @@
"File": "Файл", "File": "Файл",
"Find accounts": "Знайсці ўліковыя запісы", "Find accounts": "Знайсці ўліковыя запісы",
"Find accounts by email": "Знайсці ўліковыя запісы паводле email", "Find accounts by email": "Знайсці ўліковыя запісы паводле email",
"Flash taskbar on new message": "Успыхваць на панэлі заданняў пры новым асабістым паведамленні ", "Flash taskbar on new message": "Успыхваць на панэлі заданняў пры новым асабістым паведамленні",
"Forward": "Пераадрасаваць", "Forward": "Пераадрасаваць",
"Functionality": "Функцыянальнасць", "Functionality": "Функцыянальнасць",
"General": "Агульныя", "General": "Агульныя",
@@ -63,12 +67,13 @@
"Keyboard Shortcuts": "Спалучэнні клавішаў", "Keyboard Shortcuts": "Спалучэнні клавішаў",
"Log Out": "Выйсці з уліковага запісу", "Log Out": "Выйсці з уліковага запісу",
"Log Out of Organization": "Выйсці з уліковага запісу арганізацыі", "Log Out of Organization": "Выйсці з уліковага запісу арганізацыі",
"Look Up": "Шукаць",
"Manual proxy configuration": "Ручная налада проксі", "Manual proxy configuration": "Ручная налада проксі",
"Minimize": "Згарнуць", "Minimize": "Згарнуць",
"Mute all sounds from Zulip": "Адключыць усе гукі з Zulip", "Mute all sounds from Zulip": "Адключыць усе гукі з Zulip",
"NO": "NO",
"Network": "Сетка", "Network": "Сетка",
"Network and Proxy Settings": "Налады сеткі і проксі", "Network and Proxy Settings": "Налады сеткі і проксі",
"No Suggestion Found": "Прапановы не знойдзеныя",
"OR": "OR", "OR": "OR",
"On macOS, the OS spellchecker is used.": "У macOS выкарыстоўваецца сістэмная праверка правапісу.", "On macOS, the OS spellchecker is used.": "У macOS выкарыстоўваецца сістэмная праверка правапісу.",
"Organization URL": "URL арганізацыі", "Organization URL": "URL арганізацыі",
@@ -93,7 +98,6 @@
"Settings": "Налады", "Settings": "Налады",
"Shortcuts": "Спалучэнні клавішаў", "Shortcuts": "Спалучэнні клавішаў",
"Show app icon in system tray": "Паказаць значок праграмы ў вобласці паведамленняў", "Show app icon in system tray": "Паказаць значок праграмы ў вобласці паведамленняў",
"Show app unread badge": "Паказваць значок непрачытаных паведамленняў",
"Show desktop notifications": "Паказваць апавяшчэнні на працоўным стале", "Show desktop notifications": "Паказваць апавяшчэнні на працоўным стале",
"Show sidebar": "Паказваць бакавую панэль", "Show sidebar": "Паказваць бакавую панэль",
"Spellchecker Languages": "Мовы для праверкі правапісу", "Spellchecker Languages": "Мовы для праверкі правапісу",
@@ -104,8 +108,6 @@
"Tip": "Парада", "Tip": "Парада",
"Toggle DevTools for Active Tab": "Увамкнуць DevTools для актыўнай укладкі", "Toggle DevTools for Active Tab": "Увамкнуць DevTools для актыўнай укладкі",
"Toggle DevTools for Zulip App": "Перамкнуць DevTools для праграмы Zulip", "Toggle DevTools for Zulip App": "Перамкнуць DevTools для праграмы Zulip",
"Toggle Do Not Disturb": "Перамкнуць рэжым \"Не турбаваць\"",
"Toggle Full Screen": "Перамкнуць \"На ўвесь экран\"",
"Toggle Sidebar": "Перамкнуць бакавую панэль", "Toggle Sidebar": "Перамкнуць бакавую панэль",
"Toggle Tray Icon": "Перамкнуць значок у вобласці паведамленняў", "Toggle Tray Icon": "Перамкнуць значок у вобласці паведамленняў",
"Tools": "Інструменты", "Tools": "Інструменты",
@@ -117,11 +119,9 @@
"View Shortcuts": "Спалучэнні клавішаў прагляду", "View Shortcuts": "Спалучэнні клавішаў прагляду",
"Window": "Акно", "Window": "Акно",
"Window Shortcuts": "Спалучэнні клавішаў акна", "Window Shortcuts": "Спалучэнні клавішаў акна",
"YES": "ТАК",
"You can select a maximum of 3 languages for spellchecking.": "Вы можаце выбраць максімум 3 мовы для праверкі правапісу.", "You can select a maximum of 3 languages for spellchecking.": "Вы можаце выбраць максімум 3 мовы для праверкі правапісу.",
"Zoom In": "Павялічыць", "Zoom In": "Павялічыць",
"Zoom Out": "Паменшыць", "Zoom Out": "Паменшыць",
"keyboard shortcuts": "спалучэнні клавішаў", "keyboard shortcuts": "спалучэнні клавішаў",
"script": "скрыпт",
"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "На {{{server}}} працуе састарэлая версія сервера Zulip {{{version}}}. У гэтай праграме ён можа працаваць часткова." "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "На {{{server}}} працуе састарэлая версія сервера Zulip {{{version}}}. У гэтай праграме ён можа працаваць часткова."
} }

View File

@@ -4,20 +4,22 @@
"Add Organization": "Добавяне на организация", "Add Organization": "Добавяне на организация",
"Add a Zulip organization": "Добавете организация Zulip", "Add a Zulip organization": "Добавете организация Zulip",
"Add custom CSS": "Добавете персонализиран CSS", "Add custom CSS": "Добавете персонализиран CSS",
"AddServer": "AddServer", "Add to Dictionary": "Add to Dictionary",
"Advanced": "напреднал", "Advanced": "напреднал",
"All the connected organizations will appear here.": "All the connected organizations will appear here.", "All the connected organizations will appear here.": "Всички свързани организации ще се появят тук.",
"Always start minimized": "Винаги започвайте да минимизирате", "Always start minimized": "Винаги започвайте да минимизирате",
"App Updates": "Актуализации на приложения", "App Updates": "Актуализации на приложения",
"App language (requires restart)": "App language (requires restart)", "App language (requires restart)": "App language (requires restart)",
"Appearance": "Външен вид", "Appearance": "Външен вид",
"Application Shortcuts": "Клавишни комбинации за приложения", "Application Shortcuts": "Клавишни комбинации за приложения",
"Are you sure you want to disconnect this organization?": "Наистина ли искате да прекъснете връзката с тази организация?", "Are you sure you want to disconnect this organization?": "Наистина ли искате да прекъснете връзката с тази организация?",
"Are you sure?": "Сигурни ли сте?",
"Ask where to save files before downloading": "Ask where to save files before downloading", "Ask where to save files before downloading": "Ask where to save files before downloading",
"Auto hide Menu bar": "Автоматично скриване на лентата с менюта", "Auto hide Menu bar": "Автоматично скриване на лентата с менюта",
"Auto hide menu bar (Press Alt key to display)": "Автоматично скриване на лентата с менюта (натиснете клавиша Alt за показване)", "Auto hide menu bar (Press Alt key to display)": "Автоматично скриване на лентата с менюта (натиснете клавиша Alt за показване)",
"Back": "обратно", "Back": "обратно",
"Bounce dock on new private message": "Прескочи док в новото лично съобщение", "Bounce dock on new private message": "Прескочи док в новото лично съобщение",
"Cancel": "Откажи",
"Change": "промяна", "Change": "промяна",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", "Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Check for Updates": "Провери за обновления", "Check for Updates": "Провери за обновления",
@@ -26,6 +28,9 @@
"Connect to another organization": "Свържете се с друга организация", "Connect to another organization": "Свържете се с друга организация",
"Connected organizations": "Свързани организации", "Connected organizations": "Свързани организации",
"Copy": "копие", "Copy": "копие",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"Copy Link": "Copy Link",
"Copy Zulip URL": "Копирайте URL адреса на Zulip", "Copy Zulip URL": "Копирайте URL адреса на Zulip",
"Create a new organization": "Създайте нова организация", "Create a new organization": "Създайте нова организация",
"Cut": "Разрез", "Cut": "Разрез",
@@ -37,11 +42,9 @@
"Download App Logs": "Изтеглете регистрационни файлове на приложенията", "Download App Logs": "Изтеглете регистрационни файлове на приложенията",
"Edit": "редактиране", "Edit": "редактиране",
"Edit Shortcuts": "Редактиране на преки пътища", "Edit Shortcuts": "Редактиране на преки пътища",
"Emoji & Symbols": "Emoji & Symbols",
"Enable auto updates": "Активиране на автоматичните актуализации", "Enable auto updates": "Активиране на автоматичните актуализации",
"Enable error reporting (requires restart)": "Активиране на отчитането за грешки (изисква се рестартиране)", "Enable error reporting (requires restart)": "Активиране на отчитането за грешки (изисква се рестартиране)",
"Enable spellchecker (requires restart)": "Активиране на проверката на правописа (изисква се рестартиране)", "Enable spellchecker (requires restart)": "Активиране на проверката на правописа (изисква се рестартиране)",
"Enter Full Screen": "Enter Full Screen",
"Factory Reset": "Фабрично нулиране", "Factory Reset": "Фабрично нулиране",
"Factory Reset Data": "Factory Reset Data", "Factory Reset Data": "Factory Reset Data",
"File": "досие", "File": "досие",
@@ -57,18 +60,19 @@
"Help Center": "Помощен център", "Help Center": "Помощен център",
"Hide": "Hide", "Hide": "Hide",
"Hide Others": "Hide Others", "Hide Others": "Hide Others",
"Hide Zulip": "Hide Zulip",
"History": "история", "History": "история",
"History Shortcuts": "Преки пътища в историята", "History Shortcuts": "Преки пътища в историята",
"Keyboard Shortcuts": "Комбинация от клавиши", "Keyboard Shortcuts": "Комбинация от клавиши",
"Log Out": "Излез от профила си", "Log Out": "Излез от профила си",
"Log Out of Organization": "Излезте от организацията", "Log Out of Organization": "Излезте от организацията",
"Look Up": "Look Up",
"Manual proxy configuration": "Ръчна конфигурация на прокси", "Manual proxy configuration": "Ръчна конфигурация на прокси",
"Minimize": "Минимизиране", "Minimize": "Минимизиране",
"Mute all sounds from Zulip": "Заглуши всички звуци от Zulip", "Mute all sounds from Zulip": "Заглуши всички звуци от Zulip",
"NO": "НЕ",
"Network": "мрежа", "Network": "мрежа",
"Network and Proxy Settings": "Network and Proxy Settings", "No Suggestion Found": "No Suggestion Found",
"Notification settings": "Настройки на известията",
"OK": "OK",
"OR": "ИЛИ", "OR": "ИЛИ",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", "On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Organization URL": "URL адрес на организацията", "Organization URL": "URL адрес на организацията",
@@ -85,15 +89,12 @@
"Release Notes": "Бележки към изданието", "Release Notes": "Бележки към изданието",
"Reload": "Презареди", "Reload": "Презареди",
"Report an Issue": "Подаване на сигнал за проблем", "Report an Issue": "Подаване на сигнал за проблем",
"Reset App Settings": "Reset App Settings",
"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.",
"Save": "Запази", "Save": "Запази",
"Select All": "Избери всички", "Select All": "Избери всички",
"Services": "Services", "Services": "Services",
"Settings": "Настройки", "Settings": "Настройки",
"Shortcuts": "Shortcuts", "Shortcuts": "Shortcuts",
"Show app icon in system tray": "Показване на иконата на приложението в системната област", "Show app icon in system tray": "Показване на иконата на приложението в системната област",
"Show app unread badge": "Показване на непрочетената значка на приложението",
"Show desktop notifications": "Показване на известията на работния плот", "Show desktop notifications": "Показване на известията на работния плот",
"Show sidebar": "Показване на страничната лента", "Show sidebar": "Показване на страничната лента",
"Spellchecker Languages": "Spellchecker Languages", "Spellchecker Languages": "Spellchecker Languages",
@@ -117,11 +118,8 @@
"View Shortcuts": "Преглед на преки пътища", "View Shortcuts": "Преглед на преки пътища",
"Window": "прозорец", "Window": "прозорец",
"Window Shortcuts": "Клавишни комбинации", "Window Shortcuts": "Клавишни комбинации",
"YES": "ДА",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", "You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Zoom In": "Увеличавам", "Zoom In": "Увеличавам",
"Zoom Out": "Отдалечавам", "Zoom Out": "Отдалечавам",
"keyboard shortcuts": "комбинация от клавиши", "keyboard shortcuts": "комбинация от клавиши"
"script": "писменост",
"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app."
} }

View File

@@ -1,127 +1,60 @@
{ {
"About Zulip": "যুলিপ সম্পর্কে ", "About Zulip": "যুলিপ সম্পর্কে",
"Actual Size": "প্রকৃত সাইজ ", "Actual Size": "প্রকৃত সাইজ",
"Add Organization": "সংস্থা যুক্ত করুন", "Add Organization": "সংস্থা যুক্ত করুন",
"Add a Zulip organization": "একটি যুলিপ প্রতিষ্ঠান যুক্ত করুন", "Add a Zulip organization": "একটি যুলিপ প্রতিষ্ঠান যুক্ত করুন",
"Add custom CSS": "কাস্টম সিএসএস যুক্ত করুন", "Add custom CSS": "কাস্টম সিএসএস যুক্ত করুন",
"AddServer": "এড সার্ভার ", "Advanced": "অগ্রসর",
"Advanced": "অগ্রসর ", "Always start minimized": "সব সময় মিনিমাইজড ভাবে শুরু করুন",
"All the connected organizations will appear here.": "All the connected organizations will appear here.",
"Always start minimized": "সব সময় মিনিমাইজড ভাবে শুরু করুন ",
"App Updates": "অ্যাপ আপডেট", "App Updates": "অ্যাপ আপডেট",
"App language (requires restart)": "App language (requires restart)",
"Appearance": "প্রকাশ", "Appearance": "প্রকাশ",
"Application Shortcuts": "অ্যাপ্লিকেশান শর্টকাট ", "Application Shortcuts": "অ্যাপ্লিকেশান শর্টকাট",
"Are you sure you want to disconnect this organization?": "আপনি কি নিশ্চিত যে আপনি এই সংস্থার সংযোগ বিচ্ছিন্ন করতে চান ?", "Are you sure you want to disconnect this organization?": "আপনি কি নিশ্চিত যে আপনি এই সংস্থার সংযোগ বিচ্ছিন্ন করতে চান ?",
"Ask where to save files before downloading": "Ask where to save files before downloading", "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": "ব্যাক্তিগত মেসেজে ডক বাউন্স করুন",
"Cancel": "বাতিল",
"Change": "পরিবর্তন", "Change": "পরিবর্তন",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "ভাষা পরিবর্তন করতে সিস্টেম প্রেফারেন্স কীবোর্ড → টেক্সট → স্পেলিং এ যান ", "Change the language from System Preferences → Keyboard → Text → Spelling.": "ভাষা পরিবর্তন করতে সিস্টেম প্রেফারেন্স কীবোর্ড → টেক্সট → স্পেলিং এ যান",
"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": "যুলিপ ইউআরএল কপি করুন",
"Create a new organization": "নতুন সংস্থা তৈরি করুন ", "Create a new organization": "নতুন সংস্থা তৈরি করুন",
"Cut": "কাট ", "Cut": "কাট",
"Default download location": "Default download location",
"Delete": "ডিলিট", "Delete": "ডিলিট",
"Desktop Notifications": "ডেস্কটপ নোটিফিকেশান ", "Desktop Notifications": "ডেস্কটপ নোটিফিকেশান",
"Desktop Settings": "ডেস্কটপ সেটিংস", "Desktop Settings": "ডেস্কটপ সেটিংস",
"Disconnect": "সংযোগ বিছিন্ন করুন", "Disconnect": "সংযোগ বিছিন্ন করুন",
"Download App Logs": "অ্যাপ লগ ডাউনলোড করুন ", "Download App Logs": "অ্যাপ লগ ডাউনলোড করুন",
"Edit": "এডিট", "Edit": "এডিট",
"Edit Shortcuts": "শর্টকাটগুলো এডিট করুন ", "Edit Shortcuts": "শর্টকাটগুলো এডিট করুন",
"Emoji & Symbols": "Emoji & Symbols",
"Enable auto updates": "অটো আপডেট চালু করুন", "Enable auto updates": "অটো আপডেট চালু করুন",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Enter Full Screen": "Enter Full Screen",
"Factory Reset": "ফ্যাক্টরি রিসেট", "Factory Reset": "ফ্যাক্টরি রিসেট",
"Factory Reset Data": "Factory Reset Data",
"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": "সাহায্য কেন্দ্র",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Hide Zulip": "Hide Zulip",
"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 Suggestion Found": "কোন সাজেশন পাওয়া যায়নি",
"NO": "না",
"Network": "Network",
"Network and Proxy Settings": "Network and Proxy Settings",
"OR": "অথবা", "OR": "অথবা",
"On macOS, the OS spellchecker is used.": "ম্যাক ওএস এ , ওএস এর স্পেলচেকার ব্যাবহার করা হয় ।", "On macOS, the OS spellchecker is used.": "ম্যাক ওএস এ , ওএস এর স্পেলচেকার ব্যাবহার করা হয় ।",
"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",
"Quit when the window is closed": "Quit when the window is closed",
"Redo": "Redo",
"Release Notes": "Release Notes",
"Reload": "Reload",
"Report an Issue": "Report an Issue",
"Reset App Settings": "Reset App Settings",
"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.",
"Save": "সেভ", "Save": "সেভ",
"Select All": "Select All",
"Services": "Services",
"Settings": "সেটিংস", "Settings": "সেটিংস",
"Shortcuts": "শর্টকাট সমূহ", "Shortcuts": "শর্টকাট সমূহ",
"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 sidebar": "Show sidebar",
"Spellchecker Languages": "Spellchecker Languages",
"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",
"Tip": "টিপ", "Tip": "টিপ",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab", "Undo": "অ্যান্ডু",
"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": "অ্যান্ডু ",
"Unhide": "Unhide",
"Upload": "আপলোড", "Upload": "আপলোড",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)", "Zoom Out": "জুম আউট"
"View": "View",
"View Shortcuts": "View Shortcuts",
"Window": "Window",
"Window Shortcuts": "Window Shortcuts",
"YES": "হ্যাঁ",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Zoom In": "Zoom In",
"Zoom Out": "জুম আউট",
"keyboard shortcuts": "keyboard shortcuts",
"script": "স্ক্রিপ্ট ",
"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app."
} }

View File

@@ -1,127 +1,12 @@
{ {
"About Zulip": "About Zulip", "Cancel": "raď kerdên",
"Actual Size": "Actual Size",
"Add Organization": "Add Organization",
"Add a Zulip organization": "Add a Zulip organization",
"Add custom CSS": "Add custom CSS",
"AddServer": "AddServer",
"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",
"App language (requires restart)": "App language (requires restart)",
"Appearance": "Appearance",
"Application Shortcuts": "Application Shortcuts",
"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"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",
"Change": "ālêštkâri", "Change": "ālêštkâri",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Check for Updates": "Check for Updates",
"Close": "bastên", "Close": "bastên",
"Connect": "Connect",
"Connect to another organization": "Connect to another organization",
"Connected organizations": "Connected organizations",
"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": "pāk kerdên", "Delete": "pāk kerdên",
"Desktop Notifications": "Desktop Notifications",
"Desktop Settings": "Desktop Settings",
"Disconnect": "Disconnect",
"Download App Logs": "Download App Logs",
"Edit": "ālêšt", "Edit": "ālêšt",
"Edit Shortcuts": "Edit Shortcuts",
"Emoji & Symbols": "Emoji & Symbols",
"Enable auto updates": "Enable auto updates",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Enter Full Screen": "Enter Full Screen",
"Factory Reset": "Factory Reset",
"Factory Reset Data": "Factory Reset Data",
"File": "fāyl", "File": "fāyl",
"Find accounts": "jostên hêsāvā mêntori", "Find accounts": "jostên hêsāvā mêntori",
"Find accounts by email": "Find accounts by email", "OK": "xā",
"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",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Hide Zulip": "Hide Zulip",
"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",
"Network and Proxy Settings": "Network and Proxy Settings",
"OR": "OR",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"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",
"Quit when the window is closed": "Quit when the window is closed",
"Redo": "Redo",
"Release Notes": "Release Notes",
"Reload": "Reload",
"Report an Issue": "Report an Issue",
"Reset App Settings": "Reset App Settings",
"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.",
"Save": "zaft kerdên", "Save": "zaft kerdên",
"Select All": "Select All", "Settings": "sāmovā"
"Services": "Services",
"Settings": "sāmovā",
"Shortcuts": "Shortcuts",
"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 sidebar": "Show sidebar",
"Spellchecker Languages": "Spellchecker Languages",
"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",
"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",
"Unhide": "Unhide",
"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",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"keyboard shortcuts": "keyboard shortcuts",
"script": "script",
"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app."
} }

View File

@@ -1,23 +1,29 @@
{ {
"A new update {{{version}}} has been downloaded.": "S'ha descarregat una nova actualització {{{version}}}.",
"A new version {{{version}}} of Zulip Desktop is available.": "Hi ha disponible una nova versió de Zulip Escriptori {{{version}}}.",
"About": "Sobre",
"About Zulip": "Quant a Zulip", "About Zulip": "Quant a Zulip",
"Actual Size": "Actual Size", "Actual Size": "Mida actual",
"Add Organization": "Add Organization", "Add Organization": "Afegir organització",
"Add a Zulip organization": "Add a Zulip organization", "Add a Zulip organization": "Afegir una organització de Zulip",
"Add custom CSS": "Add custom CSS", "Add custom CSS": "Add custom CSS",
"AddServer": "AddServer", "Add to Dictionary": "Add to Dictionary",
"Advanced": "Advanced", "Advanced": "Avançat",
"All the connected organizations will appear here.": "All the connected organizations will appear here.",
"Always start minimized": "Always start minimized", "Always start minimized": "Always start minimized",
"App Updates": "App Updates", "App Updates": "App Updates",
"App language (requires restart)": "App language (requires restart)", "App language (requires restart)": "App language (requires restart)",
"Appearance": "Appearance", "Appearance": "Appearance",
"Application Shortcuts": "Application Shortcuts", "Application Shortcuts": "Application Shortcuts",
"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?", "Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?",
"Are you sure?": "Esteu segur/a?",
"Ask where to save files before downloading": "Ask where to save files before downloading", "Ask where to save files before downloading": "Ask where to save files before downloading",
"Auto hide Menu bar": "Auto hide Menu bar", "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)", "Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)",
"Back": "Back", "Back": "Back",
"Bounce dock on new private message": "Bounce dock on new private message", "Bounce dock on new private message": "Bounce dock on new private message",
"CSS file": "Arxiu CSS",
"Cancel": "Cancel·la",
"Certificate error": "Error de certificat",
"Change": "Change", "Change": "Change",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", "Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Check for Updates": "Check for Updates", "Check for Updates": "Check for Updates",
@@ -26,6 +32,9 @@
"Connect to another organization": "Connect to another organization", "Connect to another organization": "Connect to another organization",
"Connected organizations": "Connected organizations", "Connected organizations": "Connected organizations",
"Copy": "Copia", "Copy": "Copia",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"Copy Link": "Copy Link",
"Copy Zulip URL": "Copy Zulip URL", "Copy Zulip URL": "Copy Zulip URL",
"Create a new organization": "Crea una nova organització", "Create a new organization": "Crea una nova organització",
"Cut": "Cut", "Cut": "Cut",
@@ -34,14 +43,16 @@
"Desktop Notifications": "Desktop Notifications", "Desktop Notifications": "Desktop Notifications",
"Desktop Settings": "Configuració d'escriptori", "Desktop Settings": "Configuració d'escriptori",
"Disconnect": "Disconnect", "Disconnect": "Disconnect",
"Do Not Disturb": "No molesteu",
"Download App Logs": "Download App Logs", "Download App Logs": "Download App Logs",
"Edit": "Edita", "Edit": "Edita",
"Edit Shortcuts": "Edit Shortcuts", "Edit Shortcuts": "Edit Shortcuts",
"Emoji & Symbols": "Emoji & Symbols", "Emoji & Symbols": "Emojis i símbols",
"Enable auto updates": "Enable auto updates", "Enable auto updates": "Enable auto updates",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)", "Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)", "Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Enter Full Screen": "Enter Full Screen", "Enter Full Screen": "Entreu a pantalla sencera",
"Error saving new organization": "Error en guardar la nova organització",
"Factory Reset": "Factory Reset", "Factory Reset": "Factory Reset",
"Factory Reset Data": "Factory Reset Data", "Factory Reset Data": "Factory Reset Data",
"File": "Fitxer", "File": "Fitxer",
@@ -57,18 +68,18 @@
"Help Center": "Centre d'ajuda", "Help Center": "Centre d'ajuda",
"Hide": "Hide", "Hide": "Hide",
"Hide Others": "Hide Others", "Hide Others": "Hide Others",
"Hide Zulip": "Hide Zulip",
"History": "Historial", "History": "Historial",
"History Shortcuts": "Dreceres d'historial", "History Shortcuts": "Dreceres d'historial",
"Keyboard Shortcuts": "Keyboard Shortcuts", "Keyboard Shortcuts": "Keyboard Shortcuts",
"Log Out": "Tanca la sessió", "Log Out": "Tanca la sessió",
"Log Out of Organization": "Tanca la sessió de l'organització", "Log Out of Organization": "Tanca la sessió de l'organització",
"Look Up": "Look Up",
"Manual proxy configuration": "Manual proxy configuration", "Manual proxy configuration": "Manual proxy configuration",
"Minimize": "Minimize", "Minimize": "Minimize",
"Mute all sounds from Zulip": "Silencia tots els sons de Zulip", "Mute all sounds from Zulip": "Silencia tots els sons de Zulip",
"NO": "NO",
"Network": "Network", "Network": "Network",
"Network and Proxy Settings": "Network and Proxy Settings", "No Suggestion Found": "No Suggestion Found",
"OK": "D'acord",
"OR": "OR", "OR": "OR",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", "On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Organization URL": "URL d'organització", "Organization URL": "URL d'organització",
@@ -86,14 +97,12 @@
"Reload": "Recarrega", "Reload": "Recarrega",
"Report an Issue": "Report an Issue", "Report an Issue": "Report an Issue",
"Reset App Settings": "Reinicia la configuració de l'aplicació", "Reset App Settings": "Reinicia la configuració de l'aplicació",
"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.",
"Save": "Guardar", "Save": "Guardar",
"Select All": "Select All", "Select All": "Select All",
"Services": "Services", "Services": "Services",
"Settings": "Configuració", "Settings": "Configuració",
"Shortcuts": "Shortcuts", "Shortcuts": "Shortcuts",
"Show app icon in system tray": "Show app icon in system tray", "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 desktop notifications": "Show desktop notifications",
"Show sidebar": "Show sidebar", "Show sidebar": "Show sidebar",
"Spellchecker Languages": "Spellchecker Languages", "Spellchecker Languages": "Spellchecker Languages",
@@ -109,19 +118,20 @@
"Toggle Sidebar": "Toggle Sidebar", "Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon", "Toggle Tray Icon": "Toggle Tray Icon",
"Tools": "Tools", "Tools": "Tools",
"Unable to check for updates.": "No ha estat possible comprovar les actualitzacions.",
"Unable to download the update.": "No ha estat possible descarregar l'actualització.",
"Undo": "Undo", "Undo": "Undo",
"Unhide": "Unhide", "Unhide": "Unhide",
"Unknown error": "Error desconegut",
"Upload": "Pujada", "Upload": "Pujada",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)", "Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"View": "View", "View": "View",
"View Shortcuts": "View Shortcuts", "View Shortcuts": "View Shortcuts",
"Window": "Window", "Window": "Window",
"Window Shortcuts": "Window Shortcuts", "Window Shortcuts": "Window Shortcuts",
"YES": "YES", "Yes": "No",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", "You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Zoom In": "Zoom In", "Zoom In": "Zoom In",
"Zoom Out": "Zoom Out", "Zoom Out": "Zoom Out",
"keyboard shortcuts": "keyboard shortcuts", "keyboard shortcuts": "keyboard shortcuts"
"script": "script",
"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app."
} }

View File

@@ -1,10 +1,13 @@
{ {
"A new update {{{version}}} has been downloaded.": "Byla stažena nová aktualizace {{{version}}}.",
"A new version {{{version}}} of Zulip Desktop is available.": "Je dostupná nová verze {{{version}}} Zulip Desktop.",
"About": "O programu",
"About Zulip": "O Zulipu", "About Zulip": "O Zulipu",
"Actual Size": "Skutečná velikost", "Actual Size": "Skutečná velikost",
"Add Organization": "Přidat organizaci", "Add Organization": "Přidat organizaci",
"Add a Zulip organization": "Přidat organizaci Zulip", "Add a Zulip organization": "Přidat organizaci Zulip",
"Add custom CSS": "Přidat vlastní CSS", "Add custom CSS": "Přidat vlastní CSS",
"AddServer": "Přidat server", "Add to Dictionary": "Přidat do slovníku",
"Advanced": "Rozšířené", "Advanced": "Rozšířené",
"All the connected organizations will appear here.": "Všechny připojené organizace se objeví zde.", "All the connected organizations will appear here.": "Všechny připojené organizace se objeví zde.",
"Always start minimized": "Vždy spouštět minimalizované", "Always start minimized": "Vždy spouštět minimalizované",
@@ -13,11 +16,16 @@
"Appearance": "Vzhled", "Appearance": "Vzhled",
"Application Shortcuts": "Zkratky programu", "Application Shortcuts": "Zkratky programu",
"Are you sure you want to disconnect this organization?": "Opravdu chcete odpojit tuto organizaci?", "Are you sure you want to disconnect this organization?": "Opravdu chcete odpojit tuto organizaci?",
"Are you sure?": "Jste si jistý?",
"Ask where to save files before downloading": "Před stažením se zeptat kam uložit soubory", "Ask where to save files before downloading": "Před stažením se zeptat kam uložit soubory",
"Auto hide Menu bar": "Automaticky skrývat menu", "Auto hide Menu bar": "Automaticky skrývat menu",
"Auto hide menu bar (Press Alt key to display)": "Automaticky skrývat menu (pro zobrazení stiskněte klávesu Alt)", "Auto hide menu bar (Press Alt key to display)": "Automaticky skrývat menu (pro zobrazení stiskněte klávesu Alt)",
"Available under the {{{link}}}Apache 2.0 License{{{endLink}}}": "Dostupné pod {{{link}}}licencí Apache 2.0{{{endLink}}}",
"Back": "Zpět", "Back": "Zpět",
"Bounce dock on new private message": "Poskakování ikony v docku po obdržení nové soukromé zprávy", "Bounce dock on new private message": "Poskakování ikony v docku po obdržení nové soukromé zprávy",
"CSS file": "Soubor CSS",
"Cancel": "Zrušit",
"Certificate error": "Chyba certifikátu",
"Change": "Změnit", "Change": "Změnit",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Změnit jazyk v Nastavení systému → Klávesnice → Text → Kontrola pravopisu.", "Change the language from System Preferences → Keyboard → Text → Spelling.": "Změnit jazyk v Nastavení systému → Klávesnice → Text → Kontrola pravopisu.",
"Check for Updates": "Zkontrolovat aktualizace", "Check for Updates": "Zkontrolovat aktualizace",
@@ -26,14 +34,21 @@
"Connect to another organization": "Připojit 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 Email Address": "Kopírovat adresu elektronické pošty",
"Copy Image": "Kopírovat obrázek",
"Copy Image URL": "Kopírovat adresu (URL) obrázku",
"Copy Link": "Kopírovat odkaz",
"Copy Zulip URL": "Kopírovat adresu (URL) Zulipu", "Copy Zulip URL": "Kopírovat adresu (URL) Zulipu",
"Create a new organization": "Vytvořit novou organizaci", "Create a new organization": "Vytvořit novou organizaci",
"Custom CSS file deleted": "Vlastní soubor CSS smazán",
"Cut": "Vyjmout", "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 Notifications": "Oznámení na ploše", "Desktop Notifications": "Oznámení na ploše",
"Desktop Settings": "Nastavení plochy", "Desktop Settings": "Nastavení plochy",
"Disconnect": "Odpojit", "Disconnect": "Odpojit",
"Disconnect organization": "Odpojit organizaci",
"Do Not Disturb": "Nerušit",
"Download App Logs": "Stáhnout záznamy programu", "Download App Logs": "Stáhnout záznamy programu",
"Edit": "Upravit", "Edit": "Upravit",
"Edit Shortcuts": "Upravit zkratky", "Edit Shortcuts": "Upravit zkratky",
@@ -42,6 +57,9 @@
"Enable error reporting (requires restart)": "Povolit hlášení chyb (vyžaduje opětovné spuštění programu)", "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 opětovné spuštění programu)", "Enable spellchecker (requires restart)": "Povolit kontrolu pravopisu (vyžaduje opětovné spuštění programu)",
"Enter Full Screen": "Vstoupit na celou obrazovku", "Enter Full Screen": "Vstoupit na celou obrazovku",
"Error saving new organization": "Chyba při ukládání nové organizace",
"Error saving update notifications": "Chyba při ukládání oznámení aktualizace",
"Error: {{{error}}}\n\nThe latest version of Zulip Desktop is available at:\n{{{link}}}\nCurrent version: {{{version}}}": "Chyba: {{{error}}}\n\nNejnovější verze aplikace Zulip Desktop je dostupná na adrese:\n{{{link}}}\nNynější verze: {{{version}}}",
"Factory Reset": "Obnovení do továrního nastavení", "Factory Reset": "Obnovení do továrního nastavení",
"Factory Reset Data": "Obnovení dat do továrního nastavení", "Factory Reset Data": "Obnovení dat do továrního nastavení",
"File": "Soubor", "File": "Soubor",
@@ -52,6 +70,7 @@
"Functionality": "Funkce", "Functionality": "Funkce",
"General": "Obecné", "General": "Obecné",
"Get beta updates": "Dostávat beta aktualizace", "Get beta updates": "Dostávat beta aktualizace",
"Go Back": "Jít zpět",
"Hard Reload": "Tvrdé znovunahrání", "Hard Reload": "Tvrdé znovunahrání",
"Help": "Nápověda", "Help": "Nápověda",
"Help Center": "Centrum nápovědy", "Help Center": "Centrum nápovědy",
@@ -60,15 +79,28 @@
"Hide Zulip": "Skrýt Zulip", "Hide Zulip": "Skrýt Zulip",
"History": "Historie", "History": "Historie",
"History Shortcuts": "Zkratky pro historii", "History Shortcuts": "Zkratky pro historii",
"Install Later": "Instalovat později",
"Install and Relaunch": "Nainstalovat a spustit znovu",
"It will be installed the next time you restart the application.": "Nainstaluje se při příštím restartu aplikace.",
"Keyboard Shortcuts": "Klávesové zkratky", "Keyboard Shortcuts": "Klávesové zkratky",
"Later": "Později",
"Loading": "Nahrává se",
"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",
"Look Up": "Vyhledat",
"Maintained by {{{link}}}Zulip{{{endLink}}}": "Udržováno {{{link}}}Zulip{{{endLink}}}",
"Manual Download": "Ruční stažení",
"Manual proxy configuration": "Ruční nastavení proxy", "Manual proxy configuration": "Ruční nastavení proxy",
"Minimize": "Minimalizovat", "Minimize": "Minimalizovat",
"Mute all sounds from Zulip": "Ztlumit všechny zvuky ze Zulipu", "Mute all sounds from Zulip": "Ztlumit všechny zvuky ze Zulipu",
"NO": "NE",
"Network": "Síť", "Network": "Síť",
"Network and Proxy Settings": "Nastavení sítě a proxy serveru", "Network and Proxy Settings": "Nastavení sítě a proxy serveru",
"New servers added. Reload app now?": "Přidány nové servery. Nahrát nyní aplikaci znovu?",
"No": "Ne",
"No Suggestion Found": "Nenalezen žádný návrh",
"No updates available.": "Žádné dostupné aktualizace.",
"Notification settings": "Nastavení oznámení",
"OK": "OK",
"OR": "NEBO", "OR": "NEBO",
"On macOS, the OS spellchecker is used.": "Na macOS se používá kontrola pravopisu OS.", "On macOS, the OS spellchecker is used.": "Na macOS se používá kontrola pravopisu OS.",
"Organization URL": "Adresa organizace", "Organization URL": "Adresa organizace",
@@ -78,6 +110,7 @@
"Proxy": "Proxy", "Proxy": "Proxy",
"Proxy bypass rules": "Pravidla pro obejití Proxy", "Proxy bypass rules": "Pravidla pro obejití Proxy",
"Proxy rules": "Pravidla Proxy", "Proxy rules": "Pravidla Proxy",
"Proxy settings saved.": "Nastavení proxy serveru je uloženo.",
"Quit": "Ukončit", "Quit": "Ukončit",
"Quit Zulip": "Ukončit Zulip", "Quit Zulip": "Ukončit Zulip",
"Quit when the window is closed": "Ukončit, když je okno zavřeno", "Quit when the window is closed": "Ukončit, když je okno zavřeno",
@@ -89,17 +122,23 @@
"Reset the application, thus deleting all the connected organizations and accounts.": "Obnovit program do výchozího nastavení. čili smazat všechny připojené organizace a účty.", "Reset the application, thus deleting all the connected organizations and accounts.": "Obnovit program do výchozího nastavení. čili smazat všechny připojené organizace a účty.",
"Save": "Uložit", "Save": "Uložit",
"Select All": "Vybrat vše", "Select All": "Vybrat vše",
"Select Download Location": "Vyberte umístění stahování",
"Select file": "Vybrat soubor",
"Services": "Služby", "Services": "Služby",
"Settings": "Nastavení", "Settings": "Nastavení",
"Shortcuts": "Zkratky", "Shortcuts": "Zkratky",
"Show app icon in system tray": "Zobrazovat ikonu programu v oznamovací oblasti panelu", "Show app icon in system tray": "Zobrazovat ikonu programu v oznamovací oblasti panelu",
"Show app unread badge": "Zobrazovat u ikony aplikace symbol nepřečteno",
"Show desktop notifications": "Zobrazovat oznámení na ploše", "Show desktop notifications": "Zobrazovat oznámení na ploše",
"Show sidebar": "Zobrazovat postranní panel", "Show sidebar": "Zobrazovat postranní panel",
"Show unread count badge on app icon": "Ukázat počet nepřečtených na ikoně aplikace",
"Spellchecker Languages": "Kontrola pravopisu jazyků", "Spellchecker Languages": "Kontrola pravopisu jazyků",
"Start app at login": "Spustit program 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řepnout na předchozí organizaci", "Switch to Previous Organization": "Přepnout na předchozí organizaci",
"The custom CSS previously set is deleted.": "Předtím nastavený vlastní soubor CSS je smazán.",
"The server presented an invalid certificate for {{{origin}}}:\n\n{{{error}}}": "Server předložil neplatný certifikát pro {{{origin}}}:\n\n{{{error}}}",
"The update will be downloaded in the background. You will be notified when it is ready to be installed.": "Aktualizace se stáhne na pozadí. Až bude připravena k instalaci, budete o tom informováni.",
"There was an error while saving the new organization. You may have to add your previous organizations again.": "Při ukládání nové organizace došlo k chybě. Je možné, že budete muset předchozí organizace přidat znovu.",
"These desktop app shortcuts extend the Zulip webapp's": "Tyto zkratky rozšiřují webovou aplikaci Zulipu", "These desktop app shortcuts extend the Zulip webapp's": "Tyto zkratky rozšiřují webovou aplikaci Zulipu",
"Tip": "Tip", "Tip": "Tip",
"Toggle DevTools for Active Tab": "Přepnout vývojářské nástroje pro aktivní kartu", "Toggle DevTools for Active Tab": "Přepnout vývojářské nástroje pro aktivní kartu",
@@ -109,19 +148,23 @@
"Toggle Sidebar": "Přepnout zobrazení postranního panelu", "Toggle Sidebar": "Přepnout zobrazení postranního panelu",
"Toggle Tray Icon": "Přepnout ikonu v oznamovací oblasti panelu", "Toggle Tray Icon": "Přepnout ikonu v oznamovací oblasti panelu",
"Tools": "Nástroje", "Tools": "Nástroje",
"Unable to check for updates.": "Nelze zkontrolovat aktualizace.",
"Unable to download the update.": "Aktualizaci se nepodařilo stáhnout.",
"Undo": "Zpět", "Undo": "Zpět",
"Unhide": "Zobrazit", "Unhide": "Zobrazit",
"Unknown error": "Neznámá chyba",
"Upload": "Nahrát", "Upload": "Nahrát",
"Use system proxy settings (requires restart)": "Použít systémová nastavení proxy (vyžaduje opětovné spuštění programu)", "Use system proxy settings (requires restart)": "Použít systémová nastavení proxy (vyžaduje opětovné spuštění programu)",
"View": "Zobrazení", "View": "Zobrazení",
"View Shortcuts": "Zobrazit zkratky", "View Shortcuts": "Zobrazit zkratky",
"We encountered an error while saving the update notifications.": "Při ukládání oznámení o aktualizaci jsme narazili na chybu.",
"Window": "Okno", "Window": "Okno",
"Window Shortcuts": "Zkratky pro okno", "Window Shortcuts": "Zkratky pro okno",
"YES": "ANO", "Yes": "Ano",
"You are running the latest version of Zulip Desktop.\nVersion: {{{version}}}": "Používáte nejnovější verzi aplikace Zulip Desktop.\nVerze: {{{version}}}",
"You can select a maximum of 3 languages for spellchecking.": "Pro kontrolu pravopisu můžete vybrat nejvíce 3 jazyky.", "You can select a maximum of 3 languages for spellchecking.": "Pro kontrolu pravopisu můžete vybrat nejvíce 3 jazyky.",
"Zoom In": "Přiblížit", "Zoom In": "Přiblížit",
"Zoom Out": "Oddálit", "Zoom Out": "Oddálit",
"keyboard shortcuts": "klávesové zkratky", "keyboard shortcuts": "klávesové zkratky",
"script": "skript", "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} používá zastaralou verzi serveru Zulip {{{version}}}. V této aplikaci nemusí plně pracovat."
"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} používá zastaralou verzi serveru Zulip {{{verze}}}. V této aplikaci nemusí plně pracovat."
} }

View File

@@ -4,9 +4,9 @@
"Add Organization": "Ychwanegu Sefydliad", "Add Organization": "Ychwanegu Sefydliad",
"Add a Zulip organization": "Ychwanegwch sefydliad Zulip", "Add a Zulip organization": "Ychwanegwch sefydliad Zulip",
"Add custom CSS": "Ychwanegwch CSS wedi'i ddylunio'n benodol", "Add custom CSS": "Ychwanegwch CSS wedi'i ddylunio'n benodol",
"AddServer": "AddServer", "Add to Dictionary": "Ychwanegu at y Geiriadur",
"Advanced": "Uwch", "Advanced": "Uwch",
"All the connected organizations will appear here.": "All the connected organizations will appear here.", "All the connected organizations will appear here.": "Bydd yr holl sefydliadau cysylltiedig yn ymddangos yma.",
"Always start minimized": "Dechreuwch gyn lleied â phosibl bob amser", "Always start minimized": "Dechreuwch gyn lleied â phosibl bob amser",
"App Updates": "Diweddariadau Ap", "App Updates": "Diweddariadau Ap",
"App language (requires restart)": "Iaith ap (angen ailgychwyn)", "App language (requires restart)": "Iaith ap (angen ailgychwyn)",
@@ -18,6 +18,7 @@
"Auto hide menu bar (Press Alt key to display)": "Cuddiwch y bar dewislen yn awtomatig (Pwyswch Alt i'w harddangos)", "Auto hide menu bar (Press Alt key to display)": "Cuddiwch y bar dewislen yn awtomatig (Pwyswch Alt i'w harddangos)",
"Back": "Yn ôl", "Back": "Yn ôl",
"Bounce dock on new private message": "Sbonciwch doc ar neges breifat newydd", "Bounce dock on new private message": "Sbonciwch doc ar neges breifat newydd",
"Cancel": "Canslo",
"Change": "Newid", "Change": "Newid",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Newid yr iaith o Dewisiadau System → Bysellfwrdd → Testun → Sillafu.", "Change the language from System Preferences → Keyboard → Text → Spelling.": "Newid yr iaith o Dewisiadau System → Bysellfwrdd → Testun → Sillafu.",
"Check for Updates": "Chwiliwch am Ddiweddariadau", "Check for Updates": "Chwiliwch am Ddiweddariadau",
@@ -26,6 +27,9 @@
"Connect to another organization": "Cysylltu â sefydliad arall", "Connect to another organization": "Cysylltu â sefydliad arall",
"Connected organizations": "Sefydliadau cysylltiedig", "Connected organizations": "Sefydliadau cysylltiedig",
"Copy": "Copi", "Copy": "Copi",
"Copy Image": "Copi Delwedd",
"Copy Image URL": "Copi URL Delwedd",
"Copy Link": "Copi Dolen",
"Copy Zulip URL": "Copïwch URL Zulip", "Copy Zulip URL": "Copïwch URL Zulip",
"Create a new organization": "Creu sefydliad newydd", "Create a new organization": "Creu sefydliad newydd",
"Cut": "Torri", "Cut": "Torri",
@@ -37,11 +41,11 @@
"Download App Logs": "Lawrlwythwch Logiau Ap", "Download App Logs": "Lawrlwythwch Logiau Ap",
"Edit": "Golygu", "Edit": "Golygu",
"Edit Shortcuts": "Golygu Llwybrau Byr", "Edit Shortcuts": "Golygu Llwybrau Byr",
"Emoji & Symbols": "Emoji & Symbols", "Emoji & Symbols": "Emoji a Symbolau",
"Enable auto updates": "Galluogi diweddariadau yn awtomatig", "Enable auto updates": "Galluogi diweddariadau yn awtomatig",
"Enable error reporting (requires restart)": "Galluogi adrodd am wallau (angen ailgychwyn)", "Enable error reporting (requires restart)": "Galluogi adrodd am wallau (angen ailgychwyn)",
"Enable spellchecker (requires restart)": "Galluogi gwiriwr sillafu (angen ailgychwyn)", "Enable spellchecker (requires restart)": "Galluogi gwiriwr sillafu (angen ailgychwyn)",
"Enter Full Screen": "Enter Full Screen", "Enter Full Screen": "Rhowch sgrin lawn",
"Factory Reset": "Ailosod Ffatri", "Factory Reset": "Ailosod Ffatri",
"Factory Reset Data": "Ailosod Data Ffatri", "Factory Reset Data": "Ailosod Data Ffatri",
"File": "Ffeil", "File": "Ffeil",
@@ -57,24 +61,27 @@
"Help Center": "Canolfan Gymorth", "Help Center": "Canolfan Gymorth",
"Hide": "Cuddio", "Hide": "Cuddio",
"Hide Others": "Cuddio Eraill", "Hide Others": "Cuddio Eraill",
"Hide Zulip": "Hide Zulip", "Hide Zulip": "Cuddiwch Zulip",
"History": "Hanes", "History": "Hanes",
"History Shortcuts": "Hanes Llwybrau Byr ", "History Shortcuts": "Hanes Llwybrau Byr",
"Keyboard Shortcuts": "Llwybrau Byr Bysellfwrdd", "Keyboard Shortcuts": "Llwybrau Byr Bysellfwrdd",
"Log Out": "Allgofnodi", "Log Out": "Allgofnodi",
"Log Out of Organization": "Allgofnodi Sefydliad", "Log Out of Organization": "Allgofnodi Sefydliad",
"Look Up": "Ymchwiliwch",
"Manual proxy configuration": "Cyfluniad dirprwy â llaw", "Manual proxy configuration": "Cyfluniad dirprwy â llaw",
"Minimize": "Lleihau", "Minimize": "Lleihau",
"Mute all sounds from Zulip": "Tawelwch pob sain o Zulip", "Mute all sounds from Zulip": "Tawelwch pob sain o Zulip",
"NO": "NA",
"Network": "Rhwydwaith", "Network": "Rhwydwaith",
"Network and Proxy Settings": "Network and Proxy Settings", "Network and Proxy Settings": "Gosodiadau Rhwydwaith a Dirprwy",
"No Suggestion Found": "Ni chanfuwyd unrhyw awgrym",
"Notification settings": "Gosodiadau hysbysu",
"OK": "Iawn",
"OR": "NEU", "OR": "NEU",
"On macOS, the OS spellchecker is used.": "Ar macOS, defnyddir gwiriwr sillafu OS.", "On macOS, the OS spellchecker is used.": "Ar macOS, defnyddir gwiriwr sillafu OS.",
"Organization URL": "URL y sefydliad", "Organization URL": "URL y sefydliad",
"Organizations": "Sefydliadau", "Organizations": "Sefydliadau",
"Paste": "Gludo", "Paste": "Gludo",
"Paste and Match Style": "Arddull Gludo a Paru ", "Paste and Match Style": "Arddull Gludo a Paru",
"Proxy": "Dirprwy", "Proxy": "Dirprwy",
"Proxy bypass rules": "Rheolau ffordd osgoi dirprwy", "Proxy bypass rules": "Rheolau ffordd osgoi dirprwy",
"Proxy rules": "Rheolau dirprwy", "Proxy rules": "Rheolau dirprwy",
@@ -85,15 +92,14 @@
"Release Notes": "Nodiadau ar y datganiad hwn", "Release Notes": "Nodiadau ar y datganiad hwn",
"Reload": "Ail-lwytho", "Reload": "Ail-lwytho",
"Report an Issue": "Adroddiwch mater", "Report an Issue": "Adroddiwch mater",
"Reset App Settings": "Reset App Settings", "Reset App Settings": "Ailosod Gosodiadau Ap",
"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.", "Reset the application, thus deleting all the connected organizations and accounts.": "Ailosod y cais, gan ddileu'r holl sefydliadau a chyfrifon cysylltiedig.",
"Save": "Cadw", "Save": "Cadw",
"Select All": "Dewiswch Bobeth", "Select All": "Dewiswch Bobeth",
"Services": "Gwasanaethau", "Services": "Gwasanaethau",
"Settings": "Gosodiadau", "Settings": "Gosodiadau",
"Shortcuts": "Llwybrau byr", "Shortcuts": "Llwybrau byr",
"Show app icon in system tray": "Dangos eicon ap yn yr hambwrdd system", "Show app icon in system tray": "Dangos eicon ap yn yr hambwrdd system",
"Show app unread badge": "Dangos bathodyn heb ei ddarllen ",
"Show desktop notifications": "Dangos hysbysiadau bwrdd gwaith", "Show desktop notifications": "Dangos hysbysiadau bwrdd gwaith",
"Show sidebar": "Dangos bar ochr", "Show sidebar": "Dangos bar ochr",
"Spellchecker Languages": "Ieithoedd Sillafu", "Spellchecker Languages": "Ieithoedd Sillafu",
@@ -117,11 +123,10 @@
"View Shortcuts": "Gweld y Llwybrau Byr", "View Shortcuts": "Gweld y Llwybrau Byr",
"Window": "Ffenestr", "Window": "Ffenestr",
"Window Shortcuts": "Llwybrau Byr Ffenestri", "Window Shortcuts": "Llwybrau Byr Ffenestri",
"YES": "YDY", "Yes": "Ydy",
"You can select a maximum of 3 languages for spellchecking.": "Gallwch ddewis uchafswm o 3 iaith ar gyfer gwirio sillafu.", "You can select a maximum of 3 languages for spellchecking.": "Gallwch ddewis uchafswm o 3 iaith ar gyfer gwirio sillafu.",
"Zoom In": "Chwyddo Mewn", "Zoom In": "Chwyddo Mewn",
"Zoom Out": "Chwyddo allan", "Zoom Out": "Chwyddo allan",
"keyboard shortcuts": "llwybrau byr bysellfwrdd", "keyboard shortcuts": "llwybrau byr bysellfwrdd",
"script": "sgript", "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "Mae {{{server}}} yn rhedeg fersiwn Zulip Server {{{version}}} sydd wedi dyddio. Efallai na fydd yn gweithio'n llawn yn yr app hon."
"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app."
} }

View File

@@ -1,23 +1,24 @@
{ {
"About Zulip": "Om Zulip", "About Zulip": "Om Zulip",
"Actual Size": "Faktisk størrelse", "Actual Size": "Faktisk størrelse",
"Add Organization": "Opret organisation", "Add Organization": "Tilføj organisation",
"Add a Zulip organization": "Opret en Zulip organisation", "Add a Zulip organization": "Tilføj en Zulip organisation",
"Add custom CSS": "Tilføj egen CSS", "Add custom CSS": "Tilføj egen CSS",
"AddServer": "AddServer", "Add to Dictionary": "Add to Dictionary",
"Advanced": "Avanceret", "Advanced": "Avanceret",
"All the connected organizations will appear here.": "All the connected organizations will appear here.", "All the connected organizations will appear here.": "Alle forbundne organisationer vil blive vist her.",
"Always start minimized": "Start altid minimeret", "Always start minimized": "Start altid minimeret",
"App Updates": "App-opdateringer", "App Updates": "App-opdateringer",
"App language (requires restart)": "App language (requires restart)", "App language (requires restart)": "App language (requires restart)",
"Appearance": "Udseende", "Appearance": "Udseende",
"Application Shortcuts": "Genveje", "Application Shortcuts": "Genveje",
"Are you sure you want to disconnect this organization?": "Er du sikker på du vil frakoble denne organisation? ", "Are you sure you want to disconnect this organization?": "Er du sikker på du vil frakoble denne organisation?",
"Ask where to save files before downloading": "Ask where to save files before downloading", "Ask where to save files before downloading": "Ask where to save files before downloading",
"Auto hide Menu bar": "Skjul menu automatisk", "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)", "Auto hide menu bar (Press Alt key to display)": "Skjul menu automatisk (tryk på Alt-tasten for at vise)",
"Back": "Tilbage", "Back": "Tilbage",
"Bounce dock on new private message": "Animér dock ved ny privat meddelelse", "Bounce dock on new private message": "Animér dock ved ny privat meddelelse",
"Cancel": "Annuller",
"Change": "Skift", "Change": "Skift",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.", "Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Check for Updates": "Tjek for opdateringer", "Check for Updates": "Tjek for opdateringer",
@@ -26,6 +27,9 @@
"Connect to another organization": "Forbind til en anden organisation", "Connect to another organization": "Forbind til en anden organisation",
"Connected organizations": "Tilsluttede organisationer", "Connected organizations": "Tilsluttede organisationer",
"Copy": "Kopiér", "Copy": "Kopiér",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"Copy Link": "Copy Link",
"Copy Zulip URL": "Kopiér Zulip URL", "Copy Zulip URL": "Kopiér Zulip URL",
"Create a new organization": "Opret ny organisation", "Create a new organization": "Opret ny organisation",
"Cut": "Klip", "Cut": "Klip",
@@ -37,11 +41,11 @@
"Download App Logs": "Download app-logfiler", "Download App Logs": "Download app-logfiler",
"Edit": "Redigér", "Edit": "Redigér",
"Edit Shortcuts": "Redigér genveje", "Edit Shortcuts": "Redigér genveje",
"Emoji & Symbols": "Emoji & Symbols", "Emoji & Symbols": "Emoji og symboler",
"Enable auto updates": "Aktivér auto-opdateringer", "Enable auto updates": "Aktivér auto-opdateringer",
"Enable error reporting (requires restart)": "Aktivér fejlrapportering (kræver genstart)", "Enable error reporting (requires restart)": "Aktivér fejlrapportering (kræver genstart)",
"Enable spellchecker (requires restart)": "Aktivér stavekontrol (kræver genstart)", "Enable spellchecker (requires restart)": "Aktivér stavekontrol (kræver genstart)",
"Enter Full Screen": "Enter Full Screen", "Enter Full Screen": "Fuld skærm",
"Factory Reset": "Nulstil til fabriksindstillinger", "Factory Reset": "Nulstil til fabriksindstillinger",
"Factory Reset Data": "Factory Reset Data", "Factory Reset Data": "Factory Reset Data",
"File": "Fil", "File": "Fil",
@@ -55,20 +59,23 @@
"Hard Reload": "Hård reload", "Hard Reload": "Hård reload",
"Help": "Hjælp", "Help": "Hjælp",
"Help Center": "Hjælpecenter", "Help Center": "Hjælpecenter",
"Hide": "Hide", "Hide": "Skjul",
"Hide Others": "Hide Others", "Hide Others": "Skjul andre",
"Hide Zulip": "Hide Zulip", "Hide Zulip": "Skjul Zulip",
"History": "Historik", "History": "Historik",
"History Shortcuts": "Historik genveje", "History Shortcuts": "Historikgenveje",
"Keyboard Shortcuts": "Tastatur genveje", "Keyboard Shortcuts": "Tastaturgenveje",
"Log Out": "Log ud", "Log Out": "Log ud",
"Log Out of Organization": "Log ud af organisation", "Log Out of Organization": "Log ud af organisation",
"Look Up": "Look Up",
"Manual proxy configuration": "Manuel proxy opsætning", "Manual proxy configuration": "Manuel proxy opsætning",
"Minimize": "Minimér", "Minimize": "Minimer",
"Mute all sounds from Zulip": "Dæmp alle lyde fra Zulip", "Mute all sounds from Zulip": "Dæmp alle lyde fra Zulip",
"NO": "NEJ",
"Network": "Netværk", "Network": "Netværk",
"Network and Proxy Settings": "Network and Proxy Settings", "Network and Proxy Settings": "Netrk og proxy indstillinger",
"No Suggestion Found": "No Suggestion Found",
"Notification settings": "Indstillinger for notifikationer",
"OK": "OK",
"OR": "ELLER", "OR": "ELLER",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.", "On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Organization URL": "Organisation URL", "Organization URL": "Organisation URL",
@@ -85,15 +92,14 @@
"Release Notes": "Release Notes", "Release Notes": "Release Notes",
"Reload": "Reload", "Reload": "Reload",
"Report an Issue": "Report an Issue", "Report an Issue": "Report an Issue",
"Reset App Settings": "Reset App Settings", "Reset App Settings": "Nulstil App-indstillinger",
"Reset the application, thus deleting all the connected organizations and accounts.": "Reset the application, thus deleting all the connected organizations and accounts.", "Reset the application, thus deleting all the connected organizations and accounts.": "Nulstil applikationen, dvs: slet alle forbundne organisationer og konti.",
"Save": "Save", "Save": "Gem",
"Select All": "Select All", "Select All": "Vælg alle",
"Services": "Services", "Services": "Tjenester",
"Settings": "Settings", "Settings": "Indstillinger",
"Shortcuts": "Shortcuts", "Shortcuts": "Shortcuts",
"Show app icon in system tray": "Show app icon in system tray", "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 desktop notifications": "Show desktop notifications",
"Show sidebar": "Show sidebar", "Show sidebar": "Show sidebar",
"Spellchecker Languages": "Spellchecker Languages", "Spellchecker Languages": "Spellchecker Languages",
@@ -104,7 +110,7 @@
"Tip": "Tip", "Tip": "Tip",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab", "Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App", "Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle Do Not Disturb": "Toggle Do Not Disturb", "Toggle Do Not Disturb": "Slå forstyr ej til eller fra",
"Toggle Full Screen": "Toggle Full Screen", "Toggle Full Screen": "Toggle Full Screen",
"Toggle Sidebar": "Toggle Sidebar", "Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon", "Toggle Tray Icon": "Toggle Tray Icon",
@@ -117,11 +123,9 @@
"View Shortcuts": "View Shortcuts", "View Shortcuts": "View Shortcuts",
"Window": "Window", "Window": "Window",
"Window Shortcuts": "Window Shortcuts", "Window Shortcuts": "Window Shortcuts",
"YES": "YES",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.", "You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Zoom In": "Zoom In", "Zoom In": "Zoom In",
"Zoom Out": "Zoom Out", "Zoom Out": "Zoom Out",
"keyboard shortcuts": "keyboard shortcuts", "keyboard shortcuts": "keyboard shortcuts",
"script": "script", "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} kører en ældre version {{{version}}}. Den virker måske ikke fuld ud med denne app."
"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app."
} }

View File

@@ -1,47 +1,74 @@
{ {
"A new update {{{version}}} has been downloaded.": "Ein neues Update {{{version}}} wurde heruntergeladen.",
"A new version {{{version}}} is available. Please update using your package manager.": "Die neue Version {{{version}}} ist verfügbar. Bitte aktualisiere mithile deiner Paketverwaltung.",
"A new version {{{version}}} of Zulip Desktop is available.": "Eine neue Version {{{version}}} von Zulip Desktop ist verfügbar.",
"About": "Über",
"About Zulip": "Über Zulip", "About Zulip": "Über Zulip",
"Actual Size": "Tatsächliche Größe", "Actual Size": "Tatsächliche Größe",
"Add Organization": "Organisation hinzufügen", "Add Organization": "Organisation hinzufügen",
"Add a Zulip organization": "Zulip-Organisation hinzufügen", "Add a Zulip organization": "Zulip-Organisation hinzufügen",
"Add custom CSS": "Eigenes CSS hinzufügen", "Add custom CSS": "Eigene CSS hinzufügen",
"AddServer": "ServerHinzufügen", "Add to Dictionary": "Zum Wörterbuch hinzufügen",
"Advanced": "Erweitert", "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 werden hier angezeigt.",
"Always start minimized": "Immer minimiert öffnen", "Always start minimized": "Immer minimiert öffnen",
"App Updates": "App Updates", "App Updates": "App-Updates",
"App language (requires restart)": "Sprache der App (benötigt Neustart)", "App language (requires restart)": "Sprache der App (benötigt Neustart)",
"Appearance": "Erscheinungsbild", "Appearance": "Erscheinungsbild",
"Application Shortcuts": "App-Verknüpfungen", "Application Shortcuts": "App-Verknüpfungen",
"Are you sure you want to disconnect this organization?": "Bist du dir sicher, dass du die Verbindung zur Organisation trennen möchtest?", "Are you sure you want to disconnect this organization?": "Bist du dir sicher, dass du die Verbindung zur Organisation trennen möchtest?",
"Are you sure?": "Sind Sie sicher?",
"Ask where to save files before downloading": "Fragen, wo heruntergeladene Dateien gespeichert werden sollen", "Ask where to save files before downloading": "Fragen, wo heruntergeladene Dateien gespeichert werden sollen",
"Auto hide Menu bar": "Menü automatisch verstecken", "Auto hide Menu bar": "Menü automatisch verbergen",
"Auto hide menu bar (Press Alt key to display)": "Menü automatisch verstecken (zum Anzeigen die Alt-Taste drücken)", "Auto hide menu bar (Press Alt key to display)": "Menü automatisch verbergen (zum Anzeigen die Alt-Taste drücken)",
"Available under the {{{link}}}Apache 2.0 License{{{endLink}}}": "Verfügbar unter der {{{link}}}Apache-2.0-Lizenz{{{endLink}}}",
"Back": "Zurück", "Back": "Zurück",
"Bounce dock on new private message": "Im Dock hüpfen, wenn neue private Nachrichten eingehen", "Bounce dock on new private message": "Im Dock hüpfen, wenn neue private Nachrichten eingehen",
"CSS file": "CSS-Datei",
"Cancel": "Abbrechen",
"Certificate error": "Zertifikatfehler",
"Change": "Ändern", "Change": "Ändern",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Ändere die Spracheinstellung über Systemeinstellungen → Tastatur → Text → Rechtschreibung.", "Change the language from System Preferences → Keyboard → Text → Spelling.": "Ändere die Spracheinstellung über Systemeinstellungen → Tastatur → Text → Rechtschreibung.",
"Check for Updates": "Auf Updates prüfen", "Check for Updates": "Auf-Updates prüfen",
"Click to show {{{fileName}}} in folder": "Klicken, um {{{fileName}}} im Verzeichnis zu sehen",
"Close": "Schließen", "Close": "Schließen",
"Connect": "Verbinden", "Connect": "Verbinden",
"Connect to another organization": "Mit einer anderen Organisation verbinden", "Connect to another organization": "Mit einer anderen Organisation verbinden",
"Connected organizations": "Verbundene Organisationen", "Connected organizations": "Verbundene Organisationen",
"Connecting…": "Verbinde …",
"Copy": "Kopieren", "Copy": "Kopieren",
"Copy Email Address": "Email-Addresse kopieren",
"Copy Image": "Bild kopieren",
"Copy Image URL": "Bild-URL kopieren",
"Copy Link": "Link kopieren",
"Copy Zulip URL": "Zulip-URL kopieren", "Copy Zulip URL": "Zulip-URL kopieren",
"Could not add {{{domain}}}. Please contact your system administrator.": "Konnte {{{domain}}} nicht hinzufügen. Bitte kontaktiere deinen Systemadminstrator.",
"Create a new organization": "Eine neue Organisation erstellen", "Create a new organization": "Eine neue Organisation erstellen",
"Custom CSS file deleted": "Eigene CSS-Datei gelöscht",
"Cut": "Ausschneiden", "Cut": "Ausschneiden",
"Default download location": "Voreingestelltes Ziel für Downloads", "Default download location": "Voreingestelltes Ziel für Downloads",
"Delete": "Löschen", "Delete": "Löschen",
"Desktop Notifications": "Desktopbenachrichtigungen", "Desktop Notifications": "Desktopbenachrichtigungen",
"Desktop Settings": "Desktop-Einstellungen", "Desktop Settings": "Desktop-Einstellungen",
"Disable Do Not Disturb": "Bitte nicht stören ausschalten",
"Disconnect": "Verbindung trennen", "Disconnect": "Verbindung trennen",
"Disconnect organization": "Organisation trennen",
"Do Not Disturb": "Bitte nicht stören",
"Download App Logs": "Logdateien der App herunterladen", "Download App Logs": "Logdateien der App herunterladen",
"Download Complete": "Download vollständig",
"Download failed": "Download fehlgeschlagen",
"Edit": "Bearbeiten", "Edit": "Bearbeiten",
"Edit Shortcuts": "Tastenkürzel bearbeiten", "Edit Shortcuts": "Tastenkürzel bearbeiten",
"Emoji & Symbols": "Emoji & Symbole", "Emoji & Symbols": "Emoji & Symbole",
"Enable Do Not Disturb": "Bitte nicht stören einschalten",
"Enable auto updates": "Automatisch aktualisieren", "Enable auto updates": "Automatisch aktualisieren",
"Enable error reporting (requires restart)": "Fehlerberichte aktivieren (erfordert Neustart)", "Enable error reporting (requires restart)": "Fehlerberichte aktivieren (erfordert Neustart)",
"Enable spellchecker (requires restart)": "Rechtschreibprüfung aktivieren (erfordert Neustart)", "Enable spellchecker (requires restart)": "Rechtschreibprüfung aktivieren (erfordert Neustart)",
"Enter Full Screen": "Vollbildschirm aktivieren", "Enter Full Screen": "Vollbildschirm aktivieren",
"Enter Languages": "Sprachen eingeben",
"Error saving new organization": "Neue Organisation konnte nicht gespeichert werden",
"Error saving update notifications": "Update-Benachrichtigungen konnten nicht gespeichert werden",
"Error: {{{error}}}\n\nThe latest version of Zulip Desktop is available at:\n{{{link}}}\nCurrent version: {{{version}}}": "Fehler: {{{error}}}\n\nDie neueste Version von Zulip Desktop ist hier verfügbar:\n{{{link}}}\nAktuelle Version: {{{version}}}",
"Factory Reset": "Alle Einstellungen auf Standardwerte zurücksetzen", "Factory Reset": "Alle Einstellungen auf Standardwerte zurücksetzen",
"Factory Reset Data": "Auf Werkseinstellung zurücksetzen", "Factory Reset Data": "Auf Werkseinstellung zurücksetzen",
"File": "Datei", "File": "Datei",
@@ -52,54 +79,83 @@
"Functionality": "Funktionalität", "Functionality": "Funktionalität",
"General": "Allgemein", "General": "Allgemein",
"Get beta updates": "Auf Betaversionen aktualisieren", "Get beta updates": "Auf Betaversionen aktualisieren",
"Go Back": "Zurück",
"Hard Reload": "Komplett neu laden", "Hard Reload": "Komplett neu laden",
"Help": "Hilfe", "Help": "Hilfe",
"Help Center": "Hilfe-Zentrum", "Help Center": "Hilfecenter",
"Hide": "Verbergen", "Hide": "Verbergen",
"Hide Others": "Andere verbergen", "Hide Others": "Andere verbergen",
"Hide Zulip": "Zulip verbergen", "Hide Zulip": "Zulip verbergen",
"History": "Verlauf", "History": "Verlauf",
"History Shortcuts": "Kurzbefehle für Verlauf", "History Shortcuts": "Kurzbefehle für Verlauf",
"Install Later": "Später installieren",
"Install and Relaunch": "Installieren und neustarten",
"It will be installed the next time you restart the application.": "Es wird beim nächsten Neustart der Anwendung aktualisiert.",
"Keyboard Shortcuts": "Tastenkürzel", "Keyboard Shortcuts": "Tastenkürzel",
"Later": "Später",
"Loading": "Laden",
"Log Out": "Abmelden", "Log Out": "Abmelden",
"Log Out of Organization": "Von Organisation abmelden", "Log Out of Organization": "Von Organisation abmelden",
"Look Up": "Nachschlagen",
"Maintained by {{{link}}}Zulip{{{endLink}}}": "Maintained von {{{link}}}Zulip{{{endLink}}}",
"Manual Download": "Manueller Download",
"Manual proxy configuration": "Manuelle Proxy-Konfiguration", "Manual proxy configuration": "Manuelle Proxy-Konfiguration",
"Minimize": "Minimieren", "Minimize": "Minimieren",
"Mute all sounds from Zulip": "Alle Zulip-Klänge stummschalten", "Mute all sounds from Zulip": "Alle Zulip-Klänge stummschalten",
"NO": "NEIN",
"Network": "Netzwerk", "Network": "Netzwerk",
"Network and Proxy Settings": "Netzwerk- und Proxy-Einstellungen", "Network and Proxy Settings": "Netzwerk- und Proxy-Einstellungen",
"New servers added. Reload app now?": "Neue Server hinzugefügt. App jetzt erneut laden?",
"No": "Nein",
"No Suggestion Found": "Keine Vorschläge gefunden",
"No unread messages": "Keine ungelesenen Nachrichten",
"No updates available.": "Keine Updates verfügbar.",
"Notification settings": "Benachrichtigungseinstellungen",
"OK": "OK",
"OR": "ODER", "OR": "ODER",
"On macOS, the OS spellchecker is used.": "In macOS wird die OS Rechtschreibprüfung verwendet.", "On macOS, the OS spellchecker is used.": "In macOS wird die OS Rechtschreibprüfung verwendet.",
"Organization URL": "URL der Organisation", "Opening {{{link}}}…": "Öffne {{{link}}}…",
"Organization URL": "Organisations-URL",
"Organizations": "Organisationen", "Organizations": "Organisationen",
"PAC script": "PAC-Skript",
"Paste": "Einfügen", "Paste": "Einfügen",
"Paste and Match Style": "Ohne Formatierung einfügen", "Paste and Match Style": "Ohne Formatierung einfügen",
"Please contact your system administrator.": "Bitte kontaktiere deinen Systemadministrator.",
"Press {{{exitKey}}} to exit full screen": "Drücke {{{exitKey}}} um den Vollbildmodus zu beenden",
"Proxy": "Proxy", "Proxy": "Proxy",
"Proxy bypass rules": "Proxy-Ausnahmen", "Proxy bypass rules": "Proxy-Ausnahmen",
"Proxy rules": "Proxy-Regeln", "Proxy rules": "Proxy-Regeln",
"Proxy settings saved.": "Proxy-Einstellungen gespeichert.",
"Quit": "Beenden", "Quit": "Beenden",
"Quit Zulip": "Zulip beenden", "Quit Zulip": "Zulip beenden",
"Quit when the window is closed": "Beim Schließen des Fensters beenden", "Quit when the window is closed": "Beim Schließen des Fensters beenden",
"Redirecting": "Leite um",
"Redo": "Wiederholen", "Redo": "Wiederholen",
"Release Notes": "Hinweise zur Versionsfreigabe", "Release Notes": "Hinweise zur Versionsfreigabe",
"Reload": "Neu laden", "Reload": "Neu laden",
"Removing {{{url}}} is a restricted operation.": "Entfernung von {{{url}}} ist eine eingeschränkte Operation.",
"Report an Issue": "Ein Problem melden", "Report an Issue": "Ein Problem melden",
"Reset App Settings": "Einstellungen der App zurücksetzen", "Reset App Settings": "App-Einstellungen zurücksetzen",
"Reset the application, thus deleting all the connected organizations and accounts.": "Die Anwendung zurücksetzen. Dabei werden alle verbundenen Organisationen und Konten gelöscht.", "Reset the application, thus deleting all the connected organizations and accounts.": "Die Anwendung zurücksetzen. Dabei werden alle verbundenen Organisationen und Konten gelöscht.",
"Save": "Speichern", "Save": "Speichern",
"Select All": "Alles auswählen", "Select All": "Alles auswählen",
"Select Download Location": "Wählen Sie das Download-Ziel",
"Select file": "Datei auswählen",
"Services": "Dienste", "Services": "Dienste",
"Setting locked by system administrator.": "Einstellung durch Systemadministrator gesperrt.",
"Settings": "Einstellungen", "Settings": "Einstellungen",
"Shortcuts": "Kurzbefehle", "Shortcuts": "Kurzbefehle",
"Show app icon in system tray": "App-Icon in Systemleiste anzeigen", "Show app icon in system tray": "App-Icon in Systemleiste anzeigen",
"Show app unread badge": "Anzahl ungelesener Nachrichten in App-Icon einblenden",
"Show desktop notifications": "Desktopbenachrichtigungen anzeigen", "Show desktop notifications": "Desktopbenachrichtigungen anzeigen",
"Show sidebar": "Seitenleiste anzeigen", "Show sidebar": "Seitenleiste anzeigen",
"Show unread count badge on app icon": "Anzahl ungelesener Nachrichten auf dem App-Symbol anzeigen",
"Spellchecker Languages": "Sprachen für die Rechtschreibprüfung", "Spellchecker Languages": "Sprachen für die Rechtschreibprüfung",
"Start app at login": "App beim Login automatisch starten", "Start app at login": "App beim Login automatisch starten",
"Switch to Next Organization": "Zur nächsten Organisation wechseln", "Switch to Next Organization": "Zur nächsten Organisation wechseln",
"Switch to Previous Organization": "Zur vorhergehenden Organisation wechseln", "Switch to Previous Organization": "Zur vorhergehenden Organisation wechseln",
"The custom CSS previously set is deleted.": "Das zuvor eingestellte eigene CSS ist gelöscht.",
"The server presented an invalid certificate for {{{origin}}}:\n\n{{{error}}}": "Der Server hat ein ungültiges Zertifikat für {{{origin}}} vorgelegt:\n\n{{{error}}}",
"The update will be downloaded in the background. You will be notified when it is ready to be installed.": "Das Update wird im Hintegrund heruntergeladen. Du wirst benachrichigt, wenn es zur Installation bereit ist.",
"There was an error while saving the new organization. You may have to add your previous organizations again.": "Beim Speichern der neuen Organisation ist ein Fehler aufgetreten. Möglicherweise musst du deine vorherigen Organisationen erneut hinzufügen.",
"These desktop app shortcuts extend the Zulip webapp's": "Dies sind zusätzliche Kurzbefehle in der Desktop-App gegenüber der Web-App", "These desktop app shortcuts extend the Zulip webapp's": "Dies sind zusätzliche Kurzbefehle in der Desktop-App gegenüber der Web-App",
"Tip": "Tipp", "Tip": "Tipp",
"Toggle DevTools for Active Tab": "Entwickler-Tools für aktiven Tab umschalten", "Toggle DevTools for Active Tab": "Entwickler-Tools für aktiven Tab umschalten",
@@ -109,19 +165,31 @@
"Toggle Sidebar": "Seitenleiste umschalten", "Toggle Sidebar": "Seitenleiste umschalten",
"Toggle Tray Icon": "Tray-Icon umschalten", "Toggle Tray Icon": "Tray-Icon umschalten",
"Tools": "Extras", "Tools": "Extras",
"Unable to check for updates.": "Update-Verfügbarkeit konnte nicht geprüft werden.",
"Unable to download the update.": "Update konnte nicht heruntergeladen werden.",
"Undo": "Rückgängig", "Undo": "Rückgängig",
"Unhide": "Nicht mehr verbergen", "Unhide": "Nicht mehr verbergen",
"Unknown error": "Unbekannter Fehler",
"Upload": "Hochladen", "Upload": "Hochladen",
"Use system proxy settings (requires restart)": "Systemweite Proxy-Einstellungen verwenden (erfordert Neustart)", "Use system proxy settings (requires restart)": "Systemweite Proxy-Einstellungen verwenden (erfordert Neustart)",
"Verify that it works and then click Reconnect.": "Prüfe, das es funktioniert und klicke auf Wiederverbinden.",
"View": "Ansicht", "View": "Ansicht",
"View Shortcuts": "Tastenkürzel anzeigen", "View Shortcuts": "Tastenkürzel anzeigen",
"We encountered an error while saving the update notifications.": "Beim Speichern der Update-Benachrichtigungen ist ein Fehler aufgetreten.",
"When the application restarts, it will be as if you have just downloaded the Zulip app.": "Wenn die Anwendung neu startet, wird es sein als hättest du die Zulip-App erst heruntergeladen.",
"Window": "Fenster", "Window": "Fenster",
"Window Shortcuts": "Kurzbefehle für Fenster", "Window Shortcuts": "Kurzbefehle für Fenster",
"YES": "JA", "Yes": "Ja",
"You are running the latest version of Zulip Desktop.\nVersion: {{{version}}}": "Du verwendest die neueste Version von Zulip Desktop.\nVersion: {{{version}}}",
"You can select a maximum of 3 languages for spellchecking.": "Du kannst höchstens 3 Sprachen für die Rechtschreibprüfung auswählen.", "You can select a maximum of 3 languages for spellchecking.": "Du kannst höchstens 3 Sprachen für die Rechtschreibprüfung auswählen.",
"Your internet connection doesn't seem to work properly!": "Deine Internetverbindung scheint nicht ordentlich zu funktionieren!",
"Zoom In": "Vergrößern", "Zoom In": "Vergrößern",
"Zoom Out": "Verkleinern", "Zoom Out": "Verkleinern",
"Zulip": "Zulip",
"Zulip Update": "Zulip-Aktualisierung",
"keyboard shortcuts": "Tastenkürzel", "keyboard shortcuts": "Tastenkürzel",
"script": "Skript", "your-organization.zulipchat.com or zulip.your-organization.com": "your-organization.zulipchat.com oder zulip.your-organization.com",
"{number, plural, one {# unread message} other {# unread messages}}": "{number, plural, one {# ungelesene Nachricht} other {# ungelesene Nachrichten}}",
"{number, plural, one {Could not add # organization} other {Could not add # organizations}}": "{number, plural, one {konnte # Organisation nicht hinzufügen} other {konnte # Organisationen nicht hinzufügen}}",
"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "Auf {{{server}}} läuft die nicht mehr aktuelle Version {{{version}}} von Zulip Server. Es kann sein, dass diese Anwendung damit nicht vollständig funktioniert." "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.": "Auf {{{server}}} läuft die nicht mehr aktuelle Version {{{version}}} von Zulip Server. Es kann sein, dass diese Anwendung damit nicht vollständig funktioniert."
} }

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