Compare commits

...

456 Commits

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

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

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

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

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

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

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

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

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

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

Fixes #1327.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-09-14 10:30:50 -07:00
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
Anders Kaseorg
07e2ebe340 release: New release v5.7.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-30 21:33:50 -07:00
Anders Kaseorg
884d5e0e16 Upgrade dependencies, including Electron 12.0.6.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-30 21:30:36 -07:00
Anders Kaseorg
a3a79534ab Build for arm64 on macOS.
Fixes #1076.

For now we only build a DMG and zip for arm64, because the arm64 pkg
would overwrite the x64 pkg
(https://github.com/electron-userland/electron-builder/issues/5847).

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-30 21:19:34 -07:00
Anders Kaseorg
6acf1d3411 package-lock.json: Restore mysterious optional node-addon-api dependency.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-30 18:28:49 -07:00
Anders Kaseorg
e32480abfb package-lock.json: Upgrade various SHA-1 hashes to SHA-512.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-28 20:29:40 -07:00
Anders Kaseorg
07e7251d7d Upgrade dependencies, including Electron 12.0.5.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-28 20:29:40 -07:00
Anders Kaseorg
753b244630 tests: Convert if chain to switch statement.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-28 20:29:40 -07:00
Anders Kaseorg
892f7c8e47 Strongly type config options.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-28 20:29:40 -07:00
Anders Kaseorg
6a3f50d606 Use Partial types for DNDSettings, SettingsOptions.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-28 18:00:15 -07:00
Anders Kaseorg
a49cb77840 Skip startup code if another instance is running.
Commit 171d88755c removed a top-level
‘return’ statement, thereby allowing the app to proceed with running
some of its startup code even if it failed to grab the single-instance
lock.  Under some circumstances, this seems to result in an orphaned
background process constantly consuming CPU cycles.

Fixes #1102.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-26 19:02:51 -07:00
Anders Kaseorg
79f9362736 Strongly type IPC messages.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-25 20:44:05 -07:00
Anders Kaseorg
164038ec3c Specialize the renderer-callback event to permission-callback.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-25 20:39:49 -07:00
Anders Kaseorg
eacd52fb6c preference: Rename one of the two different toggletray messages.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-25 20:06:30 -07:00
Anders Kaseorg
20295ddc50 webview: Use send method.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-25 19:57:38 -07:00
Anders Kaseorg
994c412bd2 renderer: Use ipcRenderer.sendTo to communicate with other WebContents.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-25 19:57:38 -07:00
Anders Kaseorg
3b3fa88c89 electron-bridge: Check types of all event parameters.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-25 19:57:38 -07:00
Anders Kaseorg
afec96025b general-section: Remove some casts.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-25 19:57:38 -07:00
Anders Kaseorg
57b6144e7f Update dmg background.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-20 20:03:00 -07:00
Anders Kaseorg
9bff18ece3 Add dmg icon.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-20 20:02:46 -07:00
tarun8718
e89f44c87f css: Remove irregular shortcut highlighting.
Fixes #1104.

Signed-off-by: tarun8718 <tarunkumar8718@gmail.com>
2021-04-15 20:09:05 -07:00
Anders Kaseorg
99fe94fbab Upgrade dependencies, including Electron 12.0.4.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-15 20:00:30 -07:00
Anders Kaseorg
e0c2f43b2b xo: Enable import/no-cycle.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-15 19:49:32 -07:00
Tim Abbott
0c81eb93b9 docs: Create FUNDING.yml. 2021-04-08 06:32:09 -07:00
Anders Kaseorg
391c515779 injected: Use WeakMap instead of Symbol for attributeListener.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-03 15:38:52 -07:00
Anders Kaseorg
077f3e6e78 eslint: Enable sort-imports for member sorting.
This sorts the members imported within each individual declaration; we
use import/order for sorting multiple declarations.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-03 15:07:40 -07:00
Anders Kaseorg
3b2256bcec Fix Prettier formatting.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-03 00:47:53 -07:00
ashishbinu
87f5fa049f Fix vertical alignment of input placeholder 2021-04-03 12:37:52 +05:30
ashishbinu
5774b8a67b Fix input placeholder overflow in general settings 2021-04-03 12:37:52 +05:30
Anders Kaseorg
80fe51702b typescript: Use DefinitelyTyped declarations for @yaireo/tagify.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 18:20:21 -07:00
Anders Kaseorg
303ec73fa8 Move stylelint config packages to devDependencies.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 18:12:17 -07:00
Anders Kaseorg
815d9d4e28 typescript: Remove skipLibCheck.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 16:39:41 -07:00
Anders Kaseorg
6044b6328d tsconfig: Remove unused options.
noImplicitAny, noImplicitThis, and alwaysStrict are implied by strict.
noUnusedLocals, noUnusedParameters, and noImplicitReturns are handled
by xo.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 16:23:11 -07:00
Anders Kaseorg
9a81ade1c8 typescript: Enable strictNullChecks.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 15:19:42 -07:00
Anders Kaseorg
33c21d0153 ReconnectUtil: Inline clearState method.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 15:17:31 -07:00
Anders Kaseorg
1735ce6a8a tray: Remove unused return value from destroytray handler.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 15:15:36 -07:00
Anders Kaseorg
385ec00640 PreferenceNav: inline init method.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 15:08:40 -07:00
Anders Kaseorg
4f58a2a357 electron_bridge: Make the options argument of new_notification required.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 15:06:38 -07:00
Anders Kaseorg
1e10a3c406 domain-util: Make alias and icon required members of ServerConf.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 15:01:38 -07:00
Anders Kaseorg
ff3ea429d2 context-menu: Fix strictNullChecks violations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:29:14 -07:00
Anders Kaseorg
bdf7d1b813 index: Fix strictNullChecks violations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:28:55 -07:00
Anders Kaseorg
ae4b3a4778 Explode PreferenceView class to a function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:28:52 -07:00
Anders Kaseorg
66bc43674b Explode ShortcutsSection class to a function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:28:11 -07:00
Anders Kaseorg
8f0f82f98e Explode NetworkSection class to a function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:28:09 -07:00
Anders Kaseorg
43f0b4c902 Explode ConnectedOrgSection class to a function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:27:47 -07:00
Anders Kaseorg
867bb61e0d Explode GeneralSection class to a function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:26:53 -07:00
Anders Kaseorg
10912fe270 Explode ServersSection class to a function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:26:47 -07:00
Anders Kaseorg
76d9c36426 Explode NewServerForm class to a function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:26:20 -07:00
Anders Kaseorg
6ca4d77b8f Explode ServerInfoForm class to a function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:25:59 -07:00
Anders Kaseorg
7b3d40ca1e Explode FindAccounts class to a function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:25:41 -07:00
Anders Kaseorg
ea2d84e810 Explode BaseSection class to functions.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:14:48 -07:00
Anders Kaseorg
e477aed8ff generateNodeFromHTML: Check for null.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 14:14:07 -07:00
Anders Kaseorg
07ffb09391 Explode BaseComponent class to a function.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-02 13:10:15 -07:00
Anders Kaseorg
54f02c9616 PreferenceView: Lift init() calls into switch.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-01 18:45:55 -07:00
Anders Kaseorg
eeade47eb6 nav: Add NavItem literal type.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-01 18:45:55 -07:00
Anders Kaseorg
8ec9a98c86 ServerTab: Inline init method.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-01 18:13:30 -07:00
Anders Kaseorg
277d7ef824 FunctionalTab: Inline init method.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-01 18:11:55 -07:00
Anders Kaseorg
cc844e6905 Logger: Remove unused options.
All of our loggers have timestamp: true.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-01 14:06:43 -07:00
Anders Kaseorg
9b550d6e4f Logger: Remove dynamic method assignment.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-01 14:06:43 -07:00
Anders Kaseorg
9c25807b99 Avoid use of null when accessing electron.remote.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-01 14:06:43 -07:00
Anders Kaseorg
b07995c3ed Simplify querySelectorAll(…)[0] to querySelector(…).
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-04-01 14:06:43 -07:00
Anders Kaseorg
67228d295d Reformat all code with Prettier.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-31 20:04:00 -07:00
Anders Kaseorg
4521041619 Enable stylelint-config-standard.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-31 20:03:06 -07:00
Anders Kaseorg
ce9a680333 Add a tagged template function for HTML supporting HTML interpolation.
This allows better Prettier integration: Prettier recognizes and
reformats tagged template literals with a tag named ‘html’.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-31 20:00:49 -07:00
Anders Kaseorg
2c40843306 Remove zulip-electron-launcher.sh.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-31 17:59:32 -07:00
Anders Kaseorg
8d3dad234e Remove .node-version.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-31 17:59:32 -07:00
Anders Kaseorg
6da7cf6b8e Remove Gulp build system.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-31 17:59:32 -07:00
tarun8718
25d0aefe37 server-tab: Fix upper limit of generated shortcuts
Signed-off-by: tarun8718 <tarunkumar8718@gmail.com>
2021-03-29 20:23:06 -07:00
Anders Kaseorg
afb25d5b3d Upgrade dependencies, including Electron 12.0.2.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-29 19:58:26 -07:00
Anders Kaseorg
78ae1b34ab Revert GDK_BACKEND to work around Electron shell.openExternal bug.
https://github.com/electron/electron/issues/28436

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-29 19:52:18 -07:00
Anders Kaseorg
d3401cc87e xo: Fix @typescript-eslint/type-annotation-spacing.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-29 18:01:52 -07:00
Anders Kaseorg
9cbe4fdb4a xo: Enable import/no-restricted-paths.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-29 18:01:52 -07:00
Anders Kaseorg
851bb7904f Move modules shared between main and renderer processes to app/common.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-29 18:01:52 -07:00
Anders Kaseorg
d7598d3091 Replace LinkUtil.openBrowser in main process with shell.openExternal.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-29 18:01:15 -07:00
Anders Kaseorg
76d321fa79 xo: Enable @typescript-eslint/consistent-type-imports.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-29 17:48:54 -07:00
Anders Kaseorg
c42aafe0b9 request: Use a stable URL for the isOnline check.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-29 13:31:08 -07:00
Anders Kaseorg
28db945b91 Remove obsolete troubleshooting guide
None of these issues are current.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-29 12:20:19 -07:00
Saksham Agarwal
a805e260c4 docs: Fix keyboard shortcuts for Electron developer console. 2021-03-24 15:12:53 -07:00
Anders Kaseorg
6e95e5439b Upgrade dependencies, including Electron 11.3.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-08 23:14:00 -08:00
Anders Kaseorg
a9f479d60d Enable worldSafeExecuteJavaScript.
This has no effect on our use of executeJavaScript, and will become
the default in Electron 12, but for now it silences a warning in
development mode.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-08 23:14:00 -08:00
Anders Kaseorg
7b095a683c Replace electron-is-dev with app.isPackaged.
This unfortunately uses remote, but that’s what electron-is-dev was
doing anyway.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-08 23:14:00 -08:00
Anders Kaseorg
78931bbb22 xo: Fix unicorn/no-array-push-push.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-08 23:13:50 -08:00
Anders Kaseorg
12ae84b757 xo: Fix unicorn/prefer-spread.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-08 23:13:43 -08:00
Anders Kaseorg
75da0a16c1 xo: Fix unicorn/no-array-for-each.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-03-08 23:13:36 -08:00
Anders Kaseorg
2a0f9b30e6 release: New release v5.6.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-02-16 15:55:16 -08:00
Anders Kaseorg
109795ca3e package: Migrate APT repository from Bintray to our domain.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-02-16 15:40:26 -08:00
Anders Kaseorg
e6e5e8a311 Upgrade dependencies, including Electron 11.2.3.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-02-16 15:40:04 -08:00
Anders Kaseorg
bd0869ec07 preload: Move extra keyboard shortcuts to invisible menu items.
Fixes #1060.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-02-16 15:29:25 -08:00
tarun8718
07ae127cc8 shortcuts-section: Deduplicate templateMacHTML and templateWinLinHTML.
Signed-off-by: tarun8718 <tarunkumar8718@gmail.com>
2021-02-02 10:49:35 -08:00
Anders Kaseorg
baa76c3244 Upgrade dependencies, including Electron 11.2.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-01-25 11:35:02 -08:00
Anders Kaseorg
7ac31f80ed xo: Remove unused @typescript-eslint/prefer-readonly-parameter-types override.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-01-25 11:35:02 -08:00
Anders Kaseorg
a95ee64f7d xo: Use eslint-import-resolver-typescript.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-01-25 11:35:02 -08:00
Anders Kaseorg
7d6c6bc10a xo: Fix unicorn/prefer-number-properties.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-01-25 11:23:48 -08:00
Anders Kaseorg
18b41938de xo: Fix unicorn/empty-brace-spaces.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-01-25 11:23:48 -08:00
Anders Kaseorg
9fe382b27f xo: Fix unicorn/explicit-length-check.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-01-25 11:23:48 -08:00
Anders Kaseorg
f022b338e6 xo: Fix unicorn/no-lonely-if.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-01-25 11:23:48 -08:00
Anders Kaseorg
855d99dfa0 xo: Fix unicorn/prevent-abbreviations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-01-25 11:23:48 -08:00
Anders Kaseorg
cc2424e0bf xo: Fix @typescript-eslint/no-confusing-void-expression.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-01-25 11:23:48 -08:00
Anders Kaseorg
fa6d72268f Rename master branch to main.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2021-01-22 12:13:29 -08:00
Anders Kaseorg
762dd92ec3 Upgrade dependencies, including Electron 11.1.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-17 17:20:32 -08:00
Anders Kaseorg
2e90e24552 Remove fs-extra dependency.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-17 17:20:32 -08:00
Anders Kaseorg
d7adce0ebf appveyor: Use current dependencies.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-17 17:20:32 -08:00
Anders Kaseorg
a1bb6da4fb Switch Travis CI to GitHub Actions.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-16 18:45:20 -08:00
Anders Kaseorg
873fecf548 Revert "performance: Disable hardware acceleration to decrease the load on GPU."
This reverts commit fb74251a2c.

The actual problem in #213 was the infinite bouncing question mark
hotspot animation (https://github.com/zulip/zulip/issues/13760).

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-16 14:14:12 -08:00
Anders Kaseorg
682511bb68 injected: Remove unused page_params type declaration.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-08 16:40:49 -08:00
Anders Kaseorg
02fbe1a6a1 Revert to upstream undo and redo roles.
This reverts part of commit 01f6e77237
(#866).  The Electron bug was fixed upstream in Electron 9.0.0-beta.23.

Closes #899.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-02 19:54:22 -08:00
Anders Kaseorg
0cb82a6f5e release: New release v5.5.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 19:53:20 -08:00
Anders Kaseorg
79808e8ee9 preload: Provide hooks for server to robustly replace logout et al.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 18:11:45 -08:00
Anders Kaseorg
2c38df10c8 electron-bridge: Expose boolean return from emit.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 17:59:59 -08:00
Anders Kaseorg
1ca15d44a0 electron-bridge: Move mutable state out of electron_bridge.
Only the initial value of a mutable field is exposed via
exposeInMainWorld, which is why we have a bunch of setter and getter
functions.  It’s better to avoid the possibility for this confusion.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 17:54:21 -08:00
Anders Kaseorg
82450a91a9 preload: Remove retry button redirection hack.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 17:18:09 -08:00
Anders Kaseorg
62edfa6f8b Remove macOS notification inline replies feature.
node-mac-notifier no longer builds on macOS with Electron 11 (error:
no template named 'remove_cv_t' in namespace 'std').  It was
previously implicated in crashes on macOS (#1016).  And we no longer
have any macOS developers that seem to be maintaining this
feature (e.g. #1022 is stalled).

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 17:06:11 -08:00
Anders Kaseorg
fe86315ece main: Be explicit about disabling contextIsolation for the main window.
We have been relying on the default here, but the default will be
changing in Electron 12.  (We already enable contextIsolation in the
webviews that load remote content.)

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 16:34:41 -08:00
Anders Kaseorg
df3f719e89 Upgrade dependencies, including Electron 11.0.3.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 16:02:41 -08:00
Anders Kaseorg
0632d8199f injected: Condition narrow-by-topic handler on page_params.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 16:02:32 -08:00
Anders Kaseorg
047bf0ca45 webview: Pass webPreferences values as explicit booleans
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-11-30 12:39:35 -08:00
Anders Kaseorg
356c879668 Remove Devtron.
Devtron is unmaintained and no longer works.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-11-18 15:25:33 -08:00
Anders Kaseorg
ba432d32b3 Remove preventdrag script.
This was not a security feature; security is enforced using context
isolation and the same-origin policy.

Furthermore, navigation on drag-and-drop was already disabled by
default in Electron 3.0.

https://www.electronjs.org/blog/electron-3-0#breaking-api-changes

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-11-17 16:10:47 -08:00
Anders Kaseorg
c8ada3f47d Rewrite reinstall script to avoid auxilliary script files.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-11-17 15:41:46 -08:00
aryanshridhar
cd77fc6448 new-server-form: Strip whitespace from added organization URL.
Fixes #1037.
2020-11-15 19:56:53 -08:00
Anders Kaseorg
a2f926c611 README: Migrate Travis badge to travis-ci.com.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-27 15:49:22 -07:00
Anders Kaseorg
6c5eb85a16 README: Use Markdown for screenshot display.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-27 15:45:55 -07:00
Anders Kaseorg
cadb1c6eaa Upgrade dependencies, including Electron 10.1.5.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
73710319e6 xo: Fix unicorn/prevent-abbreviations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
da91dc5595 xo: Fix @typescript-eslint/consistent-indexed-object-style.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
31d5e5a092 xo: Fix unicorn/prefer-ternary, I guess.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
13ee1d0990 logger-util: Add missing space.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
d5a9063378 typescript: Fix implicit any in catch clauses.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
918064f35d checkDomain: Remove special handling for “certificate” error strings.
The fragile check has been broken by changing strings, and the default
invalidZulipServerError message is fine.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:01 -07:00
Anders Kaseorg
193b8326bc injected: Check if narrow is defined.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:32:05 -07:00
Anders Kaseorg
9abb7f376e injected: Remove unused default_language from zulipWindow type.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:32:05 -07:00
Anders Kaseorg
ac338fa438 Upgrade dependencies, including Electron 10.1.3.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-01 15:54:59 -07:00
Anders Kaseorg
f5b78ee845 Set enableRemoteModule.
We would like to disable the remote module for improved sandboxing
(#915), but until then this is required for Electron 10, which
disables the remote module by default.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-01 15:54:59 -07:00
Aryan Shridhar
126bb26a6e Tray Icon : Changed Unread tray icon in Windows.
Replaced unread messages icon in the lower tray bar in windows with a new icon.
Fixed #506.
2020-09-17 16:07:23 +05:30
Anders Kaseorg
23e86abb5b Remove support for custom certificate exceptions.
Version 5.4.0 and later uses electron.net for all network
requests (#993), so custom certificates can now be configured in the
same system certificate store that Chrome uses.

https://zulip.com/help/custom-certificates#desktop

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-11 22:25:28 -07:00
Anders Kaseorg
3a3714787f main: Fix mainWindowState scope.
Fixes a regression with the factory reset function introduced by
commit cf9d0c8aa2.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-10 21:11:41 -07:00
Anders Kaseorg
bc57aabc97 Disable unused Chromium plugins; delete old commented PDF code.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-10 18:28:35 -07:00
Anders Kaseorg
08df02a1ea changelog: Update for 5.4.3 release.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-09 23:18:59 -07:00
Akash Nimare
35ad6fbad0 release: New release v5.4.3. 2020-09-09 12:24:43 +05:30
Anders Kaseorg
97f8fe71af Escape all strings inserted into CSS selectors.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-04 22:52:42 -07:00
Anders Kaseorg
a9d59b3dcd CVE-2020-24582: Escape all strings interpolated into HTML.
Also fix various variable names to consistently indicate which strings
contain HTML.

Some of these changes close cross-site scripting vulnerabilities, and
others are for consistency.  It’s important to be meticulously
consistent about escaping so that changes that would introduce
vulnerabilities stand out as obviously wrong.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-04 22:52:38 -07:00
Anders Kaseorg
b7240e1c40 Upgrade dependencies, including Electron 9.3.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-03 17:00:51 -07:00
Anders Kaseorg
62aa849657 Upgrade dependencies, including Electron 9.2.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-08-25 15:42:06 -07:00
Anders Kaseorg
c302ebe282 general-section: Convert .filter(…)[0] to .find(…).
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-08-25 15:40:40 -07:00
Anders Kaseorg
6404bed519 tests: Fix E2E tests for Spectron 11.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-08-25 15:37:06 -07:00
Manav Mehta
8d4d168988 Update changelog.md for release 5.4.2 (#1017) 2020-08-12 23:18:38 +05:30
Akash Nimare
d4d3805be8 release: New release v5.4.2. 2020-08-11 16:09:23 +05:30
Akash Nimare
e853af40c4 electron: Update electron to v9.2.0. 2020-08-11 15:37:13 +05:30
Manav Mehta
941200cf3b changelog: Update changelog for release 5.4.1-beta. 2020-07-29 16:22:11 +05:30
Akash Nimare
cf1f659ebf release: New beta release v5.4.1-beta. 2020-07-29 13:40:59 +05:30
Akash Nimare
eb381a87bc electron-builder: Update builder to latest version. 2020-07-29 01:54:48 +05:30
Manav Mehta
68bc0ae4a0 readme: Add new screenshot URLs.
Update the screenshots to accomodate new Zulip logo and both the day and night modes
2020-07-29 01:31:57 +05:30
Manav Mehta
178bc7f401 macos: Update dock icon.
The icon in macOS was stretched to the boundaries making it larger than the other icons.
A padding of 30px on all sides makes it coherent with the others.

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

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

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-23 23:09:12 -07:00
Anders Kaseorg
8d66f05924 xo: Sort imports with import/order.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-23 23:06:41 -07:00
Manav Mehta
e7330dbff8 Update changelog for v5.4.0 and the license year to 2020 (#1000)
Co-authored-by: Akash Nimare <akashnimare@users.noreply.github.com>
2020-07-21 22:05:04 +05:30
252 changed files with 29572 additions and 24648 deletions

View File

@@ -1,16 +1,11 @@
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,*.yml}]
[{*.cjs,*.css,*.html,*.js,*.json,*.ts}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
github: zulip
patreon: zulip
open_collective: zulip

View File

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

View File

@@ -2,5 +2,3 @@
name: Custom issue template
about: Describe this issue template's purpose here.
---

View File

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

View File

@@ -1,16 +1,49 @@
---
<!--
Remove the fields that are not appropriate
Please include:
<!-- Describe your pull request here.-->
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?**
**Platforms this PR was tested on:**
**Screenshots?**
- [ ] Windows
- [ ] macOS
- [ ] Linux (specify distro)
**You have tested this PR on:**
- [ ] Windows
- [ ] Linux/Ubuntu
- [ ] macOS
<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>

18
.github/workflows/node.js.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Node.js CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- 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

View File

@@ -1,12 +1,7 @@
{
"tagname-lowercase": true,
"attr-lowercase": true,
"attr-value-double-quotes": true,
"attr-value-not-empty": false,
"attr-no-duplication": true,
"doctype-first": true,
"tag-pair": true,
"empty-tag-not-self-closed": true,
"spec-char-escape": true,
"id-unique": true,
"src-not-empty": true,
@@ -17,9 +12,8 @@
"style-disabled": false,
"inline-style-disabled": false,
"inline-script-disabled": false,
"space-tab-mixed-disabled": "space4",
"id-class-ad-disabled": false,
"href-abs-or-rel": false,
"attr-unsafe-chars": true,
"head-script-disabled": true
}
}

View File

@@ -1 +0,0 @@
6.9.4

1
.npmrc Normal file
View File

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

3
.prettierignore Normal file
View File

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

View File

@@ -1,67 +1,12 @@
{
"rules": {
# Stylistic rules for CSS.
"function-comma-space-after": "always",
"function-comma-space-before": "never",
"function-max-empty-lines": 0,
"function-whitespace-after": "always",
"value-keyword-case": "lower",
"value-list-comma-newline-after": "always-multi-line",
"value-list-comma-space-after": "always-single-line",
"value-list-comma-space-before": "never",
"value-list-max-empty-lines": 0,
"unit-case": "lower",
"property-case": "lower",
"color-hex-case": "lower",
"declaration-bang-space-before": "always",
"declaration-colon-newline-after": "always-multi-line",
"declaration-colon-space-after": "always-single-line",
"declaration-colon-space-before": "never",
"declaration-block-semicolon-newline-after": "always",
"declaration-block-semicolon-space-before": "never",
"declaration-block-trailing-semicolon": "always",
"block-closing-brace-empty-line-before": "never",
"block-closing-brace-newline-after": "always",
"block-closing-brace-newline-before": "always",
"block-opening-brace-newline-after": "always",
"block-opening-brace-space-before": "always",
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-space-after": "never",
"selector-attribute-operator-space-before": "never",
"selector-combinator-space-after": "always",
"selector-combinator-space-before": "always",
"selector-descendant-combinator-no-non-space": true,
"selector-pseudo-class-parentheses-space-inside": "never",
"selector-pseudo-element-case": "lower",
"selector-pseudo-element-colon-notation": "double",
"selector-type-case": "lower",
"selector-list-comma-newline-after": "always",
"selector-list-comma-space-before": "never",
"media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never",
"media-feature-name-case": "lower",
"media-feature-parentheses-space-inside": "never",
"media-feature-range-operator-space-after": "always",
"media-feature-range-operator-space-before": "always",
"media-query-list-comma-newline-after": "always",
"media-query-list-comma-space-before": "never",
"at-rule-name-case": "lower",
"at-rule-name-space-after": "always",
"at-rule-semicolon-newline-after": "always",
"at-rule-semicolon-space-before": "never",
"comment-whitespace-inside": "always",
"indentation": 4,
# Limit language features
"color-no-hex": true,
"color-named": "never",
}
}
"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,23 +0,0 @@
os:
- osx
- linux
addons:
apt:
packages:
- build-essential
- libxext-dev
- libxtst-dev
- libxkbfile-dev
language: node_js
node_js:
- '10'
- '12'
cache:
directories:
- node_modules
script:
- npm run test

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,11 +10,12 @@ 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).
- If you have any questions regarding zulip-desktop, open an [issue](https://github.com/zulip/zulip-desktop/issues/new/) or ask it on [chat.zulip.org](https://chat.zulip.org/#narrow/stream/16-desktop).
## Issue
Ensure the bug was not already reported by searching on GitHub under [issues](https://github.com/zulip/zulip-desktop/issues). If you're unable to find an open issue addressing the bug, open a [new issue](https://github.com/zulip/zulip-desktop/issues/new).
The [zulipbot](https://github.com/zulip/zulipbot) helps to claim an issue by commenting the following in the comment section: "**@zulipbot** claim". **@zulipbot** will assign you to the issue and label the issue as **in progress**. For more details, check out [**@zulipbot**](https://github.com/zulip/zulipbot).
@@ -22,26 +23,29 @@ The [zulipbot](https://github.com/zulip/zulipbot) helps to claim an issue by com
Please pay attention to the following points while opening an issue.
### Does it happen on web browsers? (especially Chrome)
Zulip's desktop client is based on Electron, which integrates the Chrome engine within a standalone application.
If the problem you encounter can be reproduced on web browsers, it may be an issue with [Zulip web app](https://github.com/zulip/zulip).
### Write detailed information
Detailed information is very helpful to understand an issue.
For example:
* How to reproduce the issue, step-by-step.
* The expected behavior (or what is wrong).
* Screenshots for GUI issues.
* The application version.
* The operating system.
* The Zulip-Desktop version.
- How to reproduce the issue, step-by-step.
- The expected behavior (or what is wrong).
- Screenshots for GUI issues.
- The application version.
- The operating system.
- The Zulip-Desktop version.
## Pull Requests
Pull Requests are always welcome.
1. When you edit the code, please run `npm run test` to check the formatting of your code before you `git commit`.
2. Ensure the PR description clearly describes the problem and solution. It should include:
* The operating system on which you tested.
* The Zulip-Desktop version on which you tested.
* The relevant issue number, if applicable.
- The operating system on which you tested.
- The Zulip-Desktop version on which you tested.
- The relevant issue number, if applicable.

View File

@@ -1,29 +1,33 @@
# Zulip Desktop Client
[![Build Status](https://travis-ci.org/zulip/zulip-desktop.svg?branch=master)](https://travis-ci.org/zulip/zulip-desktop)
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/zulip/zulip-desktop?branch=master&svg=true)](https://ci.appveyor.com/project/zulip/zulip-desktop/branch/master)
[![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)
Desktop client for Zulip. Available for Mac, Linux, and Windows.
<img src="http://i.imgur.com/ChzTq4F.png"/>
![screenshot](https://i.imgur.com/s1o6TRA.png)
![screenshot](https://i.imgur.com/vekKnW4.png)
# Download
Please see the [installation guide](https://zulip.com/help/desktop-app-install-guide).
# Features
* Sign in to multiple organizations
* Desktop notifications with inline reply
* Tray/dock integration
* Multi-language spell checker
* Automatic updates
- Sign in to multiple organizations
- Desktop notifications with inline reply
- Tray/dock integration
- Multi-language spell checker
- Automatic updates
# Reporting issues
This desktop client shares most of its code with the Zulip 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
project](https://github.com/zulip/zulip/issues/new). Other
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).
@@ -33,4 +37,5 @@ First, join us on the [Zulip community server](https://zulip.readthedocs.io/en/l
Also see our [contribution guidelines](./CONTRIBUTING.md) and our [development guide](./development.md).
# License
Released under the [Apache-2.0](./LICENSE) license.

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(),
};

106
app/common/config-util.ts Normal file
View File

@@ -0,0 +1,106 @@
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 ConfigSchemata, configSchemata} from "./config-schemata.ts";
import * as EnterpriseUtil from "./enterprise-util.ts";
import Logger from "./logger-util.ts";
export type Config = {
[Key in keyof ConfigSchemata]: z.output<ConfigSchemata[Key]>;
};
const logger = new Logger({
file: "config-util.log",
});
let database: JsonDB;
reloadDatabase();
export function getConfigItem<Key extends keyof Config>(
key: Key,
defaultValue: Config[Key],
): z.output<ConfigSchemata[Key]> {
try {
database.reload();
} catch (error: unknown) {
logger.error("Error while reloading settings.json: ");
logger.error(error);
}
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;
}
}
// This function returns whether a key exists in the configuration file (settings.json)
export function isConfigItemExists(key: string): boolean {
try {
database.reload();
} catch (error: unknown) {
logger.error("Error while reloading settings.json: ");
logger.error(error);
}
return database.exists(`/${key}`);
}
export function setConfigItem<Key extends keyof Config>(
key: Key,
value: Config[Key],
override?: boolean,
): void {
if (EnterpriseUtil.configItemExists(key) && !override) {
// If item is in global config and we're not trying to override
return;
}
configSchemata[key].parse(value);
database.push(`/${key}`, value, true);
database.save();
}
export function removeConfigItem(key: string): void {
database.delete(`/${key}`);
database.save();
}
function reloadDatabase(): void {
const settingsJsonPath = path.join(
app.getPath("userData"),
"/config/settings.json",
);
try {
const file = fs.readFileSync(settingsJsonPath, "utf8");
JSON.parse(file);
} catch (error: unknown) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
"Error saving settings",
"We encountered an error while saving the settings.",
);
logger.error("Error while JSON parsing settings.json: ");
logger.error(error);
Sentry.captureException(error);
}
}
database = new JsonDB(settingsJsonPath, true, true);
}

View File

@@ -0,0 +1,61 @@
import fs from "node:fs";
import {app} from "zulip:remote";
let setupCompleted = false;
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(zulipDirectory)) {
fs.mkdirSync(zulipDirectory);
}
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(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,
fileName: "domain.json",
},
{
path: settingsJson,
fileName: "settings.json",
},
{
path: updatesJson,
fileName: "updates.json",
},
];
for (const data of configData) {
if (fs.existsSync(data.path)) {
fs.copyFileSync(data.path, configDirectory + data.fileName);
fs.unlinkSync(data.path);
}
}
// `window-state.json` is only deleted not moved, as the electron-window-state
// package will recreate the file in the config folder.
if (fs.existsSync(windowStateJson)) {
fs.unlinkSync(windowStateJson);
}
}
setupCompleted = true;
}
};

60
app/common/dnd-util.ts Normal file
View File

@@ -0,0 +1,60 @@
import process from "node:process";
import type {z} from "zod";
import type {dndSettingsSchemata} from "./config-schemata.ts";
import * as ConfigUtil from "./config-util.ts";
export type DndSettings = {
[Key in keyof typeof dndSettingsSchemata]: z.output<
(typeof dndSettingsSchemata)[Key]
>;
};
type SettingName = keyof DndSettings;
type Toggle = {
dnd: boolean;
newSettings: Partial<DndSettings>;
};
export function toggle(): Toggle {
const dnd = !ConfigUtil.getConfigItem("dnd", false);
const dndSettingList: SettingName[] = ["showNotification", "silent"];
if (process.platform === "win32") {
dndSettingList.push("flashTaskbarOnMessage");
}
let newSettings: Partial<DndSettings>;
if (dnd) {
const oldSettings: Partial<DndSettings> = {};
newSettings = {};
// Iterate through the dndSettingList.
for (const settingName of dndSettingList) {
// Store the current value of setting.
oldSettings[settingName] = ConfigUtil.getConfigItem(
settingName,
settingName !== "silent",
);
// New value of setting.
newSettings[settingName] = settingName === "silent";
}
// Store old value in oldSettings.
ConfigUtil.setConfigItem("dndPreviousSettings", oldSettings);
} else {
newSettings = ConfigUtil.getConfigItem("dndPreviousSettings", {
showNotification: true,
silent: false,
...(process.platform === "win32" && {flashTaskbarOnMessage: true}),
});
}
for (const settingName of dndSettingList) {
ConfigUtil.setConfigItem(settingName, newSettings[settingName]!);
}
ConfigUtil.setConfigItem("dnd", dnd);
return {dnd, newSettings};
}

View File

@@ -0,0 +1,98 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import {z} from "zod";
import {dialog} from "zulip:remote";
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",
});
let enterpriseSettings: Partial<EnterpriseConfig>;
let configFile: boolean;
reloadDatabase();
function reloadDatabase(): void {
let enterpriseFile = "/etc/zulip-desktop-config/global_config.json";
if (process.platform === "win32") {
enterpriseFile = String.raw`C:\Program Files\Zulip-Desktop-Config\global_config.json`;
}
enterpriseFile = path.resolve(enterpriseFile);
if (fs.existsSync(enterpriseFile)) {
configFile = true;
try {
const file = fs.readFileSync(enterpriseFile, "utf8");
const data: unknown = JSON.parse(file);
enterpriseSettings = z
.object(enterpriseConfigSchemata)
.partial()
.parse(data);
} catch (error: unknown) {
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);
}
} else {
configFile = false;
}
}
export function hasConfigFile(): boolean {
return configFile;
}
export function getConfigItem<Key extends keyof EnterpriseConfig>(
key: Key,
defaultValue: EnterpriseConfig[Key],
): EnterpriseConfig[Key] {
reloadDatabase();
if (!configFile) {
return defaultValue;
}
const value = enterpriseSettings[key];
return value === undefined ? defaultValue : (value as EnterpriseConfig[Key]);
}
export function configItemExists(key: keyof EnterpriseConfig): boolean {
reloadDatabase();
if (!configFile) {
return false;
}
return enterpriseSettings[key] !== undefined;
}
export function isPresetOrg(url: string): boolean {
if (!configFile || !configItemExists("presetOrganizations")) {
return false;
}
const presetOrgs = enterpriseSettings.presetOrganizations;
if (!Array.isArray(presetOrgs)) {
throw new TypeError("Expected array for presetOrgs");
}
for (const org of presetOrgs) {
if (url.includes(org)) {
return true;
}
}
return false;
}

26
app/common/html.ts Normal file
View File

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

50
app/common/link-util.ts Normal file
View File

@@ -0,0 +1,50 @@
import {shell} from "electron/common";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
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)) {
await shell.openExternal(url.href);
} else {
// For security, indirect links to non-whitelisted protocols
// through a real web browser via a local HTML file.
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "zulip-redirect-"));
const file = path.join(directory, "redirect.html");
fs.writeFileSync(
file,
html`
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Refresh" content="0; url=${url.href}" />
<title>${t.__("Redirecting")}</title>
<style>
html {
font-family: menu, "Helvetica Neue", sans-serif;
}
</style>
</head>
<body>
<p>
${new Html({
html: t.__("Opening {{{link}}}…", {
link: html`<a href="${url.href}">${url.href}</a>`.html,
}),
})}
</p>
</body>
</html>
`.html,
);
await shell.openPath(file);
setTimeout(() => {
fs.unlinkSync(file);
fs.rmdirSync(directory);
}, 15_000);
}
}

90
app/common/logger-util.ts Normal file
View File

@@ -0,0 +1,90 @@
import {Console} from "node:console"; // eslint-disable-line n/prefer-global/console
import fs from "node:fs";
import os from "node:os";
import process from "node:process";
import {app} from "zulip:remote";
import {initSetUp} from "./default-util.ts";
type LoggerOptions = {
file?: string;
};
initSetUp();
const logDirectory = `${app.getPath("userData")}/Logs`;
type Level = "log" | "debug" | "info" | "warn" | "error";
export default class Logger {
nodeConsole: Console;
constructor(options: LoggerOptions = {}) {
let {file = "console.log"} = options;
file = `${logDirectory}/${file}`;
// Trim log according to type of process
if (process.type === "renderer") {
requestIdleCallback(async () => this.trimLog(file));
} else {
process.nextTick(async () => this.trimLog(file));
}
const fileStream = fs.createWriteStream(file, {flags: "a"});
const nodeConsole = new Console(fileStream);
this.nodeConsole = nodeConsole;
}
_log(type: Level, ...arguments_: unknown[]): void {
arguments_.unshift(this.getTimestamp() + " |\t");
arguments_.unshift(type.toUpperCase() + " |");
this.nodeConsole[type](...arguments_);
console[type](...arguments_);
}
log(...arguments_: unknown[]): void {
this._log("log", ...arguments_);
}
debug(...arguments_: unknown[]): void {
this._log("debug", ...arguments_);
}
info(...arguments_: unknown[]): void {
this._log("info", ...arguments_);
}
warn(...arguments_: unknown[]): void {
this._log("warn", ...arguments_);
}
error(...arguments_: unknown[]): void {
this._log("error", ...arguments_);
}
getTimestamp(): string {
const date = new Date();
const timestamp =
`${date.getMonth()}/${date.getDate()} ` +
`${date.getMinutes()}:${date.getSeconds()}`;
return timestamp;
}
async trimLog(file: string): Promise<void> {
const data = await fs.promises.readFile(file, "utf8");
const maxLogFileLines = 500;
const logs = data.split(os.EOL);
const logLength = logs.length - 1;
// Keep bottom maxLogFileLines of each log instance
if (logLength > maxLogFileLines) {
const trimmedLogs = logs.slice(logLength - maxLogFileLines);
const toWrite = trimmedLogs.join(os.EOL);
await fs.promises.writeFile(file, toWrite);
}
}
}

38
app/common/messages.ts Normal file
View File

@@ -0,0 +1,38 @@
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
• You can connect to that URL in a web browser.
• If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.
• It's a Zulip server. (The oldest supported version is 1.6).
• The server has a valid certificate.
• The SSL is correctly configured for the certificate. Check out the SSL troubleshooting guide -
https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`;
}
export function enterpriseOrgError(domains: string[]): DialogBoxError {
let domainList = "";
for (const domain of domains) {
domainList += `${domain}\n`;
}
return {
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: 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

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

84
app/common/typed-ipc.ts Normal file
View File

@@ -0,0 +1,84 @@
import type {DndSettings} from "./dnd-util.ts";
import type {MenuProperties, ServerConfig} from "./types.ts";
export type MainMessage = {
"clear-app-settings": () => void;
"configure-spell-checker": () => void;
"fetch-user-agent": () => string;
"focus-app": () => void;
"focus-this-webview": () => void;
"new-clipboard-key": () => {key: Uint8Array; sig: Uint8Array};
"permission-callback": (permissionCallbackId: number, grant: boolean) => void;
"quit-app": () => void;
"realm-icon-changed": (serverURL: string, iconURL: string) => void;
"realm-name-changed": (serverURL: string, realmName: string) => void;
"reload-full-app": () => void;
"save-last-tab": (index: number) => void;
"switch-server-tab": (index: number) => void;
"toggle-app": () => void;
"toggle-badge-option": (newValue: boolean) => void;
"toggle-menubar": (showMenubar: boolean) => void;
toggleAutoLauncher: (AutoLaunchValue: boolean) => void;
"unread-count": (unreadCount: number) => void;
"update-badge": (messageCount: number) => void;
"update-menu": (properties: MenuProperties) => void;
"update-taskbar-icon": (data: string, text: string) => void;
};
export type MainCall = {
"get-server-settings": (domain: string) => ServerConfig;
"is-online": (url: string) => boolean;
"poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined;
"save-server-icon": (iconURL: string) => string | null;
};
export type RendererMessage = {
back: () => void;
"copy-zulip-url": () => void;
destroytray: () => void;
"enter-fullscreen": () => void;
focus: () => void;
"focus-webview-with-id": (webviewId: number) => void;
forward: () => void;
"hard-reload": () => void;
"leave-fullscreen": () => void;
"log-out": () => void;
logout: () => void;
"new-server": () => void;
"open-about": () => void;
"open-help": () => void;
"open-network-settings": () => void;
"open-org-tab": () => void;
"open-settings": () => void;
"permission-request": (
options: {webContentsId: number | null; origin: string; permission: string},
rendererCallbackId: number,
) => void;
"play-ding-sound": () => void;
"reload-current-viewer": () => void;
"reload-proxy": (showAlert: boolean) => void;
"reload-viewer": () => void;
"render-taskbar-icon": (messageCount: number) => void;
"set-active": () => void;
"set-idle": () => void;
"show-keyboard-shortcuts": () => void;
"show-notification-settings": () => void;
"switch-server-tab": (index: number) => void;
"tab-devtools": () => void;
"toggle-autohide-menubar": (
autoHideMenubar: boolean,
updateMenu: boolean,
) => void;
"toggle-dnd": (state: boolean, newSettings: Partial<DndSettings>) => void;
"toggle-sidebar": (show: boolean) => void;
"toggle-silent": (state: boolean) => void;
"toggle-tray": (state: boolean) => void;
toggletray: () => void;
tray: (argument: number) => void;
"update-realm-icon": (serverURL: string, iconURL: string) => void;
"update-realm-name": (serverURL: string, realmName: string) => void;
"webview-reload": () => void;
zoomActualSize: () => void;
zoomIn: () => void;
zoomOut: () => void;
};

30
app/common/types.ts Normal file
View File

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

View File

@@ -1,109 +1,135 @@
import {app, dialog, session} from 'electron';
import {UpdateDownloadedEvent, UpdateInfo, autoUpdater} from 'electron-updater';
import util from 'util';
import {linuxUpdateNotification} from './linuxupdater'; // Required only in case of linux
import {shell} from "electron/common";
import {app, dialog, session} from "electron/main";
import process from "node:process";
import log from 'electron-log';
import isDev from 'electron-is-dev';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as LinkUtil from '../renderer/js/utils/link-util';
import log from "electron-log/main";
import {
type UpdateDownloadedEvent,
type UpdateInfo,
autoUpdater,
} from "electron-updater";
const sleep = util.promisify(setTimeout);
import * as ConfigUtil from "../common/config-util.ts";
import * as t from "../common/translation-util.ts";
import {linuxUpdateNotification} from "./linuxupdater.ts"; // Required only in case of linux
let quitting = false;
export function shouldQuitForUpdate(): boolean {
return quitting;
}
export async function appUpdater(updateFromMenu = false): Promise<void> {
// Don't initiate auto-updates in development
if (isDev) {
return;
}
// Don't initiate auto-updates in development
if (!app.isPackaged) {
return;
}
if (process.platform === 'linux' && !process.env.APPIMAGE) {
const ses = session.fromPartition('persist:webviewsession');
await linuxUpdateNotification(ses);
return;
}
if (process.platform === "linux" && !process.env.APPIMAGE) {
const ses = session.fromPartition("persist:webviewsession");
await linuxUpdateNotification(ses);
return;
}
let updateAvailable = false;
let updateAvailable = false;
// Create Logs directory
const LogsDir = `${app.getPath('userData')}/Logs`;
// Log what's happening
const updateLogger = log.create({logId: "updates"});
updateLogger.transports.file.fileName = "updates.log";
updateLogger.transports.file.level = "info";
autoUpdater.logger = updateLogger;
// Log whats happening
log.transports.file.file = `${LogsDir}/updates.log`;
log.transports.file.level = 'info';
autoUpdater.logger = log;
// Handle auto updates for beta/pre releases
const isBetaUpdate = ConfigUtil.getConfigItem("betaUpdate", false);
// Handle auto updates for beta/pre releases
const isBetaUpdate = ConfigUtil.getConfigItem('betaUpdate');
autoUpdater.allowPrerelease = isBetaUpdate;
autoUpdater.allowPrerelease = isBetaUpdate || false;
const eventsListenerRemove = [
"update-available",
"update-not-available",
] as const;
autoUpdater.on("update-available", async (info: UpdateInfo) => {
if (updateFromMenu) {
updateAvailable = true;
const eventsListenerRemove = ['update-available', 'update-not-available'];
autoUpdater.on('update-available', async (info: UpdateInfo) => {
if (updateFromMenu) {
updateAvailable = true;
// This is to prevent removal of 'update-downloaded' and 'error' event listener.
for (const event of eventsListenerRemove) {
autoUpdater.removeAllListeners(event);
}
// This is to prevent removal of 'update-downloaded' and 'error' event listener.
eventsListenerRemove.forEach(event => {
autoUpdater.removeAllListeners(event);
});
await dialog.showMessageBox({
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.",
),
});
}
});
await dialog.showMessageBox({
message: `A new version ${info.version}, of Zulip Desktop is available`,
detail: 'The update will be downloaded in the background. You will be notified when it is ready to be installed.'
});
}
});
autoUpdater.on("update-not-available", async () => {
if (updateFromMenu) {
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
autoUpdater.on('update-not-available', async () => {
if (updateFromMenu) {
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
await dialog.showMessageBox({
message: t.__("No updates available."),
detail: t.__(
"You are running the latest version of Zulip Desktop.\nVersion: {{{version}}}",
{version: app.getVersion()},
),
});
}
});
await dialog.showMessageBox({
message: 'No updates available',
detail: `You are running the latest version of Zulip Desktop.\nVersion: ${app.getVersion()}`
});
}
});
autoUpdater.on("error", async (error: Error) => {
if (updateFromMenu) {
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
autoUpdater.on('error', async (error: Error) => {
if (updateFromMenu) {
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
const messageText = updateAvailable
? 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: [t.__("Manual Download"), t.__("Cancel")],
message: messageText,
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(link);
}
}
});
const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates');
const {response} = await dialog.showMessageBox({
type: 'error',
buttons: ['Manual Download', 'Cancel'],
message: messageText,
detail: `Error: ${error.message}\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulip.com/apps/.\n
Current Version: ${app.getVersion()}`
});
if (response === 0) {
await LinkUtil.openBrowser(new URL('https://zulip.com/apps/'));
}
}
});
// Ask the user if update is available
autoUpdater.on('update-downloaded', async (event: UpdateDownloadedEvent) => {
// Ask user to update the app
const {response} = await dialog.showMessageBox({
type: 'question',
buttons: ['Install and Relaunch', 'Install Later'],
defaultId: 0,
message: `A new update ${event.version} has been downloaded`,
detail: 'It will be installed the next time you restart the application'
});
if (response === 0) {
await sleep(1000);
autoUpdater.quitAndInstall();
// Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit();
}
});
// Init for updates
await autoUpdater.checkForUpdates();
// Ask the user if update is available
autoUpdater.on("update-downloaded", async (event: UpdateDownloadedEvent) => {
// Ask user to update the app
const {response} = await dialog.showMessageBox({
type: "question",
buttons: [t.__("Install and Relaunch"), t.__("Install Later")],
defaultId: 0,
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) {
quitting = true;
autoUpdater.quitAndInstall();
}
});
// Init for updates
await autoUpdater.checkForUpdates();
}

View File

@@ -0,0 +1,60 @@
import {nativeImage} from "electron/common";
import {type BrowserWindow, app} from "electron/main";
import process from "node:process";
import * as ConfigUtil from "../common/config-util.ts";
import {send} from "./typed-ipc-main.ts";
function showBadgeCount(messageCount: number, mainWindow: BrowserWindow): void {
if (process.platform === "win32") {
updateOverlayIcon(messageCount, mainWindow);
} else {
app.badgeCount = messageCount;
}
}
function hideBadgeCount(mainWindow: BrowserWindow): void {
if (process.platform === "win32") {
mainWindow.setOverlayIcon(null, "");
} else {
app.badgeCount = 0;
}
}
export function updateBadge(
badgeCount: number,
mainWindow: BrowserWindow,
): void {
if (ConfigUtil.getConfigItem("badgeOption", true)) {
showBadgeCount(badgeCount, mainWindow);
} else {
hideBadgeCount(mainWindow);
}
}
function updateOverlayIcon(
messageCount: number,
mainWindow: BrowserWindow,
): void {
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(
ConfigUtil.getConfigItem("flashTaskbarOnMessage", true),
);
}
if (messageCount === 0) {
mainWindow.setOverlayIcon(null, "");
} else {
send(mainWindow.webContents, "render-taskbar-icon", messageCount);
}
}
export function updateTaskbarIcon(
data: string,
text: string,
mainWindow: BrowserWindow,
): void {
const img = nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
}

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,471 +1,486 @@
import {sentryInit} from '../renderer/js/utils/sentry-util';
import {appUpdater} from './autoupdater';
import {setAutoLaunch} from './startup';
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 windowStateKeeper from 'electron-window-state';
import path from 'path';
import fs from 'fs';
import electron, {app, dialog, ipcMain, session} from 'electron';
import * as remoteMain from "@electron/remote/main";
import windowStateKeeper from "electron-window-state";
import * as AppMenu from './menu';
import * as BadgeSettings from '../renderer/js/pages/preference/badge-settings';
import * as CertificateUtil from '../renderer/js/utils/certificate-util';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as ProxyUtil from '../renderer/js/utils/proxy-util';
import {_getServerSettings, _saveServerIcon, _isOnline} from './request';
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, 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 singleInstanceLock = app.requestSingleInstanceLock();
if (singleInstanceLock) {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
const permissionCallbacks = new Map<number, (grant: boolean) => void>();
let nextPermissionCallbackId = 0;
mainWindow.show();
}
});
} else {
app.quit();
}
const appIcon = path.join(publicPath, "resources/Icon");
const rendererCallbacks = new Map();
let nextRendererCallbackId = 0;
ipcMain.on('renderer-callback', (event: Event, rendererCallbackId: number, ...args: any[]) => {
rendererCallbacks.get(rendererCallbackId)(...args);
rendererCallbacks.delete(rendererCallbackId);
});
function makeRendererCallback(callback: (...args: any[]) => void): number {
rendererCallbacks.set(nextRendererCallbackId, callback);
return nextRendererCallbackId++;
}
const APP_ICON = path.join(__dirname, '../resources', 'Icon');
const iconPath = (): string => APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
const iconPath = (): string =>
appIcon + (process.platform === "win32" ? ".ico" : ".png");
// Toggle the app window
const toggleApp = (): void => {
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
mainWindow.show();
} else {
mainWindow.hide();
}
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
mainWindow.show();
} else {
mainWindow.hide();
}
};
function createMainWindow(): Electron.BrowserWindow {
// Load the previous state with fallback to defaults
const mainWindowState: windowStateKeeper.State = windowStateKeeper({
defaultWidth: 1100,
defaultHeight: 720,
path: `${app.getPath('userData')}/config`
});
function createMainWindow(): BrowserWindow {
// Load the previous state with fallback to defaults
mainWindowState = windowStateKeeper({
defaultWidth: 1100,
defaultHeight: 720,
path: `${app.getPath("userData")}/config`,
});
const win = new electron.BrowserWindow({
// This settings needs to be saved in config
title: 'Zulip',
icon: iconPath(),
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 500,
minHeight: 400,
webPreferences: {
plugins: true,
nodeIntegration: true,
partition: 'persist:webviewsession',
webviewTag: true
},
show: false
});
const win = new BrowserWindow({
// This settings needs to be saved in config
title: "Zulip",
icon: iconPath(),
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 500,
minHeight: 400,
webPreferences: {
preload: path.join(bundlePath, "renderer.cjs"),
sandbox: false,
webviewTag: true,
},
show: false,
});
remoteMain.enable(win.webContents);
win.on('focus', () => {
win.webContents.send('focus');
});
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 => {
if (ConfigUtil.getConfigItem('quitOnClose')) {
app.quit();
}
// Keep the app running in background on close event
win.on("close", (event) => {
if (ConfigUtil.getConfigItem("quitOnClose", false)) {
app.quit();
}
if (!isQuitting) {
event.preventDefault();
if (!isQuitting && !shouldQuitForUpdate()) {
event.preventDefault();
if (process.platform === 'darwin') {
app.hide();
} else {
win.hide();
}
}
});
if (process.platform === "darwin") {
if (win.isFullScreen()) {
win.setFullScreen(false);
win.once("leave-full-screen", () => {
app.hide();
});
} else {
app.hide();
}
} else {
win.hide();
}
}
});
win.setTitle('Zulip');
win.setTitle("Zulip");
win.on('enter-full-screen', () => {
win.webContents.send('enter-fullscreen');
});
win.on("enter-full-screen", () => {
send(win.webContents, "enter-fullscreen");
});
win.on('leave-full-screen', () => {
win.webContents.send('leave-fullscreen');
});
win.on("leave-full-screen", () => {
send(win.webContents, "leave-fullscreen");
});
// To destroy tray icon when navigate to a new URL
win.webContents.on('will-navigate', event => {
if (event) {
win.webContents.send('destroytray');
}
});
// To destroy tray icon when navigate to a new URL
win.webContents.on("will-navigate", (event) => {
if (event) {
send(win.webContents, "destroytray");
}
});
// Let us register listeners on the window, so we can update the state
// automatically (the listeners will be removed when the window is closed)
// and restore the maximized or full screen state
mainWindowState.manage(win);
// Let us register listeners on the window, so we can update the state
// automatically (the listeners will be removed when the window is closed)
// and restore the maximized or full screen state
mainWindowState.manage(win);
return win;
return win;
}
// Decrease load on GPU (experimental)
app.disableHardwareAcceleration();
(async () => {
if (!app.requestSingleInstanceLock()) {
app.quit();
return;
}
// Temporary fix for Electron render colors differently
// More info here - https://github.com/electron/electron/issues/10732
app.commandLine.appendSwitch('force-color-profile', 'srgb');
await app.whenReady();
// 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();
}
});
if (process.env.GDK_BACKEND !== GDK_BACKEND) {
console.warn(
"Reverting GDK_BACKEND to work around https://github.com/electron/electron/issues/28436",
);
if (GDK_BACKEND === undefined) {
delete process.env.GDK_BACKEND;
} else {
process.env.GDK_BACKEND = GDK_BACKEND;
}
}
app.on('ready', () => {
const ses = session.fromPartition('persist:webviewsession');
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);
// Used for notifications on Windows
app.setAppUserModelId("org.zulip.zulip-electron");
ipcMain.on('set-spellcheck-langs', () => {
ses.setSpellCheckerLanguages(ConfigUtil.getConfigItem('spellcheckerLanguages'));
});
AppMenu.setMenu({
tabs: []
});
mainWindow = createMainWindow();
remoteMain.initialize();
// Auto-hide menu bar on Windows + Linux
if (process.platform !== 'darwin') {
const shouldHideMenu = ConfigUtil.getConfigItem('autoHideMenubar') || false;
mainWindow.autoHideMenuBar = shouldHideMenu;
mainWindow.setMenuBarVisibility(!shouldHideMenu);
}
app.on("second-instance", () => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
// Initialize sentry for main process
const errorReporting = ConfigUtil.getConfigItem('errorReporting');
if (errorReporting) {
sentryInit();
}
mainWindow.show();
}
});
const isSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
ipcMain.on(
"permission-callback",
(event, permissionCallbackId: number, grant: boolean) => {
permissionCallbacks.get(permissionCallbackId)?.(grant);
permissionCallbacks.delete(permissionCallbackId);
},
);
if (isSystemProxy) {
(async () => ProxyUtil.resolveSystemProxy(mainWindow))();
}
// This event is only available on macOS. Triggers when you click on the dock icon.
app.on("activate", () => {
mainWindow.show();
});
const page = mainWindow.webContents;
app.on("web-contents-created", (_event, contents: WebContents) => {
contents.setWindowOpenHandler((details) => {
handleExternalLink(contents, details, page);
return {action: "deny"};
});
});
page.on('dom-ready', () => {
if (ConfigUtil.getConfigItem('startMinimized')) {
mainWindow.hide();
} else {
mainWindow.show();
}
});
const ses = session.fromPartition("persist:webviewsession");
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);
ipcMain.on('fetch-user-agent', event => {
event.returnValue = session.fromPartition('persist:webviewsession').getUserAgent();
});
function configureSpellChecker() {
const enable = ConfigUtil.getConfigItem("enableSpellchecker", true);
if (enable && process.platform !== "darwin") {
ses.setSpellCheckerLanguages(
ConfigUtil.getConfigItem("spellcheckerLanguages", null) ?? [],
);
}
ipcMain.handle('get-server-settings', async (event, domain: string) => _getServerSettings(domain, ses));
ses.setSpellCheckerEnabled(enable);
}
ipcMain.handle('save-server-icon', async (event, url: string) => _saveServerIcon(url, ses));
configureSpellChecker();
ipcMain.on("configure-spell-checker", configureSpellChecker);
ipcMain.handle('is-online', async (event, url: string) => _isOnline(url, ses));
const clipboardSigKey = crypto.randomBytes(32);
page.once('did-frame-finish-load', async () => {
// Initiate auto-updates on MacOS and Windows
if (ConfigUtil.getConfigItem('autoUpdate')) {
await appUpdater();
}
});
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()};
});
app.on('certificate-error', (
event: Event,
webContents: Electron.WebContents,
urlString: string,
error: string,
certificate: Electron.Certificate,
callback: (isTrusted: boolean) => void
) /* eslint-disable-line max-params */ => {
// TODO: The entire concept of selectively ignoring certificate errors
// is ill-conceived, and this handler needs to be deleted.
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;
const url = new URL(urlString);
if (url.protocol === 'wss:') {
url.protocol = 'https:';
}
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;
}
});
const filename = CertificateUtil.getCertificate(encodeURIComponent(url.origin));
if (filename !== undefined) {
try {
const savedCertificate = fs.readFileSync(
path.join(`${app.getPath('userData')}/certificates`, filename),
'utf8'
);
if (certificate.data.replace(/[\r\n]/g, '') ===
savedCertificate.replace(/[\r\n]/g, '')) {
event.preventDefault();
callback(true);
return;
}
} catch (error) {
console.error(`Error reading certificate file ${filename}:`, error);
}
}
AppMenu.setMenu({
tabs: [],
});
mainWindow = createMainWindow();
dialog.showErrorBox(
'Certificate error',
`The server presented an invalid certificate for ${url.origin}:
// Auto-hide menu bar on Windows + Linux
if (process.platform !== "darwin") {
const shouldHideMenu = ConfigUtil.getConfigItem("autoHideMenubar", false);
mainWindow.autoHideMenuBar = shouldHideMenu;
mainWindow.setMenuBarVisibility(!shouldHideMenu);
}
${error}`
);
});
const page = mainWindow.webContents;
page.session.setPermissionRequestHandler((webContents, permission, callback, details) => {
const {origin} = new URL(details.requestingUrl);
page.send('permission-request', {
webContentsId: webContents.id === mainWindow.webContents.id ?
null :
webContents.id,
origin,
permission
}, makeRendererCallback(callback));
});
page.on("dom-ready", () => {
if (ConfigUtil.getConfigItem("startMinimized", false)) {
mainWindow.hide();
} else {
mainWindow.show();
}
});
// Temporarily remove this event
// electron.powerMonitor.on('resume', () => {
// mainWindow.reload();
// page.send('destroytray');
// });
ipcMain.on("fetch-user-agent", (event) => {
event.returnValue = session
.fromPartition("persist:webviewsession")
.getUserAgent();
});
ipcMain.on('focus-app', () => {
mainWindow.show();
});
ipcMain.handle("get-server-settings", async (event, domain: string) =>
_getServerSettings(domain, ses),
);
ipcMain.on('quit-app', () => {
app.quit();
});
ipcMain.handle("save-server-icon", async (event, url: string) =>
_saveServerIcon(url, ses),
);
// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
// ipcMain.on('pdf-view', (event, url) => {
// // Paddings for pdfWindow so that it fits into the main browserWindow
// const paddingWidth = 55;
// const paddingHeight = 22;
ipcMain.handle("is-online", async (event, url: string) =>
_isOnline(url, ses),
);
// // Get the config of main browserWindow
// const mainWindowState = global.mainWindowState;
page.once("did-frame-finish-load", async () => {
// Initiate auto-updates on MacOS and Windows
if (ConfigUtil.getConfigItem("autoUpdate", true)) {
await appUpdater();
}
});
// // Window to view the pdf file
// const pdfWindow = new electron.BrowserWindow({
// x: mainWindowState.x + paddingWidth,
// y: mainWindowState.y + paddingHeight,
// width: mainWindowState.width - paddingWidth,
// height: mainWindowState.height - paddingHeight,
// webPreferences: {
// plugins: true,
// partition: 'persist:webviewsession'
// }
// });
// pdfWindow.loadURL(url);
app.on(
"certificate-error",
(
event,
webContents,
urlString,
error,
certificate,
callback,
isMainFrame,
// eslint-disable-next-line max-params
) => {
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},
),
);
}
},
);
// // We don't want to have the menu bar in pdf window
// pdfWindow.setMenu(null);
// });
ses.setPermissionRequestHandler(
(webContents, permission, callback, details) => {
const {origin} = new URL(details.requestingUrl);
const permissionCallbackId = nextPermissionCallbackId++;
permissionCallbacks.set(permissionCallbackId, callback);
send(
page,
"permission-request",
{
webContentsId:
webContents.id === mainWindow.webContents.id
? null
: webContents.id,
origin,
permission,
},
permissionCallbackId,
);
},
);
// Reload full app not just webview, useful in debugging
ipcMain.on('reload-full-app', () => {
mainWindow.reload();
page.send('destroytray');
});
// Temporarily remove this event
// powerMonitor.on('resume', () => {
// mainWindow.reload();
// send(page, 'destroytray');
// });
ipcMain.on('clear-app-settings', () => {
mainWindowState.unmanage();
app.relaunch();
app.exit();
});
ipcMain.on("focus-app", () => {
mainWindow.show();
});
ipcMain.on('toggle-app', () => {
toggleApp();
});
ipcMain.on("quit-app", () => {
app.quit();
});
ipcMain.on('toggle-badge-option', () => {
BadgeSettings.updateBadge(badgeCount, mainWindow);
});
// Reload full app not just webview, useful in debugging
ipcMain.on("reload-full-app", () => {
mainWindow.reload();
send(page, "destroytray");
});
ipcMain.on('toggle-menubar', (_event: Electron.IpcMainEvent, showMenubar: boolean) => {
mainWindow.autoHideMenuBar = showMenubar;
mainWindow.setMenuBarVisibility(!showMenubar);
page.send('toggle-autohide-menubar', showMenubar, true);
});
ipcMain.on("clear-app-settings", () => {
mainWindowState.unmanage();
app.relaunch();
app.exit();
});
ipcMain.on('update-badge', (_event: Electron.IpcMainEvent, messageCount: number) => {
badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow);
page.send('tray', messageCount);
});
ipcMain.on("toggle-app", () => {
toggleApp();
});
ipcMain.on('update-taskbar-icon', (_event: Electron.IpcMainEvent, data: string, text: string) => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
});
ipcMain.on("toggle-badge-option", () => {
BadgeSettings.updateBadge(badgeCount, mainWindow);
});
ipcMain.on('forward-message', (_event: Electron.IpcMainEvent, listener: string, ...parameters: unknown[]) => {
page.send(listener, ...parameters);
});
ipcMain.on("toggle-menubar", (_event, showMenubar: boolean) => {
mainWindow.autoHideMenuBar = showMenubar;
mainWindow.setMenuBarVisibility(!showMenubar);
send(page, "toggle-autohide-menubar", showMenubar, true);
});
ipcMain.on('update-menu', (_event: Electron.IpcMainEvent, props: AppMenu.MenuProps) => {
AppMenu.setMenu(props);
const activeTab = props.tabs[props.activeTabIndex];
if (activeTab) {
mainWindow.setTitle(`Zulip - ${activeTab.webview.props.name}`);
}
});
ipcMain.on("update-badge", (_event, messageCount: number) => {
badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow);
send(page, "tray", messageCount);
});
ipcMain.on('toggleAutoLauncher', async (_event: Electron.IpcMainEvent, AutoLaunchValue: boolean) => {
await setAutoLaunch(AutoLaunchValue);
});
ipcMain.on("update-taskbar-icon", (_event, data: string, text: string) => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
});
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();
};
ipcMain.on(
"forward-message",
<Channel extends keyof RendererMessage>(
_event: IpcMainEvent,
listener: Channel,
...parameters: Parameters<RendererMessage[Channel]>
) => {
send(page, listener, ...parameters);
},
);
const formatFile = (filePath: string): string => {
const fileExtension = path.extname(filePath);
const baseName = path.basename(filePath, fileExtension);
return `${baseName}-${getTimeStamp()}${fileExtension}`;
};
ipcMain.on(
"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);
}
},
);
const filePath = path.join(downloadPath, item.getFilename());
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}`);
}
});
// 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);
}
ipcMain.on("toggleAutoLauncher", async (_event, AutoLaunchValue: boolean) => {
await setAutoLaunch(AutoLaunchValue);
});
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;
}
ipcMain.on(
"realm-name-changed",
(_event, serverURL: string, realmName: string) => {
send(page, "update-realm-name", serverURL, realmName);
},
);
case 'progressing': {
if (item.isPaused()) {
item.cancel();
}
ipcMain.on(
"realm-icon-changed",
(_event, serverURL: string, iconURL: string) => {
send(page, "update-realm-icon", serverURL, iconURL);
},
);
// This event can also be used to show progress in percentage in future.
break;
}
ipcMain.on("save-last-tab", (_event, index: number) => {
ConfigUtil.setConfigItem("lastActiveTab", index);
});
default: {
console.info('Unknown updated state of download item');
}
}
};
ipcMain.on("focus-this-webview", (event) => {
send(page, "focus-webview-with-id", event.sender.id);
mainWindow.show();
});
item.on('updated', updatedListener);
item.once('done', (_event: Event, state) => {
if (state === 'completed') {
page.send('downloadFileCompleted', item.getSavePath(), path.basename(item.getSavePath()));
} else {
console.log('Download failed state:', state);
page.send('downloadFileFailed', state);
}
// Update user idle status for each realm after every 15s
const idleCheckInterval = 15 * 1000; // 15 seconds
setInterval(() => {
// Set user idle if no activity in 1 second (idleThresholdSeconds)
const idleThresholdSeconds = 1; // 1 second
const idleState = powerMonitor.getSystemIdleState(idleThresholdSeconds);
if (idleState === "active") {
send(page, "set-active");
} else {
send(page, "set-idle");
}
}, idleCheckInterval);
})();
// To stop item for listening to updated events of this file
item.removeListener('updated', updatedListener);
});
});
});
ipcMain.on('realm-name-changed', (_event: Electron.IpcMainEvent, serverURL: string, realmName: string) => {
page.send('update-realm-name', serverURL, realmName);
});
ipcMain.on('realm-icon-changed', (_event: Electron.IpcMainEvent, serverURL: string, iconURL: string) => {
page.send('update-realm-icon', serverURL, iconURL);
});
// Using event.sender.send instead of page.send here to
// make sure the value of errorReporting is sent only once on load.
ipcMain.on('error-reporting', (event: Electron.IpcMainEvent) => {
event.sender.send('error-reporting-val', errorReporting);
});
ipcMain.on('save-last-tab', (_event: Electron.IpcMainEvent, index: number) => {
ConfigUtil.setConfigItem('lastActiveTab', index);
});
// 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);
if (idleState === 'active') {
page.send('set-active');
} else {
page.send('set-idle');
}
}, idleCheckInterval);
});
app.on('before-quit', () => {
isQuitting = true;
app.on("before-quit", () => {
isQuitting = true;
});
// Send crash reports
process.on('uncaughtException', err => {
console.error(err);
console.error(err.stack);
process.on("uncaughtException", (error) => {
console.error(error);
console.error(error.stack);
});

View File

@@ -0,0 +1,70 @@
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.ts";
import * as t from "../common/translation-util.ts";
const logger = new Logger({
file: "linux-update-util.log",
});
let database: JsonDB;
reloadDatabase();
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;
}
return value;
}
export function setUpdateItem(key: string, value: true | null): void {
database.push(`/${key}`, value, true);
reloadDatabase();
}
export function removeUpdateItem(key: string): void {
database.delete(`/${key}`);
reloadDatabase();
}
function reloadDatabase(): void {
const linuxUpdateJsonPath = path.join(
app.getPath("userData"),
"/config/updates.json",
);
try {
const file = fs.readFileSync(linuxUpdateJsonPath, "utf8");
JSON.parse(file);
} catch (error: unknown) {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
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);
}
}
database = new JsonDB(linuxUpdateJsonPath, true, true);
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,111 +1,121 @@
import {ClientRequest, IncomingMessage, app, net} from 'electron';
import fs from 'fs';
import path from 'path';
import stream from 'stream';
import util from 'util';
import * as Messages from '../resources/messages';
import Logger from '../renderer/js/utils/logger-util';
import {ServerConf} from '../renderer/js/utils/domain-util';
import escape from 'escape-html';
import getStream from 'get-stream';
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";
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();
});
}
import * as Sentry from "@sentry/electron/main";
import {z} from "zod";
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',
timestamp: true
file: "domain-util.log",
});
const generateFilePath = (url: string): string => {
const dir = `${app.getPath('userData')}/server-icons`;
const extension = path.extname(url).split('?')[0];
const directory = `${app.getPath("userData")}/server-icons`;
const extension = path.extname(url).split("?")[0];
let hash = 5381;
let {length} = url;
let hash = 5381;
let {length} = url;
while (length) {
hash = (hash * 33) ^ url.charCodeAt(--length);
}
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);
}
// Create 'server-icons' directory if not existed
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) {
throw new Error(Messages.invalidZulipServerError(domain));
}
export const _getServerSettings = async (
domain: string,
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
// Following check handles both the cases
icon: realm_icon.startsWith('/') ? realm_uri + realm_icon : realm_icon,
url: realm_uri,
alias: escape(realm_name)
};
return {
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases
icon: realm_icon.startsWith("/") ? realm_uri + realm_icon : realm_icon,
url: realm_uri,
alias: realm_name,
zulipVersion: zulip_version,
zulipFeatureLevel: zulip_feature_level,
};
};
export const _saveServerIcon = async (url: string, session: Electron.session): Promise<string> => {
try {
const response = await fetchResponse(net.request({url, session}));
if (response.statusCode !== 200) {
logger.log('Could not get server icon.');
return defaultIconUrl;
}
export const _saveServerIcon = async (
url: string,
session: Session,
): Promise<string | null> => {
try {
const response = await session.fetch(url);
if (!response.ok) {
logger.log("Could not get server icon.");
return null;
}
const filePath = generateFilePath(url);
await pipeline(response, fs.createWriteStream(filePath));
return filePath;
} catch (error) {
logger.log('Could not get server icon.');
logger.log(error);
logger.reportSentry(error);
return defaultIconUrl;
}
const filePath = generateFilePath(url);
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);
Sentry.captureException(error);
return null;
}
};
/* Request: reconnect-util */
export const _isOnline = async (url: string, session: Electron.session): Promise<boolean> => {
try {
const response = await fetchResponse(net.request({
url: `${url}/static/favicon.ico`,
session
}));
const isValidResponse = response.statusCode >= 200 && response.statusCode < 400;
return isValidResponse;
} catch (error) {
logger.log(error);
return false;
}
export const _isOnline = async (
url: string,
session: Session,
): Promise<boolean> => {
try {
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,32 +1,36 @@
import {app} from 'electron';
import {app} from "electron/main";
import process from "node:process";
import AutoLaunch from 'auto-launch';
import isDev from 'electron-is-dev';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import AutoLaunch from "auto-launch";
export const setAutoLaunch = async (AutoLaunchValue: boolean): Promise<void> => {
// Don't run this in development
if (isDev) {
return;
}
import * as ConfigUtil from "../common/config-util.ts";
const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue);
export const setAutoLaunch = async (
AutoLaunchValue: boolean,
): Promise<void> => {
// Don't run this in development
if (!app.isPackaged) {
return;
}
// `setLoginItemSettings` doesn't support linux
if (process.platform === 'linux') {
const ZulipAutoLauncher = new AutoLaunch({
name: 'Zulip',
isHidden: false
});
if (autoLaunchOption) {
await ZulipAutoLauncher.enable();
} else {
await ZulipAutoLauncher.disable();
}
} else {
app.setLoginItemSettings({
openAtLogin: autoLaunchOption,
openAsHidden: false
});
}
const autoLaunchOption = ConfigUtil.getConfigItem(
"startAtLogin",
AutoLaunchValue,
);
// `setLoginItemSettings` doesn't support linux
if (process.platform === "linux") {
const zulipAutoLauncher = new AutoLaunch({
name: "Zulip",
isHidden: false,
});
await (autoLaunchOption
? zulipAutoLauncher.enable()
: zulipAutoLauncher.disable());
} else {
app.setLoginItemSettings({
openAtLogin: autoLaunchOption,
openAsHidden: false,
});
}
};

View File

@@ -0,0 +1,88 @@
import {
type IpcMainEvent,
type IpcMainInvokeEvent,
type WebContents,
ipcMain as untypedIpcMain, // eslint-disable-line no-restricted-imports
} from "electron/main";
import type {
MainCall,
MainMessage,
RendererMessage,
} from "../common/typed-ipc.js";
type MainListener<Channel extends keyof MainMessage> =
MainMessage[Channel] extends (...arguments_: infer Arguments) => infer Return
? (
event: IpcMainEvent & {returnValue: Return},
...arguments_: Arguments
) => void
: never;
type MainHandler<Channel extends keyof MainCall> = MainCall[Channel] extends (
...arguments_: infer Arguments
) => infer Return
? (
event: IpcMainInvokeEvent,
...arguments_: Arguments
) => Return | Promise<Return>
: never;
export const ipcMain: {
on(
channel: "forward-message",
listener: <Channel extends keyof RendererMessage>(
event: IpcMainEvent,
channel: 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>(
channel: Channel,
listener: MainListener<Channel>,
): void;
once<Channel extends keyof MainMessage>(
channel: Channel,
listener: MainListener<Channel>,
): void;
removeListener<Channel extends keyof MainMessage>(
channel: Channel,
listener: MainListener<Channel>,
): void;
removeAllListeners(channel?: keyof MainMessage): void;
handle<Channel extends keyof MainCall>(
channel: Channel,
handler: MainHandler<Channel>,
): void;
handleOnce<Channel extends keyof MainCall>(
channel: Channel,
handler: MainHandler<Channel>,
): void;
removeHandler(channel: keyof MainCall): void;
} = untypedIpcMain;
export function send<Channel extends keyof RendererMessage>(
contents: WebContents,
channel: Channel,
...arguments_: Parameters<RendererMessage[Channel]>
): void {
contents.send(channel, ...arguments_);
}
export function sendToFrame<Channel extends keyof RendererMessage>(
contents: WebContents,
frameId: number | [number, number],
channel: Channel,
...arguments_: Parameters<RendererMessage[Channel]>
): void {
contents.sendToFrame(frameId, channel, ...arguments_);
}

View File

@@ -1,33 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<!doctype html>
<meta charset="UTF-8" />
<link rel="stylesheet" href="css/about.css" />
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/about.css">
<title>Zulip - About</title>
</head>
<body>
<div class="about">
<img class="logo" src="../resources/zulip.png" />
<p class="detail" id="version">v?.?.?</p>
<div class="maintenance-info">
<p class="detail maintainer">
Maintained by
<a 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/master/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.innerHTML = 'v' + app.getVersion();
</script>
<script>require('./js/shared/preventdrag.js')</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,66 +1,68 @@
body {
background: rgba(250, 250, 250, 1.000);
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: subpixel-antialiased;
:host {
contain: strict;
display: flow-root;
background: rgb(250 250 250 / 100%);
font-family: menu, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: subpixel-antialiased;
}
.logo {
display: block;
margin: -40px auto;
display: block;
margin: -40px auto;
}
#version {
color: rgba(68, 67, 67, 1.000);
font-size: 1.3em;
padding-top: 40px;
color: rgb(68 67 67 / 100%);
font-size: 1.3em;
padding-top: 40px;
}
.about {
margin: 25vh auto;
height: 25vh;
text-align: center;
display: block !important;
margin: 25vh auto;
height: 25vh;
text-align: center;
}
.about p {
font-size: 20px;
color: rgba(0, 0, 0, 0.62);
font-size: 20px;
color: rgb(0 0 0 / 62%);
}
.about img {
width: 150px;
width: 150px;
}
.detail {
text-align: center;
text-align: center;
}
.detail.maintainer {
font-size: 1.2em;
font-weight: 500;
font-size: 1.2em;
font-weight: 500;
}
.detail.license {
font-size: 0.8em;
font-size: 0.8em;
}
.maintenance-info {
cursor: pointer;
position: absolute;
width: 100%;
left: 0px;
color: rgba(68, 68, 68, 1.000);
position: absolute;
width: 100%;
left: 0;
color: rgb(68 68 68 / 100%);
}
.maintenance-info p {
margin: 0;
font-size: 1em;
width: 100%;
margin: 0;
font-size: 1em;
width: 100%;
}
p.detail a {
color: rgba(53, 95, 76, 1.000);
color: rgb(53 95 76 / 100%);
}
p.detail a:hover {
text-decoration: underline;
text-decoration: underline;
}

View File

@@ -1,19 +1,19 @@
:host {
--button-color: rgb(69, 166, 149);
--button-color: rgb(69 166 149);
}
button {
background-color: var(--button-color);
border-color: var(--button-color);
}
button:hover,
button:focus {
border-color: var(--button-color);
color: var(--button-color);
}
button:active {
background-color: rgb(241, 241, 241);
color: var(--button-color);
border-color: var(--button-color);
color: var(--button-color);
}
button {
background-color: var(--button-color);
border-color: var(--button-color);
button:active {
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

@@ -4,280 +4,283 @@
html,
body {
height: 100%;
margin: 0;
cursor: default;
user-select: none;
height: 100%;
margin: 0;
cursor: default;
user-select: none;
}
#content {
display: flex;
height: 100%;
display: flex;
height: 100%;
}
.toggle-sidebar {
background: rgba(34, 44, 49, 1.000);
width: 54px;
padding: 27px 0 20px 0;
justify-content: space-between;
display: flex;
flex-direction: column;
-webkit-app-region: drag;
overflow: hidden;
transition: all 0.5s ease;
z-index: 2;
background: rgb(34 44 49 / 100%);
width: 54px;
padding: 27px 0 20px;
justify-content: space-between;
display: flex;
flex-direction: column;
-webkit-app-region: drag;
overflow: hidden;
transition: all 0.5s ease;
z-index: 2;
}
.toggle-sidebar div {
transition: all 0.5s ease-out;
transition: all 0.5s ease-out;
}
.sidebar-hide {
width: 0;
transition: all 0.8s ease;
width: 0;
transition: all 0.8s ease;
}
.sidebar-hide div {
transform: translateX(-100%);
transition: all 0.6s ease-out;
transform: translateX(-100%);
transition: all 0.6s ease-out;
}
#view-controls-container {
height: calc(100% - 208px);
overflow-y: hidden;
}
#view-controls-container:hover {
overflow-y: overlay;
height: calc(100% - 208px);
scrollbar-gutter: stable both-edges;
overflow-y: hidden;
}
#view-controls-container::-webkit-scrollbar {
width: 4px;
width: 4px;
}
#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.000);
outline: 1px solid rgba(169, 169, 169, 1.000);
background-color: rgb(169 169 169 / 100%);
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'), local('MaterialIcons-Regular'), url(../fonts/MaterialIcons-Regular.ttf) format('truetype');
#view-controls-container:hover {
overflow-y: scroll;
}
/*******************
* Left Sidebar *
*******************/
#tabs-container {
display: flex;
align-items: center;
flex-direction: column;
display: flex;
align-items: center;
flex-direction: column;
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
/* Preferred icon size */
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
/* Preferred icon size */
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
overflow-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizelegibility;
}
#actions-container {
display: flex;
flex-direction: column;
position: fixed;
bottom: 0;
display: flex;
flex-direction: column;
position: fixed;
bottom: 0;
}
.action-button {
display: flex;
flex-direction: column;
padding: 12px;
display: flex;
flex-direction: column;
padding: 12px;
}
.action-button:hover {
cursor: pointer;
cursor: pointer;
}
.action-button i {
color: rgba(108, 133, 146, 1.000);
font-size: 28px;
color: hsl(200.53deg 14.96% 49.8%);
font-size: 28px;
}
.action-button:hover i {
color: rgba(152, 169, 179, 1.000);
color: hsl(202.22deg 15.08% 64.9%);
}
.action-button.disable {
opacity: 0.6;
.action-button > .dnd-on {
color: hsl(200.53deg 14.96% 85%);
}
.action-button.disable:hover {
cursor: not-allowed;
}
.action-button.disable:hover i {
color: rgba(108, 133, 146, 1.000);
.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.000);
opacity: 0.9;
padding-right: 14px;
/* background-color: rgba(255, 255, 255, 0.25); */
background-color: rgb(239 239 239 / 100%);
opacity: 0.9;
padding-right: 14px;
}
.action-button.active i {
color: rgba(28, 38, 43, 1.000);
color: rgb(28 38 43 / 100%);
}
.tab:first-child {
margin-top: 9px;
.action-button.disable {
opacity: 0.6;
}
.action-button.disable:hover {
cursor: not-allowed;
}
.action-button.disable:hover i {
color: rgb(108 133 146 / 100%);
}
.tab {
position: relative;
margin: 2px 0;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
position: relative;
margin: 2px 0;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.tab:first-child {
margin-top: 9px;
}
.tab .server-icons {
width: 35px;
vertical-align: top;
border-radius: 4px;
width: 35px;
vertical-align: top;
border-radius: 4px;
}
.tab .server-tab {
width: 100%;
height: 35px;
position: relative;
margin-top: 5px;
z-index: 11;
line-height: 31px;
color: rgba(238, 238, 238, 1.000);
text-align: center;
overflow: hidden;
opacity: 0.6;
padding: 6px 0;
width: 100%;
height: 35px;
position: relative;
margin-top: 5px;
z-index: 11;
line-height: 31px;
color: rgb(238 238 238 / 100%);
text-align: center;
overflow: hidden;
opacity: 0.6;
padding: 6px 0;
}
.server-tab .alt-icon {
font-family: Verdana;
font-weight: 600;
font-size: 22px;
border: 2px solid rgba(34, 44, 49, 1.000);
margin-left: 17%;
width: 35px;
border-radius: 4px;
font-family: Verdana, sans-serif;
font-weight: 600;
font-size: 22px;
border: 2px solid rgb(34 44 49 / 100%);
margin-left: 17%;
width: 35px;
border-radius: 4px;
}
.tab .server-tab:hover {
opacity: 0.8;
}
.tab.functional-tab {
height: 46px;
padding: 0;
}
.tab.functional-tab.active .server-tab {
padding: 2px 0;
height: 40px;
background-color: rgba(255, 255, 255, 0.25);
}
.tab.functional-tab .server-tab i {
font-size: 28px;
line-height: 36px;
opacity: 0.8;
}
.tab.active .server-tab {
opacity: 1;
background-color: rgba(100, 132, 120, 1.000);
opacity: 1;
background-color: rgb(100 132 120 / 100%);
}
.tab.functional-tab {
height: 46px;
padding: 0;
}
.tab.functional-tab.active .server-tab {
padding: 2px 0;
height: 40px;
background-color: rgb(255 255 255 / 25%);
}
.tab.functional-tab .server-tab i {
font-size: 28px;
line-height: 36px;
}
.tab .server-tab-badge.active {
border-radius: 9px;
min-width: 11px;
padding: 0 3px;
height: 17px;
background-color: rgba(244, 67, 54, 1.000);
font-size: 10px;
font-family: sans-serif;
position: absolute;
right: 5px;
z-index: 15;
top: 6px;
float: right;
color: rgba(255, 255, 255, 1.000);
text-align: center;
line-height: 17px;
display: block;
right: 0;
border-radius: 9px;
min-width: 11px;
padding: 0 3px;
height: 17px;
background-color: rgb(244 67 54 / 100%);
font-size: 10px;
font-family: sans-serif;
position: absolute;
z-index: 15;
top: 6px;
float: right;
color: rgb(255 255 255 / 100%);
text-align: center;
line-height: 17px;
display: block;
right: 0;
}
.tab .server-tab-badge {
display: none;
display: none;
}
.tab .server-tab-badge.close-button {
width: 16px;
padding: 0;
width: 16px;
padding: 0;
}
.tab .server-tab-badge.close-button i {
font-size: 13px;
line-height: 17px;
font-size: 13px;
line-height: 17px;
}
#add-tab {
display: flex;
align-items: center;
flex-direction: column;
display: flex;
align-items: center;
flex-direction: column;
}
.tab .server-tab-shortcut {
color: rgba(100, 132, 120, 1.000);
font-size: 12px;
text-align: center;
font-family: sans-serif;
margin-bottom: 5px;
color: rgb(100 132 120 / 100%);
font-size: 12px;
text-align: center;
font-family: sans-serif;
margin-bottom: 5px;
}
.refresh {
animation: rotate-loader 1s linear infinite;
animation: rotate-loader 1s linear infinite;
}
@keyframes rotate-loader {
from {
transform: rotate(0);
}
to {
transform: rotate(-360deg);
}
from {
transform: rotate(0);
}
to {
transform: rotate(-360deg);
}
}
/*******************
@@ -285,57 +288,81 @@ body {
*******************/
#webviews-container {
display: flex;
height: 100%;
width: 100%;
display: flex;
height: 100%;
width: 100%;
}
/* Pseudo element for loading indicator */
#webviews-container::before {
content: "";
position: absolute;
z-index: 1;
background: rgba(255, 255, 255, 1.000) url(../img/ic_loading.gif) no-repeat;
background-size: 60px 60px;
background-position: center;
width: 100%;
height: 100%;
content: "";
position: absolute;
z-index: 1;
/* 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%;
height: 100%;
}
/* When the active webview is loaded */
#webviews-container.loaded::before {
opacity: 0;
z-index: -1;
visibility: hidden;
z-index: -1;
visibility: hidden;
}
webview {
/* transition: opacity 0.3s ease-in; */
flex-grow: 1;
position: absolute;
width: 100%;
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
.webview-pane,
.functional-view {
position: absolute;
width: 100%;
height: 100%;
flex-grow: 1;
visibility: hidden;
}
webview.onload {
transition: opacity 1s cubic-bezier(0.95, 0.05, 0.795, 0.035);
.webview-pane {
display: flex;
flex-direction: column;
}
webview.active {
opacity: 1;
z-index: 1;
visibility: visible;
.webview-pane > webview {
flex: 1;
}
webview.disabled {
opacity: 0;
.webview-pane.active,
.functional-view.active {
z-index: 1;
visibility: visible;
}
webview.focus {
outline: 0px solid transparent;
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 */
@@ -345,18 +372,18 @@ webview.focus {
#back-tooltip,
#reload-tooltip,
#setting-tooltip {
font-family: sans-serif;
background: rgba(34, 44, 49, 1.000);
margin-left: 48px;
padding: 6px 8px;
position: absolute;
margin-top: 0px;
z-index: 1000;
color: rgba(255, 255, 255, 1.000);
border-radius: 4px;
text-align: center;
width: 55px;
font-size: 14px;
font-family: sans-serif;
background: rgb(34 44 49 / 100%);
margin-left: 48px;
padding: 6px 8px;
position: absolute;
margin-top: 0;
z-index: 1000;
color: rgb(255 255 255 / 100%);
border-radius: 4px;
text-align: center;
width: 55px;
font-size: 14px;
}
#loading-tooltip::after,
@@ -364,127 +391,104 @@ webview.focus {
#back-tooltip::after,
#reload-tooltip::after,
#setting-tooltip::after {
content: " ";
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid rgba(34, 44, 49, 1.000);
position: absolute;
top: 7px;
right: 68px;
content: " ";
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid rgb(34 44 49 / 100%);
position: absolute;
top: 7px;
right: 68px;
}
#add-server-tooltip,
.server-tooltip {
font-family: 'arial';
background: rgba(34, 44, 49, 1.000);
left: 56px;
padding: 10px 20px;
position: fixed;
margin-top: 11px;
z-index: 5000 !important;
color: rgba(255, 255, 255, 1.000);
border-radius: 4px;
text-align: center;
width: max-content;
font-size: 14px;
font-family: arial, sans-serif;
background: rgb(34 44 49 / 100%);
left: 56px;
padding: 10px 20px;
position: fixed;
margin-top: 11px;
z-index: 5000 !important;
color: rgb(255 255 255 / 100%);
border-radius: 4px;
text-align: center;
width: max-content;
font-size: 14px;
}
#add-server-tooltip::after,
.server-tooltip::after {
content: " ";
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid rgba(34, 44, 49, 1.000);
position: absolute;
top: 10px;
left: -5px;
content: " ";
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid rgb(34 44 49 / 100%);
position: absolute;
top: 10px;
left: -5px;
}
#collapse-button {
bottom: 30px;
left: 20px;
position: absolute;
width: 24px;
height: 24px;
background: rgba(34, 44, 49, 1.000);
border-radius: 20px;
cursor: pointer;
box-shadow: rgba(153, 153, 153, 1.000) 1px 1px;
bottom: 30px;
left: 20px;
position: absolute;
width: 24px;
height: 24px;
background: rgb(34 44 49 / 100%);
border-radius: 20px;
cursor: pointer;
box-shadow: rgb(153 153 153 / 100%) 1px 1px;
}
#collapse-button i {
color: rgba(239, 239, 239, 1.000);
color: rgb(239 239 239 / 100%);
}
#main-container {
display: flex;
height: 100%;
width: 100%;
position: relative;
flex-grow: 1;
flex-basis: 0px;
display: flex;
height: 100%;
width: 100%;
position: relative;
flex-grow: 1;
flex-basis: 0;
}
.hidden {
display: none !important;
display: none !important;
}
/* Full screen Popup container */
.popup .popuptext {
visibility: hidden;
background-color: rgba(85, 85, 85, 1.000);
color: rgba(255, 255, 255, 1.000);
text-align: center;
border-radius: 6px;
padding: 9px 0;
position: absolute;
z-index: 1000;
font-family: arial;
width: 240px;
top: 15px;
height: 20px;
left: 43%;
visibility: hidden;
background-color: rgb(85 85 85 / 100%);
color: rgb(255 255 255 / 100%);
text-align: center;
border-radius: 6px;
padding: 9px 0;
position: absolute;
z-index: 1000;
font-family: arial, sans-serif;
width: 240px;
top: 15px;
height: 20px;
left: 43%;
}
.popup .show {
visibility: visible;
animation: cssAnimation 0s ease-in 5s forwards;
animation-fill-mode: forwards;
visibility: visible;
animation: full-screen-popup 0s ease-in 1s forwards;
animation-fill-mode: forwards;
}
@keyframes cssAnimation {
from {
opacity: 0;
}
to {
width: 0;
height: 0;
overflow: hidden;
opacity: 1;
}
}
@keyframes full-screen-popup {
from {
opacity: 0;
}
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;
to {
width: 0;
height: 0;
overflow: hidden;
opacity: 1;
}
}

View File

@@ -1,59 +1,59 @@
html,
body {
margin: 0;
cursor: default;
font-size: 14px;
color: rgba(51, 51, 51, 1.000);
background: rgba(255, 255, 255, 1.000);
user-select: none;
margin: 0;
cursor: default;
font-size: 14px;
color: rgb(51 51 51 / 100%);
background: rgb(255 255 255 / 100%);
user-select: none;
}
#content {
display: flex;
flex-direction: column;
font-family: "Trebuchet MS", Helvetica, sans-serif;
margin: 100px 200px;
text-align: center;
display: flex;
flex-direction: column;
font-family: "Trebuchet MS", Helvetica, sans-serif;
margin: 100px 200px;
text-align: center;
}
#title {
text-align: left;
font-size: 24px;
font-weight: bold;
margin: 20px 0;
text-align: left;
font-size: 24px;
font-weight: bold;
margin: 20px 0;
}
#subtitle {
font-size: 20px;
text-align: left;
margin: 12px 0;
font-size: 20px;
text-align: left;
margin: 12px 0;
}
#description {
text-align: left;
font-size: 16px;
list-style-position: inside;
text-align: left;
font-size: 16px;
list-style-position: inside;
}
#reconnect {
float: left;
float: left;
}
#settings {
margin-left: 116px;
margin-left: 116px;
}
.button {
font-size: 16px;
background: rgba(0, 150, 136, 1.000);
color: rgba(255, 255, 255, 1.000);
width: 96px;
height: 32px;
border-radius: 5px;
line-height: 32px;
cursor: pointer;
font-size: 16px;
background: rgb(0 150 136 / 100%);
color: rgb(255 255 255 / 100%);
width: 96px;
height: 32px;
border-radius: 5px;
line-height: 32px;
cursor: pointer;
}
.button:hover {
opacity: 0.8;
}
opacity: 0.8;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
/* Override css rules */
.portico-wrap > .header {
display: none;
display: none;
}
.portico-container > .footer {
display: none;
}
display: none;
}

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

View File

@@ -1,7 +1,12 @@
export default class BaseComponent {
generateNodeFromTemplate(template: string): Element | null {
const wrapper = document.createElement('div');
wrapper.innerHTML = template;
return wrapper.firstElementChild;
}
import type {Html} from "../../../common/html.ts";
export function generateNodeFromHtml(html: Html): Element {
const wrapper = document.createElement("div");
wrapper.innerHTML = html.html;
if (wrapper.firstElementChild === null) {
throw new Error("No element found in HTML");
}
return wrapper.firstElementChild;
}

View File

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

View File

@@ -1,47 +1,73 @@
import Tab, {TabProps} from './tab';
import {type Html, html} from "../../../common/html.ts";
import type {TabPage} from "../../../common/types.ts";
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 {
$closeButton: Element;
$view: Element;
$el: Element;
$closeButton?: Element;
constructor(props: TabProps) {
super(props);
this.init();
}
constructor({$view, ...properties}: FunctionalTabProperties) {
super(properties);
template(): string {
return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tab-badge close-button">
<i class="material-icons">close</i>
</div>
<div class="server-tab">
<i class="material-icons">${this.props.materialIcon}</i>
</div>
</div>`;
}
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();
}
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template());
if (this.props.name !== 'Settings') {
this.props.$root.append(this.$el);
this.$closeButton = this.$el.querySelectorAll('.server-tab-badge')[0];
this.registerListeners();
}
}
override async activate(): Promise<void> {
await super.activate();
this.$view.classList.add("active");
}
registerListeners(): void {
super.registerListeners();
override async deactivate(): Promise<void> {
await super.deactivate();
this.$view.classList.remove("active");
}
this.$el.addEventListener('mouseover', () => {
this.$closeButton.classList.add('active');
});
override async destroy(): Promise<void> {
await super.destroy();
this.$view.remove();
}
this.$el.addEventListener('mouseout', () => {
this.$closeButton.classList.remove('active');
});
templateHtml(): Html {
return html`
<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.properties.materialIcon}</i>
</div>
</div>
`;
}
this.$closeButton.addEventListener('click', (event: Event) => {
this.props.onDestroy();
event.stopPropagation();
});
}
override registerListeners(): void {
super.registerListeners();
this.$el.addEventListener("mouseover", () => {
this.$closeButton?.classList.add("active");
});
this.$el.addEventListener("mouseout", () => {
this.$closeButton?.classList.remove("active");
});
this.$closeButton?.addEventListener("click", (event) => {
this.properties.onDestroy?.();
event.stopPropagation();
});
}
}

View File

@@ -1,60 +0,0 @@
import {ipcRenderer, remote} from 'electron';
import * as LinkUtil from '../utils/link-util';
import * as ConfigUtil from '../utils/config-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')) {
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
new Notification('Download Complete', { // eslint-disable-line no-new
body: 'Download failed'
});
} else {
this.$el.downloadURL(url.href);
}
}
ipcRenderer.removeAllListeners('downloadFileCompleted');
});
} else {
(async () => LinkUtil.openBrowser(url))();
}
}

View File

@@ -1,63 +1,93 @@
import {ipcRenderer} from 'electron';
import process from "node:process";
import Tab, {TabProps} from './tab';
import * as SystemUtil from '../utils/system-util';
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 {
$badge: Element;
webview: Promise<WebView>;
$el: Element;
$name: Element;
$icon: HTMLImageElement;
$badge: Element;
constructor(props: TabProps) {
super(props);
this.init();
}
constructor({webview, ...properties}: ServerTabProperties) {
super(properties);
template(): string {
return `<div class="tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tooltip" style="display:none">${this.props.name}</div>
<div class="server-tab-badge"></div>
<div class="server-tab">
<img class="server-icons" src='${this.props.icon}'/>
</div>
<div class="server-tab-shortcut">${this.generateShortcutText()}</div>
</div>`;
}
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")!;
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template());
this.props.$root.append(this.$el);
this.registerListeners();
this.$badge = this.$el.querySelectorAll('.server-tab-badge')[0];
}
override async activate(): Promise<void> {
await super.activate();
(await this.webview).load();
}
updateBadge(count: number): void {
if (count > 0) {
const formattedCount = count > 999 ? '1K+' : count.toString();
this.$badge.innerHTML = formattedCount;
this.$badge.classList.add('active');
} else {
this.$badge.classList.remove('active');
}
}
override async deactivate(): Promise<void> {
await super.deactivate();
(await this.webview).hide();
}
generateShortcutText(): string {
// Only provide shortcuts for server [0..10]
if (this.props.index >= 10) {
return '';
}
override async destroy(): Promise<void> {
await super.destroy();
(await this.webview).destroy();
}
const shownIndex = this.props.index + 1;
templateHtml(): Html {
return html`
<div class="tab" data-tab-id="${this.properties.tabIndex}">
<div class="server-tooltip" style="display:none">
${this.properties.label}
</div>
<div class="server-tab-badge"></div>
<div class="server-tab">
<img class="server-icons" src="${this.properties.icon}" />
</div>
<div class="server-tab-shortcut">${this.generateShortcutText()}</div>
</div>
`;
}
let shortcutText = '';
setLabel(label: string): void {
this.properties.label = label;
this.$name.textContent = label;
}
if (SystemUtil.getOS() === 'Mac') {
shortcutText = `${shownIndex}`;
} else {
shortcutText = `Ctrl+${shownIndex}`;
}
setIcon(icon: string): void {
this.properties.icon = icon;
this.$icon.src = icon;
}
// Array index == Shown index - 1
ipcRenderer.send('switch-server-tab', shownIndex - 1);
updateBadge(count: number): void {
this.$badge.textContent = count > 999 ? "1K+" : count.toString();
this.$badge.classList.toggle("active", count > 0);
}
return shortcutText;
}
generateShortcutText(): string {
// Only provide shortcuts for server [0..9]
if (this.properties.index >= 9) {
return "";
}
const shownIndex = this.properties.index + 1;
// Array index == Shown index - 1
ipcRenderer.send("switch-server-tab", shownIndex - 1);
return process.platform === "darwin"
? `${shownIndex}`
: `Ctrl+${shownIndex}`;
}
}

View File

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

View File

@@ -1,311 +1,342 @@
import {ipcRenderer, remote} from 'electron';
import type {WebContents} from "electron/main";
import fs from "node:fs";
import path from 'path';
import fs from 'fs';
import * as ConfigUtil from '../utils/config-util';
import * as SystemUtil from '../utils/system-util';
import BaseComponent from './base';
import handleExternalLink from './handle-external-link';
import {contextMenu} from './context-menu';
import * as remote from "@electron/remote";
import {app, dialog} from "@electron/remote";
const {app, dialog} = remote;
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 shouldSilentWebview = ConfigUtil.getConfigItem('silent');
import {generateNodeFromHtml} from "./base.ts";
import {contextMenu} from "./context-menu.ts";
interface WebViewProps {
$root: Element;
index: number;
tabIndex: number;
url: string;
role: string;
name: string;
isActive: () => boolean;
switchLoading: (loading: boolean, url: string) => void;
onNetworkError: (index: number) => void;
nodeIntegration: boolean;
preload: boolean;
onTitleChange: () => void;
hasPermission?: (origin: string, permission: string) => boolean;
}
export default class WebView extends BaseComponent {
props: WebViewProps;
zoomFactor: number;
badgeCount: number;
loading: boolean;
customCSS: string;
$webviewsContainer: DOMTokenList;
$el: Electron.WebviewTag;
domReady?: Promise<void>;
constructor(props: WebViewProps) {
super();
this.props = props;
this.zoomFactor = 1;
this.loading = true;
this.badgeCount = 0;
this.customCSS = ConfigUtil.getConfigItem('customCSS');
this.$webviewsContainer = document.querySelector('#webviews-container').classList;
}
template(): string {
return `<webview
class="disabled"
data-tab-id="${this.props.tabIndex}"
src="${this.props.url}"
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession"
name="${this.props.name}"
webpreferences="
${this.props.nodeIntegration ? '' : 'contextIsolation,'}
${ConfigUtil.getConfigItem('enableSpellchecker') ? 'spellcheck,' : ''}
javascript
">
</webview>`;
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template()) as Electron.WebviewTag;
this.domReady = new Promise(resolve => {
this.$el.addEventListener('dom-ready', () => resolve(), true);
});
this.props.$root.append(this.$el);
this.registerListeners();
}
registerListeners(): void {
this.$el.addEventListener('new-window', event => {
handleExternalLink.call(this, event);
});
if (shouldSilentWebview) {
this.$el.addEventListener('dom-ready', () => {
this.$el.setAudioMuted(true);
});
}
this.$el.addEventListener('page-title-updated', event => {
const {title} = event;
this.badgeCount = this.getBadgeCount(title);
this.props.onTitleChange();
});
this.$el.addEventListener('did-navigate-in-page', event => {
const isSettingPage = event.url.includes('renderer/preference.html');
if (isSettingPage) {
return;
}
this.canGoBackButton();
});
this.$el.addEventListener('did-navigate', () => {
this.canGoBackButton();
});
this.$el.addEventListener('page-favicon-updated', event => {
const {favicons} = event;
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') {
// This api is only supported on macOS
app.dock.setBadge('●');
// Bounce the dock
if (ConfigUtil.getConfigItem('dockBouncing')) {
app.dock.bounce();
}
}
});
this.$el.addEventListener('dom-ready', () => {
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();
});
this.$el.addEventListener('did-fail-load', event => {
const {errorDescription} = event;
const hasConnectivityErr = SystemUtil.connectivityERR.includes(errorDescription);
if (hasConnectivityErr) {
console.error('error', errorDescription);
if (!this.props.url.includes('network.html')) {
this.props.onNetworkError(this.props.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.$el.addEventListener('did-stop-loading', () => {
this.props.switchLoading(false, this.props.url);
});
}
getBadgeCount(title: string): number {
const messageCountInTitle = (/\((\d+)\)/).exec(title);
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
}
showNotificationSettings(): void {
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'show-notification-settings');
}
show(): void {
// Do not show WebView if another tab was selected and this tab should be in background.
if (!this.props.isActive()) {
return;
}
// To show or hide the loading indicator in the the active tab
if (this.loading) {
this.$webviewsContainer.remove('loaded');
} else {
this.$webviewsContainer.add('loaded');
}
this.$el.classList.remove('disabled');
this.$el.classList.add('active');
setTimeout(() => {
if (this.props.role === 'server') {
this.$el.classList.remove('onload');
}
}, 1000);
this.focus();
this.props.onTitleChange();
// Injecting preload css in webview to override some css rules
(async () => this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8')))();
// Get customCSS again from config util to avoid warning user again
this.customCSS = ConfigUtil.getConfigItem('customCSS');
if (this.customCSS) {
if (!fs.existsSync(this.customCSS)) {
this.customCSS = null;
ConfigUtil.setConfigItem('customCSS', null);
const errorMessage = 'The custom css previously set is deleted!';
dialog.showErrorBox('custom css file deleted!', errorMessage);
return;
}
(async () => this.$el.insertCSS(fs.readFileSync(path.resolve(__dirname, this.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);
}
logOut(): void {
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'logout');
}
showShortcut(): void {
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'shortcut');
}
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: string, ...parameters: unknown[]): Promise<void> {
await this.domReady;
await this.$el.send(channel, ...parameters);
}
const shouldSilentWebview = ConfigUtil.getConfigItem("silent", false);
type WebViewProperties = {
$root: Element;
rootWebContents: WebContents;
index: number;
tabIndex: number;
url: string;
role: TabRole;
isActive: () => boolean;
switchLoading: (loading: boolean, url: string) => void;
onNetworkError: (index: number) => void;
preload?: string;
onTitleChange: () => void;
hasPermission?: (origin: string, permission: string) => boolean;
unsupportedMessage?: string;
};
export default class WebView {
static templateHtml(properties: WebViewProperties): Html {
return html`
<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>
`;
}
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,
);
});
// 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();
}
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) {
webContents.setAudioMuted(true);
}
webContents.on("page-title-updated", (_event, title) => {
this.badgeCount = this.getBadgeCount(title);
this.properties.onTitleChange();
});
this.$webview.addEventListener("did-navigate-in-page", () => {
this.canGoBackButton();
});
this.$webview.addEventListener("did-navigate", () => {
this.canGoBackButton();
});
webContents.on("page-favicon-updated", (_event, favicons) => {
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
if (favicons[0].indexOf("favicon-pms") > 0 && app.dock !== undefined) {
// This api is only supported on macOS
app.dock.setBadge("●");
// Bounce the dock
if (ConfigUtil.getConfigItem("dockBouncing", true)) {
app.dock.bounce();
}
}
});
webContents.addListener("context-menu", (event, menuParameters) => {
contextMenu(webContents, event, menuParameters);
});
this.$webview.addEventListener("dom-ready", () => {
this.loading = false;
this.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.properties.url.includes("network.html")) {
this.properties.onNetworkError(this.properties.index);
}
}
});
this.$webview.addEventListener("did-start-loading", () => {
this.properties.switchLoading(true, this.properties.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();
});
}
private getBadgeCount(title: string): number {
const messageCountInTitle = /^\((\d+)\)/.exec(title);
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
}
private show(): void {
// Do not show WebView if another tab was selected and this tab should be in background.
if (!this.properties.isActive()) {
return;
}
// To show or hide the loading indicator in the active tab
this.$webviewsContainer.toggle("loaded", !this.loading);
this.$pane.classList.add("active");
this.focus();
this.properties.onTitleChange();
// Injecting preload css in webview to override some css rules
(async () => this.getWebContents().insertCSS(preloadCss))();
// Get customCSS again from config util to avoid warning user again
const customCss = ConfigUtil.getConfigItem("customCSS", null);
this.customCss = customCss;
if (customCss) {
if (!fs.existsSync(customCss)) {
this.customCss = null;
ConfigUtil.setConfigItem("customCSS", null);
const errorMessage = t.__("The custom CSS previously set is deleted.");
dialog.showErrorBox(t.__("Custom CSS file deleted"), errorMessage);
return;
}
(async () =>
this.getWebContents().insertCSS(fs.readFileSync(customCss, "utf8")))();
}
}
}

View File

@@ -1,81 +1,114 @@
import {ipcRenderer} from 'electron';
import {EventEmitter} from "node:events";
import {EventEmitter} from 'events';
import {
type ClipboardDecrypter,
ClipboardDecrypterImplementation,
} from "./clipboard-decrypter.ts";
import {type NotificationData, newNotification} from "./notification/index.ts";
import {ipcRenderer} from "./typed-ipc-renderer.ts";
import {ClipboardDecrypterImpl} from './clipboard-decrypter';
import {NotificationData, newNotification} from './notification';
type ListenerType = (...arguments_: any[]) => void;
type ListenerType = ((...args: 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 */
class ElectronBridgeImpl extends EventEmitter implements ElectronBridge {
send_notification_reply_message_supported: boolean;
idle_on_system: boolean;
last_active_on_system: number;
let notificationReplySupported = false;
// Indicates if the user is idle or not
let idle = false;
// Indicates the time at which user was last active
let lastActive = Date.now();
constructor() {
super();
this.send_notification_reply_message_supported = false;
// Indicates if the user is idle or not
this.idle_on_system = false;
export const bridgeEvents = new EventEmitter(); // eslint-disable-line unicorn/prefer-event-target
// Indicates the time at which user was last active
this.last_active_on_system = Date.now();
}
/* eslint-disable @typescript-eslint/naming-convention */
const electron_bridge: ElectronBridge = {
send_event: (eventName: string | symbol, ...arguments_: unknown[]): boolean =>
bridgeEvents.emit(eventName, ...arguments_),
send_event = (eventName: string | symbol, ...args: unknown[]): void => {
this.emit(eventName, ...args);
};
on_event(eventName: string, listener: ListenerType): void {
bridgeEvents.on(eventName, listener);
},
on_event = (eventName: string, listener: ListenerType): void => {
this.on(eventName, listener);
};
new_notification: (
title: string,
options: NotificationOptions,
dispatch: (type: string, eventInit: EventInit) => boolean,
): NotificationData => newNotification(title, options, dispatch),
new_notification = (
title: string,
options: NotificationOptions | undefined,
dispatch: (type: string, eventInit: EventInit) => boolean
): NotificationData =>
newNotification(title, options, dispatch);
get_idle_on_system: (): boolean => idle,
get_idle_on_system = (): boolean => this.idle_on_system;
get_last_active_on_system: (): number => lastActive,
get_last_active_on_system = (): number => this.last_active_on_system;
get_send_notification_reply_message_supported: (): boolean =>
notificationReplySupported,
get_send_notification_reply_message_supported = (): boolean =>
this.send_notification_reply_message_supported;
set_send_notification_reply_message_supported(value: boolean): void {
notificationReplySupported = value;
},
set_send_notification_reply_message_supported = (value: boolean): void => {
this.send_notification_reply_message_supported = value;
};
decrypt_clipboard: (version: number): ClipboardDecrypter =>
new ClipboardDecrypterImplementation(version),
};
/* eslint-enable @typescript-eslint/naming-convention */
decrypt_clipboard = (version: number): ClipboardDecrypterImpl =>
new ClipboardDecrypterImpl(version);
}
bridgeEvents.on("total_unread_count", (unreadCount: unknown) => {
if (typeof unreadCount !== "number") {
throw new TypeError("Expected string for unreadCount");
}
const electron_bridge = new ElectronBridgeImpl();
electron_bridge.on('total_unread_count', (...args) => {
ipcRenderer.send('unread-count', ...args);
ipcRenderer.send("unread-count", unreadCount);
});
electron_bridge.on('realm_name', realmName => {
const serverURL = location.origin;
ipcRenderer.send('realm-name-changed', serverURL, realmName);
bridgeEvents.on("realm_name", (realmName: unknown) => {
if (typeof realmName !== "string") {
throw new TypeError("Expected string for realmName");
}
const serverUrl = location.origin;
ipcRenderer.send("realm-name-changed", serverUrl, realmName);
});
electron_bridge.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;
iconURL = iconURL.includes('http') ? iconURL : `${serverURL}${iconURL}`;
ipcRenderer.send('realm-icon-changed', serverURL, iconURL);
const serverUrl = location.origin;
ipcRenderer.send(
"realm-icon-changed",
serverUrl,
iconUrl.includes("http") ? iconUrl : `${serverUrl}${iconUrl}`,
);
});
// Set user as active and update the time of last activity
ipcRenderer.on("set-active", () => {
idle = false;
lastActive = Date.now();
});
// Set user as idle and time of last activity is left unchanged
ipcRenderer.on("set-idle", () => {
idle = true;
});
// This follows node's idiomatic implementation of event
// emitters to make event handling more simpler instead of using
// functions zulip side will emit event using 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,50 +0,0 @@
import {remote} from 'electron';
import SendFeedback from '@electron-elements/send-feedback';
import path from 'path';
import fs from 'fs';
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,133 +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;
narrow: {
by_subject?: (target_id: number, opts: {trigger?: string}) => void;
by_topic?: (target_id: number, opts: {trigger?: string}) => void;
};
page_params?: {
default_language?: string;
};
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;
(async () => {
if (document.readyState === 'loading') {
await new Promise(resolve => {
document.addEventListener('DOMContentLoaded', () => {
resolve();
});
});
}
const {page_params} = zulipWindow;
if (page_params) {
electron_bridge.send_event('zulip-loaded');
}
})();
electron_bridge.on_event('narrow-by-topic', (id: number) => {
const {narrow} = zulipWindow;
const narrowByTopic = narrow.by_topic || narrow.by_subject;
narrowByTopic(id, {trigger: 'notification'});
});
function attributeListener<T extends EventTarget>(type: string): PropertyDescriptor {
const symbol = Symbol('on' + type);
function listener(this: T, ev: Event): void {
if ((this as any)[symbol].call(this, ev) === false) {
ev.preventDefault();
}
}
return {
configurable: true,
enumerable: true,
get(this: T) {
return (this as any)[symbol];
},
set(this: T, value: unknown) {
if (typeof value === 'function') {
if (!(symbol in this)) {
this.addEventListener(type, listener);
}
(this as any)[symbol] = value;
} else if (symbol in this) {
this.removeEventListener(type, listener);
delete (this as any)[symbol];
}
}
};
}
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,111 +0,0 @@
import {ipcRenderer} from 'electron';
import {
appId, customReply, focusCurrentServer, parseReply
} from './helpers';
import MacNotifier from 'node-mac-notifier';
import * as ConfigUtil from '../utils/config-util';
import electron_bridge from '../electron-bridge';
type ReplyHandler = (response: string) => void;
type ClickHandler = () => void;
let replyHandler: ReplyHandler;
let clickHandler: ClickHandler;
interface NotificationHandlerArgs {
response: string;
}
class DarwinNotification {
tag: number;
constructor(title: string, options: NotificationOptions) {
const silent: boolean = ConfigUtil.getConfigItem('silent') || false;
const {icon} = options;
const profilePic = new URL(icon, location.href).href;
this.tag = Number.parseInt(options.tag, 10);
const notification = new MacNotifier(title, Object.assign(options, {
bundleId: appId,
canReply: true,
silent,
icon: profilePic
}));
notification.addEventListener('click', () => {
// Focus to the server who sent the
// notification if not focused already
if (clickHandler) {
clickHandler();
}
focusCurrentServer();
ipcRenderer.send('focus-app');
});
notification.addEventListener('reply', this.notificationHandler);
}
static requestPermission(): void {
// Do nothing
}
// Override default Notification permission
static get permission(): NotificationPermission {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
}
get onreply(): ReplyHandler {
return replyHandler;
}
set onreply(handler: ReplyHandler) {
replyHandler = handler;
}
get onclick(): ClickHandler {
return clickHandler;
}
set onclick(handler: ClickHandler) {
clickHandler = handler;
}
// Not something that is common or
// used by zulip server but added to be
// future proff.
addEventListener(event: string, handler: ClickHandler | ReplyHandler): void {
if (event === 'click') {
clickHandler = handler as ClickHandler;
}
if (event === 'reply') {
replyHandler = handler as ReplyHandler;
}
}
async notificationHandler({response}: NotificationHandlerArgs): Promise<void> {
response = await parseReply(response);
focusCurrentServer();
if (electron_bridge.send_notification_reply_message_supported) {
electron_bridge.send_event('send_notification_reply_message', this.tag, response);
return;
}
electron_bridge.emit('narrow-by-topic', this.tag);
if (replyHandler) {
replyHandler(response);
return;
}
customReply(response);
}
// Method specific to notification api
// used by zulip
close(): void {
// Do nothing
}
}
module.exports = DarwinNotification;

View File

@@ -1,28 +0,0 @@
import {ipcRenderer} from 'electron';
import {focusCurrentServer} from './helpers';
import * as ConfigUtil from '../utils/config-util';
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') ? 'granted' : 'denied';
}
}

View File

@@ -1,137 +0,0 @@
import {remote} from 'electron';
import Logger from '../utils/logger-util';
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
// Do not change this
export const appId = 'org.zulip.zulip-electron';
export type BotListItem = [string, string];
const botsList: BotListItem[] = [];
let botsListLoaded = false;
// This function load list of bots from the server
// in case botsList isn't already completely loaded when required in parseRely
export async function loadBots(): Promise<void> {
botsList.length = 0;
const response = await fetch('/json/users');
if (response.ok) {
const {members} = await response.json();
members.forEach(({is_bot, full_name}: {[key: string]: unknown}) => {
if (is_bot && typeof full_name === 'string') {
const bot = `@${full_name}`;
const mention = `@**${bot.replace(/^@/, '')}**`;
botsList.push([bot, mention]);
}
});
botsListLoaded = true;
} else {
logger.log('Load bots request failed: ', await response.text());
logger.log('Load bots request status: ', response.status);
}
}
export function checkElements(...elements: unknown[]): boolean {
let status = true;
elements.forEach(element => {
if (element === null || element === undefined) {
status = false;
}
});
return status;
}
export function customReply(reply: string): void {
// Server does not support notification reply yet.
const buttonSelector = '.messagebox #send_controls button[type=submit]';
const messageboxSelector = '.selected_message .messagebox .messagebox-border .messagebox-content';
const textarea: HTMLInputElement = document.querySelector('#compose-textarea');
const messagebox: HTMLButtonElement = document.querySelector(messageboxSelector);
const sendButton: HTMLButtonElement = document.querySelector(buttonSelector);
// Sanity check for old server versions
const elementsExists = checkElements(textarea, messagebox, sendButton);
if (!elementsExists) {
return;
}
textarea.value = reply;
messagebox.click();
sendButton.click();
}
const currentWindow = remote.getCurrentWindow();
const webContents = remote.getCurrentWebContents();
const webContentsId = webContents.id;
// This function will focus the server that sent
// the notification. Main function implemented in main.js
export function focusCurrentServer(): void {
currentWindow.webContents.send('focus-webview-with-id', webContentsId);
}
// This function parses the reply from to notification
// making it easier to reply from notification eg
// @username in reply will be converted to @**username**
// #stream in reply will be converted to #**stream**
// bot mentions are not yet supported
export async function parseReply(reply: string): Promise<string> {
const usersDiv = document.querySelectorAll('#user_presences li');
const streamHolder = document.querySelectorAll('#stream_filters li');
type UsersItem = BotListItem;
type StreamsItem = BotListItem;
const users: UsersItem[] = [];
const streams: StreamsItem[] = [];
usersDiv.forEach(userRow => {
const anchor = userRow.querySelector('span a');
if (anchor !== null) {
const user = `@${anchor.textContent.trim()}`;
const mention = `@**${user.replace(/^@/, '')}**`;
users.push([user, mention]);
}
});
streamHolder.forEach(stream => {
const streamAnchor = stream.querySelector('div a');
if (streamAnchor !== null) {
const streamName = `#${streamAnchor.textContent.trim()}`;
const streamMention = `#**${streamName.replace(/^#/, '')}**`;
streams.push([streamName, streamMention]);
}
});
users.forEach(([user, mention]) => {
if (reply.includes(user)) {
const regex = new RegExp(user, 'g');
reply = reply.replace(regex, mention);
}
});
streams.forEach(([stream, streamMention]) => {
const regex = new RegExp(stream, 'g');
reply = reply.replace(regex, streamMention);
});
// If botsList isn't completely loaded yet, make a request for list
if (!botsListLoaded) {
await loadBots();
}
// Iterate for every bot name and replace in reply
// @botname with @**botname**
botsList.forEach(([bot, mention]) => {
if (reply.includes(bot)) {
const regex = new RegExp(bot, 'g');
reply = reply.replace(regex, mention);
}
});
reply = reply.replace(/\\n/, '\n');
return reply;
}

View File

@@ -1,73 +1,41 @@
import {remote} from 'electron';
import electron_bridge from '../electron-bridge';
import {appId, loadBots} from './helpers';
import {ipcRenderer} from "../typed-ipc-renderer.ts";
import DefaultNotification from './default-notification';
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);
let Notification = DefaultNotification;
if (process.platform === 'darwin') {
Notification = require('./darwin-notifications');
}
export interface 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 type NotificationData = {
close: () => void;
title: string;
dir: NotificationDirection;
lang: string;
body: string;
tag: string;
icon: string;
data: unknown;
};
export function newNotification(
title: string,
options: NotificationOptions | undefined,
dispatch: (type: string, eventInit: EventInit) => boolean
title: string,
options: NotificationOptions,
dispatch: (type: string, eventInit: EventInit) => boolean,
): NotificationData {
const notification = new Notification(title, options);
for (const type of ['click', 'close', 'error', 'show']) {
notification.addEventListener(type, (ev: Event) => {
if (!dispatch(type, ev)) {
ev.preventDefault();
}
});
}
const notification = new Notification(title, {...options, silent: true});
for (const type of ["click", "close", "error", "show"]) {
notification.addEventListener(type, (event) => {
if (type === "click") ipcRenderer.send("focus-this-webview");
if (!dispatch(type, event)) {
event.preventDefault();
}
});
}
return {
close: () => notification.close(),
title: notification.title,
dir: notification.dir,
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
};
return {
close() {
notification.close();
},
title: notification.title,
dir: notification.dir,
lang: notification.lang,
body: notification.body,
tag: notification.tag,
icon: notification.icon,
data: notification.data,
};
}
electron_bridge.once('zulip-loaded', async () => {
await loadBots();
});

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,10 +1,13 @@
import {ipcRenderer} from 'electron';
import {ipcRenderer} from "../typed-ipc-renderer.ts";
export function init($reconnectButton: Element, $settingsButton: Element): void {
$reconnectButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
$settingsButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'open-settings');
});
export function init(
$reconnectButton: Element,
$settingsButton: Element,
): void {
$reconnectButton.addEventListener("click", () => {
ipcRenderer.send("forward-message", "reload-viewer");
});
$settingsButton.addEventListener("click", () => {
ipcRenderer.send("forward-message", "open-settings");
});
}

View File

@@ -1,99 +0,0 @@
'use-strict';
import {remote, OpenDialogOptions} from 'electron';
import path from 'path';
import BaseComponent from '../../components/base';
import * as CertificateUtil from '../../utils/certificate-util';
import * as DomainUtil from '../../utils/domain-util';
import * as t from '../../utils/translation-util';
interface AddCertificateProps {
$root: Element;
}
const {dialog} = remote;
export default class AddCertificate extends BaseComponent {
props: AddCertificateProps;
_certFile: string;
$addCertificate: Element | null;
addCertificateButton: Element | null;
serverUrl: HTMLInputElement | null;
constructor(props: AddCertificateProps) {
super();
this.props = props;
this._certFile = '';
}
template(): string {
return `
<div class="settings-card certificates-card">
<div class="certificate-input">
<div>${t.__('Organization URL')}</div>
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
</div>
<div class="certificate-input">
<div>${t.__('Certificate file')}</div>
<button class="green" id="add-certificate-button">${t.__('Upload')}</button>
</div>
</div>
`;
}
init(): void {
this.$addCertificate = this.generateNodeFromTemplate(this.template());
this.props.$root.append(this.$addCertificate);
this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button');
this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
this.initListeners();
}
async validateAndAdd(): Promise<void> {
const certificate = this._certFile;
const serverUrl = this.serverUrl.value;
if (certificate !== '' && serverUrl !== '') {
const server = encodeURIComponent(DomainUtil.formatUrl(serverUrl));
const fileName = path.basename(certificate);
const copy = CertificateUtil.copyCertificate(server, certificate, fileName);
if (!copy) {
return;
}
CertificateUtil.setCertificate(server, fileName);
this.serverUrl.value = '';
await dialog.showMessageBox({
title: 'Success',
message: 'Certificate saved!'
});
} else {
dialog.showErrorBox('Error', `Please, ${serverUrl === '' ?
'Enter an Organization URL' : 'Choose certificate file'}`);
}
}
async addHandler(): Promise<void> {
const showDialogOptions: OpenDialogOptions = {
title: 'Select file',
properties: ['openFile'],
filters: [{name: 'crt, pem', extensions: ['crt', 'pem']}]
};
const {filePaths, canceled} = await dialog.showOpenDialog(showDialogOptions);
if (!canceled) {
this._certFile = filePaths[0] || '';
await this.validateAndAdd();
}
}
initListeners(): void {
this.addCertificateButton.addEventListener('click', async () => {
await this.addHandler();
});
this.serverUrl.addEventListener('keypress', async event => {
if (event.key === 'Enter') {
await this.addHandler();
}
});
}
}

View File

@@ -1,44 +0,0 @@
import electron, {app} from 'electron';
import * as ConfigUtil from '../../utils/config-util';
function showBadgeCount(messageCount: number, mainWindow: electron.BrowserWindow): void {
if (process.platform === 'win32') {
updateOverlayIcon(messageCount, mainWindow);
} else {
app.badgeCount = messageCount;
}
}
function hideBadgeCount(mainWindow: electron.BrowserWindow): void {
if (process.platform === 'win32') {
mainWindow.setOverlayIcon(null, '');
} else {
app.badgeCount = 0;
}
}
export function updateBadge(badgeCount: number, mainWindow: electron.BrowserWindow): void {
if (ConfigUtil.getConfigItem('badgeOption', true)) {
showBadgeCount(badgeCount, mainWindow);
} else {
hideBadgeCount(mainWindow);
}
}
function updateOverlayIcon(messageCount: number, mainWindow: electron.BrowserWindow): void {
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
}
if (messageCount === 0) {
mainWindow.setOverlayIcon(null, '');
} else {
mainWindow.webContents.send('render-taskbar-icon', messageCount);
}
}
export function updateTaskbarIcon(data: string, text: string, mainWindow: electron.BrowserWindow): void {
const img = electron.nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
}

View File

@@ -1,65 +1,83 @@
import {ipcRenderer} from 'electron';
import escape from 'escape-html';
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";
import BaseComponent from '../../components/base';
type BaseSectionProperties = {
$element: HTMLElement;
disabled?: boolean;
value: boolean;
clickHandler: () => void;
};
interface BaseSectionProps {
$element: HTMLElement;
disabled?: boolean;
value: boolean;
clickHandler: () => void;
export function generateSettingOption(properties: BaseSectionProperties): void {
const {$element, disabled, value, clickHandler} = properties;
$element.textContent = "";
const $optionControl = generateNodeFromHtml(
generateOptionHtml(value, disabled),
);
$element.append($optionControl);
if (!disabled) {
$optionControl.addEventListener("click", clickHandler);
}
}
export default class BaseSection extends BaseComponent {
generateSettingOption(props: BaseSectionProps): void {
const {$element, disabled, value, clickHandler} = props;
export function generateOptionHtml(
settingOption: boolean,
disabled?: boolean,
): Html {
const labelHtml = disabled
? html`<label
class="disallowed"
title="${t.__("Setting locked by system administrator.")}"
></label>`
: html`<label></label>`;
if (settingOption) {
return html`
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox" checked disabled />
${labelHtml}
</div>
</div>
`;
}
$element.innerHTML = '';
const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value, disabled));
$element.append($optionControl);
if (!disabled) {
$optionControl.addEventListener('click', clickHandler);
}
}
generateOptionTemplate(settingOption: boolean, disabled?: boolean): string {
const label = disabled ? '<label class="disallowed" title="Setting locked by system administrator."/>' : '<label/>';
if (settingOption) {
return `
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox" checked disabled>
${label}
</div>
</div>
`;
}
return `
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox">
${label}
</div>
</div>
`;
}
/* A method that in future can be used to create dropdown menus using <select> <option> tags.
it needs an object which has ``key: value`` pairs and will return a string that can be appended to HTML
*/
generateSelectTemplate(options: {[key: string]: string}, className?: string, idName?: string): string {
let select = `<select class="${escape(className)}" id="${escape(idName)}">\n`;
Object.keys(options).forEach(key => {
select += `<option name="${escape(key)}" value="${escape(key)}">${escape(options[key])}</option>\n`;
});
select += '</select>';
return select;
}
reloadApp(): void {
ipcRenderer.send('forward-message', 'reload-viewer');
}
return html`
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox" />
${labelHtml}
</div>
</div>
`;
}
/* A method that in future can be used to create dropdown menus using <select> <option> tags.
it needs an object which has ``key: value`` pairs and will return a string that can be appended to HTML
*/
export function generateSelectHtml(
options: Record<string, string>,
className?: string,
idName?: string,
): Html {
const optionsHtml = html``.join(
Object.keys(options).map(
(key) => html`
<option name="${key}" value="${key}">${options[key]}</option>
`,
),
);
return html`
<select class="${className}" id="${idName}">
${optionsHtml}
</select>
`;
}
export function reloadApp(): void {
ipcRenderer.send("forward-message", "reload-viewer");
}

View File

@@ -1,89 +1,67 @@
import {ipcRenderer} from 'electron';
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 BaseSection from './base-section';
import * as DomainUtil from '../../utils/domain-util';
import ServerInfoForm from './server-info-form';
import AddCertificate from './add-certificate';
import FindAccounts from './find-accounts';
import * as t from '../../utils/translation-util';
import {reloadApp} from "./base-section.ts";
import {initFindAccounts} from "./find-accounts.ts";
import {initServerInfoForm} from "./server-info-form.ts";
interface ConnectedOrgSectionProps {
$root: Element;
}
export default class ConnectedOrgSection extends BaseSection {
props: ConnectedOrgSectionProps;
$serverInfoContainer: Element | null;
$existingServers: Element | null;
$newOrgButton: HTMLButtonElement | null;
$addCertificateContainer: Element | null;
$findAccountsContainer: Element | null;
constructor(props: ConnectedOrgSectionProps) {
super();
this.props = props;
}
template(): string {
return `
<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.')}</div>
<div id="server-info-container"></div>
<div id="new-org-button"><button class="green sea w-250">${t.__('Connect to another organization')}</button></div>
<div class="page-title">${t.__('Add Custom Certificates')}</div>
<div id="add-certificate-container"></div>
<div class="page-title">${t.__('Find accounts by email')}</div>
<div id="find-accounts-container"></div>
</div>
`;
}
init(): void {
this.initServers();
}
initServers(): void {
this.props.$root.innerHTML = '';
const servers = DomainUtil.getDomains();
this.props.$root.innerHTML = this.template();
this.$serverInfoContainer = document.querySelector('#server-info-container');
this.$existingServers = document.querySelector('#existing-servers');
this.$newOrgButton = document.querySelector('#new-org-button');
this.$addCertificateContainer = document.querySelector('#add-certificate-container');
this.$findAccountsContainer = document.querySelector('#find-accounts-container');
const noServerText = t.__('All the connected orgnizations will appear here');
// Show noServerText if no servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? noServerText : '';
for (const [i, server] of servers.entries()) {
new ServerInfoForm({
$root: this.$serverInfoContainer,
server,
index: i,
onChange: this.reloadApp
}).init();
}
this.$newOrgButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'open-org-tab');
});
this.initAddCertificate();
this.initFindAccounts();
}
initAddCertificate(): void {
new AddCertificate({
$root: this.$addCertificateContainer
}).init();
}
initFindAccounts(): void {
new FindAccounts({
$root: this.$findAccountsContainer
}).init();
}
type ConnectedOrgSectionProperties = {
$root: Element;
};
export function initConnectedOrgSection({
$root,
}: ConnectedOrgSectionProperties): void {
$root.textContent = "";
const servers = DomainUtil.getDomains();
$root.innerHTML = html`
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">${t.__("Connected organizations")}</div>
<div class="title" id="existing-servers">
${t.__("All the connected organizations will appear here.")}
</div>
<div id="server-info-container"></div>
<div id="new-org-button">
<button class="green sea w-250">
${t.__("Connect to another organization")}
</button>
</div>
<div class="page-title">${t.__("Find accounts by email")}</div>
<div id="find-accounts-container"></div>
</div>
`.html;
const $serverInfoContainer = $root.querySelector("#server-info-container")!;
const $existingServers = $root.querySelector("#existing-servers")!;
const $newOrgButton: HTMLButtonElement =
$root.querySelector("#new-org-button")!;
const $findAccountsContainer = $root.querySelector(
"#find-accounts-container",
)!;
const noServerText = t.__(
"All the connected organizations will appear here.",
);
// Show noServerText if no servers are there otherwise hide it
$existingServers.textContent = servers.length === 0 ? noServerText : "";
for (const [i, server] of servers.entries()) {
initServerInfoForm({
$root: $serverInfoContainer,
server,
index: i,
onChange: reloadApp,
});
}
$newOrgButton.addEventListener("click", () => {
ipcRenderer.send("forward-message", "open-org-tab");
});
initFindAccounts({
$root: $findAccountsContainer,
});
}

View File

@@ -1,80 +1,66 @@
'use-strict';
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 BaseComponent from '../../components/base';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
type FindAccountsProperties = {
$root: Element;
};
interface FindAccountsProps {
$root: Element;
async function findAccounts(url: string): Promise<void> {
if (!url) {
return;
}
if (!url.startsWith("http")) {
url = "https://" + url;
}
await LinkUtil.openBrowser(new URL("/accounts/find", url));
}
export default class FindAccounts extends BaseComponent {
props: FindAccountsProps;
$findAccounts: Element | null;
$findAccountsButton: Element | null;
$serverUrlField: HTMLInputElement | null;
constructor(props: FindAccountsProps) {
super();
this.props = props;
}
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>
<input class="setting-input-value" value="zulipchat.com" />
</div>
<div class="certificate-input">
<button class="green w-150" id="find-accounts-button">
${t.__("Find accounts")}
</button>
</div>
</div>
`);
properties.$root.append($findAccounts);
const $findAccountsButton = $findAccounts.querySelector(
"#find-accounts-button",
)!;
const $serverUrlField: HTMLInputElement = $findAccounts.querySelector(
"input.setting-input-value",
)!;
template(): string {
return `
<div class="settings-card certificate-card">
<div class="certificate-input">
<div>${t.__('Organization URL')}</div>
<input class="setting-input-value" value="zulipchat.com"/>
</div>
<div class="certificate-input">
<button class="green w-150" id="find-accounts-button">${t.__('Find accounts')}</button>
</div>
</div>
`;
}
$findAccountsButton.addEventListener("click", async () => {
await findAccounts($serverUrlField.value);
});
init(): void {
this.$findAccounts = this.generateNodeFromTemplate(this.template());
this.props.$root.append(this.$findAccounts);
this.$findAccountsButton = this.$findAccounts.querySelector('#find-accounts-button');
this.$serverUrlField = this.$findAccounts.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
this.initListeners();
}
$serverUrlField.addEventListener("click", () => {
if ($serverUrlField.value === "zulipchat.com") {
$serverUrlField.setSelectionRange(0, 0);
}
});
async findAccounts(url: string): Promise<void> {
if (!url) {
return;
}
$serverUrlField.addEventListener("keypress", async (event) => {
if (event.key === "Enter") {
await findAccounts($serverUrlField.value);
}
});
if (!url.startsWith('http')) {
url = 'https://' + url;
}
await LinkUtil.openBrowser(new URL('/accounts/find', url));
}
initListeners(): void {
this.$findAccountsButton.addEventListener('click', async () => {
await this.findAccounts(this.$serverUrlField.value);
});
this.$serverUrlField.addEventListener('click', () => {
if (this.$serverUrlField.value === 'zulipchat.com') {
this.$serverUrlField.setSelectionRange(0, 0);
}
});
this.$serverUrlField.addEventListener('keypress', async event => {
if (event.key === 'Enter') {
await this.findAccounts(this.$serverUrlField.value);
}
});
this.$serverUrlField.addEventListener('input', () => {
if (this.$serverUrlField.value) {
this.$serverUrlField.classList.remove('invalid-input-value');
} else {
this.$serverUrlField.classList.add('invalid-input-value');
}
});
}
$serverUrlField.addEventListener("input", () => {
$serverUrlField.classList.toggle(
"invalid-input-value",
$serverUrlField.value === "",
);
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,68 +1,74 @@
import BaseComponent from '../../components/base';
import * as t from '../../utils/translation-util';
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 {
$root: Element;
onItemSelected: (navItem: string) => void;
}
export default class PreferenceNav extends BaseComponent {
props: PreferenceNavProps;
navItems: string[];
$el: Element;
constructor(props: PreferenceNavProps) {
super();
this.props = props;
this.navItems = ['General', 'Network', 'AddServer', 'Organizations', 'Shortcuts'];
this.init();
}
template(): string {
let navItemsTemplate = '';
for (const navItem of this.navItems) {
navItemsTemplate += `<div class="nav" id="nav-${navItem}">${t.__(navItem)}</div>`;
}
return `
<div>
<div id="settings-header">${t.__('Settings')}</div>
<div id="nav-container">${navItemsTemplate}</div>
</div>
`;
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template());
this.props.$root.append(this.$el);
this.registerListeners();
}
registerListeners(): void {
for (const navItem of this.navItems) {
const $item = document.querySelector(`#nav-${navItem}`);
$item.addEventListener('click', () => {
this.props.onItemSelected(navItem);
});
}
}
select(navItemToSelect: string): void {
for (const navItem of this.navItems) {
if (navItem === navItemToSelect) {
this.activate(navItem);
} else {
this.deactivate(navItem);
}
}
}
activate(navItem: string): void {
const $item = document.querySelector(`#nav-${navItem}`);
$item.classList.add('active');
}
deactivate(navItem: string): void {
const $item = document.querySelector(`#nav-${navItem}`);
$item.classList.remove('active');
}
type PreferenceNavigationProperties = {
$root: Element;
onItemSelected: (navigationItem: NavigationItem) => void;
};
export default class PreferenceNavigation {
navigationItems: Array<{navigationItem: NavigationItem; label: string}>;
$el: Element;
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.properties.$root.append(this.$el);
this.registerListeners();
}
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">${navigationItemsHtml}</div>
</div>
`;
}
registerListeners(): void {
for (const {navigationItem} of this.navigationItems) {
const $item = this.$el.querySelector(
`#nav-${CSS.escape(navigationItem)}`,
)!;
$item.addEventListener("click", () => {
this.properties.onItemSelected(navigationItem);
});
}
}
select(navigationItemToSelect: NavigationItem): void {
for (const {navigationItem} of this.navigationItems) {
if (navigationItem === navigationItemToSelect) {
this.activate(navigationItem);
} else {
this.deactivate(navigationItem);
}
}
}
activate(navigationItem: NavigationItem): void {
const $item = this.$el.querySelector(`#nav-${CSS.escape(navigationItem)}`)!;
$item.classList.add("active");
}
deactivate(navigationItem: NavigationItem): void {
const $item = this.$el.querySelector(`#nav-${CSS.escape(navigationItem)}`)!;
$item.classList.remove("active");
}
}

View File

@@ -1,138 +1,136 @@
import {ipcRenderer} from 'electron';
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 BaseSection from './base-section';
import * as ConfigUtil from '../../utils/config-util';
import * as t from '../../utils/translation-util';
import {generateSettingOption} from "./base-section.ts";
interface NetworkSectionProps {
$root: Element;
}
type NetworkSectionProperties = {
$root: Element;
};
export default class NetworkSection extends BaseSection {
props: NetworkSectionProps;
$proxyPAC: HTMLInputElement;
$proxyRules: HTMLInputElement;
$proxyBypass: HTMLInputElement;
$proxySaveAction: Element;
$manualProxyBlock: Element;
constructor(props: NetworkSectionProps) {
super();
this.props = props;
}
template(): string {
return `
<div class="settings-pane">
<div class="title">${t.__('Proxy')}</div>
<div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="use-system-settings">
<div class="setting-description">${t.__('Use system proxy settings (requires restart)')}</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="use-manual-settings">
<div class="setting-description">${t.__('Manual proxy configuration')}</div>
<div class="setting-control"></div>
</div>
<div class="manual-proxy-block">
<div class="setting-row" id="proxy-pac-option">
<span class="setting-input-key">PAC ${t.__('script')}</span>
<input class="setting-input-value" placeholder="e.g. foobar.com/pacfile.js"/>
</div>
<div class="setting-row" id="proxy-rules-option">
<span class="setting-input-key">${t.__('Proxy rules')}</span>
<input class="setting-input-value" placeholder="e.g. http=foopy:80;ftp=foopy2"/>
</div>
<div class="setting-row" id="proxy-bypass-option">
<span class="setting-input-key">${t.__('Proxy bypass rules')}</span>
<input class="setting-input-value" placeholder="e.g. foobar.com"/>
</div>
<div class="setting-row">
<div class="action green" id="proxy-save-action">
<span>${t.__('Save')}</span>
</div>
</div>
</div>
</div>
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">
<div class="setting-row" id="use-system-settings">
<div class="setting-description">
${t.__("Use system proxy settings (requires restart)")}
</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="use-manual-settings">
<div class="setting-description">
${t.__("Manual proxy configuration")}
</div>
<div class="setting-control"></div>
</div>
<div class="manual-proxy-block">
<div class="setting-row" id="proxy-pac-option">
<span class="setting-input-key">${t.__("PAC script")}</span>
<input
class="setting-input-value"
placeholder="e.g. foobar.com/pacfile.js"
/>
</div>
<div class="setting-row" id="proxy-rules-option">
<span class="setting-input-key">${t.__("Proxy rules")}</span>
<input
class="setting-input-value"
placeholder="e.g. http=foopy:80;ftp=foopy2"
/>
</div>
<div class="setting-row" id="proxy-bypass-option">
<span class="setting-input-key">${t.__("Proxy bypass rules")}</span>
<input class="setting-input-value" placeholder="e.g. foobar.com" />
</div>
<div class="setting-row">
<div class="action green" id="proxy-save-action">
<span>${t.__("Save")}</span>
</div>
`;
}
</div>
</div>
</div>
</div>
`.html;
init(): void {
this.props.$root.innerHTML = this.template();
this.$proxyPAC = document.querySelector('#proxy-pac-option .setting-input-value');
this.$proxyRules = document.querySelector('#proxy-rules-option .setting-input-value');
this.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value');
this.$proxySaveAction = document.querySelector('#proxy-save-action');
this.$manualProxyBlock = this.props.$root.querySelector('.manual-proxy-block');
this.initProxyOption();
const $proxyPac: HTMLInputElement = $root.querySelector(
"#proxy-pac-option .setting-input-value",
)!;
const $proxyRules: HTMLInputElement = $root.querySelector(
"#proxy-rules-option .setting-input-value",
)!;
const $proxyBypass: HTMLInputElement = $root.querySelector(
"#proxy-bypass-option .setting-input-value",
)!;
const $proxySaveAction = $root.querySelector("#proxy-save-action")!;
const $manualProxyBlock = $root.querySelector(".manual-proxy-block")!;
this.$proxyPAC.value = ConfigUtil.getConfigItem('proxyPAC', '');
this.$proxyRules.value = ConfigUtil.getConfigItem('proxyRules', '');
this.$proxyBypass.value = ConfigUtil.getConfigItem('proxyBypass', '');
toggleManualProxySettings(ConfigUtil.getConfigItem("useManualProxy", false));
updateProxyOption();
this.$proxySaveAction.addEventListener('click', () => {
ConfigUtil.setConfigItem('proxyPAC', this.$proxyPAC.value);
ConfigUtil.setConfigItem('proxyRules', this.$proxyRules.value);
ConfigUtil.setConfigItem('proxyBypass', this.$proxyBypass.value);
$proxyPac.value = ConfigUtil.getConfigItem("proxyPAC", "");
$proxyRules.value = ConfigUtil.getConfigItem("proxyRules", "");
$proxyBypass.value = ConfigUtil.getConfigItem("proxyBypass", "");
ipcRenderer.send('forward-message', 'reload-proxy', true);
});
}
$proxySaveAction.addEventListener("click", () => {
ConfigUtil.setConfigItem("proxyPAC", $proxyPac.value);
ConfigUtil.setConfigItem("proxyRules", $proxyRules.value);
ConfigUtil.setConfigItem("proxyBypass", $proxyBypass.value);
initProxyOption(): void {
const manualProxyEnabled = ConfigUtil.getConfigItem('useManualProxy', false);
this.toggleManualProxySettings(manualProxyEnabled);
ipcRenderer.send("forward-message", "reload-proxy", true);
});
this.updateProxyOption();
}
function toggleManualProxySettings(option: boolean): void {
$manualProxyBlock.classList.toggle("hidden", !option);
}
toggleManualProxySettings(option: boolean): void {
if (option) {
this.$manualProxyBlock.classList.remove('hidden');
} else {
this.$manualProxyBlock.classList.add('hidden');
}
}
function updateProxyOption(): void {
generateSettingOption({
$element: $root.querySelector("#use-system-settings .setting-control")!,
value: ConfigUtil.getConfigItem("useSystemProxy", false),
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("useSystemProxy", false);
const manualProxyValue = ConfigUtil.getConfigItem(
"useManualProxy",
false,
);
if (manualProxyValue && newValue) {
ConfigUtil.setConfigItem("useManualProxy", !manualProxyValue);
toggleManualProxySettings(!manualProxyValue);
}
updateProxyOption(): void {
this.generateSettingOption({
$element: document.querySelector('#use-system-settings .setting-control'),
value: ConfigUtil.getConfigItem('useSystemProxy', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('useSystemProxy');
const manualProxyValue = ConfigUtil.getConfigItem('useManualProxy');
if (manualProxyValue && newValue) {
ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue);
this.toggleManualProxySettings(!manualProxyValue);
}
if (!newValue) {
// Remove proxy system proxy settings
ConfigUtil.setConfigItem("proxyRules", "");
ipcRenderer.send("forward-message", "reload-proxy", false);
}
if (!newValue) {
// Remove proxy system proxy settings
ConfigUtil.setConfigItem('proxyRules', '');
ipcRenderer.send('forward-message', 'reload-proxy', false);
}
ConfigUtil.setConfigItem("useSystemProxy", newValue);
updateProxyOption();
},
});
generateSettingOption({
$element: $root.querySelector("#use-manual-settings .setting-control")!,
value: ConfigUtil.getConfigItem("useManualProxy", false),
clickHandler() {
const newValue = !ConfigUtil.getConfigItem("useManualProxy", false);
const systemProxyValue = ConfigUtil.getConfigItem(
"useSystemProxy",
false,
);
toggleManualProxySettings(newValue);
if (systemProxyValue && newValue) {
ConfigUtil.setConfigItem("useSystemProxy", !systemProxyValue);
}
ConfigUtil.setConfigItem('useSystemProxy', newValue);
this.updateProxyOption();
}
});
this.generateSettingOption({
$element: document.querySelector('#use-manual-settings .setting-control'),
value: ConfigUtil.getConfigItem('useManualProxy', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('useManualProxy');
const systemProxyValue = ConfigUtil.getConfigItem('useSystemProxy');
this.toggleManualProxySettings(newValue);
if (systemProxyValue && newValue) {
ConfigUtil.setConfigItem('useSystemProxy', !systemProxyValue);
}
ConfigUtil.setConfigItem('proxyRules', '');
ConfigUtil.setConfigItem('useManualProxy', newValue);
// Reload app only when turning manual proxy off, hence !newValue
ipcRenderer.send('forward-message', 'reload-proxy', !newValue);
this.updateProxyOption();
}
});
}
ConfigUtil.setConfigItem("proxyRules", "");
ConfigUtil.setConfigItem("useManualProxy", newValue);
// Reload app only when turning manual proxy off, hence !newValue
ipcRenderer.send("forward-message", "reload-proxy", !newValue);
updateProxyOption();
},
});
}
}

View File

@@ -1,111 +1,108 @@
import {ipcRenderer, remote} from 'electron';
import {dialog} from "@electron/remote";
import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-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;
type NewServerFormProperties = {
$root: Element;
onChange: () => void;
};
interface NewServerFormProps {
$root: Element;
onChange: () => void;
}
export default class NewServerForm extends BaseComponent {
props: NewServerFormProps;
$newServerForm: Element;
$saveServerButton: HTMLButtonElement;
$newServerUrl: HTMLInputElement;
constructor(props: NewServerFormProps) {
super();
this.props = props;
}
template(): string {
return `
<div class="server-input-container">
<div class="title">${t.__('Organization URL')}</div>
<div class="add-server-info-row">
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
</div>
<div class="server-center">
<button id="connect">${t.__('Connect')}</button>
</div>
<div class="server-center">
<div class="divider">
<hr class="left"/>${t.__('OR')}<hr class="right" />
</div>
</div>
<div class="server-center">
<button id="open-create-org-link">${t.__('Create a new organization')}</button>
</div>
<div class="server-center">
<div class="server-network-option">
<span id="open-network-settings">${t.__('Network and Proxy Settings')}</span>
<i class="material-icons open-network-button">open_in_new</i>
</div>
</div>
</div>
`;
}
init(): void {
this.initForm();
this.initActions();
}
initForm(): void {
this.$newServerForm = this.generateNodeFromTemplate(this.template());
this.$saveServerButton = this.$newServerForm.querySelector('#connect');
this.props.$root.innerHTML = '';
this.props.$root.append(this.$newServerForm);
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
}
async submitFormHandler(): Promise<void> {
this.$saveServerButton.innerHTML = 'Connecting...';
let serverConf;
try {
serverConf = await DomainUtil.checkDomain(this.$newServerUrl.value);
} catch (error) {
this.$saveServerButton.innerHTML = 'Connect';
await dialog.showMessageBox({
type: 'error',
message: error.toString(),
buttons: ['OK']
});
return;
}
await DomainUtil.addDomain(serverConf);
this.props.onChange();
}
openCreateNewOrgExternalLink(): void {
const link = 'https://zulip.com/new/';
const externalCreateNewOrgElement = document.querySelector('#open-create-org-link');
externalCreateNewOrgElement.addEventListener('click', async () => {
await LinkUtil.openBrowser(new URL(link));
});
}
networkSettingsLink(): void {
const networkSettingsId = document.querySelectorAll('.server-network-option')[0];
networkSettingsId.addEventListener('click', () => ipcRenderer.send('forward-message', 'open-network-settings'));
}
initActions(): void {
this.$saveServerButton.addEventListener('click', async () => {
await this.submitFormHandler();
});
this.$newServerUrl.addEventListener('keypress', async event => {
if (event.key === 'Enter') {
await this.submitFormHandler();
}
});
// Open create new org link in default browser
this.openCreateNewOrgExternalLink();
this.networkSettingsLink();
}
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="${t.__(
"your-organization.zulipchat.com or zulip.your-organization.com",
)}"
/>
</div>
<div class="server-center">
<button id="connect">${t.__("Connect")}</button>
</div>
<div class="server-center">
<div class="divider">
<hr class="left" />
${t.__("OR")}
<hr class="right" />
</div>
</div>
<div class="server-center">
<button id="open-create-org-link">
${t.__("Create a new organization")}
</button>
</div>
<div class="server-center">
<div class="server-network-option">
<span id="open-network-settings"
>${t.__("Network and Proxy Settings")}</span
>
<i class="material-icons open-network-button">open_in_new</i>
</div>
</div>
</div>
`);
const $saveServerButton: HTMLButtonElement =
$newServerForm.querySelector("#connect")!;
$root.textContent = "";
$root.append($newServerForm);
const $newServerUrl: HTMLInputElement = $newServerForm.querySelector(
"input.setting-input-value",
)!;
async function submitFormHandler(): Promise<void> {
$saveServerButton.textContent = t.__("Connecting…");
let serverConfig;
try {
serverConfig = await DomainUtil.checkDomain($newServerUrl.value.trim());
} catch (error: unknown) {
$saveServerButton.textContent = t.__("Connect");
await dialog.showMessageBox({
type: "error",
message:
error instanceof Error
? `${error.name}: ${error.message}`
: t.__("Unknown error"),
buttons: [t.__("OK")],
});
return;
}
await DomainUtil.addDomain(serverConfig);
onChange();
}
$saveServerButton.addEventListener("click", async () => {
await submitFormHandler();
});
$newServerUrl.addEventListener("keypress", async (event) => {
if (event.key === "Enter") {
await submitFormHandler();
}
});
// Open create new org link in default browser
const link = "https://zulip.com/new/";
const externalCreateNewOrgElement = $root.querySelector(
"#open-create-org-link",
)!;
externalCreateNewOrgElement.addEventListener("click", async () => {
await LinkUtil.openBrowser(new URL(link));
});
const networkSettingsId = $root.querySelector(".server-network-option")!;
networkSettingsId.addEventListener("click", () => {
ipcRenderer.send("forward-message", "open-network-settings");
});
}

View File

@@ -1,130 +1,144 @@
import {ipcRenderer} from 'electron';
import type {IpcRendererEvent} from "electron/renderer";
import process from "node:process";
import BaseComponent from '../../components/base';
import Nav from './nav';
import ServersSection from './servers-section';
import GeneralSection from './general-section';
import NetworkSection from './network-section';
import ConnectedOrgSection from './connected-org-section';
import ShortcutsSection from './shortcuts-section';
import type {DNDSettings} from '../../utils/dnd-util';
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";
type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection;
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";
export default class PreferenceView extends BaseComponent {
$sidebarContainer: Element;
$settingsContainer: Element;
nav: Nav;
section: Section;
constructor() {
super();
this.$sidebarContainer = document.querySelector('#sidebar');
this.$settingsContainer = document.querySelector('#settings-container');
}
export class PreferenceView {
static async create(): Promise<PreferenceView> {
return new PreferenceView(
await (
await fetch(new URL("app/renderer/preference.html", bundleUrl))
).text(),
);
}
init(): void {
this.nav = new Nav({
$root: this.$sidebarContainer,
onItemSelected: this.handleNavigation.bind(this)
});
readonly $view: HTMLElement;
private readonly $shadow: ShadowRoot;
private readonly $settingsContainer: Element;
private readonly nav: Nav;
private navigationItem: NavigationItem = "General";
this.setDefaultView();
this.registerIpcs();
}
private constructor(templateHtml: string) {
this.$view = document.createElement("div");
this.$shadow = this.$view.attachShadow({mode: "open"});
this.$shadow.innerHTML = templateHtml;
setDefaultView(): void {
let nav = 'General';
const hasTag = window.location.hash;
if (hasTag) {
nav = hasTag.slice(1);
}
const $sidebarContainer = this.$shadow.querySelector("#sidebar")!;
this.$settingsContainer = this.$shadow.querySelector(
"#settings-container",
)!;
this.handleNavigation(nav);
}
this.nav = new Nav({
$root: $sidebarContainer,
onItemSelected: this.handleNavigation,
});
handleNavigation(navItem: string): void {
this.nav.select(navItem);
switch (navItem) {
case 'AddServer': {
this.section = new ServersSection({
$root: this.$settingsContainer
});
break;
}
ipcRenderer.on("toggle-sidebar", this.handleToggleSidebar);
ipcRenderer.on("toggle-autohide-menubar", this.handleToggleMenubar);
ipcRenderer.on("toggle-dnd", this.handleToggleDnd);
case 'General': {
this.section = new GeneralSection({
$root: this.$settingsContainer
});
break;
}
this.handleNavigation(this.navigationItem);
}
case 'Organizations': {
this.section = new ConnectedOrgSection({
$root: this.$settingsContainer
});
break;
}
handleNavigation = (navigationItem: NavigationItem): void => {
this.navigationItem = navigationItem;
this.nav.select(navigationItem);
switch (navigationItem) {
case "AddServer": {
initServersSection({
$root: this.$settingsContainer,
});
break;
}
case 'Network': {
this.section = new NetworkSection({
$root: this.$settingsContainer
});
break;
}
case "General": {
initGeneralSection({
$root: this.$settingsContainer,
});
break;
}
case 'Shortcuts': {
this.section = new ShortcutsSection({
$root: this.$settingsContainer
});
break;
}
case "Organizations": {
initConnectedOrgSection({
$root: this.$settingsContainer,
});
break;
}
default: break;
}
case "Network": {
initNetworkSection({
$root: this.$settingsContainer,
});
break;
}
this.section.init();
window.location.hash = `#${navItem}`;
}
case "Shortcuts": {
initShortcutsSection({
$root: this.$settingsContainer,
});
break;
}
}
// Handle toggling and reflect changes in preference page
handleToggle(elementName: string, state: boolean): void {
const inputSelector = `#${elementName} .action .switch input`;
const input: HTMLInputElement = document.querySelector(inputSelector);
if (input) {
input.checked = state;
}
}
location.hash = `#${navigationItem}`;
};
registerIpcs(): void {
ipcRenderer.on('switch-settings-nav', (_event: Event, navItem: string) => {
this.handleNavigation(navItem);
});
handleToggleTray(state: boolean) {
this.handleToggle("tray-option", state);
}
ipcRenderer.on('toggle-sidebar-setting', (_event: Event, state: boolean) => {
this.handleToggle('sidebar-option', state);
});
destroy(): void {
ipcRenderer.off("toggle-sidebar", this.handleToggleSidebar);
ipcRenderer.off("toggle-autohide-menubar", this.handleToggleMenubar);
ipcRenderer.off("toggle-dnd", this.handleToggleDnd);
}
ipcRenderer.on('toggle-menubar-setting', (_event: Event, state: boolean) => {
this.handleToggle('menubar-option', state);
});
// Handle toggling and reflect changes in preference page
private handleToggle(elementName: string, state = false): void {
const inputSelector = `#${elementName} .action .switch input`;
const input: HTMLInputElement = this.$shadow.querySelector(inputSelector)!;
if (input) {
input.checked = state;
}
}
ipcRenderer.on('toggletray', (_event: Event, state: boolean) => {
this.handleToggle('tray-option', state);
});
private readonly handleToggleSidebar = (
_event: IpcRendererEvent,
state: boolean,
) => {
this.handleToggle("sidebar-option", state);
};
ipcRenderer.on('toggle-dnd', (_event: Event, _state: boolean, newSettings: DNDSettings) => {
this.handleToggle('show-notification-option', newSettings.showNotification);
this.handleToggle('silent-option', newSettings.silent);
private readonly handleToggleMenubar = (
_event: IpcRendererEvent,
state: boolean,
) => {
this.handleToggle("menubar-option", state);
};
if (process.platform === 'win32') {
this.handleToggle('flash-taskbar-option', newSettings.flashTaskbarOnMessage);
}
});
}
private readonly handleToggleDnd = (
_event: IpcRendererEvent,
_state: boolean,
newSettings: Partial<DndSettings>,
) => {
this.handleToggle("show-notification-option", newSettings.showNotification);
this.handleToggle("silent-option", newSettings.silent);
if (process.platform === "win32") {
this.handleToggle(
"flash-taskbar-option",
newSettings.flashTaskbarOnMessage,
);
}
};
}
window.addEventListener('load', () => {
const preferenceView = new PreferenceView();
preferenceView.init();
});

View File

@@ -1,97 +1,83 @@
import {remote, ipcRenderer} from 'electron';
import {dialog} from "@electron/remote";
import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util';
import * as Messages from '../../../../resources/messages';
import * as t from '../../utils/translation-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;
type ServerInfoFormProperties = {
$root: Element;
server: ServerConfig;
index: number;
onChange: () => void;
};
interface ServerInfoFormProps {
$root: Element;
server: DomainUtil.ServerConf;
index: number;
onChange: () => void;
}
export default class ServerInfoForm extends BaseComponent {
props: ServerInfoFormProps;
$serverInfoForm: Element;
$serverInfoAlias: Element;
$serverIcon: Element;
$deleteServerButton: Element;
$openServerButton: Element;
constructor(props: ServerInfoFormProps) {
super();
this.props = props;
}
template(): string {
return `
<div class="settings-card">
<div class="server-info-left">
<img class="server-info-icon" src="${this.props.server.icon}"/>
<div class="server-info-row">
<span class="server-info-alias">${this.props.server.alias}</span>
<i class="material-icons open-tab-button">open_in_new</i>
</div>
</div>
<div class="server-info-right">
<div class="server-info-row server-url">
<span class="server-url-info" title="${this.props.server.url}">${this.props.server.url}</span>
</div>
<div class="server-info-row">
<div class="action red server-delete-action">
<span>${t.__('Disconnect')}</span>
</div>
</div>
</div>
</div>
`;
}
init(): void {
this.initForm();
this.initActions();
}
initForm(): void {
this.$serverInfoForm = this.generateNodeFromTemplate(this.template());
this.$serverInfoAlias = this.$serverInfoForm.querySelectorAll('.server-info-alias')[0];
this.$serverIcon = this.$serverInfoForm.querySelectorAll('.server-info-icon')[0];
this.$deleteServerButton = this.$serverInfoForm.querySelectorAll('.server-delete-action')[0];
this.$openServerButton = this.$serverInfoForm.querySelectorAll('.open-tab-button')[0];
this.props.$root.append(this.$serverInfoForm);
}
initActions(): void {
this.$deleteServerButton.addEventListener('click', async () => {
const {response} = await dialog.showMessageBox({
type: 'warning',
buttons: [t.__('YES'), t.__('NO')],
defaultId: 0,
message: t.__('Are you sure you want to disconnect this organization?')
});
if (response === 0) {
if (DomainUtil.removeDomain(this.props.index)) {
ipcRenderer.send('reload-full-app');
} else {
const {title, content} = Messages.orgRemovalError(DomainUtil.getDomain(this.props.index).url);
dialog.showErrorBox(title, content);
}
}
});
this.$openServerButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
});
this.$serverInfoAlias.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
});
this.$serverIcon.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
});
}
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="${DomainUtil.iconAsUrl(properties.server.icon)}"
/>
<div class="server-info-row">
<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="${properties.server.url}"
>${properties.server.url}</span
>
</div>
<div class="server-info-row">
<div class="action red server-delete-action">
<span>${t.__("Disconnect")}</span>
</div>
</div>
</div>
</div>
`);
const $serverInfoAlias = $serverInfoForm.querySelector(".server-info-alias")!;
const $serverIcon = $serverInfoForm.querySelector(".server-info-icon")!;
const $deleteServerButton = $serverInfoForm.querySelector(
".server-delete-action",
)!;
const $openServerButton = $serverInfoForm.querySelector(".open-tab-button")!;
properties.$root.append($serverInfoForm);
$deleteServerButton.addEventListener("click", async () => {
const {response} = await dialog.showMessageBox({
type: "warning",
buttons: [t.__("Yes"), t.__("No")],
defaultId: 0,
message: t.__("Are you sure you want to disconnect this organization?"),
});
if (response === 0) {
if (DomainUtil.removeDomain(properties.index)) {
ipcRenderer.send("reload-full-app");
} else {
const {title, content} = Messages.orgRemovalError(
DomainUtil.getDomain(properties.index).url,
);
dialog.showErrorBox(title, content);
}
}
});
$openServerButton.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", properties.index);
});
$serverInfoAlias.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", properties.index);
});
$serverIcon.addEventListener("click", () => {
ipcRenderer.send("forward-message", "switch-server-tab", properties.index);
});
}

View File

@@ -1,49 +1,28 @@
import BaseSection from './base-section';
import NewServerForm from './new-server-form';
import * as t from '../../utils/translation-util';
import {html} from "../../../../common/html.ts";
import * as t from "../../../../common/translation-util.ts";
interface ServersSectionProps {
$root: Element;
}
export default class ServersSection extends BaseSection {
props: ServersSectionProps;
$newServerContainer: Element;
constructor(props: ServersSectionProps) {
super();
this.props = props;
}
template(): string {
return `
<div class="add-server-modal">
<div class="modal-container">
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">${t.__('Add a Zulip organization')}</div>
<div id="new-server-container"></div>
</div>
</div>
</div>
`;
}
init(): void {
this.initServers();
}
initServers(): void {
this.props.$root.innerHTML = '';
this.props.$root.innerHTML = this.template();
this.$newServerContainer = document.querySelector('#new-server-container');
this.initNewServerForm();
}
initNewServerForm(): void {
new NewServerForm({
$root: this.$newServerContainer,
onChange: this.reloadApp
}).init();
}
import {reloadApp} from "./base-section.ts";
import {initNewServerForm} from "./new-server-form.ts";
type ServersSectionProperties = {
$root: Element;
};
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">
<div class="page-title">${t.__("Add a Zulip organization")}</div>
<div id="new-server-container"></div>
</div>
</div>
</div>
`.html;
const $newServerContainer = $root.querySelector("#new-server-container")!;
initNewServerForm({
$root: $newServerContainer,
onChange: reloadApp,
});
}

View File

@@ -1,345 +1,235 @@
import BaseSection from './base-section';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
import process from "node:process";
interface ShortcutsSectionProps {
$root: Element;
}
export default class ShortcutsSection extends BaseSection {
props: ShortcutsSectionProps;
constructor(props: ShortcutsSectionProps) {
super();
this.props = props;
}
// TODO - Deduplicate templateMac and templateWinLin functions. In theory
// they both should be the same the only thing different should be the userOSKey
// variable but there seems to be inconsistences between both function, one has more
// lines though one may just be using more new lines and other thing is the use of +.
templateMac(): string {
const userOSKey = '⌘';
return `
<div class="settings-pane">
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>${t.__('Tip')}: </b>${t.__('These desktop app shortcuts extend the Zulip webapp\'s')} <span id="open-hotkeys-link"> ${t.__('keyboard shortcuts')}</span>.</p></div>
<div class="title">${t.__('Application Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>,</kbd></td>
<td>${t.__('Settings')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>K</kbd></td>
<td>${t.__('Keyboard Shortcuts')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
<td>${t.__('Toggle Do Not Disturb')}</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>D</kbd></td>
<td>${t.__('Reset App Settings')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>L</kbd></td>
<td>${t.__('Log Out')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>${t.__('Hide Zulip')}</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>${t.__('Hide Others')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Q</kbd></td>
<td>${t.__('Quit Zulip')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">${t.__('Edit Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>${t.__('Undo')}</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>${t.__('Redo')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>X</kbd></td>
<td>${t.__('Cut')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>C</kbd></td>
<td>${t.__('Copy')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
<td>${t.__('Paste')}</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
<td>${t.__('Paste and Match Style')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>A</kbd></td>
<td>${t.__('Select All')}</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>Space</kbd></td>
<td>${t.__('Emoji & Symbols')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">${t.__('View Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>${t.__('Reload')}</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>${t.__('Hard Reload')}</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
<td>${t.__('Enter Full Screen')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>+</kbd></td>
<td>${t.__('Zoom In')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>-</kbd></td>
<td>${t.__('Zoom Out')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>0</kbd></td>
<td>${t.__('Actual Size')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
<td>${t.__('Toggle Sidebar')}</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>I</kbd></td>
<td>${t.__('Toggle DevTools for Zulip App')}</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>U</kbd></td>
<td>${t.__('Toggle DevTools for Active Tab')}</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Tab</kbd></td>
<td>${t.__('Switch to Next Organization')}</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
<td>${t.__('Switch to Previous Organization')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">${t.__('History Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>←</kbd></td>
<td>${t.__('Back')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>→</kbd></td>
<td>${t.__('Forward')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Window Shortcuts</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>M</kbd></td>
<td>${t.__('Minimize')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>W</kbd></td>
<td>${t.__('Close')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
</div>
`;
}
templateWinLin(): string {
const userOSKey = 'Ctrl';
return `
<div class="settings-pane">
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>${t.__('Tip')}: </b>${t.__('These desktop app shortcuts extend the Zulip webapp\'s')} <span id="open-hotkeys-link"> ${t.__('keyboard shortcuts')}</span>.</p></div>
<div class="title">${t.__('Application Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>,</kbd></td>
<td>${t.__('Settings')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>K</kbd></td>
<td>${t.__('Keyboard Shortcuts')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
<td>${t.__('Toggle Do Not Disturb')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>L</kbd></td>
<td>${t.__('Log Out')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Q</kbd></td>
<td>${t.__('Quit Zulip')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">${t.__('Edit Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Z</kbd></td>
<td>${t.__('Undo')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Y</kbd></td>
<td>${t.__('Redo')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>X</kbd></td>
<td>${t.__('Cut')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>C</kbd></td>
<td>${t.__('Copy')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>V</kbd></td>
<td>${t.__('Paste')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd></td>
<td>${t.__('Paste and Match Style')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>A</kbd></td>
<td>${t.__('Select All')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">${t.__('View Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>R</kbd></td>
<td>${t.__('Reload')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd></td>
<td>${t.__('Hard Reload')}</td>
</tr>
<tr>
<td><kbd>F11</kbd></td>
<td>${t.__('Toggle Full Screen')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>=</kbd></td>
<td>${t.__('Zoom In')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>-</kbd></td>
<td>${t.__('Zoom Out')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>0</kbd></td>
<td>${t.__('Actual Size')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
<td>${t.__('Toggle Sidebar')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd></td>
<td>${t.__('Toggle DevTools for Zulip App')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>U</kbd></td>
<td>${t.__('Toggle DevTools for Active Tab')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Tab</kbd></td>
<td>${t.__('Switch to Next Organization')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
<td>${t.__('Switch to Previous Organization')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">${t.__('History Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>Alt</kbd> + <kbd>←</kbd></td>
<td>${t.__('Back')}</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>→</kbd></td>
<td>${t.__('Forward')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">${t.__('Window Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>M</kbd></td>
<td>${t.__('Minimize')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>W</kbd></td>
<td>${t.__('Close')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
</div>
`;
}
openHotkeysExternalLink(): void {
const link = 'https://zulip.com/help/keyboard-shortcuts';
const externalCreateNewOrgElement = document.querySelector('#open-hotkeys-link');
externalCreateNewOrgElement.addEventListener('click', async () => {
await LinkUtil.openBrowser(new URL(link));
});
}
init(): void {
this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin();
this.openHotkeysExternalLink();
}
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({
$root,
}: ShortcutsSectionProperties): void {
const cmdOrCtrl = process.platform === "darwin" ? "⌘" : "Ctrl";
$root.innerHTML = html`
<div class="settings-pane">
<div class="settings-card tip">
<p>
<b><i class="material-icons md-14">settings</i>${t.__("Tip")}: </b
>${t.__("These desktop app shortcuts extend the Zulip webapp's")}
<span id="open-hotkeys-link"> ${t.__("keyboard shortcuts")}</span>.
</p>
</div>
<div class="title">${t.__("Application Shortcuts")}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>,</kbd></td>
<td>${t.__("Settings")}</td>
</tr>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>K</kbd></td>
<td>${t.__("Keyboard Shortcuts")}</td>
</tr>
<tr ${process.platform === "darwin" ? "hidden" : ""}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>Shift</kbd> + <kbd>M</kbd></td>
<td>${t.__("Toggle Do Not Disturb")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>Shift</kbd> + <kbd>${cmdOrCtrl}</kbd> + <kbd>M</kbd></td>
<td>${t.__("Toggle Do Not Disturb")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>Shift</kbd> + <kbd>${cmdOrCtrl}</kbd> + <kbd>D</kbd></td>
<td>${t.__("Reset App Settings")}</td>
</tr>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>L</kbd></td>
<td>${t.__("Log Out")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>H</kbd></td>
<td>${t.__("Hide Zulip")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>Option</kbd> + <kbd>${cmdOrCtrl}</kbd> + <kbd>H</kbd></td>
<td>${t.__("Hide Others")}</td>
</tr>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>Q</kbd></td>
<td>${t.__("Quit Zulip")}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">${t.__("Edit Shortcuts")}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>Z</kbd></td>
<td>${t.__("Undo")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>Shift</kbd> + <kbd>${cmdOrCtrl}</kbd> + <kbd>Z</kbd></td>
<td>${t.__("Redo")}</td>
</tr>
<tr ${process.platform === "darwin" ? "hidden" : ""}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>Y</kbd></td>
<td>${t.__("Redo")}</td>
</tr>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>X</kbd></td>
<td>${t.__("Cut")}</td>
</tr>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>C</kbd></td>
<td>${t.__("Copy")}</td>
</tr>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>V</kbd></td>
<td>${t.__("Paste")}</td>
</tr>
<tr ${process.platform === "darwin" ? "hidden" : ""}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd></td>
<td>${t.__("Paste and Match Style")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>Shift</kbd> + <kbd>${cmdOrCtrl}</kbd> + <kbd>V</kbd></td>
<td>${t.__("Paste and Match Style")}</td>
</tr>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>A</kbd></td>
<td>${t.__("Select All")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td>
<kbd>Control</kbd> + <kbd>${cmdOrCtrl}</kbd> + <kbd>Space</kbd>
</td>
<td>${t.__("Emoji & Symbols")}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">${t.__("View Shortcuts")}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>R</kbd></td>
<td>${t.__("Reload")}</td>
</tr>
<tr ${process.platform === "darwin" ? "hidden" : ""}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd></td>
<td>${t.__("Hard Reload")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>Shift</kbd> + <kbd>${cmdOrCtrl}</kbd> + <kbd>R</kbd></td>
<td>${t.__("Hard Reload")}</td>
</tr>
<tr ${process.platform === "darwin" ? "hidden" : ""}>
<td><kbd>F11</kbd></td>
<td>${t.__("Toggle Full Screen")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>Control</kbd> + <kbd>${cmdOrCtrl}</kbd> + <kbd>F</kbd></td>
<td>${t.__("Enter Full Screen")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>+</kbd></td>
<td>${t.__("Zoom In")}</td>
</tr>
<tr ${process.platform === "darwin" ? "hidden" : ""}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>=</kbd></td>
<td>${t.__("Zoom In")}</td>
</tr>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>-</kbd></td>
<td>${t.__("Zoom Out")}</td>
</tr>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>0</kbd></td>
<td>${t.__("Actual Size")}</td>
</tr>
<tr ${process.platform === "darwin" ? "hidden" : ""}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
<td>${t.__("Toggle Sidebar")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>Shift</kbd> + <kbd>${cmdOrCtrl}</kbd> + <kbd>S</kbd></td>
<td>${t.__("Toggle Sidebar")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>Option</kbd> + <kbd>${cmdOrCtrl}</kbd> + <kbd>I</kbd></td>
<td>${t.__("Toggle DevTools for Zulip App")}</td>
</tr>
<tr ${process.platform === "darwin" ? "hidden" : ""}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd></td>
<td>${t.__("Toggle DevTools for Zulip App")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>Option</kbd> + <kbd>${cmdOrCtrl}</kbd> + <kbd>U</kbd></td>
<td>${t.__("Toggle DevTools for Active Tab")}</td>
</tr>
<tr ${process.platform === "darwin" ? "hidden" : ""}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>Shift</kbd> + <kbd>U</kbd></td>
<td>${t.__("Toggle DevTools for Active Tab")}</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Tab</kbd></td>
<td>${t.__("Switch to Next Organization")}</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Tab</kbd></td>
<td>${t.__("Switch to Previous Organization")}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">${t.__("History Shortcuts")}</div>
<div class="settings-card">
<table>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>←</kbd></td>
<td>${t.__("Back")}</td>
</tr>
<tr ${process.platform === "darwin" ? "hidden" : ""}>
<td><kbd>Alt</kbd> + <kbd>←</kbd></td>
<td>${t.__("Back")}</td>
</tr>
<tr ${process.platform === "darwin" ? "" : "hidden"}>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>→</kbd></td>
<td>${t.__("Forward")}</td>
</tr>
<tr ${process.platform === "darwin" ? "hidden" : ""}>
<td><kbd>Alt</kbd> + <kbd>→</kbd></td>
<td>${t.__("Forward")}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">${t.__("Window Shortcuts")}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>M</kbd></td>
<td>${t.__("Minimize")}</td>
</tr>
<tr>
<td><kbd>${cmdOrCtrl}</kbd> + <kbd>W</kbd></td>
<td>${t.__("Close")}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
</div>
`.html;
const link = "https://zulip.com/help/keyboard-shortcuts";
const externalCreateNewOrgElement =
$root.querySelector("#open-hotkeys-link")!;
externalCreateNewOrgElement.addEventListener("click", async () => {
await LinkUtil.openBrowser(new URL(link));
});
}

View File

@@ -1,113 +1,29 @@
import {contextBridge, ipcRenderer, webFrame} from 'electron';
import fs from 'fs';
import {contextBridge} from "electron/renderer";
import isDev from 'electron-is-dev';
import electron_bridge, {bridgeEvents} from "./electron-bridge.ts";
import * as NetworkError from "./pages/network.ts";
import {ipcRenderer} from "./typed-ipc-renderer.ts";
import * as NetworkError from './pages/network';
contextBridge.exposeInMainWorld("electron_bridge", electron_bridge);
// eslint-disable-next-line import/no-unassigned-import
import './notification';
// Prevent drag and drop event in main process which prevents remote code executaion
// eslint-disable-next-line import/no-unassigned-import
import './shared/preventdrag';
import electron_bridge from './electron-bridge';
contextBridge.exposeInMainWorld('raw_electron_bridge', electron_bridge);
ipcRenderer.on('logout', () => {
// 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();
ipcRenderer.on("logout", () => {
bridgeEvents.emit("logout");
});
ipcRenderer.on('shortcut', () => {
// 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();
}
ipcRenderer.on("show-keyboard-shortcuts", () => {
bridgeEvents.emit("show-keyboard-shortcuts");
});
ipcRenderer.on('show-notification-settings', () => {
// 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);
ipcRenderer.on("show-notification-settings", () => {
bridgeEvents.emit("show-notification-settings");
});
electron_bridge.once('zulip-loaded', () => {
// Redirect users to network troubleshooting page
const getRestartButton = document.querySelector('.restart_get_events_button');
if (getRestartButton) {
getRestartButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
}
window.addEventListener("load", () => {
if (!location.href.includes("app/renderer/network.html")) {
return;
}
const $reconnectButton = document.querySelector("#reconnect")!;
const $settingsButton = document.querySelector("#settings")!;
NetworkError.init($reconnectButton, $settingsButton);
});
window.addEventListener('load', (event: any): void => {
if (!event.target.URL.includes('app/renderer/network.html')) {
return;
}
const $reconnectButton = document.querySelector('#reconnect');
const $settingsButton = document.querySelector('#settings');
NetworkError.init($reconnectButton, $settingsButton);
});
// Electron's globalShortcut can cause unexpected results
// so adding the reload shortcut in the old-school way
// Zoom from numpad keys is not supported by electron, so adding it through listeners.
document.addEventListener('keydown', event => {
const cmdOrCtrl = event.ctrlKey || event.metaKey;
if (event.code === 'F5') {
ipcRenderer.send('forward-message', 'hard-reload');
} else if (cmdOrCtrl && (event.code === 'NumpadAdd' || event.code === 'Equal')) {
ipcRenderer.send('forward-message', 'zoomIn');
} else if (cmdOrCtrl && event.code === 'NumpadSubtract') {
ipcRenderer.send('forward-message', 'zoomOut');
} else if (cmdOrCtrl && event.code === 'Numpad0') {
ipcRenderer.send('forward-message', 'zoomActualSize');
}
});
// Set user as active and update the time of last activity
ipcRenderer.on('set-active', () => {
if (isDev) {
console.log('active');
}
electron_bridge.idle_on_system = false;
electron_bridge.last_active_on_system = Date.now();
});
// Set user as idle and time of last activity is left unchanged
ipcRenderer.on('set-idle', () => {
if (isDev) {
console.log('idle');
}
electron_bridge.idle_on_system = true;
});
(async () => webFrame.executeJavaScript(
fs.readFileSync(require.resolve('./injected'), 'utf8')
))();

View File

@@ -1,17 +0,0 @@
// This is a security fix. Following function prevents drag and drop event in the app
// so that attackers can't execute any remote code within the app
// It doesn't affect the compose box so that users can still
// use drag and drop event to share files etc
const preventDragAndDrop = (): void => {
const preventEvents = ['dragover', 'drop'];
preventEvents.forEach(dragEvents => {
document.addEventListener(dragEvents, event => {
event.preventDefault();
});
});
};
preventDragAndDrop();
export {};

View File

@@ -1,94 +1,116 @@
import {ipcRenderer, remote, WebviewTag, NativeImage} from 'electron';
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 path from 'path';
import * as ConfigUtil from './utils/config-util';
import {BrowserWindow, Menu, Tray} from "@electron/remote";
const {Tray, Menu, nativeImage, BrowserWindow} = remote;
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";
let tray: Electron.Tray;
import type {ServerManagerView} from "./main.ts";
import {ipcRenderer} from "./typed-ipc-renderer.ts";
const ICON_DIR = '../../resources/tray';
let tray: ElectronTray | null = null;
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';
}
if (process.platform === "linux") {
return appIcon + "linux.png";
}
return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'macOSTemplate.png');
return (
appIcon + (process.platform === "win32" ? "win.ico" : "macOSTemplate.png")
);
};
const winUnreadTrayIconPath = (): string => appIcon + "unread.ico";
let unread = 0;
const trayIconSize = (): number => {
switch (process.platform) {
case 'darwin':
return 20;
case 'win32':
return 100;
case 'linux':
return 100;
default: return 80;
}
switch (process.platform) {
case "darwin": {
return 20;
}
case "win32": {
return 100;
}
case "linux": {
return 100;
}
default: {
return 80;
}
}
};
// Default config for Icon we might make it OS specific if needed like the size
const config = {
pixelRatio: window.devicePixelRatio,
unreadCount: 0,
showUnreadCount: true,
unreadColor: '#000000',
readColor: '#000000',
unreadBackgroundColor: '#B9FEEA',
readBackgroundColor: '#B9FEEA',
size: trayIconSize(),
thick: process.platform === 'win32'
pixelRatio: window.devicePixelRatio,
unreadCount: 0,
showUnreadCount: true,
unreadColor: "#000000",
readColor: "#000000",
unreadBackgroundColor: "#B9FEEA",
readBackgroundColor: "#B9FEEA",
size: trayIconSize(),
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 color = config.unreadCount ? config.unreadColor : config.readColor;
const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor;
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');
const canvas = document.createElement("canvas");
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();
// Count or Icon
if (HAS_COUNT) {
ctx.fillStyle = color;
ctx.textAlign = 'center';
if (config.unreadCount > 99) {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`;
ctx.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));
} else {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.15));
}
}
// Circle
// 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 (hasCount) {
context.fillStyle = color;
context.textAlign = "center";
if (config.unreadCount > 99) {
context.font = `${config.thick ? "bold " : ""}${size * 0.4}px Helvetica`;
context.fillText("99+", center, center + size * 0.15);
} else if (config.unreadCount < 10) {
context.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`;
context.fillText(String(config.unreadCount), center, center + size * 0.2);
} else {
context.font = `${config.thick ? "bold " : ""}${size * 0.5}px Helvetica`;
context.fillText(
String(config.unreadCount),
center,
center + size * 0.15,
);
}
}
return canvas;
return canvas;
};
/**
@@ -96,125 +118,134 @@ const renderCanvas = function (arg: number): HTMLCanvasElement {
* @param arg: Unread count
* @return the native image
*/
const renderNativeImage = function (arg: number): NativeImage {
const canvas = renderCanvas(arg);
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
return nativeImage.createFromBuffer(pngData, {
scaleFactor: config.pixelRatio
});
const renderNativeImage = function (argument: number): NativeImage {
if (process.platform === "win32") {
return nativeImage.createFromPath(winUnreadTrayIconPath());
}
const canvas = renderCanvas(argument);
const pngData = nativeImage
.createFromDataURL(canvas.toDataURL("image/png"))
.toPNG();
return nativeImage.createFromBuffer(pngData, {
scaleFactor: config.pixelRatio,
});
};
function sendAction(action: string): void {
const win = BrowserWindow.getAllWindows()[0];
function sendAction<Channel extends keyof RendererMessage>(
channel: Channel,
...arguments_: Parameters<RendererMessage[Channel]>
): void {
const win = BrowserWindow.getAllWindows()[0];
if (process.platform === 'darwin') {
win.restore();
}
if (process.platform === "darwin") {
win.restore();
}
win.webContents.send(action);
ipcRenderer.send("forward-to", win.webContents.id, channel, ...arguments_);
}
const createTray = function (): void {
const contextMenu = Menu.buildFromTemplate([
{
label: 'Zulip',
click() {
ipcRenderer.send('focus-app');
}
},
{
label: 'Settings',
click() {
ipcRenderer.send('focus-app');
sendAction('open-settings');
}
},
{
type: 'separator'
},
{
label: 'Quit',
click() {
ipcRenderer.send('quit-app');
}
}
]);
tray = new Tray(iconPath());
tray.setContextMenu(contextMenu);
if (process.platform === 'linux' || process.platform === 'win32') {
tray.on('click', () => {
ipcRenderer.send('toggle-app');
});
}
const contextMenu = Menu.buildFromTemplate([
{
label: t.__("Zulip"),
click() {
ipcRenderer.send("focus-app");
},
},
{
label: t.__("Settings"),
click() {
ipcRenderer.send("focus-app");
sendAction("open-settings");
},
},
{
type: "separator",
},
{
label: t.__("Quit"),
click() {
ipcRenderer.send("quit-app");
},
},
]);
tray = new Tray(iconPath());
tray.setContextMenu(contextMenu);
if (process.platform === "linux" || process.platform === "win32") {
tray.on("click", () => {
ipcRenderer.send("toggle-app");
});
}
};
ipcRenderer.on('destroytray', (event: Event): Event => {
if (!tray) {
return undefined;
}
export function initializeTray(serverManagerView: ServerManagerView) {
ipcRenderer.on("destroytray", () => {
if (!tray) {
return;
}
tray.destroy();
if (tray.isDestroyed()) {
tray = null;
} else {
throw new Error('Tray icon not properly destroyed.');
}
tray.destroy();
if (tray.isDestroyed()) {
tray = null;
} else {
throw new Error("Tray icon not properly destroyed.");
}
});
return event;
});
ipcRenderer.on("tray", (_event, argument: number): void => {
if (!tray) {
return;
}
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 (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}`},
),
);
}
}
});
// 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`);
}
}
});
function toggleTray(): void {
let state;
if (tray) {
state = false;
tray.destroy();
if (tray.isDestroyed()) {
tray = null;
}
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', 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);
const webContents = remote.webContents.fromId(webview.getWebContentsId());
webContents.send('toggletray', state);
ipcRenderer.on("toggletray", toggleTray);
if (ConfigUtil.getConfigItem("trayIcon", true)) {
createTray();
}
}
ipcRenderer.on('toggletray', toggleTray);
if (ConfigUtil.getConfigItem('trayIcon', true)) {
createTray();
}
export {};

View File

@@ -0,0 +1,69 @@
import {
type IpcRendererEvent,
ipcRenderer as untypedIpcRenderer, // eslint-disable-line no-restricted-imports
} from "electron/renderer";
import type {
MainCall,
MainMessage,
RendererMessage,
} from "../../common/typed-ipc.js";
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>(
channel: Channel,
listener: RendererListener<Channel>,
): void;
once<Channel extends keyof RendererMessage>(
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>,
): void;
removeAllListeners(channel: keyof RendererMessage): void;
send<Channel extends keyof RendererMessage>(
channel: "forward-message",
rendererChannel: 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,
...arguments_: Parameters<MainMessage[Channel]>
): void;
invoke<Channel extends keyof MainCall>(
channel: Channel,
...arguments_: Parameters<MainCall[Channel]>
): Promise<ReturnType<MainCall[Channel]>>;
sendSync<Channel extends keyof MainMessage>(
channel: Channel,
...arguments_: Parameters<MainMessage[Channel]>
): ReturnType<MainMessage[Channel]>;
postMessage<Channel extends keyof MainMessage>(
channel: Channel,
message: Parameters<MainMessage[Channel]> extends [infer Message]
? Message
: never,
transfer?: MessagePort[],
): void;
sendToHost<Channel extends keyof RendererMessage>(
channel: Channel,
...arguments_: Parameters<RendererMessage[Channel]>
): void;
} = untypedIpcRenderer;

View File

@@ -1,78 +0,0 @@
import electron from 'electron';
import {JsonDB} from 'node-json-db';
import {initSetUp} from './default-util';
import fs from 'fs';
import path from 'path';
import Logger from './logger-util';
const {app, dialog} =
process.type === 'renderer' ? electron.remote : electron;
initSetUp();
const logger = new Logger({
file: 'certificate-util.log',
timestamp: true
});
const certificatesDir = `${app.getPath('userData')}/certificates`;
let db: JsonDB;
reloadDB();
export function getCertificate(server: string): string | undefined {
reloadDB();
return db.getData('/')[server];
}
// Function to copy the certificate to userData folder
export function copyCertificate(_server: string, location: string, fileName: string): boolean {
let copied = false;
const filePath = `${certificatesDir}/${fileName}`;
try {
fs.copyFileSync(location, filePath);
copied = true;
} catch (error) {
dialog.showErrorBox(
'Error saving certificate',
'We encountered error while saving the certificate.'
);
logger.error('Error while copying the certificate to certificates folder.');
logger.error(error);
}
return copied;
}
export function setCertificate(server: string, fileName: string): void {
const filePath = `${fileName}`;
db.push(`/${server}`, filePath, true);
reloadDB();
}
export function removeCertificate(server: string): void {
db.delete(`/${server}`);
reloadDB();
}
function reloadDB(): void {
const settingsJsonPath = path.join(app.getPath('userData'), '/config/certificates.json');
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
} catch (error) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
'Error saving settings',
'We encountered error while saving the certificate.'
);
logger.error('Error while JSON parsing certificates.json: ');
logger.error(error);
}
}
db = new JsonDB(settingsJsonPath, true, true);
}

View File

@@ -1,8 +0,0 @@
// Unescape already encoded/escaped strings
export function decodeString(stringInput: string): string {
const parser = new DOMParser();
const dom = parser.parseFromString(
'<!doctype html><body>' + stringInput,
'text/html');
return dom.body.textContent;
}

View File

@@ -1,105 +0,0 @@
import {JsonDB} from 'node-json-db';
import fs from 'fs';
import path from 'path';
import electron from 'electron';
import Logger from './logger-util';
import * as EnterpriseUtil from './enterprise-util';
const logger = new Logger({
file: 'config-util.log',
timestamp: true
});
let dialog: Electron.Dialog = null;
let app: Electron.App = null;
/* To make the util runnable in both main and renderer process */
if (process.type === 'renderer') {
const {remote} = electron;
dialog = remote.dialog;
app = remote.app;
} else {
dialog = electron.dialog;
app = electron.app;
}
let db: JsonDB;
reloadDB();
export function getConfigItem(key: string, defaultValue: unknown = null): any {
try {
db.reload();
} catch (error) {
logger.error('Error while reloading settings.json: ');
logger.error(error);
}
const value = db.getData('/')[key];
if (value === undefined) {
setConfigItem(key, defaultValue);
return defaultValue;
}
return value;
}
export function getConfigString(key: string, defaultValue: string): string {
const value = getConfigItem(key, defaultValue);
if (typeof value === 'string') {
return value;
}
setConfigItem(key, defaultValue);
return defaultValue;
}
// This function returns whether a key exists in the configuration file (settings.json)
export function isConfigItemExists(key: string): boolean {
try {
db.reload();
} catch (error) {
logger.error('Error while reloading settings.json: ');
logger.error(error);
}
const value = db.getData('/')[key];
return (value !== undefined);
}
export function setConfigItem(key: string, value: unknown, override? : boolean): void {
if (EnterpriseUtil.configItemExists(key) && !override) {
// If item is in global config and we're not trying to override
return;
}
db.push(`/${key}`, value, true);
db.save();
}
export function removeConfigItem(key: string): void {
db.delete(`/${key}`);
db.save();
}
function reloadDB(): void {
const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.json');
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
} catch (error) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
'Error saving settings',
'We encountered an error while saving the settings.'
);
logger.error('Error while JSON parsing settings.json: ');
logger.error(error);
logger.reportSentry(error);
}
}
db = new JsonDB(settingsJsonPath, true, true);
}

View File

@@ -1,75 +0,0 @@
import electron from 'electron';
import fs from 'fs';
let app: Electron.App = null;
let setupCompleted = false;
if (process.type === 'renderer') {
app = electron.remote.app;
} else {
app = electron.app;
}
const zulipDir = app.getPath('userData');
const logDir = `${zulipDir}/Logs/`;
const certificatesDir = `${zulipDir}/certificates/`;
const configDir = `${zulipDir}/config/`;
export const initSetUp = (): void => {
// If it is the first time the app is running
// create zulip dir in userData folder to
// avoid errors
if (!setupCompleted) {
if (!fs.existsSync(zulipDir)) {
fs.mkdirSync(zulipDir);
}
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
if (!fs.existsSync(certificatesDir)) {
fs.mkdirSync(certificatesDir);
}
// Migrate config files from app data folder to config folder inside app
// data folder. This will be done once when a user updates to the new version.
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir);
const domainJson = `${zulipDir}/domain.json`;
const certificatesJson = `${zulipDir}/certificates.json`;
const settingsJson = `${zulipDir}/settings.json`;
const updatesJson = `${zulipDir}/updates.json`;
const windowStateJson = `${zulipDir}/window-state.json`;
const configData = [
{
path: domainJson,
fileName: 'domain.json'
},
{
path: certificatesJson,
fileName: 'certificates.json'
},
{
path: settingsJson,
fileName: 'settings.json'
},
{
path: updatesJson,
fileName: 'updates.json'
}
];
configData.forEach(data => {
if (fs.existsSync(data.path)) {
fs.copyFileSync(data.path, configDir + data.fileName);
fs.unlinkSync(data.path);
}
});
// `window-state.json` is only deleted not moved, as the electron-window-state
// package will recreate the file in the config folder.
if (fs.existsSync(windowStateJson)) {
fs.unlinkSync(windowStateJson);
}
}
setupCompleted = true;
}
};

View File

@@ -1,48 +0,0 @@
import * as ConfigUtil from './config-util';
type SettingName = 'showNotification' | 'silent' | 'flashTaskbarOnMessage';
export interface DNDSettings {
showNotification?: boolean;
silent?: boolean;
flashTaskbarOnMessage?: boolean;
}
interface Toggle {
dnd: boolean;
newSettings: DNDSettings;
}
export function toggle(): Toggle {
const dnd = !ConfigUtil.getConfigItem('dnd', false);
const dndSettingList: SettingName[] = ['showNotification', 'silent'];
if (process.platform === 'win32') {
dndSettingList.push('flashTaskbarOnMessage');
}
let newSettings: DNDSettings;
if (dnd) {
const oldSettings: DNDSettings = {};
newSettings = {};
// Iterate through the dndSettingList.
for (const settingName of dndSettingList) {
// Store the current value of setting.
oldSettings[settingName] = ConfigUtil.getConfigItem(settingName);
// New value of setting.
newSettings[settingName] = (settingName === 'silent');
}
// Store old value in oldSettings.
ConfigUtil.setConfigItem('dndPreviousSettings', oldSettings);
} else {
newSettings = ConfigUtil.getConfigItem('dndPreviousSettings');
}
for (const settingName of dndSettingList) {
ConfigUtil.setConfigItem(settingName, newSettings[settingName]);
}
ConfigUtil.setConfigItem('dnd', dnd);
return {dnd, newSettings};
}

View File

@@ -1,203 +1,236 @@
import {JsonDB} from 'node-json-db';
import fs from "node:fs";
import path from "node:path";
import fs from 'fs';
import path from 'path';
import Logger from './logger-util';
import {remote, ipcRenderer} from 'electron';
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 './enterprise-util';
import * as Messages from '../../../resources/messages';
const {app, dialog} = remote;
export interface ServerConf {
url: string;
alias?: string;
icon?: string;
}
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',
timestamp: true
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) {
return [];
}
return db.getData('/domains');
export function getDomains(): ServerConfig[] {
reloadDatabase();
try {
return serverConfigSchema
.array()
.parse(database.getObject<unknown>("/domains"));
} catch (error: unknown) {
if (!(error instanceof DataError)) throw error;
return [];
}
}
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: ServerConf): Promise<void> {
if (server.icon) {
const localIconUrl = await saveServerIcon(server);
server.icon = localIconUrl;
db.push('/domains[]', server, true);
reloadDB();
} else {
server.icon = defaultIconUrl;
db.push('/domains[]', server, true);
reloadDB();
}
export async function addDomain(server: {
url: string;
alias: string;
icon?: string;
}): Promise<void> {
if (server.icon) {
const localIconUrl = await saveServerIcon(server.icon);
server.icon = localIconUrl;
serverConfigSchema.parse(server);
database.push("/domains[]", server, true);
reloadDatabase();
} else {
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 {
if (EnterpriseUtil.isPresetOrg(getDomain(index).url)) {
return false;
}
if (EnterpriseUtil.isPresetOrg(getDomain(index).url)) {
return false;
}
db.delete(`/domains[${index}]`);
reloadDB();
return true;
database.delete(`/domains[${index}]`);
reloadDatabase();
return true;
}
// Check if domain is already added
export function duplicateDomain(domain: string): boolean {
domain = formatUrl(domain);
return getDomains().some(server => server.url === domain);
domain = formatUrl(domain);
return getDomains().some((server) => server.url === domain);
}
async function checkCertError(domain: string, serverConf: ServerConf, error: any, silent: boolean): Promise<ServerConf> {
if (silent) {
// Since getting server settings has already failed
return serverConf;
}
export async function checkDomain(
domain: string,
silent = false,
): Promise<ServerConfig> {
if (!silent && duplicateDomain(domain)) {
// Do not check duplicate in silent mode
throw new Error("This server has been added.");
}
// Report error to sentry to get idea of possible certificate errors
// users get when adding the servers
logger.reportSentry(error);
const certErrorMessage = Messages.certErrorMessage(domain, error);
const certErrorDetail = Messages.certErrorDetail();
domain = formatUrl(domain);
await dialog.showMessageBox({
type: 'error',
buttons: ['OK'],
message: certErrorMessage,
detail: certErrorDetail
});
throw new Error('Untrusted certificate.');
try {
return await getServerSettings(domain);
} catch {
throw new Error(Messages.invalidZulipServerError(domain));
}
}
export async function checkDomain(domain: string, silent = false): Promise<ServerConf> {
if (!silent && duplicateDomain(domain)) {
// Do not check duplicate in silent mode
throw new Error('This server has been added.');
}
domain = formatUrl(domain);
const serverConf = {
icon: defaultIconUrl,
url: domain,
alias: domain
};
try {
return await getServerSettings(domain);
} catch (error_) {
// Make sure that error is an error or string not undefined
// so validation does not throw error.
const error = error_ || '';
const certsError = error.toString().includes('certificate');
if (certsError) {
const result = await checkCertError(domain, serverConf, error, silent);
return result;
}
throw new Error(Messages.invalidZulipServerError(domain));
}
async function getServerSettings(domain: string): Promise<ServerConfig> {
return ipcRenderer.invoke("get-server-settings", domain);
}
async function getServerSettings(domain: string): Promise<ServerConf> {
return ipcRenderer.invoke('get-server-settings', domain);
export async function saveServerIcon(iconURL: string): Promise<string> {
return (
(await ipcRenderer.invoke("save-server-icon", iconURL)) ??
defaultIconSentinel
);
}
export async function saveServerIcon(server: ServerConf): Promise<string> {
return ipcRenderer.invoke('save-server-icon', server.icon);
export async function updateSavedServer(
url: string,
index: number,
): Promise<ServerConfig> {
// Does not promise successful update
const serverConfig = getDomain(index);
const oldIcon = serverConfig.icon;
try {
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);
Sentry.captureException(error);
return serverConfig;
}
}
export async function updateSavedServer(url: string, index: number): Promise<void> {
// Does not promise successful update
const oldIcon = getDomain(index).icon;
try {
const newServerConf = await checkDomain(url, true);
const localIconUrl = await saveServerIcon(newServerConf);
if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') {
newServerConf.icon = localIconUrl;
updateDomain(index, newServerConf);
reloadDB();
}
} catch (error) {
logger.log('Could not update server icon.');
logger.log(error);
logger.reportSentry(error);
}
}
function reloadDatabase(): void {
const domainJsonPath = path.join(
app.getPath("userData"),
"config/domain.json",
);
try {
const file = fs.readFileSync(domainJsonPath, "utf8");
JSON.parse(file);
} catch (error: unknown) {
if (fs.existsSync(domainJsonPath)) {
fs.unlinkSync(domainJsonPath);
dialog.showErrorBox(
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);
Sentry.captureException(error);
}
}
function reloadDB(): void {
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
try {
const file = fs.readFileSync(domainJsonPath, 'utf8');
JSON.parse(file);
} catch (error) {
if (fs.existsSync(domainJsonPath)) {
fs.unlinkSync(domainJsonPath);
dialog.showErrorBox(
'Error saving new organization',
'There seems to be error while saving new organization, ' +
'you may have to re-add your previous organizations back.'
);
logger.error('Error while JSON parsing domain.json: ');
logger.error(error);
logger.reportSentry(error);
}
}
db = new JsonDB(domainJsonPath, true, true);
database = new JsonDB(domainJsonPath, true, true);
}
export function formatUrl(domain: string): string {
if (domain.startsWith('http://') || domain.startsWith('https://')) {
return domain;
}
if (domain.startsWith("http://") || domain.startsWith("https://")) {
return domain;
}
if (domain.startsWith('localhost:')) {
return `http://${domain}`;
}
if (domain.startsWith("localhost:")) {
return `http://${domain}`;
}
return `https://${domain}`;
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,81 +0,0 @@
import fs from 'fs';
import path from 'path';
import Logger from './logger-util';
const logger = new Logger({
file: 'enterprise-util.log',
timestamp: true
});
// TODO: replace enterpriseSettings type with an interface once settings are final
let enterpriseSettings: {[key: string]: unknown};
let configFile: boolean;
reloadDB();
function reloadDB(): void {
let enterpriseFile = '/etc/zulip-desktop-config/global_config.json';
if (process.platform === 'win32') {
enterpriseFile = 'C:\\Program Files\\Zulip-Desktop-Config\\global_config.json';
}
enterpriseFile = path.resolve(enterpriseFile);
if (fs.existsSync(enterpriseFile)) {
configFile = true;
try {
const file = fs.readFileSync(enterpriseFile, 'utf8');
enterpriseSettings = JSON.parse(file);
} catch (error) {
logger.log('Error while JSON parsing global_config.json: ');
logger.log(error);
}
} else {
configFile = false;
}
}
export function hasConfigFile(): boolean {
return configFile;
}
export function getConfigItem(key: string, defaultValue?: unknown): any {
reloadDB();
if (!configFile) {
return defaultValue;
}
if (defaultValue === undefined) {
defaultValue = null;
}
return configItemExists(key) ? enterpriseSettings[key] : defaultValue;
}
export function configItemExists(key: string): boolean {
reloadDB();
if (!configFile) {
return false;
}
return (enterpriseSettings[key] !== undefined);
}
export function isPresetOrg(url: string): boolean {
if (!configFile || !configItemExists('presetOrganizations')) {
return false;
}
const presetOrgs = enterpriseSettings.presetOrganizations;
if (!Array.isArray(presetOrgs)) {
throw new TypeError('Expected array for presetOrgs');
}
for (const org of presetOrgs) {
if (url.includes(org)) {
return true;
}
}
return false;
}

View File

@@ -1,45 +0,0 @@
import {shell} from 'electron';
import escape from 'escape-html';
import fs from 'fs';
import os from 'os';
import path from 'path';
export function isUploadsUrl(server: string, url: URL): boolean {
return url.origin === server && url.pathname.startsWith('/user_uploads/');
}
export async function openBrowser(url: URL): Promise<void> {
if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
await shell.openExternal(url.href);
} else {
// For security, indirect links to non-whitelisted protocols
// through a real web browser via a local HTML file.
const dir = fs.mkdtempSync(
path.join(os.tmpdir(), 'zulip-redirect-')
);
const file = path.join(dir, 'redirect.html');
fs.writeFileSync(file, `\
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Refresh" content="0; url=${escape(url.href)}" />
<title>Redirecting</title>
<style>
html {
font-family: menu, "Helvetica Neue", sans-serif;
}
</style>
</head>
<body>
<p>Opening <a href="${escape(url.href)}">${escape(url.href)}</a>…</p>
</body>
</html>
`);
shell.openItem(file);
setTimeout(() => {
fs.unlinkSync(file);
fs.rmdirSync(dir);
}, 15000);
}
}

View File

@@ -1,62 +0,0 @@
import {JsonDB} from 'node-json-db';
import fs from 'fs';
import path from 'path';
import electron from 'electron';
import Logger from './logger-util';
const remote =
process.type === 'renderer' ? electron.remote : electron;
const logger = new Logger({
file: 'linux-update-util.log',
timestamp: true
});
/* To make the util runnable in both main and renderer process */
const {dialog, app} = remote;
let db: JsonDB;
reloadDB();
export function getUpdateItem(key: string, defaultValue: unknown = null): any {
reloadDB();
const value = db.getData('/')[key];
if (value === undefined) {
setUpdateItem(key, defaultValue);
return defaultValue;
}
return value;
}
export function setUpdateItem(key: string, value: unknown): void {
db.push(`/${key}`, value, true);
reloadDB();
}
export function removeUpdateItem(key: string): void {
db.delete(`/${key}`);
reloadDB();
}
function reloadDB(): void {
const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/config/updates.json');
try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
JSON.parse(file);
} catch (error) {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
'Error saving update notifications.',
'We encountered an error while saving the update notifications.'
);
logger.error('Error while JSON parsing updates.json: ');
logger.error(error);
}
}
db = new JsonDB(linuxUpdateJsonPath, true, true);
}

View File

@@ -1,153 +0,0 @@
import {Console} from 'console'; // eslint-disable-line node/prefer-global/console
import {initSetUp} from './default-util';
import {sentryInit, captureException} from './sentry-util';
import fs from 'fs';
import os from 'os';
import isDev from 'electron-is-dev';
import electron from 'electron';
interface LoggerOptions {
timestamp?: true | (() => string);
file?: string;
level?: boolean;
logInDevMode?: boolean;
}
initSetUp();
let app: Electron.App = null;
let reportErrors = true;
if (process.type === 'renderer') {
app = electron.remote.app;
// Report Errors to Sentry only if it is enabled in settings
// Gets the value of reportErrors from config-util for renderer process
// For main process, sentryInit() is handled in index.js
const {ipcRenderer} = electron;
ipcRenderer.send('error-reporting');
ipcRenderer.on('error-reporting-val', (_event: Event, errorReporting: boolean) => {
reportErrors = errorReporting;
if (reportErrors) {
sentryInit();
}
});
} else {
app = electron.app;
}
const logDir = `${app.getPath('userData')}/Logs`;
type Level = 'log' | 'debug' | 'info' | 'warn' | 'error';
const levels: Level[] = ['log', 'debug', 'info', 'warn', 'error'];
type LogMethod = (...args: unknown[]) => void;
export default class Logger {
log: LogMethod;
debug: LogMethod;
info: LogMethod;
warn: LogMethod;
error: LogMethod;
nodeConsole: Console;
timestamp?: () => string;
level: boolean;
logInDevMode: boolean;
constructor(options: LoggerOptions = {}) {
let {
timestamp = true,
file = 'console.log',
level = true,
logInDevMode = false
} = options;
file = `${logDir}/${file}`;
if (timestamp === true) {
timestamp = this.getTimestamp;
}
// Trim log according to type of process
if (process.type === 'renderer') {
requestIdleCallback(() => this.trimLog(file));
} else {
process.nextTick(() => this.trimLog(file));
}
const fileStream = fs.createWriteStream(file, {flags: 'a'});
const nodeConsole = new Console(fileStream);
this.nodeConsole = nodeConsole;
this.timestamp = timestamp;
this.level = level;
this.logInDevMode = logInDevMode;
this.setUpConsole();
}
_log(type: Level, ...args: unknown[]): void {
const {
nodeConsole, timestamp, level, logInDevMode
} = this;
switch (true) {
case typeof timestamp === 'function':
args.unshift(timestamp() + ' |\t');
// Fall through
case (level):
args.unshift(type.toUpperCase() + ' |');
// Fall through
case isDev || logInDevMode:
nodeConsole[type](...args);
break;
default:
}
console[type](...args);
}
setUpConsole(): void {
for (const type of levels) {
this.setupConsoleMethod(type);
}
}
setupConsoleMethod(type: Level): void {
this[type] = (...args: unknown[]) => this._log(type, ...args);
}
getTimestamp(): string {
const date = new Date();
const timestamp =
`${date.getMonth()}/${date.getDate()} ` +
`${date.getMinutes()}:${date.getSeconds()}`;
return timestamp;
}
reportSentry(err: unknown): void {
if (reportErrors) {
captureException(err);
}
}
trimLog(file: string): void{
fs.readFile(file, 'utf8', (err, data) => {
if (err) {
throw err;
}
const MAX_LOG_FILE_LINES = 500;
const logs = data.split(os.EOL);
const logLength = logs.length - 1;
// Keep bottom MAX_LOG_FILE_LINES of each log instance
if (logLength > MAX_LOG_FILE_LINES) {
const trimmedLogs = logs.slice(logLength - MAX_LOG_FILE_LINES);
const toWrite = trimmedLogs.join(os.EOL);
fs.writeFileSync(file, toWrite);
}
});
}
}

View File

@@ -1,83 +0,0 @@
import * as ConfigUtil from './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') {
// In case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
httpString = 'http=' + proxy.split('PROXY')[1] + ';';
}
}
return httpString;
})();
// Check HTTPS Proxy
const httpsProxy = (async () => {
const proxy = await ses.resolveProxy('https://' + resolveProxyUrl);
let httpsString = '';
if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) {
// In case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
httpsString += 'https=' + proxy.split('PROXY')[1] + ';';
}
}
return httpsString;
})();
// Check FTP Proxy
const ftpProxy = (async () => {
const proxy = await ses.resolveProxy('ftp://' + resolveProxyUrl);
let ftpString = '';
if (proxy !== 'DIRECT') {
if (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]);
let proxyString = '';
values.forEach(proxy => {
proxyString += proxy;
});
ConfigUtil.setConfigItem('systemProxyRules', proxyString);
const useSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
if (useSystemProxy) {
ConfigUtil.setConfigItem('proxyRules', proxyString);
}
}

View File

@@ -1,68 +1,68 @@
import {ipcRenderer} from 'electron';
import * as backoff from "backoff";
import type WebView from '../components/webview';
import backoff from 'backoff';
import Logger from './logger-util';
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',
timestamp: true
file: "domain-util.log",
});
export default class ReconnectUtil {
webview: WebView;
url: string;
alreadyReloaded: boolean;
fibonacciBackoff: backoff.Backoff;
url: string;
alreadyReloaded: boolean;
fibonacciBackoff: backoff.Backoff;
constructor(webview: WebView) {
this.webview = webview;
this.url = webview.props.url;
this.alreadyReloaded = false;
this.clearState();
}
constructor(webview: WebView) {
this.url = webview.properties.url;
this.alreadyReloaded = false;
this.fibonacciBackoff = backoff.fibonacci({
initialDelay: 5000,
maxDelay: 300_000,
});
}
clearState(): void {
this.fibonacciBackoff = backoff.fibonacci({
initialDelay: 5000,
maxDelay: 300000
});
}
async isOnline(): Promise<boolean> {
return ipcRenderer.invoke("is-online", this.url);
}
async isOnline(): Promise<boolean> {
return ipcRenderer.invoke('is-online', this.url);
}
pollInternetAndReload(): void {
this.fibonacciBackoff.backoff();
this.fibonacciBackoff.on("ready", async () => {
if (await this._checkAndReload()) {
this.fibonacciBackoff.reset();
} else {
this.fibonacciBackoff.backoff();
}
});
}
pollInternetAndReload(): void {
this.fibonacciBackoff.backoff();
this.fibonacciBackoff.on('ready', async () => {
if (await this._checkAndReload()) {
this.fibonacciBackoff.reset();
} else {
this.fibonacciBackoff.backoff();
}
});
}
async _checkAndReload(): Promise<boolean> {
if (this.alreadyReloaded) {
return true;
}
async _checkAndReload(): Promise<boolean> {
if (this.alreadyReloaded) {
return true;
}
if (await this.isOnline()) {
ipcRenderer.send("forward-message", "reload-viewer");
logger.log("You're back online.");
return true;
}
if (await this.isOnline()) {
ipcRenderer.send('forward-message', 'reload-viewer');
logger.log('You\'re back online.');
return true;
}
logger.log(
"There is no internet connection, try checking network cables, modem and router.",
);
const errorMessageHolder = document.querySelector("#description");
if (errorMessageHolder) {
errorMessageHolder.innerHTML = html`
<div>
${t.__("Your internet connection doesn't seem to work properly!")}
</div>
<div>${t.__("Verify that it works and then click Reconnect.")}</div>
`.html;
}
logger.log('There is no internet connection, try checking network cables, modem and router.');
const errorMessageHolder = document.querySelector('#description');
if (errorMessageHolder) {
errorMessageHolder.innerHTML = `
<div>Your internet connection doesn't seem to work properly!</div>
<div>Verify that it works and then click try again.</div>`;
}
return false;
}
return false;
}
}

View File

@@ -1,18 +0,0 @@
import {init} from '@sentry/electron';
import isDev from 'electron-is-dev';
export const sentryInit = (): void => {
if (!isDev) {
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,39 +1,16 @@
import {ipcRenderer} from 'electron';
import {ipcRenderer} from "../typed-ipc-renderer.ts";
import os from 'os';
export const connectivityERR: string[] = [
'ERR_INTERNET_DISCONNECTED',
'ERR_PROXY_CONNECTION_FAILED',
'ERR_CONNECTION_RESET',
'ERR_NOT_CONNECTED',
'ERR_NAME_NOT_RESOLVED',
'ERR_NETWORK_CHANGED'
export const connectivityError: string[] = [
"ERR_INTERNET_DISCONNECTED",
"ERR_PROXY_CONNECTION_FAILED",
"ERR_CONNECTION_RESET",
"ERR_NOT_CONNECTED",
"ERR_NAME_NOT_RESOLVED",
"ERR_NETWORK_CHANGED",
];
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 '';
}
const userAgent = ipcRenderer.sendSync("fetch-user-agent");
export function getUserAgent(): string {
return userAgent;
return userAgent;
}

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