Compare commits

...

294 Commits

Author SHA1 Message Date
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
Anders Kaseorg
e90f3732c5 release: New release v5.10.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-13 23:22:25 -07:00
Anders Kaseorg
6b31a8a0c4 workflows: Update actions/checkout to v4.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-13 23:22:25 -07:00
Anders Kaseorg
f8758fa303 Use electron fetch API.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-13 23:22:25 -07:00
Anders Kaseorg
d2de965106 translations: Update translations from Transifex.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-13 23:19:31 -07:00
Anders Kaseorg
a32119b55d Upgrade dependencies, including Electron 26.2.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-13 23:19:31 -07:00
Anders Kaseorg
58049a91c4 Upgrade xo and prettier.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-13 22:47:15 -07:00
Anders Kaseorg
9810d69c3b renderer: Compensate for Chrome’s removal of overflow: overlay.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-13 22:47:15 -07:00
Anders Kaseorg
d2f949d683 Use Electron Event type.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-13 19:15:08 -07:00
Anders Kaseorg
a8c283a50b renderer: Remove unused reloadView argument.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-13 19:15:08 -07:00
nooblag
dab29d4720 renderer: Improve GIF loading spinner with new SVG. 2023-09-13 19:15:08 -07:00
Anders Kaseorg
7fba8cfae9 release: New release v5.10.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 16:15:08 -07:00
Anders Kaseorg
32301656cc Upgrade dependencies, including Electron 24.2.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 16:15:04 -07:00
Anders Kaseorg
0e16283a37 stylelint: Fix declaration-block-no-redundant-longhand-properties.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 16:15:04 -07:00
Anders Kaseorg
d86482a804 stylelint: Fix media-feature-range-notation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 16:15:04 -07:00
Anders Kaseorg
3af350e4dc translations: Update translations from Transifex.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 16:14:26 -07:00
Anders Kaseorg
39fc2053c5 translations: Update en.json.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 16:10:46 -07:00
Anders Kaseorg
044f1fd0f9 preference: Fix server icon display in connected organizations list.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 15:30:51 -07:00
Anders Kaseorg
10fb0a82f9 preload: Drop compatibility code for Zulip Server < 4.0.
The server was updated in bfd9999cf874e506592fda254dfe0fe06b5b2738
(4.0-rc1~2192) to expose a proper API for this functionality, so we
don’t need to trigger fake click events to access it.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:41:25 -07:00
Anders Kaseorg
123bd5b2c0 preload: Drop compatibility injected JS for Zulip Server < 3.0.
The server was updated in a6fee2f18ef9d2ef6ac248e9ed82d580daff1a07
(3.0-dev~1674) and e701f208619b8b9b28a85f84ee16cf8d8df82b72
(3.0-dev~1667) to avoid relying on this wrapper.  We no longer support
servers older than 3.0, so we can delete it.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:39:41 -07:00
Anders Kaseorg
ad771c3da8 Display a banner for unsupported Zulip Server versions.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:37:32 -07:00
Anders Kaseorg
4c58bc3aa3 webview: Add a wrapper pane around the real <webview>.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:35:14 -07:00
Anders Kaseorg
9a8680d209 webview: Use private methods.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:35:14 -07:00
Anders Kaseorg
1569890f4d webview: Use private members.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:35:14 -07:00
Anders Kaseorg
2ed400c23c webview: Add destroy method.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:35:14 -07:00
Anders Kaseorg
70621431dc translations: Update translations from Transifex.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:32:11 -07:00
Anders Kaseorg
55b7e09796 tx: Migrate configuration to current Transifex CLI.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:28:06 -07:00
Anders Kaseorg
de2829a968 translations: Update en.json.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:23:21 -07:00
Anders Kaseorg
296de41779 translation-util: Expose the full functionality of __.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:23:21 -07:00
Anders Kaseorg
8b9ebeee25 Fix more typos.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-05 14:23:21 -07:00
Anders Kaseorg
76e81ca337 Fix updating of server names and icons at startup.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-04 15:12:18 -07:00
Anders Kaseorg
2e7a9bb4ed server-tab: Encapsulate setName and setIcon.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-05-04 15:12:18 -07:00
Anders Kaseorg
77638f6287 Fix handling of server icon updates and errors.
Fixes #1283.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-04-21 15:37:28 -07:00
Anders Kaseorg
6e8fe36876 Fix typos.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-04-19 13:50:40 -07:00
Anders Kaseorg
2eea4a32a5 preference: Fix CSS in Vite dev mode.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-04-18 14:21:06 -07:00
Anders Kaseorg
677dfe425c xo: Remove redundant exclusion of unicorn/prefer-json-parse-buffer.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-04-18 14:03:45 -07:00
Anders Kaseorg
1da3ec545a Don’t show visual notifications when they’re turned off.
Fixes #1299.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-04-18 13:13:12 -07:00
Anders Kaseorg
3cb6ea4694 Handle exceptions when reading server icons.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-04-18 12:49:09 -07:00
Anders Kaseorg
0cb7297017 preference: Fix spellchecker languages dropdown positioning.
Apparently the Tagify defaults don’t work inside a shadow root.

Fixes #1286.  Closes #1290.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-04-04 15:24:22 -07:00
Anders Kaseorg
b8d7003446 Use Zod 3 style for importing Zod.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-03-04 00:23:00 -08:00
Anders Kaseorg
6d27cf8c7d release: New release v5.9.5.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 21:17:32 -08:00
Anders Kaseorg
1ac2483cc4 Upgrade dependencies, including Electron 22.2.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 21:14:43 -08:00
Anders Kaseorg
4d3420dcd0 vite: Externalize gatemaker.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 21:13:23 -08:00
Anders Kaseorg
38450a9aed vite: Don’t externalize dependencies.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 19:14:19 -08:00
Anders Kaseorg
24de7ebb97 webview: Remove did-navigate workaround
The Electron bug seems to have been fixed upstream.  Meanwhile, the
workaround had been causing the app to hang if it can’t connect to an
organization at startup.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:40 -08:00
Anders Kaseorg
5a571d66d0 Enable Chromium sandboxing for remote webviews.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
0ae998a51e Move clipboard decryption to main process.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
447dd18b8b Read injected.js from main process.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
9a200dc40c Replace remote wrapper module with Vite alias.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
d42b752ac1 Bundle with Vite.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
2f4103248d Move icons and sounds to public/resources.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
985d731d2b Move translations to public/translations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
032f95150c renderer: Add async constructors for functional tabs.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
d1aa5778c3 renderer: Set the icon src to a data: URL.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
Anders Kaseorg
13ce24b75e webview: Remove unnecessary __dirname resolution of customCss.
We’ve already checked that the file exists without resolving via
__dirname.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-02-06 18:57:22 -08:00
fwcd
c89ec2faf1 Update installation instructions for macOS 2023-02-01 21:50:19 -08:00
Anders Kaseorg
56ab0833b8 release: New release v5.9.4.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-04 16:29:48 -08:00
Anders Kaseorg
c62b393c52 Set quarantine attribute for downloads on macOS.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-04 16:12:31 -08:00
Anders Kaseorg
991de77cad Restore default macOS security settings.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 20:36:13 -08:00
Anders Kaseorg
94780c44c8 handle-external-link: Ignore invalid URLs.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 18:06:36 -08:00
Anders Kaseorg
82542a6390 packaging: Synchronize deb-after-install.sh with upstream.
https://github.com/electron-userland/electron-builder/blob/v23.6.0/packages/app-builder-lib/templates/linux/after-install.tpl

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 17:14:50 -08:00
Anders Kaseorg
53ff8443dc Upgrade dependencies, including Electron 22.0.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 16:17:24 -08:00
Anders Kaseorg
3855ecab58 Disable sandboxing for now.
Sandboxing will default to enabled in Electron ≥ 20, but we don’t
support it yet.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 16:17:24 -08:00
Anders Kaseorg
a57cbb4aa8 package.json: Bump engines to node ≥ 16.13.2.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 16:05:28 -08:00
Anders Kaseorg
56a4461c2a xo: Fix n/file-extension-in-import, maybe.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 16:05:28 -08:00
Anders Kaseorg
cd023ec5ab xo: Fix @typescript-eslint/consistent-type-definitions.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 16:05:28 -08:00
Anders Kaseorg
1aa4ade3c0 xo: Fix @typescript-eslint/parameter-properties.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 16:05:28 -08:00
Anders Kaseorg
dcb46eef4f xo: Fix @typescript-eslint/no-useless-empty-export.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 16:05:28 -08:00
Anders Kaseorg
e3e8ef6e3e xo: Fix @typescript-eslint/consistent-generic-constructors.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 16:05:28 -08:00
Anders Kaseorg
6808b1971a xo: Fix unicorn/switch-case-braces.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 16:05:28 -08:00
Anders Kaseorg
1dd5269549 xo: Fix unicorn/prefer-node-protocol.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 16:05:28 -08:00
Anders Kaseorg
d33adca1e8 xo: Fix unicorn/prefer-logical-operator-over-ternary.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 16:05:28 -08:00
Anders Kaseorg
8ea7f7864f autoupdater: Add const assertion.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 14:09:17 -08:00
Anders Kaseorg
493ae06e52 Reformat with Prettier.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-01-03 14:08:23 -08:00
Anders Kaseorg
2b8f3536d3 Fix E2E tests broken by chat.zulip.org web-public streams.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-12-14 22:41:35 -08:00
Anders Kaseorg
544d23ec09 how-to-install: Update APT instructions.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-07-15 15:36:41 -07:00
Anders Kaseorg
588d32fd22 release: New release v5.9.3.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-28 20:25:15 -07:00
Anders Kaseorg
1c471fe624 Upgrade dependencies, including Electron 18.2.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-28 20:06:46 -07:00
Anders Kaseorg
52486d687d Allow the autoupdater to quit the app normally.
Forcing it to quit would prematurely terminate the update on some
platforms.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-28 19:51:07 -07:00
Anders Kaseorg
73441d791c release: New release v5.9.2.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-20 21:23:00 -07:00
Anders Kaseorg
1bb6423721 Upgrade dependencies, including Electron 18.1.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-20 19:26:47 -07:00
Anders Kaseorg
d6775d64a3 release: New release v5.9.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-08 17:20:31 -07:00
Anders Kaseorg
e1326eae91 sentry: Update DSN.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-08 17:18:48 -07:00
Anders Kaseorg
b93955b28f Upgrade dependencies, including Electron 18.0.3.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-08 17:10:59 -07:00
Anders Kaseorg
e3452bda22 Simplify if (…) classList.add(…) else classList.remove(…) anti-pattern.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-02 14:34:58 -07:00
Anders Kaseorg
0aab691b44 Switch to released @electron/remote.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-01 21:04:51 -07:00
Anders Kaseorg
1bfb2dd975 release: New release v5.9.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-01 17:25:21 -07:00
Anders Kaseorg
fb7937314b Upgrade dependencies.
electron-builder@next is needed to build a DMG on macOS 12.3.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-04-01 14:24:22 -07:00
Anders Kaseorg
e39d2a9b95 xo: Fix unicorn/prefer-node-protocol.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-31 21:52:32 -07:00
Anders Kaseorg
3b04b61662 Upgrade dependencies, including Electron 18.0.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-31 21:21:21 -07:00
Anders Kaseorg
829b2a0f2a package-lock.json: Upgrade to lockfileVersion 2.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-31 21:13:41 -07:00
Anders Kaseorg
5edffbdf21 Move handleExternalLink to main process.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-31 21:10:13 -07:00
Anders Kaseorg
27576c95e6 Skip unnecessary remote for clipboard, nativeImage, shell.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-14 21:48:44 -07:00
Anders Kaseorg
5acc45cba4 Use process-specific electron/{main,renderer,common} imports.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-14 21:38:18 -07:00
Anders Kaseorg
343e0ed848 xo: Simplify configuration.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-14 20:47:40 -07:00
Anders Kaseorg
0c784b12fa WebView: Enable allowpopups.
This is required for Electron ≥ 15 to continue invoking our new window
handler (handleExternalLink), following the nativeWindowOpen
migration.

https://github.com/electron/electron/issues/30886

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-11 18:23:39 -08:00
Anders Kaseorg
2b50b21752 tsconfig: Downgrade target to ES2021.
The ES2022 definition of Error#cause conflicts with @types/verror.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-09 15:54:05 -08:00
Anders Kaseorg
ad604f020d tsconfig: Remove lib setting.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-09 15:09:31 -08:00
Anders Kaseorg
4151e020f6 Revert "xo: Fix import/extensions."
This reverts commit 5623ab3866.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:58:40 -08:00
Anders Kaseorg
bc59714192 xo: Fix @typescript-eslint/naming-convention.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:24:49 -08:00
Anders Kaseorg
b43a7b6809 xo: Fix unicorn/template-indent.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:15:32 -08:00
Anders Kaseorg
fba8aa0ab0 xo: Fix object-shorthand.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:15:32 -08:00
Anders Kaseorg
5623ab3866 xo: Fix import/extensions.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:15:32 -08:00
Anders Kaseorg
a4fbf9bd28 stylelint: Fix shorthand-property-no-redundant-values.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:15:32 -08:00
Anders Kaseorg
db730da45c stylelint: Ignore selector-id-pattern for #nav-AddServer.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:15:32 -08:00
Anders Kaseorg
b5a938d3b0 stylelint: Ignore selector-class-pattern for .__tagify_input.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:15:32 -08:00
Anders Kaseorg
863d1e25ba stylelint: Fix keyframes-name-pattern.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:15:32 -08:00
Anders Kaseorg
a90aaeb86c stylelint: Fix function-url-quotes.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:15:32 -08:00
Anders Kaseorg
8b6af78f2a stylelint: Fix font-family-name-quotes. 2022-03-08 21:15:32 -08:00
Anders Kaseorg
6c2dcb450b stylelint: Fix alpha-value-notation, color-function-notation. 2022-03-08 21:15:32 -08:00
Anders Kaseorg
f57962d02f .stylelintrc: Format with Prettier.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:15:32 -08:00
Anders Kaseorg
2983c381ae Fix Electron.Session type.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:15:32 -08:00
Anders Kaseorg
1ea7fa813a Remove redundant webPreferences defaults.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 21:15:22 -08:00
Anders Kaseorg
e434c5b5d0 Untangle Sentry initialization.
Thanks to upstream for the helpful advice at
https://github.com/getsentry/sentry-electron/issues/427.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 16:55:36 -08:00
Anders Kaseorg
9c1f47badd Move server manager view to the default session.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 16:55:23 -08:00
Anders Kaseorg
4ed4328bf8 Toggle spell checker in the session rather than the webPreferences.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-08 16:05:54 -08:00
Anders Kaseorg
c6022e94bb main: Enable contextIsolation for BrowserWindow.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
06eb169c65 WebView: Restrict $el type to HTMLElement.
The extra methods on WebviewTag are not available from the
context-isolated preload script.
https://github.com/electron/electron/issues/26904

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
2f7529cd71 WebView: Get event parameters via WebContents rather than WebviewTag.
Works around https://github.com/electron/electron/issues/31924.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
3a8541f601 WebView: Call getWebContentsId in main world.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
0eb910b2e8 WebView: Use send method.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
76a879e4fd WebView: Convert WebviewTag methods to WebContents methods.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
7026e43575 WebView: Add getWebContents method.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
869361bac3 WebView: Type $el as required.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
832ea3c04e WebView: Remove async from send method.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
68232f966e WebView: Wait for did-navigate before constructing WebView.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
86b7da45ef WebView: Use a better focus() workaround.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
b853856317 WebView: Add factory function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
6676f1c6ac WebView: Switch templateHTML to a static method.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
e0243bc460 main: Disable nodeIntegration for BrowserWindow.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
fd6cb548f8 WebView: Remove nodeIntegration parameter.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
743b2d6054 WebView: Make preload a string.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
fb5c6b365e css: Simplify webview CSS.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
f092e99f42 css: Remove the melodramatic fade-in animation on load.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
751eb6ef98 Switch electron.remote to @electron/remote.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-04 16:56:44 -08:00
Anders Kaseorg
980de649e3 common: Factor out electron.remote pattern to a module.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:42:04 -08:00
Anders Kaseorg
84849d2c84 Move functional tab pages out of separate webviews.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:42:04 -08:00
Anders Kaseorg
b263997bed tray: Move initialization to a function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:07:37 -08:00
Anders Kaseorg
12c773bc71 tray: Be robust in case there’s no active webview.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:07:33 -08:00
Anders Kaseorg
d937539618 renderer: Restrict webview functions to ServerTab instances.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:07:28 -08:00
Anders Kaseorg
0a5d07f839 renderer: Inline FunctionalTabProps type.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:07:23 -08:00
Anders Kaseorg
5dcd3956ac preference: Unify duplicate toggle-sidebar-setting event.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:07:21 -08:00
Anders Kaseorg
3ffc7251f4 preference: Unify duplicate toggle-menubar-setting event.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:06:09 -08:00
Anders Kaseorg
7fb0cfd176 WebView: Remove redundant name property.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:05:25 -08:00
Anders Kaseorg
5c83952ba1 webview: Remove forceLoad method.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:04:29 -08:00
Anders Kaseorg
a7a051bb2a renderer: Remove dead show-network-error message.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:04:21 -08:00
Anders Kaseorg
2b2c5dbe5c about: Encapsulate in a custom element.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 16:04:20 -08:00
Anders Kaseorg
ffe87a9729 preference: Encapsulate in a custom element.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-28 15:58:36 -08:00
Anders Kaseorg
b366195415 Upgrade playwright-core.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-17 22:57:19 -08:00
Anders Kaseorg
f9f2b20e90 preference: Use querySelector relative to $root.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-17 22:45:10 -08:00
Anders Kaseorg
e16811065d css: Extract font definitions to fonts.css.
This works around
https://bugs.chromium.org/p/chromium/issues/detail?id=336876.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-10 00:14:00 -08:00
Anders Kaseorg
f66a1127de electron-bridge: Remove console.log debugging spew.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-03 23:09:50 -08:00
Anders Kaseorg
06ef60c4c2 notification: Remove BaseNotification wrapper class.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-03 23:02:37 -08:00
Anders Kaseorg
4b93298b58 notification: Set the AppUserModelId from the main process.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-03 22:55:15 -08:00
Anders Kaseorg
a41a771923 notification: Don’t use remote for focusCurrentServer.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-03 22:54:38 -08:00
Anders Kaseorg
a43f7d9bcf Fix glob usage in package scripts.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-02-03 03:02:07 -08:00
Anders Kaseorg
c9453f877b config-schemata: Remove unused systemProxyRules setting.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-11-23 17:55:32 -08:00
Anders Kaseorg
525fa94b18 Fix system proxy resolution.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-11-23 17:51:51 -08:00
Anders Kaseorg
460b9e5e55 main: Remove dead code for recreating main window.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-11-23 16:12:15 -08:00
Anders Kaseorg
8fc41a7ca8 system-util: Remove getOS wrapper.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-11-22 15:56:58 -08:00
Anders Kaseorg
4c7b9cf4e3 server-tab: Delete space in macOS shortcut text.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-11-22 15:50:32 -08:00
Anders Kaseorg
f4479dfda4 tests: Migrate E2E tests to Playwright.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-11-19 15:50:16 -08:00
Anders Kaseorg
377f08ad5d Fix unread count parsing from page title.
Fixes #1157

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-10-27 16:42:46 -07:00
Anders Kaseorg
add43bafda Fix ‘npm run prettier-non-js’ on Windows.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-10-10 23:15:09 -07:00
Anders Kaseorg
b35d45955b WebView: Move initialization from dom-ready event to did-attach event.
This fixes the bug where the context menu would disappear immediately
if the page had been loaded an even number of times.

Fixes #662, fixes #991, fixes #1010.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-10-08 18:10:43 -07:00
Anders Kaseorg
2ecb970da0 Revert "webview: fix focus after soft reload."
This reverts commit 6b98a49245 (#698).

The bug it worked around was fixed upstream in Electron 9.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-10-08 16:32:10 -07:00
Anders Kaseorg
edb2933dad Remove .prettierignore.non-js.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-10-06 16:13:47 -07:00
Anders Kaseorg
8141927974 tests: Remove dynamic package.json generation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-10-06 16:07:29 -07:00
Anders Kaseorg
4db89ac3a7 typescript: Enable noImplicitOverride.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-09-10 21:52:32 -07:00
Anders Kaseorg
feb67e6c2d Deglobalize ElectronBridge type.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-09-01 14:04:51 -07:00
Anders Kaseorg
014e97b563 Remove feedback widget.
@electron-elements/send-feedback won’t work with Electron 14, and all
it ever did was open your mail client.  Have the “Report an Issue”
menu item direct users to our website instead.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-08-30 19:04:20 -07:00
Anders Kaseorg
a3f4e19aa2 autoupdater: Avoid deprecated log.FileTransport.file.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-08-30 14:18:31 -07:00
Anders Kaseorg
90a65ab6cc release: New release v5.8.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-29 19:26:30 -07:00
Anders Kaseorg
c00e1618e7 Downgrade electron-updater to 4.3.5.
Newer electron-updater versions are broken on macOS by
https://github.com/electron-userland/electron-builder/issues/5935 as
well as another issue that has not yet been diagnosed.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-29 19:26:30 -07:00
Anders Kaseorg
ceb6417979 Replace Linux zip build with tar.xz.
The filename of the Linux zip now conflicts with the macOS zip needed
by the auto-updater, and zip isn’t a usual format for Linux anyway.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 23:53:40 -07:00
Anders Kaseorg
1d40ebb65f release: New release v5.8.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 17:10:10 -07:00
Anders Kaseorg
6301427ef4 Fix Windows MSI filename.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 17:06:31 -07:00
Anders Kaseorg
64d1d6c88d Build arm64 pkg for macOS.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 16:33:37 -07:00
Anders Kaseorg
adcacd7d45 Tighten tab role type.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 15:46:40 -07:00
Anders Kaseorg
b6729b0d0a menu: Skip missing elements of tabs array.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 15:31:14 -07:00
Anders Kaseorg
ec7d5b4046 Upgrade dependencies, including Electron 13.1.7.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:45:09 -07:00
Anders Kaseorg
380ea3a891 tests: Add extension to .js imports.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:45:09 -07:00
Anders Kaseorg
320e152897 xo: Fix unicorn/numeric-separators-style.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:45:09 -07:00
Anders Kaseorg
c00d0abe0d enterprise-util: Use zod for type-safe validation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:45:09 -07:00
Anders Kaseorg
aaa83da0f8 config-util: Use zod for type-safe validation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:45:09 -07:00
Anders Kaseorg
494e716dfe domain-util: Use zod for type-safe validation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:45:09 -07:00
Anders Kaseorg
50c266295e linux-update-util: Strongly type update items.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:45:06 -07:00
Anders Kaseorg
55a6122a6c general-section: Use zod for type-safe validation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:37:31 -07:00
Anders Kaseorg
2a648b79c9 linuxupdater: Use zod for type-safe validation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:37:31 -07:00
Anders Kaseorg
0bc49bf723 request: Use zod for type-safe validation.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:37:31 -07:00
Anders Kaseorg
cb7d1faa52 main: Annotate permissionCallbacks.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:37:31 -07:00
Anders Kaseorg
fa3c744e76 displayInitialCharLogo: Fix incorrect cast.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:37:31 -07:00
Anders Kaseorg
54be4dccce injected: Specify explicit type for cast.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:37:31 -07:00
Anders Kaseorg
6a407d0e42 preload: Fix weird event.target usage.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-21 13:37:31 -07:00
Anders Kaseorg
47171fffd5 Fix spell checker on macOS.
Although ses.setSpellCheckerLanguages is documented as a no-op on macOS,
ses.setSpellCheckerLanguages([]) actually disables spell checking as of
Electron 8.1.0 (https://github.com/electron/electron/issues/30215).
This effect is persistent in our persistent session, so we attempt to
undo it by copying the language list from the main BrowserWindow.

(Before commit 892f7c8e47 we were running
ses.setSpellCheckerLanguages(null), which just crashed with “TypeError:
Error processing argument at index 0, conversion failure from null”.)

Fixes #1132.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-20 16:47:05 -07:00
Anders Kaseorg
e48c9067a3 Upgrade Prettier to 2.3.2.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-07-05 16:55:47 -07:00
Anders Kaseorg
1d30c83f7a Revert "Added log-out shortcut"
This reverts commit 2a477abe5f.

This is not a common operation that needs a keyboard shortcut, and
it’s too easy to invoke by accident.  Fixes #1115.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-05-24 11:31:09 -07:00
Anders Kaseorg
9f76fb295e Remove color profile override.
Modern Chromium and Electron do color management correctly.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-05-02 14:16:10 -07:00
210 changed files with 23941 additions and 16655 deletions

View File

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

View File

@@ -1,18 +1,49 @@
---
<!-- Describe your pull request here.-->
<!--
Remove the fields that are not appropriate
Please include:
Fixes: <!-- Issue link, or clear description.-->
<!-- 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?**
**Screenshots?**
**You have tested this PR on:**
**Platforms this PR was tested on:**
- [ ] Windows
- [ ] Linux/Ubuntu
- [ ] 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:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v4
with:
node-version: lts/*
- uses: actions/checkout@v4
- run: npm ci
- run: npm test

9
.gitignore vendored
View File

@@ -4,11 +4,9 @@
# npm cache directory
.npm
# transifexrc - if user prefers it to be in working tree
.transifexrc
# Compiled binary build directory
dist/
/dist/
/dist-electron/
#snap generated files
snap/parts
@@ -39,6 +37,3 @@ config.gypi
# tests/package.json
.python-version
# Ignore all the typescript compiled files
app/**/*.js

1
.npmrc Normal file
View File

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

View File

@@ -1,3 +1,3 @@
/app/**/*.js
/app/translations/*.json
/dist
/dist-electron
/public/translations/*.json

View File

@@ -1,4 +0,0 @@
*.js
*.ts
/app/translations/*.json
/dist

View File

@@ -1,9 +1,12 @@
{
"extends": ["stylelint-config-standard", "stylelint-config-prettier"],
"rules": {
"color-named": "never",
"color-no-hex": true,
"font-family-no-missing-generic-family-keyword": [true, {"ignoreFontFamilies": ["Material Icons"]}],
"selector-type-no-unknown": [true, {"ignoreTypes": ["send-feedback", "webview"]}],
}
"extends": ["stylelint-config-standard"],
"rules": {
"color-named": "never",
"color-no-hex": true,
"font-family-no-missing-generic-family-keyword": [
true,
{"ignoreFontFamilies": ["Material Icons"]}
],
"selector-type-no-unknown": [true, {"ignoreTypes": ["webview"]}]
}
}

View File

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

View File

@@ -10,7 +10,7 @@ Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If yo
## Community
- The whole Zulip documentation, such as setting up a development environment, setting up with the Zulip webapp project, and testing, can be read [here](https://zulip.readthedocs.io).
- The whole Zulip documentation, such as setting up a development environment, setting up with the Zulip web app project, and testing, can be read [here](https://zulip.readthedocs.io).
- If you have any questions regarding zulip-desktop, open an [issue](https://github.com/zulip/zulip-desktop/issues/new/) or ask it on [chat.zulip.org](https://chat.zulip.org/#narrow/stream/16-desktop).

View File

@@ -1,6 +1,6 @@
# 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)
[![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)
@@ -24,9 +24,9 @@ Please see the [installation guide](https://zulip.com/help/desktop-app-install-g
# Reporting issues
This desktop client shares most of its code with the Zulip webapp.
This desktop client shares most of its code with the Zulip web app.
Issues in an individual organization's Zulip window should be reported
in the [Zulip server and webapp
in the [Zulip server and web app
project](https://github.com/zulip/zulip/issues/new). Other
issues in the desktop app and its settings should be reported [in this
project](https://github.com/zulip/zulip-desktop/issues/new).

View File

@@ -0,0 +1,45 @@
import {z} from "zod";
export const dndSettingsSchemata = {
showNotification: z.boolean(),
silent: z.boolean(),
flashTaskbarOnMessage: z.boolean(),
};
export const configSchemata = {
...dndSettingsSchemata,
appLanguage: z.string().nullable(),
autoHideMenubar: z.boolean(),
autoUpdate: z.boolean(),
badgeOption: z.boolean(),
betaUpdate: z.boolean(),
// eslint-disable-next-line @typescript-eslint/naming-convention
customCSS: z.string().or(z.literal(false)).nullable(),
dnd: z.boolean(),
dndPreviousSettings: z.object(dndSettingsSchemata).partial(),
dockBouncing: z.boolean(),
downloadsPath: z.string(),
enableSpellchecker: z.boolean(),
errorReporting: z.boolean(),
lastActiveTab: z.number(),
promptDownload: z.boolean(),
proxyBypass: z.string(),
// eslint-disable-next-line @typescript-eslint/naming-convention
proxyPAC: z.string(),
proxyRules: z.string(),
quitOnClose: z.boolean(),
showSidebar: z.boolean(),
spellcheckerLanguages: z.string().array().nullable(),
startAtLogin: z.boolean(),
startMinimized: z.boolean(),
trayIcon: z.boolean(),
useManualProxy: z.boolean(),
useProxy: z.boolean(),
useSystemProxy: z.boolean(),
};
export type ConfigSchemata = typeof configSchemata;
export const enterpriseConfigSchemata = {
...configSchemata,
presetOrganizations: z.string().array(),
};

View File

@@ -1,85 +1,64 @@
import electron from "electron";
import fs from "fs";
import path from "path";
import fs from "node:fs";
import path from "node:path";
import * as Sentry from "@sentry/core";
import {JsonDB} from "node-json-db";
import {DataError} from "node-json-db/dist/lib/Errors.js";
import type {z} from "zod";
import {app, dialog} from "zulip:remote";
import type {DNDSettings} from "./dnd-util";
import * as EnterpriseUtil from "./enterprise-util";
import Logger from "./logger-util";
import {type ConfigSchemata, configSchemata} from "./config-schemata.ts";
import * as EnterpriseUtil from "./enterprise-util.ts";
import Logger from "./logger-util.ts";
export interface Config extends DNDSettings {
appLanguage: string | null;
autoHideMenubar: boolean;
autoUpdate: boolean;
badgeOption: boolean;
betaUpdate: boolean;
customCSS: string | false | null;
dnd: boolean;
dndPreviousSettings: Partial<DNDSettings>;
dockBouncing: boolean;
downloadsPath: string;
enableSpellchecker: boolean;
errorReporting: boolean;
lastActiveTab: number;
promptDownload: boolean;
proxyBypass: string;
proxyPAC: string;
proxyRules: string;
quitOnClose: boolean;
showSidebar: boolean;
spellcheckerLanguages: string[] | null;
startAtLogin: boolean;
startMinimized: boolean;
systemProxyRules: string;
trayIcon: boolean;
useManualProxy: boolean;
useProxy: boolean;
useSystemProxy: boolean;
}
/* To make the util runnable in both main and renderer process */
const {app, dialog} = process.type === "renderer" ? electron.remote : electron;
export type Config = {
[Key in keyof ConfigSchemata]: z.output<ConfigSchemata[Key]>;
};
const logger = new Logger({
file: "config-util.log",
});
let db: JsonDB;
let database: JsonDB;
reloadDB();
reloadDatabase();
export function getConfigItem<Key extends keyof Config>(
key: Key,
defaultValue: Config[Key],
): Config[Key] {
): z.output<ConfigSchemata[Key]> {
try {
db.reload();
database.reload();
} catch (error: unknown) {
logger.error("Error while reloading settings.json: ");
logger.error(error);
}
const value = db.getData("/")[key];
if (value === undefined) {
try {
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) {
if (!(error instanceof DataError)) throw error;
setConfigItem(key, defaultValue);
return defaultValue;
}
return value;
}
// This function returns whether a key exists in the configuration file (settings.json)
export function isConfigItemExists(key: string): boolean {
try {
db.reload();
database.reload();
} catch (error: unknown) {
logger.error("Error while reloading settings.json: ");
logger.error(error);
}
const value = db.getData("/")[key];
return value !== undefined;
return database.exists(`/${key}`);
}
export function setConfigItem<Key extends keyof Config>(
@@ -92,16 +71,17 @@ export function setConfigItem<Key extends keyof Config>(
return;
}
db.push(`/${key}`, value, true);
db.save();
configSchemata[key].parse(value);
database.push(`/${key}`, value, true);
database.save();
}
export function removeConfigItem(key: string): void {
db.delete(`/${key}`);
db.save();
database.delete(`/${key}`);
database.save();
}
function reloadDB(): void {
function reloadDatabase(): void {
const settingsJsonPath = path.join(
app.getPath("userData"),
"/config/settings.json",
@@ -118,9 +98,9 @@ function reloadDB(): void {
);
logger.error("Error while JSON parsing settings.json: ");
logger.error(error);
logger.reportSentry(error);
Sentry.captureException(error);
}
}
db = new JsonDB(settingsJsonPath, true, true);
database = new JsonDB(settingsJsonPath, true, true);
}

View File

@@ -1,34 +1,33 @@
import electron from "electron";
import fs from "fs";
import fs from "node:fs";
const {app} = process.type === "renderer" ? electron.remote : electron;
import {app} from "zulip:remote";
let setupCompleted = false;
const zulipDir = app.getPath("userData");
const logDir = `${zulipDir}/Logs/`;
const configDir = `${zulipDir}/config/`;
const zulipDirectory = app.getPath("userData");
const logDirectory = `${zulipDirectory}/Logs/`;
const configDirectory = `${zulipDirectory}/config/`;
export const initSetUp = (): void => {
// If it is the first time the app is running
// create zulip dir in userData folder to
// avoid errors
if (!setupCompleted) {
if (!fs.existsSync(zulipDir)) {
fs.mkdirSync(zulipDir);
if (!fs.existsSync(zulipDirectory)) {
fs.mkdirSync(zulipDirectory);
}
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDirectory);
}
// Migrate config files from app data folder to config folder inside app
// data folder. This will be done once when a user updates to the new version.
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir);
const domainJson = `${zulipDir}/domain.json`;
const settingsJson = `${zulipDir}/settings.json`;
const updatesJson = `${zulipDir}/updates.json`;
const windowStateJson = `${zulipDir}/window-state.json`;
if (!fs.existsSync(configDirectory)) {
fs.mkdirSync(configDirectory);
const domainJson = `${zulipDirectory}/domain.json`;
const settingsJson = `${zulipDirectory}/settings.json`;
const updatesJson = `${zulipDirectory}/updates.json`;
const windowStateJson = `${zulipDirectory}/window-state.json`;
const configData = [
{
path: domainJson,
@@ -45,7 +44,7 @@ export const initSetUp = (): void => {
];
for (const data of configData) {
if (fs.existsSync(data.path)) {
fs.copyFileSync(data.path, configDir + data.fileName);
fs.copyFileSync(data.path, configDirectory + data.fileName);
fs.unlinkSync(data.path);
}
}

View File

@@ -1,17 +1,22 @@
import * as ConfigUtil from "./config-util";
import process from "node:process";
type SettingName = keyof DNDSettings;
import type {z} from "zod";
export interface DNDSettings {
showNotification: boolean;
silent: boolean;
flashTaskbarOnMessage: boolean;
}
import type {dndSettingsSchemata} from "./config-schemata.ts";
import * as ConfigUtil from "./config-util.ts";
interface Toggle {
export type DndSettings = {
[Key in keyof typeof dndSettingsSchemata]: z.output<
(typeof dndSettingsSchemata)[Key]
>;
};
type SettingName = keyof DndSettings;
type Toggle = {
dnd: boolean;
newSettings: Partial<DNDSettings>;
}
newSettings: Partial<DndSettings>;
};
export function toggle(): Toggle {
const dnd = !ConfigUtil.getConfigItem("dnd", false);
@@ -20,9 +25,9 @@ export function toggle(): Toggle {
dndSettingList.push("flashTaskbarOnMessage");
}
let newSettings: Partial<DNDSettings>;
let newSettings: Partial<DndSettings>;
if (dnd) {
const oldSettings: Partial<DNDSettings> = {};
const oldSettings: Partial<DndSettings> = {};
newSettings = {};
// Iterate through the dndSettingList.

View File

@@ -1,12 +1,18 @@
import fs from "fs";
import path from "path";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import type {Config} from "./config-util";
import Logger from "./logger-util";
import {z} from "zod";
import {dialog} from "zulip:remote";
interface EnterpriseConfig extends Config {
presetOrganizations: string[];
}
import {enterpriseConfigSchemata} from "./config-schemata.ts";
import Logger from "./logger-util.ts";
type EnterpriseConfig = {
[Key in keyof typeof enterpriseConfigSchemata]: z.output<
(typeof enterpriseConfigSchemata)[Key]
>;
};
const logger = new Logger({
file: "enterprise-util.log",
@@ -15,13 +21,12 @@ const logger = new Logger({
let enterpriseSettings: Partial<EnterpriseConfig>;
let configFile: boolean;
reloadDB();
reloadDatabase();
function reloadDB(): void {
function reloadDatabase(): void {
let enterpriseFile = "/etc/zulip-desktop-config/global_config.json";
if (process.platform === "win32") {
enterpriseFile =
"C:\\Program Files\\Zulip-Desktop-Config\\global_config.json";
enterpriseFile = String.raw`C:\Program Files\Zulip-Desktop-Config\global_config.json`;
}
enterpriseFile = path.resolve(enterpriseFile);
@@ -29,8 +34,16 @@ function reloadDB(): void {
configFile = true;
try {
const file = fs.readFileSync(enterpriseFile, "utf8");
enterpriseSettings = JSON.parse(file);
const data: unknown = JSON.parse(file);
enterpriseSettings = z
.object(enterpriseConfigSchemata)
.partial()
.parse(data);
} 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);
}
@@ -47,7 +60,7 @@ export function getConfigItem<Key extends keyof EnterpriseConfig>(
key: Key,
defaultValue: EnterpriseConfig[Key],
): EnterpriseConfig[Key] {
reloadDB();
reloadDatabase();
if (!configFile) {
return defaultValue;
}
@@ -57,7 +70,7 @@ export function getConfigItem<Key extends keyof EnterpriseConfig>(
}
export function configItemExists(key: keyof EnterpriseConfig): boolean {
reloadDB();
reloadDatabase();
if (!configFile) {
return false;
}

View File

@@ -1,26 +1,26 @@
import {htmlEscape} from "escape-goat";
export class HTML {
export class Html {
html: string;
constructor({html}: {html: string}) {
this.html = html;
}
join(htmls: readonly HTML[]): HTML {
return new HTML({html: htmls.map((html) => html.html).join(this.html)});
join(htmls: readonly Html[]): Html {
return new Html({html: htmls.map((html) => html.html).join(this.html)});
}
}
export function html(
template: TemplateStringsArray,
...values: unknown[]
): HTML {
): Html {
let html = template[0];
for (const [index, value] of values.entries()) {
html += value instanceof HTML ? value.html : htmlEscape(String(value));
html += value instanceof Html ? value.html : htmlEscape(String(value));
html += template[index + 1];
}
return new HTML({html});
return new Html({html});
}

View File

@@ -1,13 +1,10 @@
import {shell} from "electron";
import fs from "fs";
import os from "os";
import path from "path";
import {shell} from "electron/common";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {html} from "../../../common/html";
export function isUploadsUrl(server: string, url: URL): boolean {
return url.origin === server && url.pathname.startsWith("/user_uploads/");
}
import {Html, html} from "./html.ts";
import * as t from "./translation-util.ts";
export async function openBrowser(url: URL): Promise<void> {
if (["http:", "https:", "mailto:"].includes(url.protocol)) {
@@ -15,16 +12,17 @@ export async function openBrowser(url: URL): Promise<void> {
} else {
// For security, indirect links to non-whitelisted protocols
// through a real web browser via a local HTML file.
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "zulip-redirect-"));
const file = path.join(dir, "redirect.html");
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "zulip-redirect-"));
const file = path.join(directory, "redirect.html");
fs.writeFileSync(
file,
html`<!DOCTYPE html>
html`
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Refresh" content="0; url=${url.href}" />
<title>Redirecting</title>
<title>${t.__("Redirecting")}</title>
<style>
html {
font-family: menu, "Helvetica Neue", sans-serif;
@@ -32,14 +30,21 @@ export async function openBrowser(url: URL): Promise<void> {
</style>
</head>
<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>
</html> `.html,
</html>
`.html,
);
await shell.openPath(file);
setTimeout(() => {
fs.unlinkSync(file);
fs.rmdirSync(dir);
}, 15000);
fs.rmdirSync(directory);
}, 15_000);
}
}

View File

@@ -1,38 +1,19 @@
import {Console} from "console"; // eslint-disable-line node/prefer-global/console
import electron from "electron";
import fs from "fs";
import os from "os";
import {Console} from "node:console"; // eslint-disable-line n/prefer-global/console
import fs from "node:fs";
import os from "node:os";
import process from "node:process";
import {initSetUp} from "./default-util";
import {captureException, sentryInit} from "./sentry-util";
import {app} from "zulip:remote";
const {app} = process.type === "renderer" ? electron.remote : electron;
import {initSetUp} from "./default-util.ts";
interface LoggerOptions {
type LoggerOptions = {
file?: string;
}
};
initSetUp();
let reportErrors = true;
if (process.type === "renderer") {
// Report Errors to Sentry only if it is enabled in settings
// Gets the value of reportErrors from config-util for renderer process
// For main process, sentryInit() is handled in index.js
const {ipcRenderer} = electron;
ipcRenderer.send("error-reporting");
ipcRenderer.on(
"error-reporting-val",
(_event: Event, errorReporting: boolean) => {
reportErrors = errorReporting;
if (reportErrors) {
sentryInit();
}
},
);
}
const logDir = `${app.getPath("userData")}/Logs`;
const logDirectory = `${app.getPath("userData")}/Logs`;
type Level = "log" | "debug" | "info" | "warn" | "error";
@@ -42,7 +23,7 @@ export default class Logger {
constructor(options: LoggerOptions = {}) {
let {file = "console.log"} = options;
file = `${logDir}/${file}`;
file = `${logDirectory}/${file}`;
// Trim log according to type of process
if (process.type === "renderer") {
@@ -57,31 +38,31 @@ export default class Logger {
this.nodeConsole = nodeConsole;
}
_log(type: Level, ...args: unknown[]): void {
args.unshift(this.getTimestamp() + " |\t");
args.unshift(type.toUpperCase() + " |");
this.nodeConsole[type](...args);
console[type](...args);
_log(type: Level, ...arguments_: unknown[]): void {
arguments_.unshift(this.getTimestamp() + " |\t");
arguments_.unshift(type.toUpperCase() + " |");
this.nodeConsole[type](...arguments_);
console[type](...arguments_);
}
log(...args: unknown[]): void {
this._log("log", ...args);
log(...arguments_: unknown[]): void {
this._log("log", ...arguments_);
}
debug(...args: unknown[]): void {
this._log("debug", ...args);
debug(...arguments_: unknown[]): void {
this._log("debug", ...arguments_);
}
info(...args: unknown[]): void {
this._log("info", ...args);
info(...arguments_: unknown[]): void {
this._log("info", ...arguments_);
}
warn(...args: unknown[]): void {
this._log("warn", ...args);
warn(...arguments_: unknown[]): void {
this._log("warn", ...arguments_);
}
error(...args: unknown[]): void {
this._log("error", ...args);
error(...arguments_: unknown[]): void {
this._log("error", ...arguments_);
}
getTimestamp(): string {
@@ -92,22 +73,16 @@ export default class Logger {
return timestamp;
}
reportSentry(error: unknown): void {
if (reportErrors) {
captureException(error);
}
}
async trimLog(file: string): Promise<void> {
const data = await fs.promises.readFile(file, "utf8");
const MAX_LOG_FILE_LINES = 500;
const maxLogFileLines = 500;
const logs = data.split(os.EOL);
const logLength = logs.length - 1;
// Keep bottom MAX_LOG_FILE_LINES of each log instance
if (logLength > MAX_LOG_FILE_LINES) {
const trimmedLogs = logs.slice(logLength - MAX_LOG_FILE_LINES);
// Keep bottom maxLogFileLines of each log instance
if (logLength > maxLogFileLines) {
const trimmedLogs = logs.slice(logLength - maxLogFileLines);
const toWrite = trimmedLogs.join(os.EOL);
await fs.promises.writeFile(file, toWrite);
}

View File

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

15
app/common/paths.ts Normal file
View File

@@ -0,0 +1,15 @@
import path from "node:path";
import process from "node:process";
import url from "node:url";
export const bundlePath = __dirname;
export const publicPath = import.meta.env.DEV
? path.join(bundlePath, "../public")
: bundlePath;
export const bundleUrl = import.meta.env.DEV
? process.env.VITE_DEV_SERVER_URL
: url.pathToFileURL(__dirname).href + "/";
export const publicUrl = bundleUrl;

View File

@@ -1,20 +0,0 @@
import electron from "electron";
import {init} from "@sentry/electron";
const {app} = process.type === "renderer" ? electron.remote : electron;
export const sentryInit = (): void => {
if (app.isPackaged) {
init({
dsn: "https://628dc2f2864243a08ead72e63f94c7b1@sentry.io/204668",
// We should ignore this error since it's harmless and we know the reason behind this
// This error mainly comes from the console logs.
// This is a temp solution until Sentry supports disabling the console logs
ignoreErrors: ["does not appear to be a valid Zulip server"],
/// sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second
});
}
};
export {captureException} from "@sentry/electron";

View File

@@ -1,18 +1,16 @@
import path from "path";
import path from "node:path";
import i18n from "i18n";
import * as ConfigUtil from "./config-util";
import * as ConfigUtil from "./config-util.ts";
import {publicPath} from "./paths.ts";
i18n.configure({
directory: path.join(__dirname, "../translations/"),
directory: path.join(publicPath, "translations/"),
updateFiles: false,
});
/* Fetches the current appLocale from settings.json */
const appLocale = ConfigUtil.getConfigItem("appLanguage", "en");
i18n.setLocale(ConfigUtil.getConfigItem("appLanguage", "en") ?? "en");
/* If no locale present in the json, en is set default */
export function __(phrase: string): string {
return i18n.__({phrase, locale: appLocale ? appLocale : "en"});
}
export {__, __mf} from "i18n";

View File

@@ -1,19 +1,19 @@
import type {DNDSettings} from "./dnd-util";
import type {MenuProps, NavItem, ServerConf} from "./types";
import type {DndSettings} from "./dnd-util.ts";
import type {MenuProperties, ServerConfig} from "./types.ts";
export interface MainMessage {
export type MainMessage = {
"clear-app-settings": () => void;
downloadFile: (url: string, downloadPath: string) => void;
"error-reporting": () => void;
"configure-spell-checker": () => void;
"fetch-user-agent": () => string;
"focus-app": () => void;
"focus-this-webview": () => void;
"new-clipboard-key": () => {key: Uint8Array; sig: Uint8Array};
"permission-callback": (permissionCallbackId: number, grant: boolean) => void;
"quit-app": () => void;
"realm-icon-changed": (serverURL: string, iconURL: string) => void;
"realm-name-changed": (serverURL: string, realmName: string) => void;
"reload-full-app": () => void;
"save-last-tab": (index: number) => void;
"set-spellcheck-langs": () => void;
"switch-server-tab": (index: number) => void;
"toggle-app": () => void;
"toggle-badge-option": (newValue: boolean) => void;
@@ -21,24 +21,22 @@ export interface MainMessage {
toggleAutoLauncher: (AutoLaunchValue: boolean) => void;
"unread-count": (unreadCount: 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;
}
};
export interface MainCall {
"get-server-settings": (domain: string) => ServerConf;
export type MainCall = {
"get-server-settings": (domain: string) => ServerConfig;
"is-online": (url: string) => boolean;
"save-server-icon": (iconURL: string) => string;
}
"poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined;
"save-server-icon": (iconURL: string) => string | null;
};
export interface RendererMessage {
export type RendererMessage = {
back: () => void;
"copy-zulip-url": () => void;
destroytray: () => void;
downloadFileCompleted: (filePath: string, fileName: string) => void;
downloadFileFailed: (state: string) => void;
"enter-fullscreen": () => void;
"error-reporting-val": (errorReporting: boolean) => void;
focus: () => void;
"focus-webview-with-id": (webviewId: number) => void;
forward: () => void;
@@ -48,7 +46,6 @@ export interface RendererMessage {
logout: () => void;
"new-server": () => void;
"open-about": () => void;
"open-feedback-modal": () => void;
"open-help": () => void;
"open-network-settings": () => void;
"open-org-tab": () => void;
@@ -57,6 +54,7 @@ export interface RendererMessage {
options: {webContentsId: number | null; origin: string; permission: string},
rendererCallbackId: number,
) => void;
"play-ding-sound": () => void;
"reload-current-viewer": () => void;
"reload-proxy": (showAlert: boolean) => void;
"reload-viewer": () => void;
@@ -64,27 +62,23 @@ export interface RendererMessage {
"set-active": () => void;
"set-idle": () => void;
"show-keyboard-shortcuts": () => void;
"show-network-error": (index: number) => void;
"show-notification-settings": () => void;
"switch-server-tab": (index: number) => void;
"switch-settings-nav": (navItem: NavItem) => void;
"tab-devtools": () => void;
"toggle-autohide-menubar": (
autoHideMenubar: boolean,
updateMenu: boolean,
) => void;
"toggle-dnd": (state: boolean, newSettings: Partial<DNDSettings>) => void;
"toggle-menubar-setting": (state: boolean) => void;
"toggle-dnd": (state: boolean, newSettings: Partial<DndSettings>) => void;
"toggle-sidebar": (show: boolean) => void;
"toggle-sidebar-setting": (state: boolean) => void;
"toggle-silent": (state: boolean) => void;
"toggle-tray": (state: boolean) => void;
toggletray: () => void;
tray: (arg: number) => void;
tray: (argument: number) => 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;
zoomActualSize: () => void;
zoomIn: () => void;
zoomOut: () => void;
}
};

View File

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

View File

@@ -1,15 +1,24 @@
import {app, dialog, session, shell} from "electron";
import util from "util";
import {shell} from "electron/common";
import {app, dialog, session} from "electron/main";
import process from "node:process";
import log from "electron-log";
import type {UpdateDownloadedEvent, UpdateInfo} from "electron-updater";
import {autoUpdater} from "electron-updater";
import log from "electron-log/main";
import {
type UpdateDownloadedEvent,
type UpdateInfo,
autoUpdater,
} from "electron-updater";
import * as ConfigUtil from "../common/config-util";
import * as ConfigUtil from "../common/config-util.ts";
import * as t from "../common/translation-util.ts";
import {linuxUpdateNotification} from "./linuxupdater"; // Required only in case of linux
import {linuxUpdateNotification} from "./linuxupdater.ts"; // Required only in case of linux
const sleep = util.promisify(setTimeout);
let quitting = false;
export function shouldQuitForUpdate(): boolean {
return quitting;
}
export async function appUpdater(updateFromMenu = false): Promise<void> {
// Don't initiate auto-updates in development
@@ -25,20 +34,21 @@ export async function appUpdater(updateFromMenu = false): Promise<void> {
let updateAvailable = false;
// Create Logs directory
const LogsDir = `${app.getPath("userData")}/Logs`;
// Log whats happening
log.transports.file.file = `${LogsDir}/updates.log`;
log.transports.file.level = "info";
autoUpdater.logger = log;
// Log what's happening
const updateLogger = log.create({logId: "updates"});
updateLogger.transports.file.fileName = "updates.log";
updateLogger.transports.file.level = "info";
autoUpdater.logger = updateLogger;
// Handle auto updates for beta/pre releases
const isBetaUpdate = ConfigUtil.getConfigItem("betaUpdate", false);
autoUpdater.allowPrerelease = isBetaUpdate;
const eventsListenerRemove = ["update-available", "update-not-available"];
const eventsListenerRemove = [
"update-available",
"update-not-available",
] as const;
autoUpdater.on("update-available", async (info: UpdateInfo) => {
if (updateFromMenu) {
updateAvailable = true;
@@ -49,9 +59,13 @@ export async function appUpdater(updateFromMenu = false): Promise<void> {
}
await dialog.showMessageBox({
message: `A new version ${info.version}, of Zulip Desktop is available`,
detail:
message: t.__(
"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.",
),
});
}
});
@@ -63,8 +77,11 @@ export async function appUpdater(updateFromMenu = false): Promise<void> {
autoUpdater.removeAllListeners();
await dialog.showMessageBox({
message: "No updates available",
detail: `You are running the latest version of Zulip Desktop.\nVersion: ${app.getVersion()}`,
message: t.__("No updates available."),
detail: t.__(
"You are running the latest version of Zulip Desktop.\nVersion: {{{version}}}",
{version: app.getVersion()},
),
});
}
});
@@ -76,20 +93,20 @@ export async function appUpdater(updateFromMenu = false): Promise<void> {
autoUpdater.removeAllListeners();
const messageText = updateAvailable
? "Unable to download the updates"
: "Unable to check for updates";
? t.__("Unable to download the update.")
: t.__("Unable to check for updates.");
const link = "https://zulip.com/apps/";
const {response} = await dialog.showMessageBox({
type: "error",
buttons: ["Manual Download", "Cancel"],
buttons: [t.__("Manual Download"), t.__("Cancel")],
message: messageText,
detail: `Error: ${error.message}
The latest version of Zulip Desktop is available at -
https://zulip.com/apps/.
Current Version: ${app.getVersion()}`,
detail: t.__(
"Error: {{{error}}}\n\nThe latest version of Zulip Desktop is available at:\n{{{link}}}\nCurrent version: {{{version}}}",
{error: error.message, link, version: app.getVersion()},
),
});
if (response === 0) {
await shell.openExternal("https://zulip.com/apps/");
await shell.openExternal(link);
}
}
});
@@ -99,16 +116,18 @@ Current Version: ${app.getVersion()}`,
// Ask user to update the app
const {response} = await dialog.showMessageBox({
type: "question",
buttons: ["Install and Relaunch", "Install Later"],
buttons: [t.__("Install and Relaunch"), t.__("Install Later")],
defaultId: 0,
message: `A new update ${event.version} has been downloaded`,
detail: "It will be installed the next time you restart the application",
message: t.__("A new update {{{version}}} has been downloaded.", {
version: event.version,
}),
detail: t.__(
"It will be installed the next time you restart the application.",
),
});
if (response === 0) {
await sleep(1000);
quitting = true;
autoUpdater.quitAndInstall();
// Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit();
}
});
// Init for updates

View File

@@ -1,13 +1,12 @@
import electron, {app} from "electron";
import {nativeImage} from "electron/common";
import {type BrowserWindow, app} from "electron/main";
import process from "node:process";
import * as ConfigUtil from "../common/config-util";
import * as ConfigUtil from "../common/config-util.ts";
import {send} from "./typed-ipc-main";
import {send} from "./typed-ipc-main.ts";
function showBadgeCount(
messageCount: number,
mainWindow: electron.BrowserWindow,
): void {
function showBadgeCount(messageCount: number, mainWindow: BrowserWindow): void {
if (process.platform === "win32") {
updateOverlayIcon(messageCount, mainWindow);
} else {
@@ -15,7 +14,7 @@ function showBadgeCount(
}
}
function hideBadgeCount(mainWindow: electron.BrowserWindow): void {
function hideBadgeCount(mainWindow: BrowserWindow): void {
if (process.platform === "win32") {
mainWindow.setOverlayIcon(null, "");
} else {
@@ -25,7 +24,7 @@ function hideBadgeCount(mainWindow: electron.BrowserWindow): void {
export function updateBadge(
badgeCount: number,
mainWindow: electron.BrowserWindow,
mainWindow: BrowserWindow,
): void {
if (ConfigUtil.getConfigItem("badgeOption", true)) {
showBadgeCount(badgeCount, mainWindow);
@@ -36,7 +35,7 @@ export function updateBadge(
function updateOverlayIcon(
messageCount: number,
mainWindow: electron.BrowserWindow,
mainWindow: BrowserWindow,
): void {
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(
@@ -54,8 +53,8 @@ function updateOverlayIcon(
export function updateTaskbarIcon(
data: string,
text: string,
mainWindow: electron.BrowserWindow,
mainWindow: BrowserWindow,
): void {
const img = electron.nativeImage.createFromDataURL(data);
const img = nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
}

View File

@@ -0,0 +1,165 @@
import {type Event, shell} from "electron/common";
import {
type HandlerDetails,
Notification,
type SaveDialogOptions,
type WebContents,
app,
} from "electron/main";
import fs from "node:fs";
import path from "node:path";
import * as ConfigUtil from "../common/config-util.ts";
import * as LinkUtil from "../common/link-util.ts";
import * as t from "../common/translation-util.ts";
import {send} from "./typed-ipc-main.ts";
function isUploadsUrl(server: string, url: URL): boolean {
return url.origin === server && url.pathname.startsWith("/user_uploads/");
}
function downloadFile({
contents,
url,
downloadPath,
completed,
failed,
}: {
contents: WebContents;
url: string;
downloadPath: string;
completed(filePath: string, fileName: string): Promise<void>;
failed(state: string): void;
}) {
contents.downloadURL(url);
contents.session.once("will-download", async (_event, item) => {
if (ConfigUtil.getConfigItem("promptDownload", false)) {
const showDialogOptions: SaveDialogOptions = {
defaultPath: path.join(downloadPath, item.getFilename()),
};
item.setSaveDialogOptions(showDialogOptions);
} else {
const getTimeStamp = (): number => {
const date = new Date();
return date.getTime();
};
const formatFile = (filePath: string): string => {
const fileExtension = path.extname(filePath);
const baseName = path.basename(filePath, fileExtension);
return `${baseName}-${getTimeStamp()}${fileExtension}`;
};
const filePath = path.join(downloadPath, item.getFilename());
// Update the name and path of the file if it already exists
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
const setFilePath: string = fs.existsSync(filePath)
? updatedFilePath
: filePath;
item.setSavePath(setFilePath);
}
const updatedListener = (_event: Event, state: string): void => {
switch (state) {
case "interrupted": {
// Can interrupted to due to network error, cancel download then
console.log(
"Download interrupted, cancelling and fallback to dialog download.",
);
item.cancel();
break;
}
case "progressing": {
if (item.isPaused()) {
item.cancel();
}
// This event can also be used to show progress in percentage in future.
break;
}
default: {
console.info("Unknown updated state of download item");
}
}
};
item.on("updated", updatedListener);
item.once("done", async (_event, state) => {
if (state === "completed") {
await completed(item.getSavePath(), path.basename(item.getSavePath()));
} else {
console.log("Download failed state:", state);
failed(state);
}
// To stop item for listening to updated events of this file
item.removeListener("updated", updatedListener);
});
});
}
export default function handleExternalLink(
contents: WebContents,
details: HandlerDetails,
mainContents: WebContents,
): void {
let url: URL;
try {
url = new URL(details.url);
} catch {
return;
}
const downloadPath = ConfigUtil.getConfigItem(
"downloadsPath",
`${app.getPath("downloads")}`,
);
if (isUploadsUrl(new URL(contents.getURL()).origin, url)) {
downloadFile({
contents,
url: url.href,
downloadPath,
async completed(filePath: string, fileName: string) {
const downloadNotification = new Notification({
title: t.__("Download Complete"),
body: t.__("Click to show {{{fileName}}} in folder", {fileName}),
silent: true, // We'll play our own sound - ding.ogg
});
downloadNotification.on("click", () => {
// Reveal file in download folder
shell.showItemInFolder(filePath);
});
downloadNotification.show();
// Play sound to indicate download complete
if (!ConfigUtil.getConfigItem("silent", false)) {
send(mainContents, "play-ding-sound");
}
},
failed(state: string) {
// Automatic download failed, so show save dialog prompt and download
// through webview
// Only do this if it is the automatic download, otherwise show an error (so we aren't showing two save
// prompts right after each other)
// Check that the download is not cancelled by user
if (state !== "cancelled") {
if (ConfigUtil.getConfigItem("promptDownload", false)) {
new Notification({
title: t.__("Download Complete"),
body: t.__("Download failed"),
}).show();
} else {
contents.downloadURL(url.href);
}
}
},
});
} else {
(async () => LinkUtil.openBrowser(url))();
}
}

View File

@@ -1,42 +1,63 @@
import electron, {app, dialog, session} from "electron";
import fs from "fs";
import path from "path";
import {clipboard} from "electron/common";
import {
BrowserWindow,
type IpcMainEvent,
type WebContents,
app,
dialog,
powerMonitor,
session,
webContents,
} from "electron/main";
import {Buffer} from "node:buffer";
import crypto from "node:crypto";
import path from "node:path";
import process from "node:process";
import * as remoteMain from "@electron/remote/main";
import windowStateKeeper from "electron-window-state";
import * as ConfigUtil from "../common/config-util";
import {sentryInit} from "../common/sentry-util";
import type {RendererMessage} from "../common/typed-ipc";
import type {MenuProps} from "../common/types";
import * as ConfigUtil from "../common/config-util.ts";
import {bundlePath, bundleUrl, publicPath} from "../common/paths.ts";
import * as t from "../common/translation-util.ts";
import type {RendererMessage} from "../common/typed-ipc.ts";
import type {MenuProperties} from "../common/types.ts";
import {appUpdater} from "./autoupdater";
import * as BadgeSettings from "./badge-settings";
import * as AppMenu from "./menu";
import * as ProxyUtil from "./proxy-util";
import {_getServerSettings, _isOnline, _saveServerIcon} from "./request";
import {setAutoLaunch} from "./startup";
import {ipcMain, send} from "./typed-ipc-main";
import {appUpdater, shouldQuitForUpdate} from "./autoupdater.ts";
import * as BadgeSettings from "./badge-settings.ts";
import handleExternalLink from "./handle-external-link.ts";
import * as AppMenu from "./menu.ts";
import {_getServerSettings, _isOnline, _saveServerIcon} from "./request.ts";
import {sentryInit} from "./sentry.ts";
import {setAutoLaunch} from "./startup.ts";
import {ipcMain, send} from "./typed-ipc-main.ts";
import "gatemaker/electron-setup.js"; // eslint-disable-line import-x/no-unassigned-import
// eslint-disable-next-line @typescript-eslint/naming-convention
const {GDK_BACKEND} = process.env;
// Initialize sentry for main process
sentryInit();
let mainWindowState: windowStateKeeper.State;
// Prevent window being garbage collected
let mainWindow: Electron.BrowserWindow;
let mainWindow: BrowserWindow;
let badgeCount: number;
let isQuitting = false;
// Load this url in main window
const mainURL = "file://" + path.join(__dirname, "../renderer", "main.html");
// Load this file in main window
const mainUrl = new URL("app/renderer/main.html", bundleUrl).href;
const permissionCallbacks = new Map();
const permissionCallbacks = new Map<number, (grant: boolean) => void>();
let nextPermissionCallbackId = 0;
const APP_ICON = path.join(__dirname, "../resources", "Icon");
const appIcon = path.join(publicPath, "resources/Icon");
const iconPath = (): string =>
APP_ICON + (process.platform === "win32" ? ".ico" : ".png");
appIcon + (process.platform === "win32" ? ".ico" : ".png");
// Toggle the app window
const toggleApp = (): void => {
@@ -47,7 +68,7 @@ const toggleApp = (): void => {
}
};
function createMainWindow(): Electron.BrowserWindow {
function createMainWindow(): BrowserWindow {
// Load the previous state with fallback to defaults
mainWindowState = windowStateKeeper({
defaultWidth: 1100,
@@ -55,7 +76,7 @@ function createMainWindow(): Electron.BrowserWindow {
path: `${app.getPath("userData")}/config`,
});
const win = new electron.BrowserWindow({
const win = new BrowserWindow({
// This settings needs to be saved in config
title: "Zulip",
icon: iconPath(),
@@ -66,21 +87,19 @@ function createMainWindow(): Electron.BrowserWindow {
minWidth: 500,
minHeight: 400,
webPreferences: {
contextIsolation: false,
enableRemoteModule: true,
nodeIntegration: true,
partition: "persist:webviewsession",
preload: path.join(bundlePath, "renderer.cjs"),
sandbox: false,
webviewTag: true,
worldSafeExecuteJavaScript: true,
},
show: false,
});
remoteMain.enable(win.webContents);
win.on("focus", () => {
send(win.webContents, "focus");
});
(async () => win.loadURL(mainURL))();
(async () => win.loadURL(mainUrl))();
// Keep the app running in background on close event
win.on("close", (event) => {
@@ -88,11 +107,18 @@ function createMainWindow(): Electron.BrowserWindow {
app.quit();
}
if (!isQuitting) {
if (!isQuitting && !shouldQuitForUpdate()) {
event.preventDefault();
if (process.platform === "darwin") {
app.hide();
if (win.isFullScreen()) {
win.setFullScreen(false);
win.once("leave-full-screen", () => {
app.hide();
});
} else {
app.hide();
}
} else {
win.hide();
}
@@ -124,10 +150,6 @@ function createMainWindow(): Electron.BrowserWindow {
return win;
}
// Temporary fix for Electron render colors differently
// More info here - https://github.com/electron/electron/issues/10732
app.commandLine.appendSwitch("force-color-profile", "srgb");
(async () => {
if (!app.requestSingleInstanceLock()) {
app.quit();
@@ -147,6 +169,11 @@ app.commandLine.appendSwitch("force-color-profile", "srgb");
}
}
// Used for notifications on Windows
app.setAppUserModelId("org.zulip.zulip-electron");
remoteMain.initialize();
app.on("second-instance", () => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
@@ -159,30 +186,77 @@ app.commandLine.appendSwitch("force-color-profile", "srgb");
ipcMain.on(
"permission-callback",
(event: Event, permissionCallbackId: number, grant: boolean) => {
permissionCallbacks.get(permissionCallbackId)(grant);
(event, permissionCallbackId: number, grant: boolean) => {
permissionCallbacks.get(permissionCallbackId)?.(grant);
permissionCallbacks.delete(permissionCallbackId);
},
);
// This event is only available on macOS. Triggers when you click on the dock icon.
app.on("activate", () => {
if (mainWindow) {
// If there is already a window show it
mainWindow.show();
} else {
mainWindow = createMainWindow();
}
mainWindow.show();
});
app.on("web-contents-created", (_event, contents: WebContents) => {
contents.setWindowOpenHandler((details) => {
handleExternalLink(contents, details, page);
return {action: "deny"};
});
});
const ses = session.fromPartition("persist:webviewsession");
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);
ipcMain.on("set-spellcheck-langs", () => {
ses.setSpellCheckerLanguages(
ConfigUtil.getConfigItem("spellcheckerLanguages", null) ?? [],
);
function configureSpellChecker() {
const enable = ConfigUtil.getConfigItem("enableSpellchecker", true);
if (enable && process.platform !== "darwin") {
ses.setSpellCheckerLanguages(
ConfigUtil.getConfigItem("spellcheckerLanguages", null) ?? [],
);
}
ses.setSpellCheckerEnabled(enable);
}
configureSpellChecker();
ipcMain.on("configure-spell-checker", configureSpellChecker);
const clipboardSigKey = crypto.randomBytes(32);
ipcMain.on("new-clipboard-key", (event) => {
const key = crypto.randomBytes(32);
const hmac = crypto.createHmac("sha256", clipboardSigKey);
hmac.update(key);
event.returnValue = {key, sig: hmac.digest()};
});
ipcMain.handle("poll-clipboard", (event, key, sig) => {
// Check that the key was generated here.
const hmac = crypto.createHmac("sha256", clipboardSigKey);
hmac.update(key);
if (!crypto.timingSafeEqual(sig, hmac.digest())) return;
try {
// Check that the data on the clipboard was encrypted to the key.
const data = Buffer.from(clipboard.readText(), "hex");
const iv = data.subarray(0, 12);
const ciphertext = data.subarray(12, -16);
const authTag = data.subarray(-16);
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, {
authTagLength: 16,
});
decipher.setAuthTag(authTag);
return (
decipher.update(ciphertext, undefined, "utf8") + decipher.final("utf8")
);
} catch {
// If the parsing or decryption failed in any way,
// the correct token hasnt been copied yet; try
// again next time.
return undefined;
}
});
AppMenu.setMenu({
tabs: [],
});
@@ -195,18 +269,6 @@ app.commandLine.appendSwitch("force-color-profile", "srgb");
mainWindow.setMenuBarVisibility(!shouldHideMenu);
}
// Initialize sentry for main process
const errorReporting = ConfigUtil.getConfigItem("errorReporting", true);
if (errorReporting) {
sentryInit();
}
const isSystemProxy = ConfigUtil.getConfigItem("useSystemProxy", false);
if (isSystemProxy) {
(async () => ProxyUtil.resolveSystemProxy(mainWindow))();
}
const page = mainWindow.webContents;
page.on("dom-ready", () => {
@@ -245,22 +307,29 @@ app.commandLine.appendSwitch("force-color-profile", "srgb");
app.on(
"certificate-error",
(
event: Event,
webContents: Electron.WebContents,
urlString: string,
error: string,
event,
webContents,
urlString,
error,
certificate,
callback,
isMainFrame,
// eslint-disable-next-line max-params
) => {
const url = new URL(urlString);
dialog.showErrorBox(
"Certificate error",
`The server presented an invalid certificate for ${url.origin}:
${error}`,
);
if (isMainFrame) {
const url = new URL(urlString);
dialog.showErrorBox(
t.__("Certificate error"),
t.__(
"The server presented an invalid certificate for {{{origin}}}:\n\n{{{error}}}",
{origin: url.origin, error},
),
);
}
},
);
page.session.setPermissionRequestHandler(
ses.setPermissionRequestHandler(
(webContents, permission, callback, details) => {
const {origin} = new URL(details.requestingUrl);
const permissionCallbackId = nextPermissionCallbackId++;
@@ -282,7 +351,7 @@ ${error}`,
);
// Temporarily remove this event
// electron.powerMonitor.on('resume', () => {
// powerMonitor.on('resume', () => {
// mainWindow.reload();
// send(page, 'destroytray');
// });
@@ -315,35 +384,26 @@ ${error}`,
BadgeSettings.updateBadge(badgeCount, mainWindow);
});
ipcMain.on(
"toggle-menubar",
(_event: Electron.IpcMainEvent, showMenubar: boolean) => {
mainWindow.autoHideMenuBar = showMenubar;
mainWindow.setMenuBarVisibility(!showMenubar);
send(page, "toggle-autohide-menubar", showMenubar, true);
},
);
ipcMain.on("toggle-menubar", (_event, showMenubar: boolean) => {
mainWindow.autoHideMenuBar = showMenubar;
mainWindow.setMenuBarVisibility(!showMenubar);
send(page, "toggle-autohide-menubar", showMenubar, true);
});
ipcMain.on(
"update-badge",
(_event: Electron.IpcMainEvent, messageCount: number) => {
badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow);
send(page, "tray", messageCount);
},
);
ipcMain.on("update-badge", (_event, messageCount: number) => {
badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow);
send(page, "tray", messageCount);
});
ipcMain.on(
"update-taskbar-icon",
(_event: Electron.IpcMainEvent, data: string, text: string) => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
},
);
ipcMain.on("update-taskbar-icon", (_event, data: string, text: string) => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
});
ipcMain.on(
"forward-message",
<Channel extends keyof RendererMessage>(
_event: Electron.IpcMainEvent,
_event: IpcMainEvent,
listener: Channel,
...parameters: Parameters<RendererMessage[Channel]>
) => {
@@ -352,137 +412,61 @@ ${error}`,
);
ipcMain.on(
"update-menu",
(_event: Electron.IpcMainEvent, props: MenuProps) => {
AppMenu.setMenu(props);
if (props.activeTabIndex !== undefined) {
const activeTab = props.tabs[props.activeTabIndex];
mainWindow.setTitle(`Zulip - ${activeTab.webviewName}`);
"forward-to",
<Channel extends keyof RendererMessage>(
_event: IpcMainEvent,
webContentsId: number,
listener: Channel,
...parameters: Parameters<RendererMessage[Channel]>
) => {
const contents = webContents.fromId(webContentsId);
if (contents !== undefined) {
send(contents, listener, ...parameters);
}
},
);
ipcMain.on(
"toggleAutoLauncher",
async (_event: Electron.IpcMainEvent, AutoLaunchValue: boolean) => {
await setAutoLaunch(AutoLaunchValue);
},
);
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}`);
}
});
ipcMain.on(
"downloadFile",
(_event: Electron.IpcMainEvent, url: string, downloadPath: string) => {
page.downloadURL(url);
page.session.once("will-download", async (_event: Event, item) => {
if (ConfigUtil.getConfigItem("promptDownload", false)) {
const showDialogOptions: electron.SaveDialogOptions = {
defaultPath: path.join(downloadPath, item.getFilename()),
};
item.setSaveDialogOptions(showDialogOptions);
} else {
const getTimeStamp = (): number => {
const date = new Date();
return date.getTime();
};
const formatFile = (filePath: string): string => {
const fileExtension = path.extname(filePath);
const baseName = path.basename(filePath, fileExtension);
return `${baseName}-${getTimeStamp()}${fileExtension}`;
};
const filePath = path.join(downloadPath, item.getFilename());
// Update the name and path of the file if it already exists
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
const setFilePath: string = fs.existsSync(filePath)
? updatedFilePath
: filePath;
item.setSavePath(setFilePath);
}
const updatedListener = (_event: Event, state: string): void => {
switch (state) {
case "interrupted": {
// Can interrupted to due to network error, cancel download then
console.log(
"Download interrupted, cancelling and fallback to dialog download.",
);
item.cancel();
break;
}
case "progressing": {
if (item.isPaused()) {
item.cancel();
}
// This event can also be used to show progress in percentage in future.
break;
}
default: {
console.info("Unknown updated state of download item");
}
}
};
item.on("updated", updatedListener);
item.once("done", (_event: Event, state) => {
if (state === "completed") {
send(
page,
"downloadFileCompleted",
item.getSavePath(),
path.basename(item.getSavePath()),
);
} else {
console.log("Download failed state:", state);
send(page, "downloadFileFailed", state);
}
// To stop item for listening to updated events of this file
item.removeListener("updated", updatedListener);
});
});
},
);
ipcMain.on("toggleAutoLauncher", async (_event, AutoLaunchValue: boolean) => {
await setAutoLaunch(AutoLaunchValue);
});
ipcMain.on(
"realm-name-changed",
(_event: Electron.IpcMainEvent, serverURL: string, realmName: string) => {
(_event, serverURL: string, realmName: string) => {
send(page, "update-realm-name", serverURL, realmName);
},
);
ipcMain.on(
"realm-icon-changed",
(_event: Electron.IpcMainEvent, serverURL: string, iconURL: string) => {
(_event, serverURL: string, iconURL: string) => {
send(page, "update-realm-icon", serverURL, iconURL);
},
);
// Using event.sender.send instead of page.send here to
// make sure the value of errorReporting is sent only once on load.
ipcMain.on("error-reporting", (event: Electron.IpcMainEvent) => {
send(event.sender, "error-reporting-val", errorReporting);
ipcMain.on("save-last-tab", (_event, index: number) => {
ConfigUtil.setConfigItem("lastActiveTab", index);
});
ipcMain.on(
"save-last-tab",
(_event: Electron.IpcMainEvent, index: number) => {
ConfigUtil.setConfigItem("lastActiveTab", index);
},
);
ipcMain.on("focus-this-webview", (event) => {
send(page, "focus-webview-with-id", event.sender.id);
mainWindow.show();
});
// Update user idle status for each realm after every 15s
const idleCheckInterval = 15 * 1000; // 15 seconds
setInterval(() => {
// Set user idle if no activity in 1 second (idleThresholdSeconds)
const idleThresholdSeconds = 1; // 1 second
const idleState = electron.powerMonitor.getSystemIdleState(
idleThresholdSeconds,
);
const idleState = powerMonitor.getSystemIdleState(idleThresholdSeconds);
if (idleState === "active") {
send(page, "set-active");
} else {

View File

@@ -1,23 +1,34 @@
import {app, dialog} from "electron";
import fs from "fs";
import path from "path";
import {app, dialog} from "electron/main";
import fs from "node:fs";
import path from "node:path";
import {JsonDB} from "node-json-db";
import {DataError} from "node-json-db/dist/lib/Errors.js";
import Logger from "../common/logger-util";
import Logger from "../common/logger-util.ts";
import * as t from "../common/translation-util.ts";
const logger = new Logger({
file: "linux-update-util.log",
});
let db: JsonDB;
let database: JsonDB;
reloadDB();
reloadDatabase();
export function getUpdateItem(key: string, defaultValue: unknown = null): any {
reloadDB();
const value = db.getData("/")[key];
if (value === undefined) {
export function getUpdateItem(
key: string,
defaultValue: true | null = null,
): true | null {
reloadDatabase();
let value: unknown;
try {
value = database.getObject<unknown>(`/${key}`);
} catch (error: unknown) {
if (!(error instanceof DataError)) throw error;
}
if (value !== true && value !== null) {
setUpdateItem(key, defaultValue);
return defaultValue;
}
@@ -25,17 +36,17 @@ export function getUpdateItem(key: string, defaultValue: unknown = null): any {
return value;
}
export function setUpdateItem(key: string, value: unknown): void {
db.push(`/${key}`, value, true);
reloadDB();
export function setUpdateItem(key: string, value: true | null): void {
database.push(`/${key}`, value, true);
reloadDatabase();
}
export function removeUpdateItem(key: string): void {
db.delete(`/${key}`);
reloadDB();
database.delete(`/${key}`);
reloadDatabase();
}
function reloadDB(): void {
function reloadDatabase(): void {
const linuxUpdateJsonPath = path.join(
app.getPath("userData"),
"/config/updates.json",
@@ -47,13 +58,13 @@ function reloadDB(): void {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
"Error saving update notifications.",
"We encountered an error while saving the update notifications.",
t.__("Error saving update notifications"),
t.__("We encountered an error while saving the update notifications."),
);
logger.error("Error while JSON parsing updates.json: ");
logger.error(error);
}
}
db = new JsonDB(linuxUpdateJsonPath, true, true);
database = new JsonDB(linuxUpdateJsonPath, true, true);
}

View File

@@ -1,45 +1,45 @@
import {Notification, app, net} from "electron";
import {Notification, type Session, app} from "electron/main";
import getStream from "get-stream";
import * as semver from "semver";
import {z} from "zod";
import * as ConfigUtil from "../common/config-util";
import Logger from "../common/logger-util";
import * as ConfigUtil from "../common/config-util.ts";
import Logger from "../common/logger-util.ts";
import * as t from "../common/translation-util.ts";
import * as LinuxUpdateUtil from "./linux-update-util";
import {fetchResponse} from "./request";
import * as LinuxUpdateUtil from "./linux-update-util.ts";
const logger = new Logger({
file: "linux-update-util.log",
});
export async function linuxUpdateNotification(
session: Electron.session,
): Promise<void> {
export async function linuxUpdateNotification(session: Session): Promise<void> {
let url = "https://api.github.com/repos/zulip/zulip-desktop/releases";
url = ConfigUtil.getConfigItem("betaUpdate", false) ? url : url + "/latest";
try {
const response = await fetchResponse(net.request({url, session}));
if (response.statusCode !== 200) {
logger.log("Linux update response status: ", response.statusCode);
const response = await session.fetch(url);
if (!response.ok) {
logger.log("Linux update response status: ", response.status);
return;
}
const data = JSON.parse(await getStream(response));
const data: unknown = await response.json();
/* eslint-disable @typescript-eslint/naming-convention */
const latestVersion = ConfigUtil.getConfigItem("betaUpdate", false)
? data[0].tag_name
: data.tag_name;
if (typeof latestVersion !== "string") {
throw new TypeError("Expected string for tag_name");
}
? z.array(z.object({tag_name: z.string()})).parse(data)[0].tag_name
: z.object({tag_name: z.string()}).parse(data).tag_name;
/* eslint-enable @typescript-eslint/naming-convention */
if (semver.gt(latestVersion, app.getVersion())) {
const notified = LinuxUpdateUtil.getUpdateItem(latestVersion);
if (notified === null) {
new Notification({
title: "Zulip Update",
body: `A new version ${latestVersion} is available. Please update using your package manager.`,
title: t.__("Zulip Update"),
body: t.__(
"A new version {{{version}}} is available. Please update using your package manager.",
{version: latestVersion},
),
}).show();
LinuxUpdateUtil.setUpdateItem(latestVersion, true);
}

View File

@@ -1,21 +1,26 @@
import {BrowserWindow, Menu, app, shell} from "electron";
import {shell} from "electron/common";
import {
BrowserWindow,
Menu,
type MenuItemConstructorOptions,
app,
} from "electron/main";
import process from "node:process";
import AdmZip from "adm-zip";
import * as ConfigUtil from "../common/config-util";
import * as DNDUtil from "../common/dnd-util";
import * as t from "../common/translation-util";
import type {RendererMessage} from "../common/typed-ipc";
import type {MenuProps, TabData} from "../common/types";
import * as ConfigUtil from "../common/config-util.ts";
import * as DNDUtil from "../common/dnd-util.ts";
import * as t from "../common/translation-util.ts";
import type {RendererMessage} from "../common/typed-ipc.ts";
import type {MenuProperties, TabData} from "../common/types.ts";
import {appUpdater} from "./autoupdater";
import {send} from "./typed-ipc-main";
import {appUpdater} from "./autoupdater.ts";
import {send} from "./typed-ipc-main.ts";
const appName = app.name;
function getHistorySubmenu(
enableMenu: boolean,
): Electron.MenuItemConstructorOptions[] {
function getHistorySubmenu(enableMenu: boolean): MenuItemConstructorOptions[] {
return [
{
label: t.__("Back"),
@@ -41,7 +46,7 @@ function getHistorySubmenu(
];
}
function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
function getToolsSubmenu(): MenuItemConstructorOptions[] {
return [
{
label: t.__("Check for Updates"),
@@ -65,7 +70,7 @@ function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
click() {
const zip = new AdmZip();
const date = new Date();
const dateString = date.toLocaleDateString().replace(/\//g, "-");
const dateString = date.toLocaleDateString().replaceAll("/", "-");
// Create a zip file of all the logs and config data
zip.addLocalFolder(`${app.getPath("appData")}/${appName}/Logs`);
@@ -89,7 +94,7 @@ function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
accelerator:
process.platform === "darwin" ? "Alt+Command+I" : "Ctrl+Shift+I",
click(_item, focusedWindow) {
if (focusedWindow) {
if (focusedWindow instanceof BrowserWindow) {
focusedWindow.webContents.openDevTools({mode: "undocked"});
}
},
@@ -107,7 +112,7 @@ function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
];
}
function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
function getViewSubmenu(): MenuItemConstructorOptions[] {
return [
{
label: t.__("Reload"),
@@ -217,7 +222,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
{
label: t.__("Toggle Tray Icon"),
click(_item, focusedWindow) {
if (focusedWindow) {
if (focusedWindow instanceof BrowserWindow) {
send(focusedWindow.webContents, "toggletray");
}
},
@@ -226,7 +231,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
label: t.__("Toggle Sidebar"),
accelerator: "CommandOrControl+Shift+S",
click(_item, focusedWindow) {
if (focusedWindow) {
if (focusedWindow instanceof BrowserWindow) {
const newValue = !ConfigUtil.getConfigItem("showSidebar", true);
send(focusedWindow.webContents, "toggle-sidebar", newValue);
ConfigUtil.setConfigItem("showSidebar", newValue);
@@ -238,7 +243,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
checked: ConfigUtil.getConfigItem("autoHideMenubar", false),
visible: process.platform !== "darwin",
click(_item, focusedWindow) {
if (focusedWindow) {
if (focusedWindow instanceof BrowserWindow) {
const newValue = !ConfigUtil.getConfigItem("autoHideMenubar", false);
focusedWindow.autoHideMenuBar = newValue;
focusedWindow.setMenuBarVisibility(!newValue);
@@ -256,7 +261,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
];
}
function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
function getHelpSubmenu(): MenuItemConstructorOptions[] {
return [
{
label: `${appName + " Desktop"} v${app.getVersion()}`,
@@ -280,12 +285,8 @@ function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
},
{
label: t.__("Report an Issue"),
click() {
// The goal is to notify the main.html BrowserWindow
// which may not be the focused window.
for (const window of BrowserWindow.getAllWindows()) {
send(window.webContents, "open-feedback-modal");
}
async click() {
await shell.openExternal("https://zulip.com/help/contact-support");
},
},
];
@@ -294,8 +295,8 @@ function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
function getWindowSubmenu(
tabs: TabData[],
activeTabIndex?: number,
): Electron.MenuItemConstructorOptions[] {
const initialSubmenu: Electron.MenuItemConstructorOptions[] = [
): MenuItemConstructorOptions[] {
const initialSubmenu: MenuItemConstructorOptions[] = [
{
label: t.__("Minimize"),
role: "minimize",
@@ -307,20 +308,24 @@ function getWindowSubmenu(
];
if (tabs.length > 0) {
const ShortcutKey = process.platform === "darwin" ? "Cmd" : "Ctrl";
const shortcutKey = process.platform === "darwin" ? "Cmd" : "Ctrl";
initialSubmenu.push({
type: "separator",
});
for (const tab of tabs) {
// Skip missing elements left by `delete this.tabs[index]` in
// ServerManagerView.
if (tab === undefined) continue;
// Do not add functional tab settings to list of windows in menu bar
if (tab.role === "function" && tab.name === "Settings") {
if (tab.role === "function" && tab.page === "Settings") {
continue;
}
initialSubmenu.push({
label: tab.name,
label: tab.label,
accelerator:
tab.role === "function" ? "" : `${ShortcutKey} + ${tab.index + 1}`,
tab.role === "function" ? "" : `${shortcutKey} + ${tab.index + 1}`,
checked: tab.index === activeTabIndex,
click(_item, focusedWindow) {
if (focusedWindow) {
@@ -367,8 +372,10 @@ function getWindowSubmenu(
return initialSubmenu;
}
function getDarwinTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu = false} = props;
function getDarwinTpl(
properties: MenuProperties,
): MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu = false} = properties;
return [
{
@@ -425,7 +432,6 @@ function getDarwinTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
},
{
label: t.__("Log Out of Organization"),
accelerator: "Cmd+L",
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
@@ -533,8 +539,8 @@ function getDarwinTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
];
}
function getOtherTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu = false} = props;
function getOtherTpl(properties: MenuProperties): MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu = false} = properties;
return [
{
label: t.__("File"),
@@ -593,7 +599,6 @@ function getOtherTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
},
{
label: t.__("Log Out of Organization"),
accelerator: "Ctrl+L",
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
@@ -684,7 +689,7 @@ function getOtherTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
function sendAction<Channel extends keyof RendererMessage>(
channel: Channel,
...args: Parameters<RendererMessage[Channel]>
...arguments_: Parameters<RendererMessage[Channel]>
): void {
const win = BrowserWindow.getAllWindows()[0];
@@ -692,7 +697,7 @@ function sendAction<Channel extends keyof RendererMessage>(
win.restore();
}
send(win.webContents, channel, ...args);
send(win.webContents, channel, ...arguments_);
}
async function checkForUpdate(): Promise<void> {
@@ -702,7 +707,7 @@ async function checkForUpdate(): Promise<void> {
function getNextServer(tabs: TabData[], activeTabIndex: number): number {
do {
activeTabIndex = (activeTabIndex + 1) % tabs.length;
} while (tabs[activeTabIndex].role !== "server");
} while (tabs[activeTabIndex]?.role !== "server");
return activeTabIndex;
}
@@ -710,14 +715,16 @@ function getNextServer(tabs: TabData[], activeTabIndex: number): number {
function getPreviousServer(tabs: TabData[], activeTabIndex: number): number {
do {
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
} while (tabs[activeTabIndex].role !== "server");
} while (tabs[activeTabIndex]?.role !== "server");
return activeTabIndex;
}
export function setMenu(props: MenuProps): void {
export function setMenu(properties: MenuProperties): void {
const tpl =
process.platform === "darwin" ? getDarwinTpl(props) : getOtherTpl(props);
process.platform === "darwin"
? getDarwinTpl(properties)
: getOtherTpl(properties);
const menu = Menu.buildFromTemplate(tpl);
Menu.setApplicationMenu(menu);
}

View File

@@ -1,87 +0,0 @@
import * as ConfigUtil from "../common/config-util";
export interface ProxyRule {
hostname?: string;
port?: number;
}
// TODO: Refactor to async function
export async function resolveSystemProxy(
mainWindow: Electron.BrowserWindow,
): Promise<void> {
const page = mainWindow.webContents;
const ses = page.session;
const resolveProxyUrl = "www.example.com";
// Check HTTP Proxy
const httpProxy = (async () => {
const proxy = await ses.resolveProxy("http://" + resolveProxyUrl);
let httpString = "";
if (
proxy !== "DIRECT" &&
(proxy.includes("PROXY") || proxy.includes("HTTPS"))
) {
// In case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
httpString = "http=" + proxy.split("PROXY")[1] + ";";
}
return httpString;
})();
// Check HTTPS Proxy
const httpsProxy = (async () => {
const proxy = await ses.resolveProxy("https://" + resolveProxyUrl);
let httpsString = "";
if (
(proxy !== "DIRECT" || proxy.includes("HTTPS")) &&
(proxy.includes("PROXY") || proxy.includes("HTTPS"))
) {
// In case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
httpsString += "https=" + proxy.split("PROXY")[1] + ";";
}
return httpsString;
})();
// Check FTP Proxy
const ftpProxy = (async () => {
const proxy = await ses.resolveProxy("ftp://" + resolveProxyUrl);
let ftpString = "";
if (proxy !== "DIRECT" && proxy.includes("PROXY")) {
ftpString += "ftp=" + proxy.split("PROXY")[1] + ";";
}
return ftpString;
})();
// Check SOCKS Proxy
const socksProxy = (async () => {
const proxy = await ses.resolveProxy("socks4://" + resolveProxyUrl);
let socksString = "";
if (proxy !== "DIRECT") {
if (proxy.includes("SOCKS5")) {
socksString += "socks=" + proxy.split("SOCKS5")[1] + ";";
} else if (proxy.includes("SOCKS4")) {
socksString += "socks=" + proxy.split("SOCKS4")[1] + ";";
} else if (proxy.includes("PROXY")) {
socksString += "socks=" + proxy.split("PROXY")[1] + ";";
}
}
return socksString;
})();
const values = await Promise.all([
httpProxy,
httpsProxy,
ftpProxy,
socksProxy,
]);
const proxyString = values.join("");
ConfigUtil.setConfigItem("systemProxyRules", proxyString);
const useSystemProxy = ConfigUtil.getConfigItem("useSystemProxy", false);
if (useSystemProxy) {
ConfigUtil.setConfigItem("proxyRules", proxyString);
}
}

View File

@@ -1,82 +1,71 @@
import type {ClientRequest, IncomingMessage} from "electron";
import {app, net} from "electron";
import fs from "fs";
import path from "path";
import stream from "stream";
import util from "util";
import {type Session, app} from "electron/main";
import fs from "node:fs";
import path from "node:path";
import {Readable} from "node:stream";
import {pipeline} from "node:stream/promises";
import type {ReadableStream} from "node:stream/web";
import getStream from "get-stream";
import * as Sentry from "@sentry/electron/main";
import {z} from "zod";
import Logger from "../common/logger-util";
import * as Messages from "../common/messages";
import type {ServerConf} from "../common/types";
export async function fetchResponse(
request: ClientRequest,
): Promise<IncomingMessage> {
return new Promise((resolve, reject) => {
request.on("response", resolve);
request.on("abort", () => {
reject(new Error("Request aborted"));
});
request.on("error", reject);
request.end();
});
}
const pipeline = util.promisify(stream.pipeline);
import Logger from "../common/logger-util.ts";
import * as Messages from "../common/messages.ts";
import type {ServerConfig} from "../common/types.ts";
/* Request: domain-util */
const defaultIconUrl = "../renderer/img/icon.png";
const logger = new Logger({
file: "domain-util.log",
});
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];
let hash = 5381;
let {length} = url;
while (length) {
// eslint-disable-next-line no-bitwise, unicorn/prefer-code-point
hash = (hash * 33) ^ url.charCodeAt(--length);
}
// Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory);
}
return `${dir}/${hash >>> 0}${extension}`;
// eslint-disable-next-line no-bitwise
return `${directory}/${hash >>> 0}${extension}`;
};
export const _getServerSettings = async (
domain: string,
session: Electron.session,
): Promise<ServerConf> => {
const response = await fetchResponse(
net.request({
url: domain + "/api/v1/server_settings",
session,
}),
);
if (response.statusCode !== 200) {
session: Session,
): Promise<ServerConfig> => {
const response = await session.fetch(domain + "/api/v1/server_settings");
if (!response.ok) {
throw new Error(Messages.invalidZulipServerError(domain));
}
const {realm_name, realm_uri, realm_icon} = JSON.parse(
await getStream(response),
);
if (
typeof realm_name !== "string" ||
typeof realm_uri !== "string" ||
typeof realm_icon !== "string"
) {
throw new TypeError(Messages.noOrgsError(domain));
}
const data: unknown = await response.json();
/* eslint-disable @typescript-eslint/naming-convention */
const {
realm_name,
realm_uri,
realm_icon,
zulip_version,
zulip_feature_level,
} = z
.object({
realm_name: z.string(),
realm_uri: z.url(),
realm_icon: z.string(),
zulip_version: z.string().default("unknown"),
zulip_feature_level: z.number().default(0),
})
.parse(data);
/* eslint-enable @typescript-eslint/naming-convention */
return {
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
@@ -84,28 +73,33 @@ export const _getServerSettings = async (
icon: realm_icon.startsWith("/") ? realm_uri + realm_icon : realm_icon,
url: realm_uri,
alias: realm_name,
zulipVersion: zulip_version,
zulipFeatureLevel: zulip_feature_level,
};
};
export const _saveServerIcon = async (
url: string,
session: Electron.session,
): Promise<string> => {
session: Session,
): Promise<string | null> => {
try {
const response = await fetchResponse(net.request({url, session}));
if (response.statusCode !== 200) {
const response = await session.fetch(url);
if (!response.ok) {
logger.log("Could not get server icon.");
return defaultIconUrl;
return null;
}
const filePath = generateFilePath(url);
await pipeline(response, fs.createWriteStream(filePath));
await pipeline(
Readable.fromWeb(response.body as ReadableStream<Uint8Array>),
fs.createWriteStream(filePath),
);
return filePath;
} catch (error: unknown) {
logger.log("Could not get server icon.");
logger.log(error);
logger.reportSentry(error);
return defaultIconUrl;
Sentry.captureException(error);
return null;
}
};
@@ -113,19 +107,13 @@ export const _saveServerIcon = async (
export const _isOnline = async (
url: string,
session: Electron.session,
session: Session,
): Promise<boolean> => {
try {
const response = await fetchResponse(
net.request({
method: "HEAD",
url: `${url}/api/v1/server_settings`,
session,
}),
);
const isValidResponse =
response.statusCode >= 200 && response.statusCode < 400;
return isValidResponse;
const response = await session.fetch(`${url}/api/v1/server_settings`, {
method: "HEAD",
});
return response.ok;
} catch (error: unknown) {
logger.log(error);
return false;

22
app/main/sentry.ts Normal file
View File

@@ -0,0 +1,22 @@
import {app} from "electron/main";
import * as Sentry from "@sentry/electron/main";
import {getConfigItem} from "../common/config-util.ts";
export const sentryInit = (): void => {
Sentry.init({
dsn: "https://628dc2f2864243a08ead72e63f94c7b1@o48127.ingest.sentry.io/204668",
// Don't report errors in development or if disabled by the user.
beforeSend: (event) =>
app.isPackaged && getConfigItem("errorReporting", true) ? event : null,
// We should ignore this error since it's harmless and we know the reason behind this
// This error mainly comes from the console logs.
// This is a temp solution until Sentry supports disabling the console logs
ignoreErrors: ["does not appear to be a valid Zulip server"],
/// sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second
});
};

View File

@@ -1,8 +1,9 @@
import {app} from "electron";
import {app} from "electron/main";
import process from "node:process";
import AutoLaunch from "auto-launch";
import * as ConfigUtil from "../common/config-util";
import * as ConfigUtil from "../common/config-util.ts";
export const setAutoLaunch = async (
AutoLaunchValue: boolean,
@@ -19,13 +20,13 @@ export const setAutoLaunch = async (
// `setLoginItemSettings` doesn't support linux
if (process.platform === "linux") {
const ZulipAutoLauncher = new AutoLaunch({
const zulipAutoLauncher = new AutoLaunch({
name: "Zulip",
isHidden: false,
});
await (autoLaunchOption
? ZulipAutoLauncher.enable()
: ZulipAutoLauncher.disable());
? zulipAutoLauncher.enable()
: zulipAutoLauncher.disable());
} else {
app.setLoginItemSettings({
openAtLogin: autoLaunchOption,

View File

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

View File

@@ -1,37 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="css/about.css" />
<title>Zulip - About</title>
</head>
<!doctype html>
<meta charset="UTF-8" />
<link rel="stylesheet" href="css/about.css" />
<body>
<div class="about">
<img class="logo" src="../resources/zulip.png" />
<p class="detail" id="version">v?.?.?</p>
<div class="maintenance-info">
<p class="detail maintainer">
Maintained by
<a href="https://zulip.com" target="_blank" rel="noopener noreferrer"
>Zulip</a
>
</p>
<p class="detail license">
Available under the
<a
href="https://github.com/zulip/zulip-desktop/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
>Apache 2.0 License</a
>
</p>
</div>
</div>
<script>
const {app} = require("electron").remote;
const version_tag = document.querySelector("#version");
version_tag.textContent = "v" + app.getVersion();
</script>
</body>
</html>
<!-- Initially hidden to prevent FOUC -->
<div class="about" hidden>
<img class="logo" src="../resources/zulip.png" />
<p class="detail" id="version"></p>
</div>

View File

@@ -1,5 +1,7 @@
body {
background: rgba(250, 250, 250, 1);
:host {
contain: strict;
display: flow-root;
background: rgb(250 250 250 / 100%);
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: subpixel-antialiased;
}
@@ -10,12 +12,13 @@ body {
}
#version {
color: rgba(68, 67, 67, 1);
color: rgb(68 67 67 / 100%);
font-size: 1.3em;
padding-top: 40px;
}
.about {
display: block !important;
margin: 25vh auto;
height: 25vh;
text-align: center;
@@ -23,7 +26,7 @@ body {
.about p {
font-size: 20px;
color: rgba(0, 0, 0, 0.62);
color: rgb(0 0 0 / 62%);
}
.about img {
@@ -44,11 +47,10 @@ body {
}
.maintenance-info {
cursor: pointer;
position: absolute;
width: 100%;
left: 0;
color: rgba(68, 68, 68, 1);
color: rgb(68 68 68 / 100%);
}
.maintenance-info p {
@@ -58,7 +60,7 @@ body {
}
p.detail a {
color: rgba(53, 95, 76, 1);
color: rgb(53 95 76 / 100%);
}
p.detail a:hover {

View File

@@ -1,5 +1,5 @@
:host {
--button-color: rgb(69, 166, 149);
--button-color: rgb(69 166 149);
}
button {
@@ -14,6 +14,6 @@ button:focus {
}
button:active {
background-color: rgb(241, 241, 241);
background-color: rgb(241 241 241);
color: var(--button-color);
}

View File

@@ -0,0 +1,14 @@
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src:
local("Material Icons"),
local("MaterialIcons-Regular"),
url("../fonts/MaterialIcons-Regular.ttf") format("truetype");
}
@font-face {
font-family: Montserrat;
src: url("../fonts/Montserrat-Regular.ttf") format("truetype");
}

View File

@@ -16,9 +16,9 @@ body {
}
.toggle-sidebar {
background: rgba(34, 44, 49, 1);
background: rgb(34 44 49 / 100%);
width: 54px;
padding: 27px 0 20px 0;
padding: 27px 0 20px;
justify-content: space-between;
display: flex;
flex-direction: column;
@@ -44,6 +44,7 @@ body {
#view-controls-container {
height: calc(100% - 208px);
scrollbar-gutter: stable both-edges;
overflow-y: hidden;
}
@@ -52,24 +53,15 @@ body {
}
#view-controls-container::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
background-color: rgb(0 0 0 / 30%);
}
#view-controls-container::-webkit-scrollbar-thumb {
background-color: rgba(169, 169, 169, 1);
outline: 1px solid rgba(169, 169, 169, 1);
background-color: rgb(169 169 169 / 100%);
}
#view-controls-container:hover {
overflow-y: overlay;
}
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: local("Material Icons"), local("MaterialIcons-Regular"),
url(../fonts/MaterialIcons-Regular.ttf) format("truetype");
overflow-y: scroll;
}
/*******************
@@ -93,7 +85,7 @@ body {
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
overflow-wrap: normal;
white-space: nowrap;
direction: ltr;
@@ -101,7 +93,7 @@ body {
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
text-rendering: optimizelegibility;
}
#actions-container {
@@ -122,23 +114,31 @@ body {
}
.action-button i {
color: rgba(108, 133, 146, 1);
color: hsl(200.53deg 14.96% 49.8%);
font-size: 28px;
}
.action-button:hover i {
color: rgba(152, 169, 179, 1);
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 {
/* background-color: rgba(255, 255, 255, 0.25); */
background-color: rgba(239, 239, 239, 1);
background-color: rgb(239 239 239 / 100%);
opacity: 0.9;
padding-right: 14px;
}
.action-button.active i {
color: rgba(28, 38, 43, 1);
color: rgb(28 38 43 / 100%);
}
.action-button.disable {
@@ -150,7 +150,7 @@ body {
}
.action-button.disable:hover i {
color: rgba(108, 133, 146, 1);
color: rgb(108 133 146 / 100%);
}
.tab {
@@ -180,7 +180,7 @@ body {
margin-top: 5px;
z-index: 11;
line-height: 31px;
color: rgba(238, 238, 238, 1);
color: rgb(238 238 238 / 100%);
text-align: center;
overflow: hidden;
opacity: 0.6;
@@ -191,7 +191,7 @@ body {
font-family: Verdana, sans-serif;
font-weight: 600;
font-size: 22px;
border: 2px solid rgba(34, 44, 49, 1);
border: 2px solid rgb(34 44 49 / 100%);
margin-left: 17%;
width: 35px;
border-radius: 4px;
@@ -203,7 +203,7 @@ body {
.tab.active .server-tab {
opacity: 1;
background-color: rgba(100, 132, 120, 1);
background-color: rgb(100 132 120 / 100%);
}
.tab.functional-tab {
@@ -214,7 +214,7 @@ body {
.tab.functional-tab.active .server-tab {
padding: 2px 0;
height: 40px;
background-color: rgba(255, 255, 255, 0.25);
background-color: rgb(255 255 255 / 25%);
}
.tab.functional-tab .server-tab i {
@@ -227,14 +227,14 @@ body {
min-width: 11px;
padding: 0 3px;
height: 17px;
background-color: rgba(244, 67, 54, 1);
background-color: rgb(244 67 54 / 100%);
font-size: 10px;
font-family: sans-serif;
position: absolute;
z-index: 15;
top: 6px;
float: right;
color: rgba(255, 255, 255, 1);
color: rgb(255 255 255 / 100%);
text-align: center;
line-height: 17px;
display: block;
@@ -262,7 +262,7 @@ body {
}
.tab .server-tab-shortcut {
color: rgba(100, 132, 120, 1);
color: rgb(100 132 120 / 100%);
font-size: 12px;
text-align: center;
font-family: sans-serif;
@@ -298,7 +298,9 @@ body {
content: "";
position: absolute;
z-index: 1;
background: rgba(255, 255, 255, 1) url(../img/ic_loading.gif) no-repeat;
/* Spinner is released under loading.io free License: https://loading.io/license/#free-license */
background: rgb(255 255 255 / 100%) url("../img/ic_loading.svg") no-repeat;
background-size: 60px 60px;
background-position: center;
width: 100%;
@@ -307,39 +309,62 @@ body {
/* When the active webview is loaded */
#webviews-container.loaded::before {
opacity: 0;
z-index: -1;
visibility: hidden;
}
webview {
/* transition: opacity 0.3s ease-in; */
.webview-pane,
.functional-view {
position: absolute;
width: 100%;
height: 100%;
flex-grow: 1;
visibility: hidden;
}
.webview-pane {
display: flex;
flex-direction: column;
}
webview.onload {
transition: opacity 1s cubic-bezier(0.95, 0.05, 0.795, 0.035);
.webview-pane > webview {
flex: 1;
}
webview.active {
opacity: 1;
.webview-pane.active,
.functional-view.active {
z-index: 1;
visibility: visible;
}
webview.disabled {
opacity: 0;
}
webview.focus {
outline: 0 solid transparent;
}
.webview-unsupported {
background: rgb(254 243 199);
border: 1px solid rgb(253 230 138);
color: rgb(69 26 3);
font-family: system-ui;
font-size: 14px;
display: flex;
}
.webview-unsupported[hidden] {
display: none;
}
.webview-unsupported-message {
padding: 0.3em;
flex: 1;
text-align: center;
}
.webview-unsupported-dismiss {
padding: 0.3em;
cursor: pointer;
}
/* Tooltip styling */
#loading-tooltip,
@@ -348,13 +373,13 @@ webview.focus {
#reload-tooltip,
#setting-tooltip {
font-family: sans-serif;
background: rgba(34, 44, 49, 1);
background: rgb(34 44 49 / 100%);
margin-left: 48px;
padding: 6px 8px;
position: absolute;
margin-top: 0;
z-index: 1000;
color: rgba(255, 255, 255, 1);
color: rgb(255 255 255 / 100%);
border-radius: 4px;
text-align: center;
width: 55px;
@@ -369,7 +394,7 @@ webview.focus {
content: " ";
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid rgba(34, 44, 49, 1);
border-right: 8px solid rgb(34 44 49 / 100%);
position: absolute;
top: 7px;
right: 68px;
@@ -377,14 +402,14 @@ webview.focus {
#add-server-tooltip,
.server-tooltip {
font-family: "arial", sans-serif;
background: rgba(34, 44, 49, 1);
font-family: arial, sans-serif;
background: rgb(34 44 49 / 100%);
left: 56px;
padding: 10px 20px;
position: fixed;
margin-top: 11px;
z-index: 5000 !important;
color: rgba(255, 255, 255, 1);
color: rgb(255 255 255 / 100%);
border-radius: 4px;
text-align: center;
width: max-content;
@@ -396,7 +421,7 @@ webview.focus {
content: " ";
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid rgba(34, 44, 49, 1);
border-right: 8px solid rgb(34 44 49 / 100%);
position: absolute;
top: 10px;
left: -5px;
@@ -408,14 +433,14 @@ webview.focus {
position: absolute;
width: 24px;
height: 24px;
background: rgba(34, 44, 49, 1);
background: rgb(34 44 49 / 100%);
border-radius: 20px;
cursor: pointer;
box-shadow: rgba(153, 153, 153, 1) 1px 1px;
box-shadow: rgb(153 153 153 / 100%) 1px 1px;
}
#collapse-button i {
color: rgba(239, 239, 239, 1);
color: rgb(239 239 239 / 100%);
}
#main-container {
@@ -435,8 +460,8 @@ webview.focus {
.popup .popuptext {
visibility: hidden;
background-color: rgba(85, 85, 85, 1);
color: rgba(255, 255, 255, 1);
background-color: rgb(85 85 85 / 100%);
color: rgb(255 255 255 / 100%);
text-align: center;
border-radius: 6px;
padding: 9px 0;
@@ -451,11 +476,11 @@ webview.focus {
.popup .show {
visibility: visible;
animation: cssAnimation 0s ease-in 5s forwards;
animation: full-screen-popup 0s ease-in 1s forwards;
animation-fill-mode: forwards;
}
@keyframes cssAnimation {
@keyframes full-screen-popup {
from {
opacity: 0;
}
@@ -467,26 +492,3 @@ webview.focus {
opacity: 1;
}
}
send-feedback {
width: 60%;
height: 85%;
}
#feedback-modal {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(68, 67, 67, 0.81);
align-items: center;
justify-content: center;
z-index: 2;
transition: all 1s ease-out;
}
#feedback-modal.show {
display: flex;
}

View File

@@ -3,8 +3,8 @@ body {
margin: 0;
cursor: default;
font-size: 14px;
color: rgba(51, 51, 51, 1);
background: rgba(255, 255, 255, 1);
color: rgb(51 51 51 / 100%);
background: rgb(255 255 255 / 100%);
user-select: none;
}
@@ -45,8 +45,8 @@ body {
.button {
font-size: 16px;
background: rgba(0, 150, 136, 1);
color: rgba(255, 255, 255, 1);
background: rgb(0 150 136 / 100%);
color: rgb(255 255 255 / 100%);
width: 96px;
height: 32px;
border-radius: 5px;

View File

@@ -1,31 +1,37 @@
html,
body {
height: 100%;
margin: 0;
@import url("@yaireo/tagify/dist/tagify.css");
:host {
contain: strict;
display: flow-root;
cursor: default;
user-select: none;
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
background: rgba(239, 239, 239, 1);
background: rgb(239 239 239 / 100%);
letter-spacing: -0.08px;
line-height: 18px;
color: rgba(139, 142, 143, 1);
color: rgb(34 44 49 / 100%);
/* 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-bg-color: rgb(255 255 255);
--tagify-dd-item-pad: 0.3em 0.5em;
}
kbd {
display: inline-block;
border: 1px solid rgba(204, 204, 204, 1);
border: 1px solid rgb(204 204 204 / 100%);
border-radius: 4px;
font-size: 15px;
font-family: Courier New, Courier, monospace;
font-family: "Courier New", Courier, monospace;
font-weight: bold;
white-space: nowrap;
background-color: rgba(247, 247, 247, 1);
color: rgba(51, 51, 51, 1);
background-color: rgb(247 247 247 / 100%);
color: rgb(51 51 51 / 100%);
margin: 0 0.1em;
padding: 0.3em 0.8em;
text-shadow: 0 1px 0 rgba(255, 255, 255, 1);
text-shadow: 0 1px 0 rgb(255 255 255 / 100%);
line-height: 1.4;
}
@@ -33,7 +39,7 @@ table,
th,
td {
border-collapse: collapse;
color: rgba(56, 52, 48, 1);
color: rgb(56 52 48 / 100%);
}
table {
@@ -51,19 +57,6 @@ td:nth-child(odd) {
width: 50%;
}
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: local("Material Icons"), local("MaterialIcons-Regular"),
url(../fonts/MaterialIcons-Regular.ttf) format("truetype");
}
@font-face {
font-family: "Montserrat";
src: url(../fonts/Montserrat-Regular.ttf) format("truetype");
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
@@ -75,7 +68,7 @@ td:nth-child(odd) {
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
overflow-wrap: normal;
white-space: nowrap;
direction: ltr;
@@ -83,13 +76,13 @@ td:nth-child(odd) {
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
text-rendering: optimizelegibility;
}
#content {
display: flex;
display: flex !important;
height: 100%;
font-family: "Montserrat", sans-serif;
font-family: Montserrat, sans-serif;
}
#sidebar {
@@ -99,7 +92,7 @@ td:nth-child(odd) {
display: flex;
flex-direction: column;
font-size: 16px;
background: rgba(242, 242, 242, 1);
background: rgb(242 242 242 / 100%);
}
#nav-container {
@@ -108,18 +101,18 @@ td:nth-child(odd) {
.nav {
padding: 7px 0;
color: rgba(153, 153, 153, 1);
color: rgb(70 78 90 / 100%);
cursor: pointer;
}
.nav.active {
color: rgba(78, 191, 172, 1);
color: rgb(78 191 172 / 100%);
cursor: default;
position: relative;
}
.nav.active::before {
background: rgba(70, 78, 90, 1);
background: rgb(70 78 90 / 100%);
width: 3px;
height: 18px;
position: absolute;
@@ -129,13 +122,14 @@ td:nth-child(odd) {
/* We don't want to show this in nav item since we have the + button for adding an Organization */
/* stylelint-disable-next-line selector-id-pattern */
#nav-AddServer {
display: none;
}
#settings-header {
font-size: 22px;
color: rgba(34, 44, 49, 1);
color: rgb(34 44 49 / 100%);
font-weight: bold;
text-transform: uppercase;
}
@@ -155,19 +149,19 @@ td:nth-child(odd) {
.title {
font-weight: 500;
color: rgba(34, 44, 49, 1);
color: rgb(34 44 49 / 100%);
}
.page-title {
color: rgba(34, 44, 49, 1);
color: rgb(34 44 49 / 100%);
font-size: 15px;
font-weight: bold;
padding: 4px 0 6px 0;
padding: 4px 0 6px;
}
.add-server-info-row {
display: flex;
margin: 8px 0 0 0;
margin: 8px 0 0;
}
.add-server-info-right {
@@ -176,9 +170,9 @@ td:nth-child(odd) {
}
.sub-title {
padding: 4px 0 6px 0;
padding: 4px 0 6px;
font-weight: bold;
color: rgba(97, 97, 97, 1);
color: rgb(97 97 97 / 100%);
}
img.server-info-icon {
@@ -205,7 +199,7 @@ img.server-info-icon {
.server-info-row {
display: inline-block;
margin: 5px 0 0 0;
margin: 5px 0 0;
}
.server-info-left .server-info-row {
@@ -245,18 +239,18 @@ img.server-info-icon {
font-size: 14px;
border-radius: 4px;
padding: 13px;
border: rgba(237, 237, 237, 1) 2px solid;
border: rgb(237 237 237 / 100%) 2px solid;
outline-width: 0;
background: transparent;
max-width: 450px;
}
.setting-input-value:focus {
border: rgba(78, 191, 172, 1) 2px solid;
border: rgb(78 191 172 / 100%) 2px solid;
}
.invalid-input-value:focus {
border: rgba(239, 83, 80, 1) 2px solid;
border: rgb(239 83 80 / 100%) 2px solid;
}
.manual-proxy-block {
@@ -266,7 +260,7 @@ img.server-info-icon {
.actions-container {
display: flex;
font-size: 14px;
color: rgba(35, 93, 58, 1);
color: rgb(35 93 58 / 100%);
vertical-align: middle;
margin: 10px 0;
flex-wrap: wrap;
@@ -295,7 +289,7 @@ img.server-info-icon {
}
.action.disabled {
color: rgba(153, 153, 153, 1);
color: rgb(153 153 153 / 100%);
}
.action.disabled:hover {
@@ -306,14 +300,16 @@ img.server-info-icon {
display: flex;
flex-wrap: wrap;
padding: 12px 30px;
margin: 10px 0 20px 0;
background: rgba(255, 255, 255, 1);
margin: 10px 0 20px;
background: rgb(255 255 255 / 100%);
width: 80%;
transition: all 0.2s;
}
.settings-card:hover {
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 0 0 rgba(0, 0, 0, 0.12);
box-shadow:
0 2px 5px 0 rgb(0 0 0 / 16%),
0 2px 0 0 rgb(0 0 0 / 12%);
}
.hidden {
@@ -322,11 +318,11 @@ img.server-info-icon {
}
.red {
color: rgb(240, 148, 148);
background: rgba(255, 255, 255, 1);
color: rgb(240 148 148);
background: rgb(255 255 255 / 100%);
border-radius: 4px;
display: inline-block;
border: 2px solid rgb(240, 148, 148);
border: 2px solid rgb(240 148 148);
padding: 10px;
width: 100px;
cursor: pointer;
@@ -338,13 +334,13 @@ img.server-info-icon {
}
.red:hover {
background-color: rgb(240, 148, 148);
color: rgba(255, 255, 255, 1);
background-color: rgb(240 148 148);
color: rgb(255 255 255 / 100%);
}
.green {
color: rgba(255, 255, 255, 1);
background: rgba(78, 191, 172, 1);
color: rgb(255 255 255 / 100%);
background: rgb(78 191 172 / 100%);
border-radius: 4px;
display: inline-block;
border: none;
@@ -359,8 +355,8 @@ img.server-info-icon {
}
.green:hover {
background-color: rgba(60, 159, 141, 1);
color: rgba(255, 255, 255, 1);
background-color: rgb(60 159 141 / 100%);
color: rgb(255 255 255 / 100%);
}
.w-150 {
@@ -372,9 +368,9 @@ img.server-info-icon {
}
.grey {
color: rgba(158, 158, 158, 1);
background: rgba(250, 250, 250, 1);
border: 1px solid rgba(158, 158, 158, 1);
color: rgb(158 158 158 / 100%);
background: rgb(250 250 250 / 100%);
border: 1px solid rgb(158 158 158 / 100%);
}
.setting-row {
@@ -390,7 +386,7 @@ img.server-info-icon {
}
.code {
font-family: Courier New, Courier, monospace;
font-family: "Courier New", Courier, monospace;
}
i.open-tab-button {
@@ -414,7 +410,7 @@ i.open-tab-button {
.selected-css-path,
.download-folder-path {
background: rgba(238, 238, 238, 1);
background: rgb(238 238 238 / 100%);
padding: 5px 10px;
margin-right: 10px;
display: flex;
@@ -431,7 +427,7 @@ i.open-tab-button {
}
#new-org-button {
margin: 30px 0 30px 0;
margin: 30px 0;
}
#create-organization-container {
@@ -463,7 +459,7 @@ i.open-tab-button {
}
.disallowed:hover {
background-color: rgba(241, 241, 241, 1);
background-color: rgb(241 241 241 / 100%);
cursor: not-allowed;
}
@@ -471,7 +467,7 @@ input.toggle-round + label {
padding: 2px;
width: 50px;
height: 25px;
background-color: rgba(221, 221, 221, 1);
background-color: rgb(221 221 221 / 100%);
border-radius: 25px;
}
@@ -486,27 +482,21 @@ input.toggle-round + label::after {
}
input.toggle-round + label::before {
background-color: rgba(241, 241, 241, 1);
background-color: rgb(241 241 241 / 100%);
border-radius: 25px;
top: 0;
right: 0;
left: 0;
bottom: 0;
inset: 0;
}
input.toggle-round + label::after {
width: 25px;
height: 25px;
background-color: rgba(255, 255, 255, 1);
background-color: rgb(255 255 255 / 100%);
border-radius: 100%;
}
input.toggle-round:checked + label::before {
background-color: rgba(78, 191, 172, 1);
top: 0;
right: 0;
left: 0;
bottom: 0;
background-color: rgb(78 191 172 / 100%);
inset: 0;
}
input.toggle-round:checked + label::after {
@@ -527,17 +517,21 @@ input.toggle-round:checked + label::after {
height: 100%;
/* background: rgba(61, 64, 67, 15); */
background: linear-gradient(35deg, rgba(0, 59, 82, 1), rgba(69, 181, 155, 1));
background: linear-gradient(
35deg,
rgb(0 59 82 / 100%),
rgb(69 181 155 / 100%)
);
overflow: auto;
}
/* Modal Content */
.modal-container {
background-color: rgba(244, 247, 248, 1);
background-color: rgb(244 247 248 / 100%);
margin: auto;
padding: 57px;
border: rgba(218, 225, 227, 1) 1px solid;
border: rgb(218 225 227 / 100%) 1px solid;
width: 550px;
height: 370px;
border-radius: 4px;
@@ -551,7 +545,7 @@ input.toggle-round:checked + label::after {
.divider {
margin-bottom: 30px;
margin-top: 30px;
color: rgba(125, 135, 138, 1);
color: rgb(125 135 138 / 100%);
}
.divider hr {
@@ -582,9 +576,8 @@ input.toggle-round:checked + label::after {
margin: auto;
align-items: center;
text-align: center;
color: rgba(255, 255, 255, 1);
background: rgba(78, 191, 172, 1);
border-color: none;
color: rgb(255 255 255 / 100%);
background: rgb(78 191 172 / 100%);
border: none;
width: 98%;
height: 46px;
@@ -593,11 +586,11 @@ input.toggle-round:checked + label::after {
}
.server-center button:hover {
background: rgba(50, 149, 136, 1);
background: rgb(50 149 136 / 100%);
}
.server-center button:focus {
background: rgba(50, 149, 136, 1);
background: rgb(50 149 136 / 100%);
}
.certificates-card {
@@ -646,7 +639,7 @@ input.toggle-round:checked + label::after {
padding-top: 15px;
align-items: center;
text-align: center;
color: rgb(78, 191, 172);
color: rgb(78 191 172);
width: 98%;
height: 46px;
cursor: pointer;
@@ -660,7 +653,7 @@ i.open-network-button {
}
/* responsive grid */
@media (min-width: 500px) and (max-width: 720px) {
@media (width >= 500px) and (width <= 720px) {
#new-server-container {
padding-left: 0;
width: 60vw;
@@ -672,7 +665,7 @@ i.open-network-button {
}
}
@media (max-width: 500px) {
@media (width <= 500px) {
#new-server-container {
padding-left: 0;
width: 54%;
@@ -683,7 +676,7 @@ i.open-network-button {
}
}
@media (max-width: 650px) {
@media (width <= 650px) {
.selected-css-path,
.download-folder-path {
margin-right: 15px;
@@ -698,7 +691,7 @@ i.open-network-button {
}
}
@media (max-width: 720px) {
@media (width <= 720px) {
.modal-container {
width: 60vw;
padding: 40px;
@@ -721,7 +714,7 @@ i.open-network-button {
}
}
@media (max-width: 600px) {
@media (width <= 600px) {
.divider {
margin-left: 4%;
}
@@ -733,7 +726,7 @@ i.open-network-button {
}
}
@media (max-width: 900px) {
@media (width <= 900px) {
.settings-card {
flex-direction: column;
align-items: center;
@@ -752,18 +745,26 @@ i.open-network-button {
.lang-menu {
font-size: 13px;
font-weight: bold;
background: rgba(78, 191, 172, 1);
background: rgb(78 191 172 / 100%);
width: 100px;
height: 38px;
color: rgba(255, 255, 255, 1);
border-color: rgba(0, 0, 0, 0);
color: rgb(255 255 255 / 100%);
border-color: rgb(0 0 0 / 0%);
}
/* stylelint-disable-next-line selector-class-pattern */
.tagify__input {
min-width: 130px !important;
}
/* stylelint-disable-next-line selector-class-pattern */
.tagify__input::before {
top: 0;
bottom: 0;
}
.settings-tagify-dropdown {
position: relative;
z-index: 9999;
height: 0;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgba(0, 0, 0, 0) none repeat scroll 0% 0%; display: block; shape-rendering: auto; animation-play-state: running; animation-delay: 0s;" width="150px" height="150px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" fill="none" stroke="#759ed4" stroke-width="10" r="42" stroke-dasharray="197.92033717615698 67.97344572538566" style="animation-play-state: running; animation-delay: 0s;">
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1" style="animation-play-state: running; animation-delay: 0s;"></animateTransform>
</circle>
<!-- Created with loading.io (https://loading.io/spinner/rolling/-bar-circle-curve-round-rotate) -->
<!-- "The Rolling spinner is released under loading.io free License." (https://loading.io/license/#free-license) -->
</svg>

After

Width:  |  Height:  |  Size: 1018 B

View File

@@ -1,5 +1,4 @@
import crypto from "crypto";
import {clipboard} from "electron";
import {ipcRenderer} from "./typed-ipc-renderer.ts";
// This helper is exposed via electron_bridge for use in the social
// login flow.
@@ -15,7 +14,13 @@ import {clipboard} from "electron";
// dont leak anything from the users clipboard other than the token
// intended for us.
export class ClipboardDecrypterImpl implements ClipboardDecrypter {
export type ClipboardDecrypter = {
version: number;
key: Uint8Array;
pasted: Promise<string>;
};
export class ClipboardDecrypterImplementation implements ClipboardDecrypter {
version: number;
key: Uint8Array;
pasted: Promise<string>;
@@ -23,15 +28,13 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter {
constructor(_: number) {
// At this time, the only version is 1.
this.version = 1;
this.key = crypto.randomBytes(32);
const {key, sig} = ipcRenderer.sendSync("new-clipboard-key");
this.key = key;
this.pasted = new Promise((resolve) => {
let interval: NodeJS.Timeout | null = null;
const startPolling = () => {
if (interval === null) {
interval = setInterval(poll, 1000);
}
poll();
interval ??= setInterval(poll, 1000);
void poll();
};
const stopPolling = () => {
@@ -41,30 +44,9 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter {
}
};
const poll = () => {
let plaintext;
try {
const data = Buffer.from(clipboard.readText(), "hex");
const iv = data.slice(0, 12);
const ciphertext = data.slice(12, -16);
const authTag = data.slice(-16);
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
this.key,
iv,
{authTagLength: 16},
);
decipher.setAuthTag(authTag);
plaintext =
decipher.update(ciphertext, undefined, "utf8") +
decipher.final("utf8");
} catch {
// If the parsing or decryption failed in any way,
// the correct token hasnt been copied yet; try
// again next time.
return;
}
const poll = async () => {
const plaintext = await ipcRenderer.invoke("poll-clipboard", key, sig);
if (plaintext === undefined) return;
window.removeEventListener("focus", startPolling);
window.removeEventListener("blur", stopPolling);

View File

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

View File

@@ -1,18 +1,23 @@
import type {ContextMenuParams} from "electron";
import {remote} from "electron";
import {type Event, clipboard} from "electron/common";
import type {WebContents} from "electron/main";
import type {
ContextMenuParams,
MenuItemConstructorOptions,
} from "electron/renderer";
import process from "node:process";
import * as t from "../../../common/translation-util";
import {BrowserWindow, Menu} from "@electron/remote";
const {clipboard, Menu} = remote;
import * as t from "../../../common/translation-util.ts";
export const contextMenu = (
webContents: Electron.WebContents,
webContents: WebContents,
event: Event,
props: ContextMenuParams,
properties: ContextMenuParams,
) => {
const isText = props.selectionText !== "";
const isLink = props.linkURL !== "";
const linkURL = isLink ? new URL(props.linkURL) : undefined;
const isText = properties.selectionText !== "";
const isLink = properties.linkURL !== "";
const linkUrl = isLink ? new URL(properties.linkURL) : undefined;
const makeSuggestion = (suggestion: string) => ({
label: suggestion,
@@ -22,22 +27,24 @@ export const contextMenu = (
},
});
let menuTemplate: Electron.MenuItemConstructorOptions[] = [
let menuTemplate: MenuItemConstructorOptions[] = [
{
label: t.__("Add to Dictionary"),
visible: props.isEditable && isText && props.misspelledWord.length > 0,
visible:
properties.isEditable && isText && properties.misspelledWord.length > 0,
click(_item) {
webContents.session.addWordToSpellCheckerDictionary(
props.misspelledWord,
properties.misspelledWord,
);
},
},
{
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,
click(_item) {
webContents.showDefinitionForSelection();
@@ -50,7 +57,7 @@ export const contextMenu = (
{
label: t.__("Cut"),
visible: isText,
enabled: props.isEditable,
enabled: properties.isEditable,
accelerator: "CommandOrControl+X",
click(_item) {
webContents.cut();
@@ -59,7 +66,7 @@ export const contextMenu = (
{
label: t.__("Copy"),
accelerator: "CommandOrControl+C",
enabled: props.editFlags.canCopy,
enabled: properties.editFlags.canCopy,
click(_item) {
webContents.copy();
},
@@ -67,7 +74,7 @@ export const contextMenu = (
{
label: t.__("Paste"), // Bug: Paste replaces text
accelerator: "CommandOrControl+V",
enabled: props.isEditable,
enabled: properties.isEditable,
click() {
webContents.paste();
},
@@ -77,51 +84,45 @@ export const contextMenu = (
},
{
label:
linkURL?.protocol === "mailto:"
linkUrl?.protocol === "mailto:"
? t.__("Copy Email Address")
: t.__("Copy Link"),
visible: isLink,
click(_item) {
clipboard.write({
bookmark: props.linkText,
bookmark: properties.linkText,
text:
linkURL?.protocol === "mailto:" ? linkURL.pathname : props.linkURL,
linkUrl?.protocol === "mailto:"
? linkUrl.pathname
: properties.linkURL,
});
},
},
{
label: t.__("Copy Image"),
visible: props.mediaType === "image",
visible: properties.mediaType === "image",
click(_item) {
webContents.copyImageAt(props.x, props.y);
webContents.copyImageAt(properties.x, properties.y);
},
},
{
label: t.__("Copy Image URL"),
visible: props.mediaType === "image",
visible: properties.mediaType === "image",
click(_item) {
clipboard.write({
bookmark: props.srcURL,
text: props.srcURL,
bookmark: properties.srcURL,
text: properties.srcURL,
});
},
},
{
type: "separator",
visible: isLink || props.mediaType === "image",
},
{
label: t.__("Services"),
visible: process.platform === "darwin",
role: "services",
},
];
if (props.misspelledWord) {
if (props.dictionarySuggestions.length > 0) {
const suggestions: Electron.MenuItemConstructorOptions[] = props.dictionarySuggestions.map(
(suggestion: string) => makeSuggestion(suggestion),
);
if (properties.misspelledWord) {
if (properties.dictionarySuggestions.length > 0) {
const suggestions: MenuItemConstructorOptions[] =
properties.dictionarySuggestions.map((suggestion: string) =>
makeSuggestion(suggestion),
);
menuTemplate = [...suggestions, ...menuTemplate];
} else {
menuTemplate.unshift({
@@ -131,7 +132,7 @@ export const contextMenu = (
}
}
// Hide the invisible separators on Linux and Windows
// Electron has a bug which ignores visible: false parameter for separator menuitems. So we remove them here.
// Electron has a bug which ignores visible: false parameter for separator menu items. So we remove them here.
// https://github.com/electron/electron/issues/5869
// https://github.com/electron/electron/issues/6906
@@ -139,5 +140,11 @@ export const contextMenu = (
(menuItem) => menuItem.visible ?? true,
);
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,39 +1,60 @@
import type {HTML} from "../../../common/html";
import {html} from "../../../common/html";
import {type Html, html} from "../../../common/html.ts";
import type {TabPage} from "../../../common/types.ts";
import {generateNodeFromHTML} from "./base";
import type {TabProps} from "./tab";
import Tab from "./tab";
import {generateNodeFromHtml} from "./base.ts";
import Tab, {type TabProperties} from "./tab.ts";
export type FunctionalTabProperties = {
$view: Element;
page: TabPage;
} & TabProperties;
export default class FunctionalTab extends Tab {
$view: Element;
$el: Element;
$closeButton?: Element;
constructor(props: TabProps) {
super(props);
constructor({$view, ...properties}: FunctionalTabProperties) {
super(properties);
this.$el = generateNodeFromHTML(this.templateHTML());
if (this.props.name !== "Settings") {
this.props.$root.append(this.$el);
this.$view = $view;
this.$el = generateNodeFromHtml(this.templateHtml());
if (properties.page !== "Settings") {
this.properties.$root.append(this.$el);
this.$closeButton = this.$el.querySelector(".server-tab-badge")!;
this.registerListeners();
}
}
templateHTML(): HTML {
override async activate(): Promise<void> {
await super.activate();
this.$view.classList.add("active");
}
override async deactivate(): Promise<void> {
await super.deactivate();
this.$view.classList.remove("active");
}
override async destroy(): Promise<void> {
await super.destroy();
this.$view.remove();
}
templateHtml(): Html {
return html`
<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
<div class="tab functional-tab" data-tab-id="${this.properties.tabIndex}">
<div class="server-tab-badge close-button">
<i class="material-icons">close</i>
</div>
<div class="server-tab">
<i class="material-icons">${this.props.materialIcon}</i>
<i class="material-icons">${this.properties.materialIcon}</i>
</div>
</div>
`;
}
registerListeners(): void {
override registerListeners(): void {
super.registerListeners();
this.$el.addEventListener("mouseover", () => {
@@ -44,8 +65,8 @@ export default class FunctionalTab extends Tab {
this.$closeButton?.classList.remove("active");
});
this.$closeButton?.addEventListener("click", (event: Event) => {
this.props.onDestroy?.();
this.$closeButton?.addEventListener("click", (event) => {
this.properties.onDestroy?.();
event.stopPropagation();
});
}

View File

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

View File

@@ -1,66 +1,93 @@
import type {HTML} from "../../../common/html";
import {html} from "../../../common/html";
import {ipcRenderer} from "../typed-ipc-renderer";
import * as SystemUtil from "../utils/system-util";
import process from "node:process";
import {generateNodeFromHTML} from "./base";
import type {TabProps} from "./tab";
import Tab from "./tab";
import {type Html, html} from "../../../common/html.ts";
import {ipcRenderer} from "../typed-ipc-renderer.ts";
import {generateNodeFromHtml} from "./base.ts";
import Tab, {type TabProperties} from "./tab.ts";
import type WebView from "./webview.ts";
export type ServerTabProperties = {
webview: Promise<WebView>;
} & TabProperties;
export default class ServerTab extends Tab {
webview: Promise<WebView>;
$el: Element;
$name: Element;
$icon: HTMLImageElement;
$badge: Element;
constructor(props: TabProps) {
super(props);
constructor({webview, ...properties}: ServerTabProperties) {
super(properties);
this.$el = generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$el);
this.webview = webview;
this.$el = generateNodeFromHtml(this.templateHtml());
this.properties.$root.append(this.$el);
this.registerListeners();
this.$name = this.$el.querySelector(".server-tooltip")!;
this.$icon = this.$el.querySelector(".server-icons")!;
this.$badge = this.$el.querySelector(".server-tab-badge")!;
}
templateHTML(): HTML {
override async activate(): Promise<void> {
await super.activate();
(await this.webview).load();
}
override async deactivate(): Promise<void> {
await super.deactivate();
(await this.webview).hide();
}
override async destroy(): Promise<void> {
await super.destroy();
(await this.webview).destroy();
}
templateHtml(): Html {
return html`
<div class="tab" data-tab-id="${this.props.tabIndex}">
<div class="tab" data-tab-id="${this.properties.tabIndex}">
<div class="server-tooltip" style="display:none">
${this.props.name}
${this.properties.label}
</div>
<div class="server-tab-badge"></div>
<div class="server-tab">
<img class="server-icons" src="${this.props.icon}" />
<img class="server-icons" src="${this.properties.icon}" />
</div>
<div class="server-tab-shortcut">${this.generateShortcutText()}</div>
</div>
`;
}
setLabel(label: string): void {
this.properties.label = label;
this.$name.textContent = label;
}
setIcon(icon: string): void {
this.properties.icon = icon;
this.$icon.src = icon;
}
updateBadge(count: number): void {
if (count > 0) {
const formattedCount = count > 999 ? "1K+" : count.toString();
this.$badge.textContent = formattedCount;
this.$badge.classList.add("active");
} else {
this.$badge.classList.remove("active");
}
this.$badge.textContent = count > 999 ? "1K+" : count.toString();
this.$badge.classList.toggle("active", count > 0);
}
generateShortcutText(): string {
// Only provide shortcuts for server [0..9]
if (this.props.index >= 9) {
if (this.properties.index >= 9) {
return "";
}
const shownIndex = this.props.index + 1;
let shortcutText = "";
shortcutText =
SystemUtil.getOS() === "Mac" ? `${shownIndex}` : `Ctrl+${shownIndex}`;
const shownIndex = this.properties.index + 1;
// Array index == Shown index - 1
ipcRenderer.send("switch-server-tab", shownIndex - 1);
return shortcutText;
return process.platform === "darwin"
? `${shownIndex}`
: `Ctrl+${shownIndex}`;
}
}

View File

@@ -1,58 +1,46 @@
import type WebView from "./webview";
import type {TabPage, TabRole} from "../../../common/types.ts";
export interface TabProps {
role: string;
export type TabProperties = {
role: TabRole;
page?: TabPage;
icon?: string;
name: string;
label: string;
$root: Element;
onClick: () => void;
index: number;
tabIndex: number;
onHover?: () => void;
onHoverOut?: () => void;
webview: WebView;
materialIcon?: string;
onDestroy?: () => void;
}
};
export default abstract class Tab {
props: TabProps;
webview: WebView;
abstract $el: Element;
constructor(props: TabProps) {
this.props = props;
this.webview = this.props.webview;
}
constructor(readonly properties: TabProperties) {}
registerListeners(): void {
this.$el.addEventListener("click", this.props.onClick);
this.$el.addEventListener("click", this.properties.onClick);
if (this.props.onHover !== undefined) {
this.$el.addEventListener("mouseover", this.props.onHover);
if (this.properties.onHover !== undefined) {
this.$el.addEventListener("mouseover", this.properties.onHover);
}
if (this.props.onHoverOut !== undefined) {
this.$el.addEventListener("mouseout", this.props.onHoverOut);
if (this.properties.onHoverOut !== undefined) {
this.$el.addEventListener("mouseout", this.properties.onHoverOut);
}
}
showNetworkError(): void {
this.webview.forceLoad();
}
activate(): void {
async activate(): Promise<void> {
this.$el.classList.add("active");
this.webview.load();
}
deactivate(): void {
async deactivate(): Promise<void> {
this.$el.classList.remove("active");
this.webview.hide();
}
destroy(): void {
async destroy(): Promise<void> {
this.$el.remove();
this.webview.$el!.remove();
}
}

View File

@@ -1,135 +1,258 @@
import {remote} from "electron";
import fs from "fs";
import path from "path";
import type {WebContents} from "electron/main";
import fs from "node:fs";
import * as ConfigUtil from "../../../common/config-util";
import {HTML, html} from "../../../common/html";
import type {RendererMessage} from "../../../common/typed-ipc";
import {ipcRenderer} from "../typed-ipc-renderer";
import * as SystemUtil from "../utils/system-util";
import * as remote from "@electron/remote";
import {app, dialog} from "@electron/remote";
import {generateNodeFromHTML} from "./base";
import {contextMenu} from "./context-menu";
import handleExternalLink from "./handle-external-link";
import * as ConfigUtil from "../../../common/config-util.ts";
import {type Html, html} from "../../../common/html.ts";
import * as t from "../../../common/translation-util.ts";
import type {RendererMessage} from "../../../common/typed-ipc.ts";
import type {TabRole} from "../../../common/types.ts";
import preloadCss from "../../css/preload.css?raw";
import {ipcRenderer} from "../typed-ipc-renderer.ts";
import * as SystemUtil from "../utils/system-util.ts";
const {app, dialog} = remote;
import {generateNodeFromHtml} from "./base.ts";
import {contextMenu} from "./context-menu.ts";
const shouldSilentWebview = ConfigUtil.getConfigItem("silent", false);
interface WebViewProps {
type WebViewProperties = {
$root: Element;
rootWebContents: WebContents;
index: number;
tabIndex: number;
url: string;
role: string;
name: string;
role: TabRole;
isActive: () => boolean;
switchLoading: (loading: boolean, url: string) => void;
onNetworkError: (index: number) => void;
nodeIntegration: boolean;
preload: boolean;
preload?: string;
onTitleChange: () => void;
hasPermission?: (origin: string, permission: string) => boolean;
}
unsupportedMessage?: string;
};
export default class WebView {
props: WebViewProps;
zoomFactor: number;
badgeCount: number;
loading: boolean;
customCSS: string | false | null;
$webviewsContainer: DOMTokenList;
$el?: Electron.WebviewTag;
domReady?: Promise<void>;
constructor(props: WebViewProps) {
this.props = props;
this.zoomFactor = 1;
this.loading = true;
this.badgeCount = 0;
this.customCSS = ConfigUtil.getConfigItem("customCSS", null);
this.$webviewsContainer = document.querySelector(
"#webviews-container",
)!.classList;
}
templateHTML(): HTML {
static templateHtml(properties: WebViewProperties): Html {
return html`
<webview
class="disabled"
data-tab-id="${this.props.tabIndex}"
src="${this.props.url}"
${new HTML({html: this.props.nodeIntegration ? "nodeIntegration" : ""})}
${new HTML({html: this.props.preload ? 'preload="js/preload.js"' : ""})}
partition="persist:webviewsession"
name="${this.props.name}"
webpreferences="
contextIsolation=${!this.props.nodeIntegration},
spellcheck=${Boolean(
ConfigUtil.getConfigItem("enableSpellchecker", true),
)},
worldSafeExecuteJavaScript=true
"
>
</webview>
<div class="webview-pane">
<div
class="webview-unsupported"
${properties.unsupportedMessage === undefined ? html`hidden` : html``}
>
<span class="webview-unsupported-message"
>${properties.unsupportedMessage ?? ""}</span
>
<span class="webview-unsupported-dismiss">×</span>
</div>
<webview
data-tab-id="${properties.tabIndex}"
src="${properties.url}"
${properties.preload === undefined
? html``
: html`preload="${properties.preload}"`}
partition="persist:webviewsession"
allowpopups
>
</webview>
</div>
`;
}
init(): void {
this.$el = generateNodeFromHTML(this.templateHTML()) as Electron.WebviewTag;
this.domReady = new Promise((resolve) => {
this.$el!.addEventListener(
"dom-ready",
static async create(properties: WebViewProperties): Promise<WebView> {
const $pane = generateNodeFromHtml(
WebView.templateHtml(properties),
) as HTMLElement;
properties.$root.append($pane);
const $webview: HTMLElement = $pane.querySelector(":scope > webview")!;
await new Promise<void>((resolve) => {
$webview.addEventListener(
"did-attach",
() => {
resolve();
},
true,
);
});
this.props.$root.append(this.$el);
// Work around https://github.com/electron/electron/issues/26904
function getWebContentsIdFunction(
this: undefined,
selector: string,
): number {
return document
.querySelector<Electron.WebviewTag>(selector)!
.getWebContentsId();
}
const selector = `webview[data-tab-id="${CSS.escape(
`${properties.tabIndex}`,
)}"]`;
const webContentsId: unknown =
await properties.rootWebContents.executeJavaScript(
`(${getWebContentsIdFunction.toString()})(${JSON.stringify(selector)})`,
);
if (typeof webContentsId !== "number") {
throw new TypeError("Failed to get WebContents ID");
}
return new WebView(properties, $pane, $webview, webContentsId);
}
badgeCount = 0;
loading = true;
private customCss: string | false | null;
private readonly $webviewsContainer: DOMTokenList;
private readonly $unsupported: HTMLElement;
private readonly $unsupportedMessage: HTMLElement;
private readonly $unsupportedDismiss: HTMLElement;
private unsupportedDismissed = false;
private constructor(
readonly properties: WebViewProperties,
private readonly $pane: HTMLElement,
private readonly $webview: HTMLElement,
readonly webContentsId: number,
) {
this.customCss = ConfigUtil.getConfigItem("customCSS", null);
this.$webviewsContainer = document.querySelector(
"#webviews-container",
)!.classList;
this.$unsupported = $pane.querySelector(".webview-unsupported")!;
this.$unsupportedMessage = $pane.querySelector(
".webview-unsupported-message",
)!;
this.$unsupportedDismiss = $pane.querySelector(
".webview-unsupported-dismiss",
)!;
this.registerListeners();
}
registerListeners(): void {
this.$el!.addEventListener("new-window", (event) => {
handleExternalLink.call(this, event);
});
destroy(): void {
this.$pane.remove();
}
getWebContents(): WebContents {
return remote.webContents.fromId(this.webContentsId)!;
}
showNotificationSettings(): void {
this.send("show-notification-settings");
this.focus();
}
focus(): void {
this.$webview.focus();
// Work around https://github.com/electron/electron/issues/31918
this.$webview.shadowRoot?.querySelector("iframe")?.focus();
}
hide(): void {
this.$pane.classList.remove("active");
}
load(): void {
this.show();
}
zoomIn(): void {
this.getWebContents().zoomLevel += 0.5;
}
zoomOut(): void {
this.getWebContents().zoomLevel -= 0.5;
}
zoomActualSize(): void {
this.getWebContents().zoomLevel = 0;
}
logOut(): void {
this.send("logout");
}
showKeyboardShortcuts(): void {
this.send("show-keyboard-shortcuts");
this.focus();
}
openDevTools(): void {
this.getWebContents().openDevTools();
}
back(): void {
if (this.getWebContents().navigationHistory.canGoBack()) {
this.getWebContents().navigationHistory.goBack();
this.focus();
}
}
canGoBackButton(): void {
const $backButton = document.querySelector(
"#actions-container #back-action",
)!;
$backButton.classList.toggle(
"disable",
!this.getWebContents().navigationHistory.canGoBack(),
);
}
forward(): void {
if (this.getWebContents().navigationHistory.canGoForward()) {
this.getWebContents().navigationHistory.goForward();
}
}
reload(): void {
this.hide();
// Shows the loading indicator till the webview is reloaded
this.$webviewsContainer.remove("loaded");
this.loading = true;
this.properties.switchLoading(true, this.properties.url);
this.getWebContents().reload();
}
setUnsupportedMessage(unsupportedMessage: string | undefined) {
this.$unsupported.hidden =
unsupportedMessage === undefined || this.unsupportedDismissed;
this.$unsupportedMessage.textContent = unsupportedMessage ?? "";
}
send<Channel extends keyof RendererMessage>(
channel: Channel,
...arguments_: Parameters<RendererMessage[Channel]>
): void {
ipcRenderer.send("forward-to", this.webContentsId, channel, ...arguments_);
}
private registerListeners(): void {
const webContents = this.getWebContents();
if (shouldSilentWebview) {
this.$el!.addEventListener("dom-ready", () => {
this.$el!.setAudioMuted(true);
});
webContents.setAudioMuted(true);
}
this.$el!.addEventListener("page-title-updated", (event) => {
const {title} = event;
webContents.on("page-title-updated", (_event, title) => {
this.badgeCount = this.getBadgeCount(title);
this.props.onTitleChange();
this.properties.onTitleChange();
});
this.$el!.addEventListener("did-navigate-in-page", (event) => {
const isSettingPage = event.url.includes("renderer/preference.html");
if (isSettingPage) {
return;
}
this.$webview.addEventListener("did-navigate-in-page", () => {
this.canGoBackButton();
});
this.$el!.addEventListener("did-navigate", () => {
this.$webview.addEventListener("did-navigate", () => {
this.canGoBackButton();
});
this.$el!.addEventListener("page-favicon-updated", (event) => {
const {favicons} = event;
webContents.on("page-favicon-updated", (_event, favicons) => {
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
if (
favicons[0].indexOf("favicon-pms") > 0 &&
process.platform === "darwin"
) {
if (favicons[0].indexOf("favicon-pms") > 0 && app.dock !== undefined) {
// This api is only supported on macOS
app.dock.setBadge("●");
// Bounce the dock
@@ -139,207 +262,81 @@ export default class WebView {
}
});
this.$el!.addEventListener("dom-ready", () => {
const webContents = remote.webContents.fromId(
this.$el!.getWebContentsId(),
);
webContents.addListener("context-menu", (event, menuParameters) => {
contextMenu(webContents, event, menuParameters);
});
if (this.props.role === "server") {
this.$el!.classList.add("onload");
}
this.loading = false;
this.props.switchLoading(false, this.props.url);
this.show();
// Refocus text boxes after reload
// Remove when upstream issue https://github.com/electron/electron/issues/14474 is fixed
this.$el!.blur();
this.$el!.focus();
webContents.addListener("context-menu", (event, menuParameters) => {
contextMenu(webContents, event, menuParameters);
});
this.$el!.addEventListener("did-fail-load", (event) => {
const {errorDescription} = event;
const hasConnectivityError = SystemUtil.connectivityERR.includes(
errorDescription,
);
this.$webview.addEventListener("dom-ready", () => {
this.loading = false;
this.properties.switchLoading(false, this.properties.url);
this.show();
});
webContents.on("did-fail-load", (_event, _errorCode, errorDescription) => {
const hasConnectivityError =
SystemUtil.connectivityError.includes(errorDescription);
if (hasConnectivityError) {
console.error("error", errorDescription);
if (!this.props.url.includes("network.html")) {
this.props.onNetworkError(this.props.index);
if (!this.properties.url.includes("network.html")) {
this.properties.onNetworkError(this.properties.index);
}
}
});
this.$el!.addEventListener("did-start-loading", () => {
const isSettingPage = this.props.url.includes("renderer/preference.html");
if (!isSettingPage) {
this.props.switchLoading(true, this.props.url);
}
this.$webview.addEventListener("did-start-loading", () => {
this.properties.switchLoading(true, this.properties.url);
});
this.$el!.addEventListener("did-stop-loading", () => {
this.props.switchLoading(false, this.props.url);
this.$webview.addEventListener("did-stop-loading", () => {
this.properties.switchLoading(false, this.properties.url);
});
this.$unsupportedDismiss.addEventListener("click", () => {
this.unsupportedDismissed = true;
this.$unsupported.hidden = true;
});
webContents.on("zoom-changed", (event, zoomDirection) => {
if (zoomDirection === "in") this.zoomIn();
else if (zoomDirection === "out") this.zoomOut();
});
}
getBadgeCount(title: string): number {
const messageCountInTitle = /\((\d+)\)/.exec(title);
private getBadgeCount(title: string): number {
const messageCountInTitle = /^\((\d+)\)/.exec(title);
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
}
async showNotificationSettings(): Promise<void> {
await this.send("show-notification-settings");
}
show(): void {
private show(): void {
// 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;
}
// To show or hide the loading indicator in the the active tab
if (this.loading) {
this.$webviewsContainer.remove("loaded");
} else {
this.$webviewsContainer.add("loaded");
}
// To show or hide the loading indicator in the active tab
this.$webviewsContainer.toggle("loaded", !this.loading);
this.$el!.classList.remove("disabled");
this.$el!.classList.add("active");
setTimeout(() => {
if (this.props.role === "server") {
this.$el!.classList.remove("onload");
}
}, 1000);
this.$pane.classList.add("active");
this.focus();
this.props.onTitleChange();
this.properties.onTitleChange();
// Injecting preload css in webview to override some css rules
(async () =>
this.$el!.insertCSS(
fs.readFileSync(path.join(__dirname, "/../../css/preload.css"), "utf8"),
))();
(async () => this.getWebContents().insertCSS(preloadCss))();
// Get customCSS again from config util to avoid warning user again
const customCSS = ConfigUtil.getConfigItem("customCSS", null);
this.customCSS = customCSS;
if (customCSS) {
if (!fs.existsSync(customCSS)) {
this.customCSS = null;
const customCss = ConfigUtil.getConfigItem("customCSS", null);
this.customCss = customCss;
if (customCss) {
if (!fs.existsSync(customCss)) {
this.customCss = null;
ConfigUtil.setConfigItem("customCSS", null);
const errorMessage = "The custom css previously set is deleted!";
dialog.showErrorBox("custom css file deleted!", errorMessage);
const errorMessage = t.__("The custom CSS previously set is deleted.");
dialog.showErrorBox(t.__("Custom CSS file deleted"), errorMessage);
return;
}
(async () =>
this.$el!.insertCSS(
fs.readFileSync(path.resolve(__dirname, customCSS), "utf8"),
))();
this.getWebContents().insertCSS(fs.readFileSync(customCss, "utf8")))();
}
}
focus(): void {
// Focus Webview and it's contents when Window regain focus.
const webContents = remote.webContents.fromId(this.$el!.getWebContentsId());
// HACK: webContents.isFocused() seems to be true even without the element
// being in focus. So, we check against `document.activeElement`.
if (webContents && this.$el !== document.activeElement) {
// HACK: Looks like blur needs to be called on the previously focused
// element to transfer focus correctly, in Electron v3.0.10
// See https://github.com/electron/electron/issues/15718
(document.activeElement as HTMLElement).blur();
this.$el!.focus();
webContents.focus();
}
}
hide(): void {
this.$el!.classList.add("disabled");
this.$el!.classList.remove("active");
}
load(): void {
if (this.$el) {
this.show();
} else {
this.init();
}
}
zoomIn(): void {
this.zoomFactor += 0.1;
this.$el!.setZoomFactor(this.zoomFactor);
}
zoomOut(): void {
this.zoomFactor -= 0.1;
this.$el!.setZoomFactor(this.zoomFactor);
}
zoomActualSize(): void {
this.zoomFactor = 1;
this.$el!.setZoomFactor(this.zoomFactor);
}
async logOut(): Promise<void> {
await this.send("logout");
}
async showKeyboardShortcuts(): Promise<void> {
await this.send("show-keyboard-shortcuts");
}
openDevTools(): void {
this.$el!.openDevTools();
}
back(): void {
if (this.$el!.canGoBack()) {
this.$el!.goBack();
this.focus();
}
}
canGoBackButton(): void {
const $backButton = document.querySelector(
"#actions-container #back-action",
)!;
if (this.$el!.canGoBack()) {
$backButton.classList.remove("disable");
} else {
$backButton.classList.add("disable");
}
}
forward(): void {
if (this.$el!.canGoForward()) {
this.$el!.goForward();
}
}
reload(): void {
this.hide();
// Shows the loading indicator till the webview is reloaded
this.$webviewsContainer.remove("loaded");
this.loading = true;
this.props.switchLoading(true, this.props.url);
this.$el!.reload();
}
forceLoad(): void {
this.init();
}
async send<Channel extends keyof RendererMessage>(
channel: Channel,
...args: Parameters<RendererMessage[Channel]>
): Promise<void> {
await this.domReady;
ipcRenderer.sendTo(this.$el!.getWebContentsId(), channel, ...args);
}
}

View File

@@ -1,12 +1,30 @@
import {remote} from "electron";
import {EventEmitter} from "events";
import {EventEmitter} from "node:events";
import {ClipboardDecrypterImpl} from "./clipboard-decrypter";
import type {NotificationData} from "./notification";
import {newNotification} from "./notification";
import {ipcRenderer} from "./typed-ipc-renderer";
import {
type ClipboardDecrypter,
ClipboardDecrypterImplementation,
} from "./clipboard-decrypter.ts";
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 */
export type ElectronBridge = {
send_event: (eventName: string | symbol, ...arguments_: unknown[]) => boolean;
on_event: (eventName: string, listener: ListenerType) => void;
new_notification: (
title: string,
options: NotificationOptions,
dispatch: (type: string, eventInit: EventInit) => boolean,
) => NotificationData;
get_idle_on_system: () => boolean;
get_last_active_on_system: () => number;
get_send_notification_reply_message_supported: () => boolean;
set_send_notification_reply_message_supported: (value: boolean) => void;
decrypt_clipboard: (version: number) => ClipboardDecrypter;
};
/* eslint-enable @typescript-eslint/naming-convention */
let notificationReplySupported = false;
// Indicates if the user is idle or not
@@ -14,13 +32,14 @@ let idle = false;
// Indicates the time at which user was last active
let lastActive = Date.now();
export const bridgeEvents = new EventEmitter();
export const bridgeEvents = new EventEmitter(); // eslint-disable-line unicorn/prefer-event-target
/* eslint-disable @typescript-eslint/naming-convention */
const electron_bridge: ElectronBridge = {
send_event: (eventName: string | symbol, ...args: unknown[]): boolean =>
bridgeEvents.emit(eventName, ...args),
send_event: (eventName: string | symbol, ...arguments_: unknown[]): boolean =>
bridgeEvents.emit(eventName, ...arguments_),
on_event: (eventName: string, listener: ListenerType): void => {
on_event(eventName: string, listener: ListenerType): void {
bridgeEvents.on(eventName, listener);
},
@@ -37,13 +56,14 @@ const electron_bridge: ElectronBridge = {
get_send_notification_reply_message_supported: (): boolean =>
notificationReplySupported,
set_send_notification_reply_message_supported: (value: boolean): void => {
set_send_notification_reply_message_supported(value: boolean): void {
notificationReplySupported = value;
},
decrypt_clipboard: (version: number): ClipboardDecrypterImpl =>
new ClipboardDecrypterImpl(version),
decrypt_clipboard: (version: number): ClipboardDecrypter =>
new ClipboardDecrypterImplementation(version),
};
/* eslint-enable @typescript-eslint/naming-convention */
bridgeEvents.on("total_unread_count", (unreadCount: unknown) => {
if (typeof unreadCount !== "number") {
@@ -58,45 +78,37 @@ bridgeEvents.on("realm_name", (realmName: unknown) => {
throw new TypeError("Expected string for realmName");
}
const serverURL = location.origin;
ipcRenderer.send("realm-name-changed", serverURL, realmName);
const serverUrl = location.origin;
ipcRenderer.send("realm-name-changed", serverUrl, realmName);
});
bridgeEvents.on("realm_icon_url", (iconURL: unknown) => {
if (typeof iconURL !== "string") {
throw new TypeError("Expected string for iconURL");
bridgeEvents.on("realm_icon_url", (iconUrl: unknown) => {
if (typeof iconUrl !== "string") {
throw new TypeError("Expected string for iconUrl");
}
const serverURL = location.origin;
const serverUrl = location.origin;
ipcRenderer.send(
"realm-icon-changed",
serverURL,
iconURL.includes("http") ? iconURL : `${serverURL}${iconURL}`,
serverUrl,
iconUrl.includes("http") ? iconUrl : `${serverUrl}${iconUrl}`,
);
});
// Set user as active and update the time of last activity
ipcRenderer.on("set-active", () => {
if (!remote.app.isPackaged) {
console.log("active");
}
idle = false;
lastActive = Date.now();
});
// Set user as idle and time of last activity is left unchanged
ipcRenderer.on("set-idle", () => {
if (!remote.app.isPackaged) {
console.log("idle");
}
idle = true;
});
// This follows node's idiomatic implementation of event
// emitters to make event handling more simpler instead of using
// functions zulip side will emit event using ElectronBrigde.send_event
// functions zulip side will emit event using ElectronBridge.send_event
// which is alias of .emit and on this side we can handle the data by adding
// a listener for the event.
export default electron_bridge;

View File

@@ -1,55 +0,0 @@
import {remote} from "electron";
import fs from "fs";
import path from "path";
import SendFeedback from "@electron-elements/send-feedback";
const {app} = remote;
customElements.define("send-feedback", SendFeedback);
export const sendFeedback: SendFeedback = document.querySelector(
"send-feedback",
)!;
export const feedbackHolder = sendFeedback.parentElement!;
// Make the button color match zulip app's theme
sendFeedback.customStylesheet = "css/feedback.css";
// Customize the fields of custom elements
sendFeedback.title = "Report Issue";
sendFeedback.titleLabel = "Issue title:";
sendFeedback.titlePlaceholder = "Enter issue title";
sendFeedback.textareaLabel = "Describe the issue:";
sendFeedback.textareaPlaceholder =
"Succinctly describe your issue and steps to reproduce it...";
sendFeedback.buttonLabel = "Report Issue";
sendFeedback.loaderSuccessText = "";
sendFeedback.useReporter("emailReporter", {
email: "support@zulip.com",
});
feedbackHolder.addEventListener("click", (event: Event) => {
// Only remove the class if the grey out faded
// part is clicked and not the feedback element itself
if (event.target === event.currentTarget) {
feedbackHolder.classList.remove("show");
}
});
sendFeedback.addEventListener("feedback-submitted", () => {
setTimeout(() => {
feedbackHolder.classList.remove("show");
}, 1000);
});
sendFeedback.addEventListener("feedback-cancelled", () => {
feedbackHolder.classList.remove("show");
});
const dataDir = app.getPath("userData");
const logsDir = path.join(dataDir, "/Logs");
sendFeedback.logs.push(
...fs.readdirSync(logsDir).map((file) => path.join(logsDir, file)),
);

View File

@@ -1,112 +0,0 @@
"use strict";
interface CompatElectronBridge extends ElectronBridge {
readonly idle_on_system: boolean;
readonly last_active_on_system: number;
send_notification_reply_message_supported: boolean;
}
(() => {
const zulipWindow = window as typeof window & {
electron_bridge: CompatElectronBridge;
raw_electron_bridge: ElectronBridge;
};
const electron_bridge: CompatElectronBridge = {
...zulipWindow.raw_electron_bridge,
get idle_on_system(): boolean {
return this.get_idle_on_system();
},
get last_active_on_system(): number {
return this.get_last_active_on_system();
},
get send_notification_reply_message_supported(): boolean {
return this.get_send_notification_reply_message_supported();
},
set send_notification_reply_message_supported(value: boolean) {
this.set_send_notification_reply_message_supported(value);
},
};
zulipWindow.electron_bridge = electron_bridge;
function attributeListener<T extends EventTarget>(
type: string,
): PropertyDescriptor {
const handlers = new WeakMap<T, (event: Event) => unknown>();
function listener(this: T, event: Event): void {
if (handlers.get(this)!.call(this, event) === false) {
event.preventDefault();
}
}
return {
configurable: true,
enumerable: true,
get(this: T) {
return handlers.get(this);
},
set(this: T, value: unknown) {
if (typeof value === "function") {
if (!handlers.has(this)) {
this.addEventListener(type, listener);
}
handlers.set(this, value as (event: Event) => unknown);
} else if (handlers.has(this)) {
this.removeEventListener(type, listener);
handlers.delete(this);
}
},
};
}
const NativeNotification = Notification;
class InjectedNotification extends EventTarget {
constructor(title: string, options: NotificationOptions = {}) {
super();
Object.assign(
this,
electron_bridge.new_notification(
title,
options,
(type: string, eventInit: EventInit) =>
this.dispatchEvent(new Event(type, eventInit)),
),
);
}
static get maxActions(): number {
return NativeNotification.maxActions;
}
static get permission(): NotificationPermission {
return NativeNotification.permission;
}
static async requestPermission(
callback?: NotificationPermissionCallback,
): Promise<NotificationPermission> {
if (callback) {
callback(await Promise.resolve(NativeNotification.permission));
}
return NativeNotification.permission;
}
}
Object.defineProperties(InjectedNotification.prototype, {
onclick: attributeListener("click"),
onclose: attributeListener("close"),
onerror: attributeListener("error"),
onshow: attributeListener("show"),
});
window.Notification = InjectedNotification as any;
})();

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
import * as ConfigUtil from "../../../common/config-util";
import {ipcRenderer} from "../typed-ipc-renderer";
import {focusCurrentServer} from "./helpers";
const NativeNotification = window.Notification;
export default class BaseNotification extends NativeNotification {
constructor(title: string, options: NotificationOptions) {
options.silent = true;
super(title, options);
this.addEventListener("click", () => {
// Focus to the server who sent the
// notification if not focused already
focusCurrentServer();
ipcRenderer.send("focus-app");
});
}
static async requestPermission(): Promise<NotificationPermission> {
return this.permission;
}
// Override default Notification permission
static get permission(): NotificationPermission {
return ConfigUtil.getConfigItem("showNotification", true)
? "granted"
: "denied";
}
}

View File

@@ -1,20 +0,0 @@
import {remote} from "electron";
import {ipcRenderer} from "../typed-ipc-renderer";
// Do not change this
export const appId = "org.zulip.zulip-electron";
const currentWindow = remote.getCurrentWindow();
const webContents = remote.getCurrentWebContents();
const webContentsId = webContents.id;
// This function will focus the server that sent
// the notification. Main function implemented in main.js
export function focusCurrentServer(): void {
ipcRenderer.sendTo(
currentWindow.webContents.id,
"focus-webview-with-id",
webContentsId,
);
}

View File

@@ -1,49 +1,33 @@
import {remote} from "electron";
import {ipcRenderer} from "../typed-ipc-renderer.ts";
import DefaultNotification from "./default-notification";
import {appId} from "./helpers";
const {app} = remote;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
app.setAppUserModelId(appId);
export interface NotificationData {
export type NotificationData = {
close: () => void;
title: string;
dir: NotificationDirection;
lang: string;
body: string;
tag: string;
image: string;
icon: string;
badge: string;
vibrate: readonly number[];
timestamp: number;
renotify: boolean;
silent: boolean;
requireInteraction: boolean;
data: unknown;
actions: readonly NotificationAction[];
}
};
export function newNotification(
title: string,
options: NotificationOptions,
dispatch: (type: string, eventInit: EventInit) => boolean,
): NotificationData {
const notification = new DefaultNotification(title, options);
const notification = new Notification(title, {...options, silent: true});
for (const type of ["click", "close", "error", "show"]) {
notification.addEventListener(type, (ev: Event) => {
if (!dispatch(type, ev)) {
ev.preventDefault();
notification.addEventListener(type, (event) => {
if (type === "click") ipcRenderer.send("focus-this-webview");
if (!dispatch(type, event)) {
event.preventDefault();
}
});
}
return {
close: () => {
close() {
notification.close();
},
title: notification.title,
@@ -51,15 +35,7 @@ export function newNotification(
lang: notification.lang,
body: notification.body,
tag: notification.tag,
image: notification.image,
icon: notification.icon,
badge: notification.badge,
vibrate: notification.vibrate,
timestamp: notification.timestamp,
renotify: notification.renotify,
silent: notification.silent,
requireInteraction: notification.requireInteraction,
data: notification.data,
actions: notification.actions,
};
}

View File

@@ -0,0 +1,53 @@
import {app} from "@electron/remote";
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 {
static async create(): Promise<AboutView> {
return new AboutView(
await (await fetch(new URL("app/renderer/about.html", bundleUrl))).text(),
);
}
readonly $view: HTMLElement;
private constructor(templateHtml: string) {
this.$view = document.createElement("div");
const $shadow = this.$view.attachShadow({mode: "open"});
$shadow.innerHTML = templateHtml;
$shadow.querySelector("#version")!.textContent = `v${app.getVersion()}`;
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() {
// Do nothing.
}
}

View File

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

View File

@@ -1,22 +1,22 @@
import type {HTML} from "../../../../common/html";
import {html} from "../../../../common/html";
import {generateNodeFromHTML} from "../../components/base";
import {ipcRenderer} from "../../typed-ipc-renderer";
import {type Html, html} from "../../../../common/html.ts";
import * as t from "../../../../common/translation-util.ts";
import {generateNodeFromHtml} from "../../components/base.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.ts";
interface BaseSectionProps {
type BaseSectionProperties = {
$element: HTMLElement;
disabled?: boolean;
value: boolean;
clickHandler: () => void;
}
};
export function generateSettingOption(props: BaseSectionProps): void {
const {$element, disabled, value, clickHandler} = props;
export function generateSettingOption(properties: BaseSectionProperties): void {
const {$element, disabled, value, clickHandler} = properties;
$element.textContent = "";
const $optionControl = generateNodeFromHTML(
generateOptionHTML(value, disabled),
const $optionControl = generateNodeFromHtml(
generateOptionHtml(value, disabled),
);
$element.append($optionControl);
@@ -25,14 +25,14 @@ export function generateSettingOption(props: BaseSectionProps): void {
}
}
export function generateOptionHTML(
export function generateOptionHtml(
settingOption: boolean,
disabled?: boolean,
): HTML {
const labelHTML = disabled
): Html {
const labelHtml = disabled
? html`<label
class="disallowed"
title="Setting locked by system administrator."
title="${t.__("Setting locked by system administrator.")}"
></label>`
: html`<label></label>`;
if (settingOption) {
@@ -40,7 +40,7 @@ export function generateOptionHTML(
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox" checked disabled />
${labelHTML}
${labelHtml}
</div>
</div>
`;
@@ -50,7 +50,7 @@ export function generateOptionHTML(
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox" />
${labelHTML}
${labelHtml}
</div>
</div>
`;
@@ -59,12 +59,12 @@ export function generateOptionHTML(
/* A method that in future can be used to create dropdown menus using <select> <option> tags.
it needs an object which has ``key: value`` pairs and will return a string that can be appended to HTML
*/
export function generateSelectHTML(
export function generateSelectHtml(
options: Record<string, string>,
className?: string,
idName?: string,
): HTML {
const optionsHTML = html``.join(
): Html {
const optionsHtml = html``.join(
Object.keys(options).map(
(key) => html`
<option name="${key}" value="${key}">${options[key]}</option>
@@ -73,7 +73,7 @@ export function generateSelectHTML(
);
return html`
<select class="${className}" id="${idName}">
${optionsHTML}
${optionsHtml}
</select>
`;
}

View File

@@ -1,25 +1,27 @@
import {html} from "../../../../common/html";
import * as t from "../../../../common/translation-util";
import {ipcRenderer} from "../../typed-ipc-renderer";
import * as DomainUtil from "../../utils/domain-util";
import {html} from "../../../../common/html.ts";
import * as t from "../../../../common/translation-util.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.ts";
import * as DomainUtil from "../../utils/domain-util.ts";
import {reloadApp} from "./base-section";
import {initFindAccounts} from "./find-accounts";
import {initServerInfoForm} from "./server-info-form";
import {reloadApp} from "./base-section.ts";
import {initFindAccounts} from "./find-accounts.ts";
import {initServerInfoForm} from "./server-info-form.ts";
interface ConnectedOrgSectionProps {
type ConnectedOrgSectionProperties = {
$root: Element;
}
};
export function initConnectedOrgSection(props: ConnectedOrgSectionProps): void {
props.$root.textContent = "";
export function initConnectedOrgSection({
$root,
}: ConnectedOrgSectionProperties): void {
$root.textContent = "";
const servers = DomainUtil.getDomains();
props.$root.innerHTML = html`
$root.innerHTML = html`
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">${t.__("Connected organizations")}</div>
<div class="title" id="existing-servers">
${t.__("All the connected orgnizations will appear here.")}
${t.__("All the connected organizations will appear here.")}
</div>
<div id="server-info-container"></div>
<div id="new-org-button">
@@ -32,18 +34,17 @@ export function initConnectedOrgSection(props: ConnectedOrgSectionProps): void {
</div>
`.html;
const $serverInfoContainer = document.querySelector(
"#server-info-container",
)!;
const $existingServers = document.querySelector("#existing-servers")!;
const $newOrgButton: HTMLButtonElement = document.querySelector(
"#new-org-button",
)!;
const $findAccountsContainer = document.querySelector(
const $serverInfoContainer = $root.querySelector("#server-info-container")!;
const $existingServers = $root.querySelector("#existing-servers")!;
const $newOrgButton: HTMLButtonElement =
$root.querySelector("#new-org-button")!;
const $findAccountsContainer = $root.querySelector(
"#find-accounts-container",
)!;
const noServerText = t.__("All the connected orgnizations will appear here");
const noServerText = t.__(
"All the connected organizations will appear here.",
);
// Show noServerText if no servers are there otherwise hide it
$existingServers.textContent = servers.length === 0 ? noServerText : "";

View File

@@ -1,11 +1,11 @@
import {html} from "../../../../common/html";
import * as t from "../../../../common/translation-util";
import {generateNodeFromHTML} from "../../components/base";
import * as LinkUtil from "../../utils/link-util";
import {html} from "../../../../common/html.ts";
import * as LinkUtil from "../../../../common/link-util.ts";
import * as t from "../../../../common/translation-util.ts";
import {generateNodeFromHtml} from "../../components/base.ts";
interface FindAccountsProps {
type FindAccountsProperties = {
$root: Element;
}
};
async function findAccounts(url: string): Promise<void> {
if (!url) {
@@ -19,8 +19,8 @@ async function findAccounts(url: string): Promise<void> {
await LinkUtil.openBrowser(new URL("/accounts/find", url));
}
export function initFindAccounts(props: FindAccountsProps): void {
const $findAccounts = generateNodeFromHTML(html`
export function initFindAccounts(properties: FindAccountsProperties): void {
const $findAccounts = generateNodeFromHtml(html`
<div class="settings-card certificate-card">
<div class="certificate-input">
<div>${t.__("Organization URL")}</div>
@@ -33,7 +33,7 @@ export function initFindAccounts(props: FindAccountsProps): void {
</div>
</div>
`);
props.$root.append($findAccounts);
properties.$root.append($findAccounts);
const $findAccountsButton = $findAccounts.querySelector(
"#find-accounts-button",
)!;
@@ -58,10 +58,9 @@ export function initFindAccounts(props: FindAccountsProps): void {
});
$serverUrlField.addEventListener("input", () => {
if ($serverUrlField.value) {
$serverUrlField.classList.remove("invalid-input-value");
} else {
$serverUrlField.classList.add("invalid-input-value");
}
$serverUrlField.classList.toggle(
"invalid-input-value",
$serverUrlField.value === "",
);
});
}

View File

@@ -1,29 +1,30 @@
import type {OpenDialogOptions} from "electron";
import {remote} from "electron";
import fs from "fs";
import path from "path";
import type {OpenDialogOptions} from "electron/renderer";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import * as remote from "@electron/remote";
import {app, dialog, session} from "@electron/remote";
import Tagify from "@yaireo/tagify";
import ISO6391 from "iso-639-1";
import {z} from "zod";
import * as ConfigUtil from "../../../../common/config-util";
import * as EnterpriseUtil from "../../../../common/enterprise-util";
import {html} from "../../../../common/html";
import * as t from "../../../../common/translation-util";
import supportedLocales from "../../../../translations/supported-locales.json";
import {ipcRenderer} from "../../typed-ipc-renderer";
import supportedLocales from "../../../../../public/translations/supported-locales.json";
import * as ConfigUtil from "../../../../common/config-util.ts";
import * as EnterpriseUtil from "../../../../common/enterprise-util.ts";
import {html} from "../../../../common/html.ts";
import * as t from "../../../../common/translation-util.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.ts";
import {generateSelectHTML, generateSettingOption} from "./base-section";
import {generateSelectHtml, generateSettingOption} from "./base-section.ts";
const {app, dialog, session} = remote;
const currentBrowserWindow = remote.getCurrentWindow();
interface GeneralSectionProps {
type GeneralSectionProperties = {
$root: Element;
}
};
export function initGeneralSection(props: GeneralSectionProps): void {
props.$root.innerHTML = html`
export function initGeneralSection({$root}: GeneralSectionProperties): void {
$root.innerHTML = html`
<div class="settings-pane">
<div class="title">${t.__("Appearance")}</div>
<div id="appearance-option-settings" class="settings-card">
@@ -55,7 +56,7 @@ export function initGeneralSection(props: GeneralSectionProps): void {
</div>
<div class="setting-row" id="badge-option">
<div class="setting-description">
${t.__("Show app unread badge")}
${t.__("Show unread count badge on app icon")}
</div>
<div class="setting-control"></div>
</div>
@@ -221,9 +222,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
showDesktopNotification();
enableSpellchecker();
minimizeOnStart();
addCustomCSS();
showCustomCSSPath();
removeCustomCSS();
addCustomCss();
showCustomCssPath();
removeCustomCss();
downloadFolder();
updateQuitOnCloseOption();
updatePromptDownloadOption();
@@ -250,9 +251,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function updateTrayOption(): void {
generateSettingOption({
$element: document.querySelector("#tray-option .setting-control")!,
$element: $root.querySelector("#tray-option .setting-control")!,
value: ConfigUtil.getConfigItem("trayIcon", true),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("trayIcon", true);
ConfigUtil.setConfigItem("trayIcon", newValue);
ipcRenderer.send("forward-message", "toggletray");
@@ -263,9 +264,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function updateMenubarOption(): void {
generateSettingOption({
$element: document.querySelector("#menubar-option .setting-control")!,
$element: $root.querySelector("#menubar-option .setting-control")!,
value: ConfigUtil.getConfigItem("autoHideMenubar", false),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("autoHideMenubar", false);
ConfigUtil.setConfigItem("autoHideMenubar", newValue);
ipcRenderer.send("toggle-menubar", newValue);
@@ -276,9 +277,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function updateBadgeOption(): void {
generateSettingOption({
$element: document.querySelector("#badge-option .setting-control")!,
$element: $root.querySelector("#badge-option .setting-control")!,
value: ConfigUtil.getConfigItem("badgeOption", true),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("badgeOption", true);
ConfigUtil.setConfigItem("badgeOption", newValue);
ipcRenderer.send("toggle-badge-option", newValue);
@@ -289,9 +290,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function updateDockBouncing(): void {
generateSettingOption({
$element: document.querySelector("#dock-bounce-option .setting-control")!,
$element: $root.querySelector("#dock-bounce-option .setting-control")!,
value: ConfigUtil.getConfigItem("dockBouncing", true),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("dockBouncing", true);
ConfigUtil.setConfigItem("dockBouncing", newValue);
updateDockBouncing();
@@ -301,11 +302,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function updateFlashTaskbar(): void {
generateSettingOption({
$element: document.querySelector(
"#flash-taskbar-option .setting-control",
)!,
$element: $root.querySelector("#flash-taskbar-option .setting-control")!,
value: ConfigUtil.getConfigItem("flashTaskbarOnMessage", true),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem(
"flashTaskbarOnMessage",
true,
@@ -318,10 +317,10 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function autoUpdateOption(): void {
generateSettingOption({
$element: document.querySelector("#autoupdate-option .setting-control")!,
$element: $root.querySelector("#autoupdate-option .setting-control")!,
disabled: EnterpriseUtil.configItemExists("autoUpdate"),
value: ConfigUtil.getConfigItem("autoUpdate", true),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("autoUpdate", true);
ConfigUtil.setConfigItem("autoUpdate", newValue);
if (!newValue) {
@@ -336,9 +335,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function betaUpdateOption(): void {
generateSettingOption({
$element: document.querySelector("#betaupdate-option .setting-control")!,
$element: $root.querySelector("#betaupdate-option .setting-control")!,
value: ConfigUtil.getConfigItem("betaUpdate", false),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("betaUpdate", false);
if (ConfigUtil.getConfigItem("autoUpdate", true)) {
ConfigUtil.setConfigItem("betaUpdate", newValue);
@@ -350,13 +349,14 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function updateSilentOption(): void {
generateSettingOption({
$element: document.querySelector("#silent-option .setting-control")!,
$element: $root.querySelector("#silent-option .setting-control")!,
value: ConfigUtil.getConfigItem("silent", false),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("silent", true);
ConfigUtil.setConfigItem("silent", newValue);
updateSilentOption();
ipcRenderer.sendTo(
ipcRenderer.send(
"forward-to",
currentBrowserWindow.webContents.id,
"toggle-silent",
newValue,
@@ -367,11 +367,11 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function showDesktopNotification(): void {
generateSettingOption({
$element: document.querySelector(
$element: $root.querySelector(
"#show-notification-option .setting-control",
)!,
value: ConfigUtil.getConfigItem("showNotification", true),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("showNotification", true);
ConfigUtil.setConfigItem("showNotification", newValue);
showDesktopNotification();
@@ -381,9 +381,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function updateSidebarOption(): void {
generateSettingOption({
$element: document.querySelector("#sidebar-option .setting-control")!,
$element: $root.querySelector("#sidebar-option .setting-control")!,
value: ConfigUtil.getConfigItem("showSidebar", true),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("showSidebar", true);
ConfigUtil.setConfigItem("showSidebar", newValue);
ipcRenderer.send("forward-message", "toggle-sidebar", newValue);
@@ -394,11 +394,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function updateStartAtLoginOption(): void {
generateSettingOption({
$element: document.querySelector(
"#startAtLogin-option .setting-control",
)!,
$element: $root.querySelector("#startAtLogin-option .setting-control")!,
value: ConfigUtil.getConfigItem("startAtLogin", false),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("startAtLogin", false);
ConfigUtil.setConfigItem("startAtLogin", newValue);
ipcRenderer.send("toggleAutoLauncher", newValue);
@@ -409,9 +407,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function updateQuitOnCloseOption(): void {
generateSettingOption({
$element: document.querySelector("#quitOnClose-option .setting-control")!,
$element: $root.querySelector("#quitOnClose-option .setting-control")!,
value: ConfigUtil.getConfigItem("quitOnClose", false),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("quitOnClose", false);
ConfigUtil.setConfigItem("quitOnClose", newValue);
updateQuitOnCloseOption();
@@ -421,18 +419,18 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function enableSpellchecker(): void {
generateSettingOption({
$element: document.querySelector(
$element: $root.querySelector(
"#enable-spellchecker-option .setting-control",
)!,
value: ConfigUtil.getConfigItem("enableSpellchecker", true),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("enableSpellchecker", true);
ConfigUtil.setConfigItem("enableSpellchecker", newValue);
ipcRenderer.send("configure-spell-checker");
enableSpellchecker();
const spellcheckerLanguageInput: HTMLElement = document.querySelector(
"#spellcheck-langs",
)!;
const spellcheckerNote: HTMLElement = document.querySelector("#note")!;
const spellcheckerLanguageInput: HTMLElement =
$root.querySelector("#spellcheck-langs")!;
const spellcheckerNote: HTMLElement = $root.querySelector("#note")!;
spellcheckerLanguageInput.style.display =
spellcheckerLanguageInput.style.display === "none" ? "" : "none";
spellcheckerNote.style.display =
@@ -443,11 +441,11 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function enableErrorReporting(): void {
generateSettingOption({
$element: document.querySelector(
$element: $root.querySelector(
"#enable-error-reporting .setting-control",
)!,
value: ConfigUtil.getConfigItem("errorReporting", true),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("errorReporting", true);
ConfigUtil.setConfigItem("errorReporting", newValue);
enableErrorReporting();
@@ -457,14 +455,13 @@ export function initGeneralSection(props: GeneralSectionProps): void {
async function customCssDialog(): Promise<void> {
const showDialogOptions: OpenDialogOptions = {
title: "Select file",
title: t.__("Select file"),
properties: ["openFile"],
filters: [{name: "CSS file", extensions: ["css"]}],
filters: [{name: t.__("CSS file"), extensions: ["css"]}],
};
const {filePaths, canceled} = await dialog.showOpenDialog(
showDialogOptions,
);
const {filePaths, canceled} =
await dialog.showOpenDialog(showDialogOptions);
if (!canceled) {
ConfigUtil.setConfigItem("customCSS", filePaths[0]);
ipcRenderer.send("forward-message", "hard-reload");
@@ -472,11 +469,11 @@ export function initGeneralSection(props: GeneralSectionProps): void {
}
function setLocale(): void {
const langDiv: HTMLSelectElement = document.querySelector(".lang-div")!;
const langListHTML = generateSelectHTML(supportedLocales, "lang-menu");
langDiv.innerHTML += langListHTML.html;
const langDiv: HTMLSelectElement = $root.querySelector(".lang-div")!;
const langListHtml = generateSelectHtml(supportedLocales, "lang-menu");
langDiv.innerHTML += langListHtml.html;
// `langMenu` is the select-option dropdown menu formed after executing the previous command
const langMenu: HTMLSelectElement = document.querySelector(".lang-menu")!;
const langMenu: HTMLSelectElement = $root.querySelector(".lang-menu")!;
// The next three lines set the selected language visible on the dropdown button
let language = ConfigUtil.getConfigItem("appLanguage", "en");
@@ -491,11 +488,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function minimizeOnStart(): void {
generateSettingOption({
$element: document.querySelector(
"#start-minimize-option .setting-control",
)!,
$element: $root.querySelector("#start-minimize-option .setting-control")!,
value: ConfigUtil.getConfigItem("startMinimized", false),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("startMinimized", false);
ConfigUtil.setConfigItem("startMinimized", newValue);
minimizeOnStart();
@@ -503,27 +498,25 @@ export function initGeneralSection(props: GeneralSectionProps): void {
});
}
function addCustomCSS(): void {
const customCSSButton = document.querySelector(
function addCustomCss(): void {
const customCssButton = $root.querySelector(
"#add-custom-css .custom-css-button",
)!;
customCSSButton.addEventListener("click", async () => {
customCssButton.addEventListener("click", async () => {
await customCssDialog();
});
}
function showCustomCSSPath(): void {
function showCustomCssPath(): void {
if (!ConfigUtil.getConfigItem("customCSS", null)) {
const cssPATH: HTMLElement = document.querySelector(
"#remove-custom-css",
)!;
cssPATH.style.display = "none";
const cssPath: HTMLElement = $root.querySelector("#remove-custom-css")!;
cssPath.style.display = "none";
}
}
function removeCustomCSS(): void {
const removeCSSButton = document.querySelector("#css-delete-action")!;
removeCSSButton.addEventListener("click", () => {
function removeCustomCss(): void {
const removeCssButton = $root.querySelector("#css-delete-action")!;
removeCssButton.addEventListener("click", () => {
ConfigUtil.setConfigItem("customCSS", "");
ipcRenderer.send("forward-message", "hard-reload");
});
@@ -531,16 +524,15 @@ export function initGeneralSection(props: GeneralSectionProps): void {
async function downloadFolderDialog(): Promise<void> {
const showDialogOptions: OpenDialogOptions = {
title: "Select Download Location",
title: t.__("Select Download Location"),
properties: ["openDirectory"],
};
const {filePaths, canceled} = await dialog.showOpenDialog(
showDialogOptions,
);
const {filePaths, canceled} =
await dialog.showOpenDialog(showDialogOptions);
if (!canceled) {
ConfigUtil.setConfigItem("downloadsPath", filePaths[0]);
const downloadFolderPath: HTMLElement = document.querySelector(
const downloadFolderPath: HTMLElement = $root.querySelector(
".download-folder-path",
)!;
downloadFolderPath.textContent = filePaths[0];
@@ -548,7 +540,7 @@ export function initGeneralSection(props: GeneralSectionProps): void {
}
function downloadFolder(): void {
const downloadFolder = document.querySelector(
const downloadFolder = $root.querySelector(
"#download-folder .download-folder-button",
)!;
downloadFolder.addEventListener("click", async () => {
@@ -558,9 +550,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
function updatePromptDownloadOption(): void {
generateSettingOption({
$element: document.querySelector("#prompt-download .setting-control")!,
$element: $root.querySelector("#prompt-download .setting-control")!,
value: ConfigUtil.getConfigItem("promptDownload", false),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("promptDownload", false);
ConfigUtil.setConfigItem("promptDownload", newValue);
updatePromptDownloadOption();
@@ -569,15 +561,16 @@ export function initGeneralSection(props: GeneralSectionProps): void {
}
async function factoryResetSettings(): Promise<void> {
const clearAppDataMessage =
"When the application restarts, it will be as if you have just downloaded Zulip app.";
const clearAppDataMessage = t.__(
"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 {response} = await dialog.showMessageBox({
type: "warning",
buttons: ["YES", "NO"],
buttons: [t.__("Yes"), t.__("No")],
defaultId: 0,
message: "Are you sure?",
message: t.__("Are you sure?"),
detail: clearAppDataMessage,
});
if (response === 0) {
@@ -589,7 +582,7 @@ export function initGeneralSection(props: GeneralSectionProps): void {
}
function factoryReset(): void {
const factoryResetButton = document.querySelector(
const factoryResetButton = $root.querySelector(
"#factory-reset-option .factory-reset-button",
)!;
factoryResetButton.addEventListener("click", async () => {
@@ -598,9 +591,9 @@ export function initGeneralSection(props: GeneralSectionProps): void {
}
function initSpellChecker(): void {
// The elctron API is a no-op on macOS and macOS default spellchecker is used.
// The Electron API is a no-op on macOS and macOS default spellchecker is used.
if (process.platform === "darwin") {
const note: HTMLElement = document.querySelector("#note")!;
const note: HTMLElement = $root.querySelector("#note")!;
note.append(t.__("On macOS, the OS spellchecker is used."));
note.append(document.createElement("br"));
note.append(
@@ -609,45 +602,43 @@ export function initGeneralSection(props: GeneralSectionProps): void {
),
);
} else {
const note: HTMLElement = document.querySelector("#note")!;
const note: HTMLElement = $root.querySelector("#note")!;
note.append(
t.__("You can select a maximum of 3 languages for spellchecking."),
);
const spellDiv: HTMLElement = document.querySelector(
"#spellcheck-langs",
)!;
const spellDiv: HTMLElement = $root.querySelector("#spellcheck-langs")!;
spellDiv.innerHTML += html`
<div class="setting-description">${t.__("Spellchecker Languages")}</div>
<input name="spellcheck" placeholder="Enter Languages" />
<div id="spellcheck-langs-value">
<input name="spellcheck" placeholder="${t.__("Enter Languages")}" />
</div>
`.html;
const availableLanguages = session.fromPartition("persist:webviewsession")
.availableSpellCheckerLanguages;
let languagePairs: Map<string, string> = new Map();
const availableLanguages = session.fromPartition(
"persist:webviewsession",
).availableSpellCheckerLanguages;
let languagePairs = new Map<string, string>();
for (const l of availableLanguages) {
if (ISO6391.validate(l)) {
languagePairs.set(ISO6391.getName(l), l);
}
const locale = new Intl.Locale(l.replaceAll("_", "-"));
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].sort((a, b) => (a[0] < b[0] ? -1 : 1)),
[...languagePairs].sort((a, b) => a[0].localeCompare(b[1])),
);
const tagField: HTMLInputElement = document.querySelector(
const tagField: HTMLInputElement = $root.querySelector(
"input[name=spellcheck]",
)!;
const tagify = new Tagify(tagField, {
@@ -659,8 +650,20 @@ export function initGeneralSection(props: GeneralSectionProps): void {
maxItems: Number.POSITIVE_INFINITY,
closeOnSelect: false,
highlightFirst: true,
position: "manual",
classname: "settings-tagify-dropdown",
},
});
tagify.DOM.input.addEventListener("focus", () => {
tagify.dropdown.show();
$root
.querySelector("#spellcheck-langs-value")!
.append(tagify.DOM.dropdown);
});
tagify.DOM.input.addEventListener("blur", () => {
tagify.dropdown.hide(true);
tagify.DOM.dropdown.remove();
});
const configuredLanguages: string[] = (
ConfigUtil.getConfigItem("spellcheckerLanguages", null) ?? []
@@ -673,23 +676,24 @@ export function initGeneralSection(props: GeneralSectionProps): void {
tagField.addEventListener("change", () => {
if (tagField.value.length === 0) {
ConfigUtil.setConfigItem("spellcheckerLanguages", []);
ipcRenderer.send("set-spellcheck-langs");
ipcRenderer.send("configure-spell-checker");
} else {
const spellLangs: string[] = [...JSON.parse(tagField.value)].map(
(elt: {value: string}) => languagePairs.get(elt.value)!,
);
const data: unknown = JSON.parse(tagField.value);
const spellLangs: string[] = z
.array(z.object({value: z.string()}))
.parse(data)
.map((elt) => languagePairs.get(elt.value)!);
ConfigUtil.setConfigItem("spellcheckerLanguages", spellLangs);
ipcRenderer.send("set-spellcheck-langs");
ipcRenderer.send("configure-spell-checker");
}
});
}
// Do not display the spellchecker input and note if it is disabled
if (!ConfigUtil.getConfigItem("enableSpellchecker", true)) {
const spellcheckerLanguageInput: HTMLElement = document.querySelector(
"#spellcheck-langs",
)!;
const spellcheckerNote: HTMLElement = document.querySelector("#note")!;
const spellcheckerLanguageInput: HTMLElement =
$root.querySelector("#spellcheck-langs")!;
const spellcheckerNote: HTMLElement = $root.querySelector("#note")!;
spellcheckerLanguageInput.style.display = "none";
spellcheckerNote.style.display = "none";
}

View File

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

View File

@@ -1,16 +1,16 @@
import * as ConfigUtil from "../../../../common/config-util";
import {html} from "../../../../common/html";
import * as t from "../../../../common/translation-util";
import {ipcRenderer} from "../../typed-ipc-renderer";
import * as ConfigUtil from "../../../../common/config-util.ts";
import {html} from "../../../../common/html.ts";
import * as t from "../../../../common/translation-util.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.ts";
import {generateSettingOption} from "./base-section";
import {generateSettingOption} from "./base-section.ts";
interface NetworkSectionProps {
type NetworkSectionProperties = {
$root: Element;
}
};
export function initNetworkSection(props: NetworkSectionProps): void {
props.$root.innerHTML = html`
export function initNetworkSection({$root}: NetworkSectionProperties): void {
$root.innerHTML = html`
<div class="settings-pane">
<div class="title">${t.__("Proxy")}</div>
<div id="appearance-option-settings" class="settings-card">
@@ -28,7 +28,7 @@ export function initNetworkSection(props: NetworkSectionProps): void {
</div>
<div class="manual-proxy-block">
<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
class="setting-input-value"
placeholder="e.g. foobar.com/pacfile.js"
@@ -55,27 +55,27 @@ export function initNetworkSection(props: NetworkSectionProps): void {
</div>
`.html;
const $proxyPAC: HTMLInputElement = document.querySelector(
const $proxyPac: HTMLInputElement = $root.querySelector(
"#proxy-pac-option .setting-input-value",
)!;
const $proxyRules: HTMLInputElement = document.querySelector(
const $proxyRules: HTMLInputElement = $root.querySelector(
"#proxy-rules-option .setting-input-value",
)!;
const $proxyBypass: HTMLInputElement = document.querySelector(
const $proxyBypass: HTMLInputElement = $root.querySelector(
"#proxy-bypass-option .setting-input-value",
)!;
const $proxySaveAction = document.querySelector("#proxy-save-action")!;
const $manualProxyBlock = props.$root.querySelector(".manual-proxy-block")!;
const $proxySaveAction = $root.querySelector("#proxy-save-action")!;
const $manualProxyBlock = $root.querySelector(".manual-proxy-block")!;
toggleManualProxySettings(ConfigUtil.getConfigItem("useManualProxy", false));
updateProxyOption();
$proxyPAC.value = ConfigUtil.getConfigItem("proxyPAC", "");
$proxyPac.value = ConfigUtil.getConfigItem("proxyPAC", "");
$proxyRules.value = ConfigUtil.getConfigItem("proxyRules", "");
$proxyBypass.value = ConfigUtil.getConfigItem("proxyBypass", "");
$proxySaveAction.addEventListener("click", () => {
ConfigUtil.setConfigItem("proxyPAC", $proxyPAC.value);
ConfigUtil.setConfigItem("proxyPAC", $proxyPac.value);
ConfigUtil.setConfigItem("proxyRules", $proxyRules.value);
ConfigUtil.setConfigItem("proxyBypass", $proxyBypass.value);
@@ -83,20 +83,14 @@ export function initNetworkSection(props: NetworkSectionProps): void {
});
function toggleManualProxySettings(option: boolean): void {
if (option) {
$manualProxyBlock.classList.remove("hidden");
} else {
$manualProxyBlock.classList.add("hidden");
}
$manualProxyBlock.classList.toggle("hidden", !option);
}
function updateProxyOption(): void {
generateSettingOption({
$element: document.querySelector(
"#use-system-settings .setting-control",
)!,
$element: $root.querySelector("#use-system-settings .setting-control")!,
value: ConfigUtil.getConfigItem("useSystemProxy", false),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("useSystemProxy", false);
const manualProxyValue = ConfigUtil.getConfigItem(
"useManualProxy",
@@ -118,11 +112,9 @@ export function initNetworkSection(props: NetworkSectionProps): void {
},
});
generateSettingOption({
$element: document.querySelector(
"#use-manual-settings .setting-control",
)!,
$element: $root.querySelector("#use-manual-settings .setting-control")!,
value: ConfigUtil.getConfigItem("useManualProxy", false),
clickHandler: () => {
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("useManualProxy", false);
const systemProxyValue = ConfigUtil.getConfigItem(
"useSystemProxy",

View File

@@ -1,28 +1,31 @@
import {remote} from "electron";
import {dialog} from "@electron/remote";
import {html} from "../../../../common/html";
import * as t from "../../../../common/translation-util";
import {generateNodeFromHTML} from "../../components/base";
import {ipcRenderer} from "../../typed-ipc-renderer";
import * as DomainUtil from "../../utils/domain-util";
import * as LinkUtil from "../../utils/link-util";
import {html} from "../../../../common/html.ts";
import * as LinkUtil from "../../../../common/link-util.ts";
import * as t from "../../../../common/translation-util.ts";
import {generateNodeFromHtml} from "../../components/base.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.ts";
import * as DomainUtil from "../../utils/domain-util.ts";
const {dialog} = remote;
interface NewServerFormProps {
type NewServerFormProperties = {
$root: Element;
onChange: () => void;
}
};
export function initNewServerForm(props: NewServerFormProps): void {
const $newServerForm = generateNodeFromHTML(html`
export function initNewServerForm({
$root,
onChange,
}: NewServerFormProperties): void {
const $newServerForm = generateNodeFromHtml(html`
<div class="server-input-container">
<div class="title">${t.__("Organization URL")}</div>
<div class="add-server-info-row">
<input
class="setting-input-value"
autofocus
placeholder="your-organization.zulipchat.com or zulip.your-organization.com"
placeholder="${t.__(
"your-organization.zulipchat.com or zulip.your-organization.com",
)}"
/>
</div>
<div class="server-center">
@@ -50,35 +53,34 @@ export function initNewServerForm(props: NewServerFormProps): void {
</div>
</div>
`);
const $saveServerButton: HTMLButtonElement = $newServerForm.querySelector(
"#connect",
)!;
props.$root.textContent = "";
props.$root.append($newServerForm);
const $saveServerButton: HTMLButtonElement =
$newServerForm.querySelector("#connect")!;
$root.textContent = "";
$root.append($newServerForm);
const $newServerUrl: HTMLInputElement = $newServerForm.querySelector(
"input.setting-input-value",
)!;
async function submitFormHandler(): Promise<void> {
$saveServerButton.textContent = "Connecting...";
let serverConf;
$saveServerButton.textContent = t.__("Connecting…");
let serverConfig;
try {
serverConf = await DomainUtil.checkDomain($newServerUrl.value.trim());
serverConfig = await DomainUtil.checkDomain($newServerUrl.value.trim());
} catch (error: unknown) {
$saveServerButton.textContent = "Connect";
$saveServerButton.textContent = t.__("Connect");
await dialog.showMessageBox({
type: "error",
message:
error instanceof Error
? `${error.name}: ${error.message}`
: "Unknown error",
buttons: ["OK"],
: t.__("Unknown error"),
buttons: [t.__("OK")],
});
return;
}
await DomainUtil.addDomain(serverConf);
props.onChange();
await DomainUtil.addDomain(serverConfig);
onChange();
}
$saveServerButton.addEventListener("click", async () => {
@@ -92,14 +94,14 @@ export function initNewServerForm(props: NewServerFormProps): void {
// Open create new org link in default browser
const link = "https://zulip.com/new/";
const externalCreateNewOrgElement = document.querySelector(
const externalCreateNewOrgElement = $root.querySelector(
"#open-create-org-link",
)!;
externalCreateNewOrgElement.addEventListener("click", async () => {
await LinkUtil.openBrowser(new URL(link));
});
const networkSettingsId = document.querySelector(".server-network-option")!;
const networkSettingsId = $root.querySelector(".server-network-option")!;
networkSettingsId.addEventListener("click", () => {
ipcRenderer.send("forward-message", "open-network-settings");
});

View File

@@ -1,106 +1,144 @@
import type {DNDSettings} from "../../../../common/dnd-util";
import type {NavItem} from "../../../../common/types";
import {ipcRenderer} from "../../typed-ipc-renderer";
import type {IpcRendererEvent} from "electron/renderer";
import process from "node:process";
import {initConnectedOrgSection} from "./connected-org-section";
import {initGeneralSection} from "./general-section";
import Nav from "./nav";
import {initNetworkSection} from "./network-section";
import {initServersSection} from "./servers-section";
import {initShortcutsSection} from "./shortcuts-section";
import type {DndSettings} from "../../../../common/dnd-util.ts";
import {bundleUrl} from "../../../../common/paths.ts";
import type {NavigationItem} from "../../../../common/types.ts";
import {ipcRenderer} from "../../typed-ipc-renderer.ts";
export function initPreferenceView(): void {
const $sidebarContainer = document.querySelector("#sidebar")!;
const $settingsContainer = document.querySelector("#settings-container")!;
import {initConnectedOrgSection} from "./connected-org-section.ts";
import {initGeneralSection} from "./general-section.ts";
import Nav from "./nav.ts";
import {initNetworkSection} from "./network-section.ts";
import {initServersSection} from "./servers-section.ts";
import {initShortcutsSection} from "./shortcuts-section.ts";
const nav = new Nav({
$root: $sidebarContainer,
onItemSelected: handleNavigation,
});
export class PreferenceView {
static async create(): Promise<PreferenceView> {
return new PreferenceView(
await (
await fetch(new URL("app/renderer/preference.html", bundleUrl))
).text(),
);
}
const navItem =
nav.navItems.find((navItem) => window.location.hash === `#${navItem}`) ??
"General";
readonly $view: HTMLElement;
private readonly $shadow: ShadowRoot;
private readonly $settingsContainer: Element;
private readonly nav: Nav;
private navigationItem: NavigationItem = "General";
handleNavigation(navItem);
private constructor(templateHtml: string) {
this.$view = document.createElement("div");
this.$shadow = this.$view.attachShadow({mode: "open"});
this.$shadow.innerHTML = templateHtml;
function handleNavigation(navItem: NavItem): void {
nav.select(navItem);
switch (navItem) {
case "AddServer":
const $sidebarContainer = this.$shadow.querySelector("#sidebar")!;
this.$settingsContainer = this.$shadow.querySelector(
"#settings-container",
)!;
this.nav = new Nav({
$root: $sidebarContainer,
onItemSelected: this.handleNavigation,
});
ipcRenderer.on("toggle-sidebar", this.handleToggleSidebar);
ipcRenderer.on("toggle-autohide-menubar", this.handleToggleMenubar);
ipcRenderer.on("toggle-dnd", this.handleToggleDnd);
this.handleNavigation(this.navigationItem);
}
handleNavigation = (navigationItem: NavigationItem): void => {
this.navigationItem = navigationItem;
this.nav.select(navigationItem);
switch (navigationItem) {
case "AddServer": {
initServersSection({
$root: $settingsContainer,
});
break;
case "General":
initGeneralSection({
$root: $settingsContainer,
});
break;
case "Organizations":
initConnectedOrgSection({
$root: $settingsContainer,
});
break;
case "Network":
initNetworkSection({
$root: $settingsContainer,
});
break;
case "Shortcuts": {
initShortcutsSection({
$root: $settingsContainer,
$root: this.$settingsContainer,
});
break;
}
default:
((n: never) => n)(navItem);
case "General": {
initGeneralSection({
$root: this.$settingsContainer,
});
break;
}
case "Organizations": {
initConnectedOrgSection({
$root: this.$settingsContainer,
});
break;
}
case "Network": {
initNetworkSection({
$root: this.$settingsContainer,
});
break;
}
case "Shortcuts": {
initShortcutsSection({
$root: this.$settingsContainer,
});
break;
}
}
window.location.hash = `#${navItem}`;
location.hash = `#${navigationItem}`;
};
handleToggleTray(state: boolean) {
this.handleToggle("tray-option", state);
}
destroy(): void {
ipcRenderer.off("toggle-sidebar", this.handleToggleSidebar);
ipcRenderer.off("toggle-autohide-menubar", this.handleToggleMenubar);
ipcRenderer.off("toggle-dnd", this.handleToggleDnd);
}
// Handle toggling and reflect changes in preference page
function handleToggle(elementName: string, state = false): void {
private handleToggle(elementName: string, state = false): void {
const inputSelector = `#${elementName} .action .switch input`;
const input: HTMLInputElement = document.querySelector(inputSelector)!;
const input: HTMLInputElement = this.$shadow.querySelector(inputSelector)!;
if (input) {
input.checked = state;
}
}
ipcRenderer.on("switch-settings-nav", (_event: Event, navItem: NavItem) => {
handleNavigation(navItem);
});
private readonly handleToggleSidebar = (
_event: IpcRendererEvent,
state: boolean,
) => {
this.handleToggle("sidebar-option", state);
};
ipcRenderer.on("toggle-sidebar-setting", (_event: Event, state: boolean) => {
handleToggle("sidebar-option", state);
});
private readonly handleToggleMenubar = (
_event: IpcRendererEvent,
state: boolean,
) => {
this.handleToggle("menubar-option", state);
};
ipcRenderer.on("toggle-menubar-setting", (_event: Event, state: boolean) => {
handleToggle("menubar-option", state);
});
private readonly handleToggleDnd = (
_event: IpcRendererEvent,
_state: boolean,
newSettings: Partial<DndSettings>,
) => {
this.handleToggle("show-notification-option", newSettings.showNotification);
this.handleToggle("silent-option", newSettings.silent);
ipcRenderer.on("toggle-tray", (_event: Event, state: boolean) => {
handleToggle("tray-option", state);
});
ipcRenderer.on(
"toggle-dnd",
(_event: Event, _state: boolean, newSettings: Partial<DNDSettings>) => {
handleToggle("show-notification-option", newSettings.showNotification);
handleToggle("silent-option", newSettings.silent);
if (process.platform === "win32") {
handleToggle("flash-taskbar-option", newSettings.flashTaskbarOnMessage);
}
},
);
if (process.platform === "win32") {
this.handleToggle(
"flash-taskbar-option",
newSettings.flashTaskbarOnMessage,
);
}
};
}
window.addEventListener("load", initPreferenceView);

View File

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

View File

@@ -1,17 +1,15 @@
import {html} from "../../../../common/html";
import * as t from "../../../../common/translation-util";
import {html} from "../../../../common/html.ts";
import * as t from "../../../../common/translation-util.ts";
import {reloadApp} from "./base-section";
import {initNewServerForm} from "./new-server-form";
import {reloadApp} from "./base-section.ts";
import {initNewServerForm} from "./new-server-form.ts";
interface ServersSectionProps {
type ServersSectionProperties = {
$root: Element;
}
};
export function initServersSection(props: ServersSectionProps): void {
props.$root.textContent = "";
props.$root.innerHTML = html`
export function initServersSection({$root}: ServersSectionProperties): void {
$root.innerHTML = html`
<div class="add-server-modal">
<div class="modal-container">
<div class="settings-pane" id="server-settings-pane">
@@ -21,7 +19,7 @@ export function initServersSection(props: ServersSectionProps): void {
</div>
</div>
`.html;
const $newServerContainer = document.querySelector("#new-server-container")!;
const $newServerContainer = $root.querySelector("#new-server-container")!;
initNewServerForm({
$root: $newServerContainer,

View File

@@ -1,16 +1,20 @@
import {html} from "../../../../common/html";
import * as t from "../../../../common/translation-util";
import * as LinkUtil from "../../utils/link-util";
import process from "node:process";
interface ShortcutsSectionProps {
import {html} from "../../../../common/html.ts";
import * as LinkUtil from "../../../../common/link-util.ts";
import * as t from "../../../../common/translation-util.ts";
type ShortcutsSectionProperties = {
$root: Element;
}
};
// eslint-disable-next-line complexity
export function initShortcutsSection(props: ShortcutsSectionProps): void {
export function initShortcutsSection({
$root,
}: ShortcutsSectionProperties): void {
const cmdOrCtrl = process.platform === "darwin" ? "⌘" : "Ctrl";
props.$root.innerHTML = html`
$root.innerHTML = html`
<div class="settings-pane">
<div class="settings-card tip">
<p>
@@ -223,9 +227,8 @@ export function initShortcutsSection(props: ShortcutsSectionProps): void {
`.html;
const link = "https://zulip.com/help/keyboard-shortcuts";
const externalCreateNewOrgElement = document.querySelector(
"#open-hotkeys-link",
)!;
const externalCreateNewOrgElement =
$root.querySelector("#open-hotkeys-link")!;
externalCreateNewOrgElement.addEventListener("click", async () => {
await LinkUtil.openBrowser(new URL(link));
});

View File

@@ -1,72 +1,25 @@
import {contextBridge, webFrame} from "electron";
import fs from "fs";
import {contextBridge} from "electron/renderer";
import electron_bridge, {bridgeEvents} from "./electron-bridge";
import * as NetworkError from "./pages/network";
import {ipcRenderer} from "./typed-ipc-renderer";
import electron_bridge, {bridgeEvents} from "./electron-bridge.ts";
import * as NetworkError from "./pages/network.ts";
import {ipcRenderer} from "./typed-ipc-renderer.ts";
contextBridge.exposeInMainWorld("raw_electron_bridge", electron_bridge);
contextBridge.exposeInMainWorld("electron_bridge", electron_bridge);
ipcRenderer.on("logout", () => {
if (bridgeEvents.emit("logout")) {
return;
}
// Create the menu for the below
const dropdown: HTMLElement = document.querySelector(".dropdown-toggle")!;
dropdown.click();
const nodes: NodeListOf<HTMLElement> = document.querySelectorAll(
".dropdown-menu li:last-child a",
);
nodes[nodes.length - 1].click();
bridgeEvents.emit("logout");
});
ipcRenderer.on("show-keyboard-shortcuts", () => {
if (bridgeEvents.emit("show-keyboard-shortcuts")) {
return;
}
// Create the menu for the below
const node: HTMLElement = document.querySelector(
"a[data-overlay-trigger=keyboard-shortcuts]",
)!;
// Additional check
if (node.textContent!.trim().toLowerCase() === "keyboard shortcuts (?)") {
node.click();
} else {
// Atleast click the dropdown
const dropdown: HTMLElement = document.querySelector(".dropdown-toggle")!;
dropdown.click();
}
bridgeEvents.emit("show-keyboard-shortcuts");
});
ipcRenderer.on("show-notification-settings", () => {
if (bridgeEvents.emit("show-notification-settings")) {
return;
}
// Create the menu for the below
const dropdown: HTMLElement = document.querySelector(".dropdown-toggle")!;
dropdown.click();
const nodes: NodeListOf<HTMLElement> = document.querySelectorAll(
".dropdown-menu li a",
);
nodes[2].click();
const notificationItem: NodeListOf<HTMLElement> = document.querySelectorAll(
".normal-settings-list li div",
);
// Wait until the notification dom element shows up
setTimeout(() => {
notificationItem[2].click();
}, 100);
bridgeEvents.emit("show-notification-settings");
});
window.addEventListener("load", (event: any): void => {
if (!event.target.URL.includes("app/renderer/network.html")) {
window.addEventListener("load", () => {
if (!location.href.includes("app/renderer/network.html")) {
return;
}
@@ -74,8 +27,3 @@ window.addEventListener("load", (event: any): void => {
const $settingsButton = document.querySelector("#settings")!;
NetworkError.init($reconnectButton, $settingsButton);
});
(async () =>
webFrame.executeJavaScript(
fs.readFileSync(require.resolve("./injected"), "utf8"),
))();

View File

@@ -1,46 +1,53 @@
import type {NativeImage, WebviewTag} from "electron";
import {remote} from "electron";
import path from "path";
import {type NativeImage, nativeImage} from "electron/common";
import type {Tray as ElectronTray} from "electron/main";
import path from "node:path";
import process from "node:process";
import * as ConfigUtil from "../../common/config-util";
import type {RendererMessage} from "../../common/typed-ipc";
import {BrowserWindow, Menu, Tray} from "@electron/remote";
import {ipcRenderer} from "./typed-ipc-renderer";
import * as ConfigUtil from "../../common/config-util.ts";
import {publicPath} from "../../common/paths.ts";
import * as t from "../../common/translation-util.ts";
import type {RendererMessage} from "../../common/typed-ipc.ts";
const {Tray, Menu, nativeImage, BrowserWindow} = remote;
import type {ServerManagerView} from "./main.ts";
import {ipcRenderer} from "./typed-ipc-renderer.ts";
let tray: Electron.Tray | null = null;
let tray: ElectronTray | null = null;
const ICON_DIR = "../../resources/tray";
const TRAY_SUFFIX = "tray";
const APP_ICON = path.join(__dirname, ICON_DIR, TRAY_SUFFIX);
const appIcon = path.join(publicPath, "resources/tray/tray");
const iconPath = (): string => {
if (process.platform === "linux") {
return APP_ICON + "linux.png";
return appIcon + "linux.png";
}
return (
APP_ICON + (process.platform === "win32" ? "win.ico" : "macOSTemplate.png")
appIcon + (process.platform === "win32" ? "win.ico" : "macOSTemplate.png")
);
};
const winUnreadTrayIconPath = (): string => APP_ICON + "unread.ico";
const winUnreadTrayIconPath = (): string => appIcon + "unread.ico";
let unread = 0;
const trayIconSize = (): number => {
switch (process.platform) {
case "darwin":
case "darwin": {
return 20;
case "win32":
}
case "win32": {
return 100;
case "linux":
}
case "linux": {
return 100;
default:
}
default: {
return 80;
}
}
};
@@ -57,45 +64,49 @@ const config = {
thick: process.platform === "win32",
};
const renderCanvas = function (arg: number): HTMLCanvasElement {
config.unreadCount = arg;
const renderCanvas = function (argument: number): HTMLCanvasElement {
config.unreadCount = argument;
const SIZE = config.size * config.pixelRatio;
const PADDING = SIZE * 0.05;
const CENTER = SIZE / 2;
const HAS_COUNT = config.showUnreadCount && config.unreadCount;
const size = config.size * config.pixelRatio;
const padding = size * 0.05;
const center = size / 2;
const hasCount = config.showUnreadCount && config.unreadCount;
const color = config.unreadCount ? config.unreadColor : config.readColor;
const backgroundColor = config.unreadCount
? config.unreadBackgroundColor
: config.readBackgroundColor;
const canvas = document.createElement("canvas");
canvas.width = SIZE;
canvas.height = SIZE;
const ctx = canvas.getContext("2d")!;
canvas.width = size;
canvas.height = size;
const context = canvas.getContext("2d")!;
// Circle
// If (!config.thick || config.thick && HAS_COUNT) {
ctx.beginPath();
ctx.arc(CENTER, CENTER, SIZE / 2 - PADDING, 0, 2 * Math.PI, false);
ctx.fillStyle = backgroundColor;
ctx.fill();
ctx.lineWidth = SIZE / (config.thick ? 10 : 20);
ctx.strokeStyle = backgroundColor;
ctx.stroke();
// If (!config.thick || config.thick && hasCount) {
context.beginPath();
context.arc(center, center, size / 2 - padding, 0, 2 * Math.PI, false);
context.fillStyle = backgroundColor;
context.fill();
context.lineWidth = size / (config.thick ? 10 : 20);
context.strokeStyle = backgroundColor;
context.stroke();
// Count or Icon
if (HAS_COUNT) {
ctx.fillStyle = color;
ctx.textAlign = "center";
if (hasCount) {
context.fillStyle = color;
context.textAlign = "center";
if (config.unreadCount > 99) {
ctx.font = `${config.thick ? "bold " : ""}${SIZE * 0.4}px Helvetica`;
ctx.fillText("99+", CENTER, CENTER + SIZE * 0.15);
context.font = `${config.thick ? "bold " : ""}${size * 0.4}px Helvetica`;
context.fillText("99+", center, center + size * 0.15);
} else if (config.unreadCount < 10) {
ctx.font = `${config.thick ? "bold " : ""}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + SIZE * 0.2);
context.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`;
context.fillText(String(config.unreadCount), center, center + size * 0.2);
} else {
ctx.font = `${config.thick ? "bold " : ""}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + SIZE * 0.15);
context.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`;
context.fillText(
String(config.unreadCount),
center,
center + size * 0.15,
);
}
}
@@ -107,12 +118,12 @@ const renderCanvas = function (arg: number): HTMLCanvasElement {
* @param arg: Unread count
* @return the native image
*/
const renderNativeImage = function (arg: number): NativeImage {
const renderNativeImage = function (argument: number): NativeImage {
if (process.platform === "win32") {
return nativeImage.createFromPath(winUnreadTrayIconPath());
}
const canvas = renderCanvas(arg);
const canvas = renderCanvas(argument);
const pngData = nativeImage
.createFromDataURL(canvas.toDataURL("image/png"))
.toPNG();
@@ -123,7 +134,7 @@ const renderNativeImage = function (arg: number): NativeImage {
function sendAction<Channel extends keyof RendererMessage>(
channel: Channel,
...args: Parameters<RendererMessage[Channel]>
...arguments_: Parameters<RendererMessage[Channel]>
): void {
const win = BrowserWindow.getAllWindows()[0];
@@ -131,19 +142,19 @@ function sendAction<Channel extends keyof RendererMessage>(
win.restore();
}
ipcRenderer.sendTo(win.webContents.id, channel, ...args);
ipcRenderer.send("forward-to", win.webContents.id, channel, ...arguments_);
}
const createTray = function (): void {
const contextMenu = Menu.buildFromTemplate([
{
label: "Zulip",
label: t.__("Zulip"),
click() {
ipcRenderer.send("focus-app");
},
},
{
label: "Settings",
label: t.__("Settings"),
click() {
ipcRenderer.send("focus-app");
sendAction("open-settings");
@@ -153,7 +164,7 @@ const createTray = function (): void {
type: "separator",
},
{
label: "Quit",
label: t.__("Quit"),
click() {
ipcRenderer.send("quit-app");
},
@@ -168,70 +179,73 @@ const createTray = function (): void {
}
};
ipcRenderer.on("destroytray", (_event: Event) => {
if (!tray) {
return;
}
tray.destroy();
if (tray.isDestroyed()) {
tray = null;
} else {
throw new Error("Tray icon not properly destroyed.");
}
});
ipcRenderer.on("tray", (_event: Event, arg: number): void => {
if (!tray) {
return;
}
// We don't want to create tray from unread messages on macOS since it already has dock badges.
if (process.platform === "linux" || process.platform === "win32") {
if (arg === 0) {
unread = arg;
tray.setImage(iconPath());
tray.setToolTip("No unread messages");
} else {
unread = arg;
const image = renderNativeImage(arg);
tray.setImage(image);
tray.setToolTip(`${arg} unread messages`);
export function initializeTray(serverManagerView: ServerManagerView) {
ipcRenderer.on("destroytray", () => {
if (!tray) {
return;
}
}
});
function toggleTray(): void {
let state;
if (tray) {
state = false;
tray.destroy();
if (tray.isDestroyed()) {
tray = null;
} else {
throw new Error("Tray icon not properly destroyed.");
}
});
ipcRenderer.on("tray", (_event, argument: number): void => {
if (!tray) {
return;
}
ConfigUtil.setConfigItem("trayIcon", false);
} else {
state = true;
createTray();
// 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") {
const image = renderNativeImage(unread);
tray!.setImage(image);
tray!.setToolTip(`${unread} unread messages`);
if (argument === 0) {
unread = argument;
tray.setImage(iconPath());
tray.setToolTip(t.__("No unread messages"));
} else {
unread = argument;
const image = renderNativeImage(argument);
tray.setImage(image);
tray.setToolTip(
t.__mf(
"{number, plural, one {# unread message} other {# unread messages}}",
{number: `${argument}`},
),
);
}
}
});
function toggleTray(): void {
let state;
if (tray) {
state = false;
tray.destroy();
if (tray.isDestroyed()) {
tray = null;
}
ConfigUtil.setConfigItem("trayIcon", false);
} else {
state = true;
createTray();
if (process.platform === "linux" || process.platform === "win32") {
const image = renderNativeImage(unread);
tray!.setImage(image);
tray!.setToolTip(`${unread} unread messages`);
}
ConfigUtil.setConfigItem("trayIcon", true);
}
ConfigUtil.setConfigItem("trayIcon", true);
serverManagerView.preferenceView?.handleToggleTray(state);
}
const selector = "webview:not([class*=disabled])";
const webview: WebviewTag = document.querySelector(selector)!;
ipcRenderer.sendTo(webview.getWebContentsId(), "toggle-tray", state);
ipcRenderer.on("toggletray", toggleTray);
if (ConfigUtil.getConfigItem("trayIcon", true)) {
createTray();
}
}
ipcRenderer.on("toggletray", toggleTray);
if (ConfigUtil.getConfigItem("trayIcon", true)) {
createTray();
}
export {};

View File

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

View File

@@ -1,54 +1,79 @@
import {remote} from "electron";
import fs from "fs";
import path from "path";
import fs from "node:fs";
import path from "node:path";
import {app, dialog} from "@electron/remote";
import * as Sentry from "@sentry/electron/renderer";
import {JsonDB} from "node-json-db";
import {DataError} from "node-json-db/dist/lib/Errors.js";
import {z} from "zod";
import * as EnterpriseUtil from "../../../common/enterprise-util";
import Logger from "../../../common/logger-util";
import * as Messages from "../../../common/messages";
import type {ServerConf} from "../../../common/types";
import {ipcRenderer} from "../typed-ipc-renderer";
const {app, dialog} = remote;
import * as EnterpriseUtil from "../../../common/enterprise-util.ts";
import Logger from "../../../common/logger-util.ts";
import * as Messages from "../../../common/messages.ts";
import * as t from "../../../common/translation-util.ts";
import type {ServerConfig} from "../../../common/types.ts";
import defaultIcon from "../../img/icon.png";
import {ipcRenderer} from "../typed-ipc-renderer.ts";
const logger = new Logger({
file: "domain-util.log",
});
const defaultIconUrl = "../renderer/img/icon.png";
// For historical reasons, we store this string in domain.json to denote a
// missing icon; it does not change with the actual icon location.
export const defaultIconSentinel = "../renderer/img/icon.png";
let db!: JsonDB;
const serverConfigSchema = z.object({
url: z.url(),
alias: z.string(),
icon: z.string(),
zulipVersion: z.string().default("unknown"),
zulipFeatureLevel: z.number().default(0),
});
let database!: JsonDB;
reloadDatabase();
reloadDB();
// Migrate from old schema
if (db.getData("/").domain) {
(async () => {
await addDomain({
alias: "Zulip",
url: db.getData("/domain"),
});
db.delete("/domain");
})();
try {
const oldDomain = database.getObject<unknown>("/domain");
if (typeof oldDomain === "string") {
(async () => {
await addDomain({
alias: "Zulip",
url: oldDomain,
});
database.delete("/domain");
})();
}
} catch (error: unknown) {
if (!(error instanceof DataError)) throw error;
}
export function getDomains(): ServerConf[] {
reloadDB();
if (db.getData("/").domains === undefined) {
export function getDomains(): ServerConfig[] {
reloadDatabase();
try {
return serverConfigSchema
.array()
.parse(database.getObject<unknown>("/domains"));
} catch (error: unknown) {
if (!(error instanceof DataError)) throw error;
return [];
}
return db.getData("/domains");
}
export function getDomain(index: number): ServerConf {
reloadDB();
return db.getData(`/domains[${index}]`);
export function getDomain(index: number): ServerConfig {
reloadDatabase();
return serverConfigSchema.parse(
database.getObject<unknown>(`/domains[${index}]`),
);
}
export function updateDomain(index: number, server: ServerConf): void {
reloadDB();
db.push(`/domains[${index}]`, server, true);
export function updateDomain(index: number, server: ServerConfig): void {
reloadDatabase();
serverConfigSchema.parse(server);
database.push(`/domains[${index}]`, server, true);
}
export async function addDomain(server: {
@@ -59,18 +84,20 @@ export async function addDomain(server: {
if (server.icon) {
const localIconUrl = await saveServerIcon(server.icon);
server.icon = localIconUrl;
db.push("/domains[]", server, true);
reloadDB();
serverConfigSchema.parse(server);
database.push("/domains[]", server, true);
reloadDatabase();
} else {
server.icon = defaultIconUrl;
db.push("/domains[]", server, true);
reloadDB();
server.icon = defaultIconSentinel;
serverConfigSchema.parse(server);
database.push("/domains[]", server, true);
reloadDatabase();
}
}
export function removeDomains(): void {
db.delete("/domains");
reloadDB();
database.delete("/domains");
reloadDatabase();
}
export function removeDomain(index: number): boolean {
@@ -78,8 +105,8 @@ export function removeDomain(index: number): boolean {
return false;
}
db.delete(`/domains[${index}]`);
reloadDB();
database.delete(`/domains[${index}]`);
reloadDatabase();
return true;
}
@@ -92,7 +119,7 @@ export function duplicateDomain(domain: string): boolean {
export async function checkDomain(
domain: string,
silent = false,
): Promise<ServerConf> {
): Promise<ServerConfig> {
if (!silent && duplicateDomain(domain)) {
// Do not check duplicate in silent mode
throw new Error("This server has been added.");
@@ -107,36 +134,43 @@ 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);
}
export async function saveServerIcon(iconURL: string): Promise<string> {
return ipcRenderer.invoke("save-server-icon", iconURL);
return (
(await ipcRenderer.invoke("save-server-icon", iconURL)) ??
defaultIconSentinel
);
}
export async function updateSavedServer(
url: string,
index: number,
): Promise<void> {
): Promise<ServerConfig> {
// Does not promise successful update
const oldIcon = getDomain(index).icon;
const serverConfig = getDomain(index);
const oldIcon = serverConfig.icon;
try {
const newServerConf = await checkDomain(url, true);
const localIconUrl = await saveServerIcon(newServerConf.icon);
if (!oldIcon || localIconUrl !== "../renderer/img/icon.png") {
newServerConf.icon = localIconUrl;
updateDomain(index, newServerConf);
reloadDB();
const newServerConfig = await checkDomain(url, true);
const localIconUrl = await saveServerIcon(newServerConfig.icon);
if (!oldIcon || localIconUrl !== defaultIconSentinel) {
newServerConfig.icon = localIconUrl;
updateDomain(index, newServerConfig);
reloadDatabase();
}
return newServerConfig;
} catch (error: unknown) {
logger.log("Could not update server icon.");
logger.log(error);
logger.reportSentry(error);
Sentry.captureException(error);
return serverConfig;
}
}
function reloadDB(): void {
function reloadDatabase(): void {
const domainJsonPath = path.join(
app.getPath("userData"),
"config/domain.json",
@@ -148,17 +182,18 @@ function reloadDB(): void {
if (fs.existsSync(domainJsonPath)) {
fs.unlinkSync(domainJsonPath);
dialog.showErrorBox(
"Error saving new organization",
"There seems to be error while saving new organization, " +
"you may have to re-add your previous organizations back.",
t.__("Error saving new organization"),
t.__(
"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);
logger.reportSentry(error);
Sentry.captureException(error);
}
}
db = new JsonDB(domainJsonPath, true, true);
database = new JsonDB(domainJsonPath, true, true);
}
export function formatUrl(domain: string): string {
@@ -172,3 +207,30 @@ export function formatUrl(domain: string): string {
return `https://${domain}`;
}
export function getUnsupportedMessage(
server: ServerConfig,
): string | undefined {
if (server.zulipFeatureLevel < 65 /* Zulip Server 4.0 */) {
const realm = new URL(server.url).hostname;
return t.__(
"{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.",
{server: realm, version: server.zulipVersion},
);
}
return undefined;
}
export function iconAsUrl(iconPath: string): string {
if (iconPath === defaultIconSentinel) return defaultIcon;
try {
return `data:application/octet-stream;base64,${fs.readFileSync(
iconPath,
"base64",
)}`;
} catch {
return defaultIcon;
}
}

View File

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

View File

@@ -1,8 +1,6 @@
import os from "os";
import {ipcRenderer} from "../typed-ipc-renderer.ts";
import {ipcRenderer} from "../typed-ipc-renderer";
export const connectivityERR: string[] = [
export const connectivityError: string[] = [
"ERR_INTERNET_DISCONNECTED",
"ERR_PROXY_CONNECTION_FAILED",
"ERR_CONNECTION_RESET",
@@ -13,27 +11,6 @@ export const connectivityERR: string[] = [
const userAgent = ipcRenderer.sendSync("fetch-user-agent");
export function getOS(): string {
const platform = os.platform();
if (platform === "darwin") {
return "Mac";
}
if (platform === "linux") {
return "Linux";
}
if (platform === "win32") {
if (Number.parseFloat(os.release()) < 6.2) {
return "Windows 7";
}
return "Windows 10";
}
return "";
}
export function getUserAgent(): string {
return userAgent;
}

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

@@ -1,65 +1,16 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" class="responsive desktop">
<head>
<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" />
<title>Zulip</title>
<link rel="stylesheet" href="css/main.css" type="text/css" media="screen" />
<link rel="stylesheet" href="css/fonts.css" />
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<div id="content">
<div class="popup">
<span class="popuptext hidden" id="fullscreen-popup"></span>
</div>
<div id="sidebar" class="toggle-sidebar">
<div id="view-controls-container">
<div id="tabs-container"></div>
<div id="add-tab" class="tab functional-tab">
<div class="server-tab" id="add-action">
<i class="material-icons">add</i>
</div>
<span id="add-server-tooltip" style="display: none"
>Add organization</span
>
</div>
</div>
<div id="actions-container">
<div class="action-button" id="dnd-action">
<i class="material-icons md-48">notifications</i>
<span id="dnd-tooltip" style="display: none">Do Not Disturb</span>
</div>
<div class="action-button" id="reload-action">
<i class="material-icons md-48">refresh</i>
<span id="reload-tooltip" style="display: none">Reload</span>
</div>
<div class="action-button disable" id="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>
<div id="feedback-modal">
<send-feedback show-cancel-button="show"></send-feedback>
</div>
</body>
<script>
// we don't use src='./js/main' in the script tag because
// it messes up require module path resolution
require("./js/main");
</script>
<body></body>
</html>

View File

@@ -1,7 +1,11 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" class="responsive desktop">
<head>
<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" />
<title>Zulip - Network Troubleshooting</title>
<link

View File

@@ -1,27 +1,10 @@
<!DOCTYPE html>
<html lang="en" class="responsive desktop">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Zulip - Settings</title>
<link
rel="stylesheet"
href="css/preference.css"
type="text/css"
media="screen"
/>
<link id="tagify-css" rel="stylesheet" href="data:text/css," />
</head>
<body>
<div id="content">
<div id="sidebar"></div>
<div id="settings-container"></div>
</div>
</body>
<script>
document.querySelector("#tagify-css").href = require.resolve(
"@yaireo/tagify/dist/tagify.css",
);
require("./js/pages/preference/preference.js");
</script>
</html>
<!doctype html>
<meta charset="UTF-8" />
<link rel="stylesheet" href="css/fonts.css" />
<link rel="stylesheet" href="css/preference.css" />
<!-- Initially hidden to prevent FOUC -->
<div id="content" hidden>
<div id="sidebar"></div>
<div id="settings-container"></div>
</div>

Binary file not shown.

View File

@@ -1,20 +0,0 @@
# How to help translate Zulip Desktop
These are _generated_ files (\*) that contain translations of the strings in
the app.
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; a maintainer then syncs those translations into this repo.
To help out, [join the Zulip project on
Transifex](https://www.transifex.com/zulip/zulip/) and enter translations
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
at `desktop.json`. The other resources there are for the Zulip web/mobile
app, where translations are also very welcome.
(\*) One file is an exception: `en.json` is manually maintained as a
list of (English) messages in the source code, and is used when we upload to
Transifex a list of strings to be translated. It doesn't contain any
translations.

View File

@@ -1,134 +0,0 @@
{
"About Zulip": "Über Zulip",
"Actual Size": "Tatsächliche Größe",
"Add Custom Certificates": "Eigene Zertifikate hinzufügen",
"Add Organization": "Organisation hinzufügen",
"Add a Zulip organization": "Zulip-Organisation hinzufügen",
"Add custom CSS": "Eigenes CSS hinzufügen",
"Advanced": "Erweitert",
"All the connected organizations will appear here": "Alle verbundenen Organisationen erscheinen hier",
"Always start minimized": "Immer minimiert öffnen",
"App Updates": "App Updates",
"Appearance": "Erscheinungsbild",
"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?",
"Auto hide Menu bar": "Menü automatisch verstecken",
"Auto hide menu bar (Press Alt key to display)": "Menü automatisch verstecken (zum Anzeigen die Alt-Taste drücken)",
"Back": "Zurück",
"Bounce dock on new private message": "Im Dock hüpfen, wenn neue private Nachrichten eingehen",
"Certificate file": "Zertifikatsdatei",
"Change": "Ändern",
"Check for Updates": "Auf Updates prüfen",
"Close": "Schließen",
"Connect": "Verbinden",
"Connect to another organization": "Mit einer anderen Organisation verbinden",
"Connected organizations": "Verbundene Organisationen",
"Copy": "Kopieren",
"Copy Zulip URL": "Zulip-URL kopieren",
"Create a new organization": "Eine neue Organisation erstellen",
"Cut": "Ausschneiden",
"Default download location": "Voreingestelltes Ziel für Downloads",
"Delete": "Löschen",
"Desktop App Settings": "Einstellungen für Desktop-App",
"Desktop Notifications": "Desktopbenachrichtigungen",
"Desktop Settings": "Desktop-Einstellungen",
"Disconnect": "Verbindung trennen",
"Download App Logs": "Logdateien der App herunterladen",
"Edit": "Bearbeiten",
"Edit Shortcuts": "Tastenkürzel bearbeiten",
"Enable auto updates": "Automatisch aktualisieren",
"Enable error reporting (requires restart)": "Fehlerberichte aktivieren (erfordert Neustart)",
"Enable spellchecker (requires restart)": "Rechtschreibprüfung aktivieren (erfordert Neustart)",
"Factory Reset": "Alle Einstellungen auf Standardwerte zurücksetzen",
"File": "Datei",
"Find accounts": "Accounts finden",
"Find accounts by email": "Accounts anhand E-Mail-Adresse finden",
"Flash taskbar on new message": "Farbliche Hervorhebung in Taskbar bei neuen Nachrichten",
"Forward": "Weiter",
"Functionality": "Funktionalität",
"General": "Allgemein",
"Get beta updates": "Auf Betaversionen aktualisieren",
"Hard Reload": "Komplett neu laden",
"Help": "Hilfe",
"Help Center": "Hilfe-Zentrum",
"History": "Verlauf",
"History Shortcuts": "Kurzbefehle für Verlauf",
"Keyboard Shortcuts": "Tastenkürzel",
"Log Out": "Abmelden",
"Log Out of Organization": "Von Organisation abmelden",
"Manual proxy configuration": "Manuelle Proxy-Konfiguration",
"Minimize": "Minimieren",
"Mute all sounds from Zulip": "Alle Zulip-Klänge stummschalten",
"NO": "NEIN",
"Network": "Netzwerk",
"OR": "ODER",
"Organization URL": "URL der Organisation",
"Organizations": "Organisationen",
"Paste": "Einfügen",
"Paste and Match Style": "Ohne Formatierung einfügen",
"Proxy": "Proxy",
"Proxy bypass rules": "Proxy-Ausnahmen",
"Proxy rules": "Proxy-Regeln",
"Quit": "Beenden",
"Quit Zulip": "Zulip beenden",
"Redo": "Wiederholen",
"Release Notes": "Hinweise zur Versionsfreigabe",
"Reload": "Neu laden",
"Report an Issue": "Ein Problem melden",
"Save": "Speichern",
"Select All": "Alles auswählen",
"Settings": "Einstellungen",
"Shortcuts": "Kurzbefehle",
"Show App Logs": "Logdateien der App 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 downloaded files in file manager": "Heruntergeladene Dateien in Dateimanager anzeigen",
"Show sidebar": "Seitenleiste anzeigen",
"Start app at login": "App beim Login automatisch starten",
"Switch to Next Organization": "Zur nächsten Organisation wechseln",
"Switch to Previous Organization": "Zur vorhergehenden Organisation wechseln",
"These desktop app shortcuts extend the Zulip webapp's": "Dies sind zusätzliche Kurzbefehle in der Desktop-App gegenüber der Web-App",
"This will delete all application data including all added accounts and preferences": "Hiermit werden alle Anwendungsdaten einschließlich aller Accounts und Einstellungen gelöscht",
"Tip": "Tipp",
"Toggle DevTools for Active Tab": "Entwickler-Tools für aktiven Tab umschalten",
"Toggle DevTools for Zulip App": "Entwickler-Tools für Zulip-App umschalten",
"Toggle Do Not Disturb": "Nicht-Stören-Modus umschalten",
"Toggle Full Screen": "Vollbildschirm umschalten",
"Toggle Sidebar": "Seitenleiste umschalten",
"Toggle Tray Icon": "Tray-Icon umschalten",
"Tools": "Extras",
"Undo": "Rückgängig",
"Upload": "Hochladen",
"Use system proxy settings (requires restart)": "Systemweite Proxy-Einstellungen verwenden (erfordert Neustart)",
"View": "Ansicht",
"View Shortcuts": "Tastenkürzel anzeigen",
"Window": "Fenster",
"Window Shortcuts": "Kurzbefehle für Fenster",
"YES": "JA",
"Zoom In": "Vergrößern",
"Zoom Out": "Verkleinern",
"Zulip Help": "Hilfe zu Zulip",
"keyboard shortcuts": "Tastenkürzel",
"script": "Skript",
"Quit when the window is closed": "Beim Schließen des Fensters beenden",
"Ask where to save files before downloading": "Fragen, wo heruntergeladene Dateien gespeichert werden sollen",
"Services": "Dienste",
"Hide": "Verbergen",
"Hide Others": "Andere verbergen",
"Unhide": "Nicht mehr verbergen",
"AddServer": "ServerHinzufügen",
"App language (requires restart)": "Sprache der App (benötigt Neustart)",
"Factory Reset Data": "Auf Werkseinstellung zurücksetzen",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Die App wird zurückgesetzt, somit werden alle verbundenen Organisationen, Konten und Zertifikate gelöscht.",
"On macOS, the OS spellchecker is used.": "In macOS wird die OS Rechtschreibprüfung verwendet.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Ändere die Spracheinstellung über Systemeinstellungen → Tastatur → Text → Rechtschreibung.",
"Copy Link": "Link kopieren",
"Copy Image": "Bild kopieren",
"Copy Image URL": "Bild-URL kopieren",
"No Suggestion Found": "Keine Vorschläge gefunden",
"You can select a maximum of 3 languages for spellchecking.": "Du kannst höchstens 3 Sprachen für die Rechtschreibprüfung auswählen.",
"Spellchecker Languages": "Sprachen für die Rechtschreibprüfung",
"Add to Dictionary": "Zum Wörterbuch hinzufügen",
"Look Up": "Nachschlagen"
}

View File

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

View File

@@ -1,119 +0,0 @@
{
"About Zulip": "About Zulip",
"Actual Size": "Actual Size",
"Add Custom Certificates": "Add Custom Certificates",
"Add Organization": "Add Organization",
"Add a Zulip organization": "Add a Zulip organization",
"Add custom CSS": "Add custom CSS",
"Advanced": "Advanced",
"All the connected organizations will appear here": "All the connected organizations will appear here",
"Always start minimized": "Always start minimized",
"App Updates": "App Updates",
"Appearance": "Appearance",
"Application Shortcuts": "Application Shortcuts",
"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?",
"Auto hide Menu bar": "Auto hide Menu bar",
"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)",
"Back": "Back",
"Bounce dock on new private message": "Bounce dock on new private message",
"Certificate file": "Certificate file",
"Change": "Change",
"Check for Updates": "Check for Updates",
"Close": "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 App Settings": "Desktop App Settings",
"Desktop Notifications": "Desktop Notifications",
"Desktop Settings": "Desktop Settings",
"Disconnect": "Disconnect",
"Download App Logs": "Download App Logs",
"Edit": "Edit",
"Edit Shortcuts": "Edit Shortcuts",
"Enable auto updates": "Enable auto updates",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Factory Reset": "Factory Reset",
"File": "File",
"Find accounts": "Find accounts",
"Find accounts by email": "Find accounts by email",
"Flash taskbar on new message": "Flash taskbar on new message",
"Forward": "Forward",
"Functionality": "Functionality",
"General": "General",
"Get beta updates": "Get beta updates",
"Hard Reload": "Hard Reload",
"Help": "Help",
"Help Center": "Help Center",
"History": "History",
"History Shortcuts": "History Shortcuts",
"Keyboard Shortcuts": "Keyboard Shortcuts",
"Log Out": "Log Out",
"Log Out of Organization": "Log Out of Organization",
"Manual proxy configuration": "Manual proxy configuration",
"Minimize": "Minimize",
"Mute all sounds from Zulip": "Mute all sounds from Zulip",
"NO": "NO",
"Network": "Network",
"OR": "OR",
"Organization URL": "Organization URL",
"Organizations": "Organizations",
"Paste": "Paste",
"Paste and Match Style": "Paste and Match Style",
"Proxy": "Proxy",
"Proxy bypass rules": "Proxy bypass rules",
"Proxy rules": "Proxy rules",
"Quit": "Quit",
"Quit Zulip": "Quit Zulip",
"Redo": "Redo",
"Release Notes": "Release Notes",
"Reload": "Reload",
"Report an Issue": "Report an Issue",
"Reset App Data": "Reset App Data",
"Reset App Settings": "Reset App Settings",
"Reset Application Data": "Reset Application Data",
"Save": "Save",
"Select All": "Select All",
"Settings": "Settings",
"Shortcuts": "Shortcuts",
"Show App Logs": "Show App Logs",
"Show app icon in system tray": "Show app icon in system tray",
"Show app unread badge": "Show app unread badge",
"Show desktop notifications": "Show desktop notifications",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Show sidebar": "Show sidebar",
"Start app at login": "Start app at login",
"Switch to Next Organization": "Switch to Next Organization",
"Switch to Previous Organization": "Switch to Previous Organization",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Tip": "Tip",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle Do Not Disturb": "Toggle Do Not Disturb",
"Toggle Full Screen": "Toggle Full Screen",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon",
"Tools": "Tools",
"Undo": "Undo",
"Upload": "Upload",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"View": "View",
"View Shortcuts": "View Shortcuts",
"Window": "Window",
"Window Shortcuts": "Window Shortcuts",
"YES": "YES",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Zulip Help": "Zulip Help",
"keyboard shortcuts": "keyboard shortcuts",
"script": "script",
"Quit when the window is closed": "Quit when the window is closed",
"Ask where to save files before downloading": "Ask where to save files before downloading"
}

View File

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

View File

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

View File

@@ -1,134 +0,0 @@
{
"About Zulip": "Tietoa Zulipista",
"Actual Size": "Varsinainen koko",
"Add Custom Certificates": "Lisää omia sertifikaatteja",
"Add Organization": "Lisää organisaatio",
"Add a Zulip organization": "Lisää Zulip-organisaatio",
"Add custom CSS": "Lisää oma CSS",
"Advanced": "Edistynyt",
"All the connected organizations will appear here": "All the connected organizations will appear here",
"Always start minimized": "Aloita aina pienennettynä",
"App Updates": "Sovellspäivitykset",
"Appearance": "Ulkonäkö",
"Application Shortcuts": "Sovellusoikotiet",
"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?",
"Auto hide Menu bar": "Auto hide Menu bar",
"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)",
"Back": "Back",
"Bounce dock on new private message": "Bounce dock on new private message",
"Certificate file": "Certificate file",
"Change": "Muuta",
"Check for Updates": "Check for Updates",
"Close": "Sulje",
"Connect": "Yhdistä",
"Connect to another organization": "Connect to another organization",
"Connected organizations": "Connected organizations",
"Copy": "Kopioi",
"Copy Zulip URL": "Kopioi Zulip-URL",
"Create a new organization": "Luo uusi organisaatio",
"Cut": "Leikkaa",
"Default download location": "Default download location",
"Delete": "Poista",
"Desktop App Settings": "Desktop App Settings",
"Desktop Notifications": "Desktop Notifications",
"Desktop Settings": "Desktop Settings",
"Disconnect": "Katkaise",
"Download App Logs": "Download App Logs",
"Edit": "Muokkaa",
"Edit Shortcuts": "Edit Shortcuts",
"Enable auto updates": "Enable auto updates",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Factory Reset": "Factory Reset",
"File": "Tiedosto",
"Find accounts": "Löydä tilit",
"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": "Ohje",
"Help Center": "Tukikeskus",
"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": "EI",
"Network": "Network",
"OR": "TAI",
"Organization URL": "Organisaation URL",
"Organizations": "Organisaatiot",
"Paste": "Liitä",
"Paste and Match Style": "Liitä ja täsmää tyylit",
"Proxy": "Välipalvelin",
"Proxy bypass rules": "Proxy bypass rules",
"Proxy rules": "Proxy rules",
"Quit": "Lopeta",
"Quit Zulip": "Lopeta Zulip",
"Redo": "Toista",
"Release Notes": "Release Notes",
"Reload": "Lataa uudelleen",
"Report an Issue": "Report an Issue",
"Save": "Tallenna",
"Select All": "Valitse kaikki",
"Settings": "Asetukset",
"Shortcuts": "Oikopolut",
"Show App Logs": "Show App Logs",
"Show app icon in system tray": "Show app icon in system tray",
"Show app unread badge": "Show app unread badge",
"Show desktop notifications": "Show desktop notifications",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Show sidebar": "Show sidebar",
"Start app at login": "Start app at login",
"Switch to Next Organization": "Switch to Next Organization",
"Switch to Previous Organization": "Switch to Previous Organization",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Tip": "Vinkki",
"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": "Työkalut",
"Undo": "Peru",
"Upload": "Upload",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"View": "Näytä",
"View Shortcuts": "View Shortcuts",
"Window": "Ikkuna",
"Window Shortcuts": "Window Shortcuts",
"YES": "KYLLÄ",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Zulip Help": "Zulip Help",
"keyboard shortcuts": "keyboard shortcuts",
"script": "script",
"Quit when the window is closed": "Quit when the window is closed",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"AddServer": "AddServer",
"App language (requires restart)": "App language (requires restart)",
"Factory Reset Data": "Factory Reset Data",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Copy Link": "Copy Link",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"No Suggestion Found": "No Suggestion Found",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Spellchecker Languages": "Spellchecker Languages",
"Add to Dictionary": "Add to Dictionary",
"Look Up": "Look Up"
}

View File

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

View File

@@ -1,134 +0,0 @@
{
"About Zulip": "O Zulipie",
"Actual Size": "Rzeczywisty rozmiar",
"Add Custom Certificates": "Dodaj niestandardowe certyfikaty",
"Add Organization": "Dodaj organizację",
"Add a Zulip organization": "Dodaj organizację Zulip",
"Add custom CSS": "Dodaj niestandardowy CSS",
"Advanced": "zaawansowane",
"All the connected organizations will appear here": "Tutaj pojawią się wszystkie połączone organizacje",
"Always start minimized": "Zawsze zaczynaj zminimalizowany",
"App Updates": "Aktualizacje aplikacji",
"Appearance": "Wygląd",
"Application Shortcuts": "Skróty do aplikacji",
"Are you sure you want to disconnect this organization?": "Czy na pewno chcesz odłączyć tę organizację?",
"Auto hide Menu bar": "Automatyczne ukrywanie paska menu",
"Auto hide menu bar (Press Alt key to display)": "Automatyczne ukrywanie paska menu (naciśnij klawisz Alt, aby wyświetlić)",
"Back": "Wstecz",
"Bounce dock on new private message": "Dok odbijania na nowej prywatnej wiadomości",
"Certificate file": "Plik certyfikatu",
"Change": "Zmiana",
"Check for Updates": "Sprawdź aktualizacje",
"Close": "Zamknij",
"Connect": "Połączyć",
"Connect to another organization": "Połącz się z inną organizacją",
"Connected organizations": "Połączone organizacje",
"Copy": "Kopiuj",
"Copy Zulip URL": "Skopiuj adres URL Zulip",
"Create a new organization": "Utwórz nową organizację",
"Cut": "Wytnij",
"Default download location": "Domyślna lokalizacja pobierania",
"Delete": "Usuń",
"Desktop App Settings": "Ustawienia aplikacji desktopowej",
"Desktop Notifications": "Powiadomienia na pulpicie",
"Desktop Settings": "Ustawienia pulpitu",
"Disconnect": "Rozłącz",
"Download App Logs": "Pobierz logi aplikacji",
"Edit": "Edytuj",
"Edit Shortcuts": "Edytuj skróty",
"Enable auto updates": "Włącz automatyczne aktualizacje",
"Enable error reporting (requires restart)": "Włącz raportowanie błędów (wymaga ponownego uruchomienia)",
"Enable spellchecker (requires restart)": "Włącz sprawdzanie pisowni (wymaga ponownego uruchomienia)",
"Factory Reset": "przywrócenie ustawień fabrycznych",
"File": "Plik",
"Find accounts": "Znajdź konta",
"Find accounts by email": "Znajdź konta po adresach email",
"Flash taskbar on new message": "Błyskaj w pasku zadań przy nowej wiadomości",
"Forward": "Naprzód",
"Functionality": "Funkcjonalność",
"General": "Ogólne",
"Get beta updates": "Pobierz aktualizacje beta",
"Hard Reload": "Twarde przeładowanie",
"Help": "Pomoc",
"Help Center": "Centrum pomocy",
"History": "Historia",
"History Shortcuts": "Skróty historii",
"Keyboard Shortcuts": "Skróty klawiszowe",
"Log Out": "Wyloguj",
"Log Out of Organization": "Wyloguj się z organizacji",
"Manual proxy configuration": "Ręczna konfiguracja proxy",
"Minimize": "Zminimalizuj",
"Mute all sounds from Zulip": "Wycisz wszystkie dźwięki z Zulipa",
"NO": "NIE",
"Network": "Sieć",
"OR": "LUB",
"Organization URL": "Adres URL organizacji",
"Organizations": "Organizacje",
"Paste": "Wklej",
"Paste and Match Style": "Wklej i dopasuj styl",
"Proxy": "Proxy",
"Proxy bypass rules": "Zasady omijania proxy",
"Proxy rules": "Reguły proxy",
"Quit": "Wyjdź",
"Quit Zulip": "Wyjdź z Zulipa",
"Redo": "Ponów",
"Release Notes": "Informacje o wydaniu",
"Reload": "Przeładuj",
"Report an Issue": "Zgłoś problem",
"Save": "Zapisz",
"Select All": "Zaznacz wszystko",
"Settings": "Ustawienia",
"Shortcuts": "Skróty",
"Show App Logs": "Pokaż dzienniki aplikacji",
"Show app icon in system tray": "Pokaż ikonę aplikacji w zasobniku systemowym",
"Show app unread badge": "Pokaż nieprzeczytane na ikonie aplikacji",
"Show desktop notifications": "Pokaż powiadomienia na pulpicie",
"Show downloaded files in file manager": "Pokaż pobrane pliki w menedżerze plików",
"Show sidebar": "Pokaż pasek boczny",
"Start app at login": "Uruchom aplikację przy logowaniu",
"Switch to Next Organization": "Przełącz na następną organizację",
"Switch to Previous Organization": "Przełącz na poprzednią organizację",
"These desktop app shortcuts extend the Zulip webapp's": "Poniższe skróty są dostępne tylko w kliencie Zulip",
"This will delete all application data including all added accounts and preferences": "Spowoduje to usunięcie wszystkich danych aplikacji, w tym wszystkich dodanych kont i preferencji",
"Tip": "Wskazówka",
"Toggle DevTools for Active Tab": "Włącz/wyłącz DevTools w aktywnej zakładce",
"Toggle DevTools for Zulip App": "Włącz/wyłącz DevTools dla klienta Zulip",
"Toggle Do Not Disturb": "Przełącz nie przeszkadzać",
"Toggle Full Screen": "Przełącz tryb pełnoekranowy",
"Toggle Sidebar": "Przełącz pasek boczny",
"Toggle Tray Icon": "Przełącz ikonę tacy",
"Tools": "Narzędzia",
"Undo": "Cofnij",
"Upload": "Przekazać plik",
"Use system proxy settings (requires restart)": "Użyj ustawień systemowych proxy (wymaga restartu aplikacji)",
"View": "Widok",
"View Shortcuts": "Wyświetl skróty",
"Window": "Okno",
"Window Shortcuts": "Skróty do okien",
"YES": "TAK",
"Zoom In": "Powiększ",
"Zoom Out": "Pomniejsz",
"Zulip Help": "Pomoc Zulip",
"keyboard shortcuts": "Skróty klawiszowe",
"script": "skrypt",
"Quit when the window is closed": "Wyłącz przy zamykaniu okna",
"Ask where to save files before downloading": "Zapytaj przed pobraniem gdzie zachować pliki",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"AddServer": "AddServer",
"App language (requires restart)": "App language (requires restart)",
"Factory Reset Data": "Factory Reset Data",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Copy Link": "Copy Link",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"No Suggestion Found": "No Suggestion Found",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Spellchecker Languages": "Spellchecker Languages",
"Add to Dictionary": "Add to Dictionary",
"Look Up": "Look Up"
}

View File

@@ -1,134 +0,0 @@
{
"About Zulip": "Sobre o Zulip",
"Actual Size": "Tamanho atual",
"Add Custom Certificates": "Adicionar certificados personalizados",
"Add Organization": "Adicionar Organização",
"Add a Zulip organization": "Adicione uma organização Zulip",
"Add custom CSS": "Adicionar CSS personalizado",
"Advanced": "Avançado",
"All the connected organizations will appear here": "Todas as organizações conectadas aparecerão aqui",
"Always start minimized": "Começar sempre minimizado",
"App Updates": "Atualizações de aplicativos",
"Appearance": "Aparência",
"Application Shortcuts": "Atalhos de aplicativos",
"Are you sure you want to disconnect this organization?": "Tem certeza de que deseja desconectar essa organização?",
"Auto hide Menu bar": "Auto ocultar barra de Menu",
"Auto hide menu bar (Press Alt key to display)": "Ocultar barra de menu automaticamente (pressione a tecla Alt para exibir)",
"Back": "De volta",
"Bounce dock on new private message": "Bounce doca em nova mensagem privada",
"Certificate file": "Arquivo de certificado",
"Change": "mudança",
"Check for Updates": "Verificar se há atualizações",
"Close": "Fechar",
"Connect": "Conectar",
"Connect to another organization": "Conecte-se a outra organização",
"Connected organizations": "Organizações conectadas",
"Copy": "Copiar",
"Copy Zulip URL": "Copiar URL do Zulip",
"Create a new organization": "Crie uma nova organização",
"Cut": "Cortar",
"Default download location": "Local de download padrão",
"Delete": "Excluir",
"Desktop App Settings": "Configurações do aplicativo de desktop",
"Desktop Notifications": "Notificações da área de trabalho",
"Desktop Settings": "Configurações da área de trabalho",
"Disconnect": "desconectar",
"Download App Logs": "Download de registros de aplicativos",
"Edit": "Editar",
"Edit Shortcuts": "Editar atalhos",
"Enable auto updates": "Ativar atualizações automáticas",
"Enable error reporting (requires restart)": "Ativar relatório de erros (requer reinicialização)",
"Enable spellchecker (requires restart)": "Ativar verificação ortográfica (requer reinicialização)",
"Factory Reset": "Restauração de fábrica",
"File": "Arquivo",
"Find accounts": "Encontrar contas",
"Find accounts by email": "Encontre contas por email",
"Flash taskbar on new message": "Barra de tarefas em Flash na nova mensagem",
"Forward": "frente",
"Functionality": "Funcionalidade",
"General": "Geral",
"Get beta updates": "Receba atualizações beta",
"Hard Reload": "Hard Reload",
"Help": "Socorro",
"Help Center": "Centro de ajuda",
"History": "História",
"History Shortcuts": "Atalhos da História",
"Keyboard Shortcuts": "Atalhos do teclado",
"Log Out": "Sair",
"Log Out of Organization": "Sair da organização",
"Manual proxy configuration": "Configuração manual de proxy",
"Minimize": "Minimizar",
"Mute all sounds from Zulip": "Silencie todos os sons de Zulip",
"NO": "NÃO",
"Network": "Rede",
"OR": "OU",
"Organization URL": "URL da organização",
"Organizations": "Organizações",
"Paste": "Colar",
"Paste and Match Style": "Colar e combinar estilo",
"Proxy": "Procuração",
"Proxy bypass rules": "Regras de desvio de proxy",
"Proxy rules": "Regras de proxy",
"Quit": "Sair",
"Quit Zulip": "Saia de Zulip",
"Redo": "Refazer",
"Release Notes": "Notas de Lançamento",
"Reload": "recarregar",
"Report an Issue": "Comunicar um problema",
"Save": "Salve ",
"Select All": "Selecionar tudo",
"Settings": "Configurações",
"Shortcuts": "Atalhos",
"Show App Logs": "Mostrar registros do aplicativo",
"Show app icon in system tray": "Mostrar ícone do aplicativo na bandeja do sistema",
"Show app unread badge": "Mostrar crachá não lido do aplicativo",
"Show desktop notifications": "Mostrar notificações da área de trabalho",
"Show downloaded files in file manager": "Mostrar arquivos baixados no gerenciador de arquivos",
"Show sidebar": "Mostrar barra lateral",
"Start app at login": "Inicie o aplicativo no login",
"Switch to Next Organization": "Mudar para a próxima organização",
"Switch to Previous Organization": "Mudar para a organização anterior",
"These desktop app shortcuts extend the Zulip webapp's": "Esses atalhos para aplicativos de desktop estendem o aplicativo da web do Zulip",
"This will delete all application data including all added accounts and preferences": "Isso excluirá todos os dados do aplicativo, incluindo todas as contas e preferências adicionadas",
"Tip": "Gorjeta",
"Toggle DevTools for Active Tab": "Alternar DevTools para a guia ativa",
"Toggle DevTools for Zulip App": "Alternar DevTools para Zulip App",
"Toggle Do Not Disturb": "Alternar não perturbe",
"Toggle Full Screen": "Alternar para o modo tela cheia",
"Toggle Sidebar": "Alternar Barra Lateral",
"Toggle Tray Icon": "Alternar ícone da bandeja",
"Tools": "Ferramentas",
"Undo": "Desfazer",
"Upload": "Envio",
"Use system proxy settings (requires restart)": "Use as configurações de proxy do sistema (requer reinicialização)",
"View": "Visão",
"View Shortcuts": "Exibir atalhos",
"Window": "Janela",
"Window Shortcuts": "Atalhos de janela",
"YES": "SIM",
"Zoom In": "Mais Zoom",
"Zoom Out": "Reduzir o zoom",
"Zulip Help": "Zulip Ajuda",
"keyboard shortcuts": "atalhos do teclado",
"script": "roteiro",
"Quit when the window is closed": "Sair quando fechar a janela",
"Ask where to save files before downloading": "Ask where to save files before downloading",
"Services": "Services",
"Hide": "Hide",
"Hide Others": "Hide Others",
"Unhide": "Unhide",
"AddServer": "AddServer",
"App language (requires restart)": "App language (requires restart)",
"Factory Reset Data": "Factory Reset Data",
"Reset the application, thus deleting all the connected organizations, accounts, and certificates.": "Reset the application, thus deleting all the connected organizations, accounts, and certificates.",
"On macOS, the OS spellchecker is used.": "On macOS, the OS spellchecker is used.",
"Change the language from System Preferences → Keyboard → Text → Spelling.": "Change the language from System Preferences → Keyboard → Text → Spelling.",
"Copy Link": "Copy Link",
"Copy Image": "Copy Image",
"Copy Image URL": "Copy Image URL",
"No Suggestion Found": "No Suggestion Found",
"You can select a maximum of 3 languages for spellchecking.": "You can select a maximum of 3 languages for spellchecking.",
"Spellchecker Languages": "Spellchecker Languages",
"Add to Dictionary": "Add to Dictionary",
"Look Up": "Look Up"
}

View File

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

View File

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

View File

@@ -1,43 +0,0 @@
{
"ar": "عربى",
"bg": "български",
"ca": "català",
"cs": "česky",
"da": "Dansk",
"de": "Deutsch",
"el_GR": "Greek (Greece)",
"el": "Ελληνικά",
"en_GB": "English (UK)",
"en": "English (US)",
"es": "Español",
"fa": "فارسی",
"fi": "suomi",
"fr": "français",
"gl": "Galego",
"hi": "हिन्दी",
"hr": "Croata",
"hu": "Magyar",
"id_ID": "Indonesian (Indonesia)",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어" ,
"lt": "Lietuvis" ,
"ml": "മലയാളം",
"nb_NO": "norsk (Norge)",
"nl": "Nederlands",
"pl": "Polski",
"pt": "Português",
"ro": "Română",
"ru": "Русский",
"sk": "Slovak",
"sr": "српски",
"sv": "svenska",
"ta": "தமிழ்",
"tr": "Türkçe",
"uk": "Українська",
"uz": "O'zbek",
"vi": "Tiếng Việt",
"zh_TW": "中文 (傳統的)",
"zh-Hans": "简体中文",
"zh-Hant": "繁體中文"
}

View File

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

View File

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

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