Compare commits

..

163 Commits

Author SHA1 Message Date
Manav Mehta
16f0af8853 Remove storing of User-Agent on disk.
Fixes: #921.

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

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

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

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

Fixes: #914.
2020-04-07 20:16:50 +05:30
Anders Kaseorg
814ce071a3 release: New release v5.0.0.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:38:33 -07:00
Anders Kaseorg
92fb176f67 Revert "auth: Move social login process to browser."
This reverts commit 49b29bfed6 (#863).

The design of this feature is still under discussion; we expect it to
return after the security release.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
a03f569af9 CVE-2020-10857: Whitelist safe URL protocols for shell.openExternal.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
af59bb7c99 handleExternalLink: Do not navigate the current window.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
4390966a62 Always show downloaded files in file manager.
shell.openItem is unsafe.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
a6d942fe6c CVE-2020-10858: Lock down session permission requests.
This fixes a vulnerability reported by Matt Austin.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
9d4093b3d8 CVE-2020-10856: Enable context isolation.
This fixes a vulnerability reported by Matt Austin.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
20a6c5d128 preload: Use IPC for logout, shortcut, showNotificationSettings.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
c843e179fc tray: Remove tray variable from window.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:33:24 -07:00
Anders Kaseorg
438d4fffa7 notification: Convert loadBots from jQuery to fetch.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:32:23 -07:00
Tim Abbott
5c164bfa7d webview: Disable insecure content.
Zulip servers in production are designed to only serve content over
HTTPS.  And a development environment's root page will be served over
HTTP.

So there is no purpose in enabling allowInsecureContent, even
conditionally for use against Zulip development environments; we should
just remove the setting.
2020-03-30 19:32:23 -07:00
Anders Kaseorg
cbc89a72a2 tray: Work around Electron segfault on certain platforms.
Set the tray icon’s context menu immediately after creating the Tray
object.  This seems to prevent an Electron segfault at startup on
certain platforms, such as Ubuntu 16.04 i386.  See
https://github.com/electron/electron/issues/22652 and its linked
issues.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-30 19:26:29 -07:00
Anders Kaseorg
fb1e163130 typescript: Fix errors hidden by skipLibCheck.
This requires temporarily downgrading to @types/node@^12 (see
https://github.com/electron/electron/issues/21612).

Leave skipLibCheck on for now as it still saves a few seconds when
running tsc.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-26 16:35:13 -07:00
Anders Kaseorg
2ebeeedba8 dependencies: Move fs-extra from devDependencies to dependencies.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-24 21:07:25 -07:00
Anders Kaseorg
82a7f97ca6 dependencies: Upgrade dependencies.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-23 17:22:48 -07:00
Anders Kaseorg
55eb768064 xo: Upgrade xo to 0.28.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-23 16:53:14 -07:00
Anders Kaseorg
611932c66d xo: Unabbreviate variable names.
To satisfy unicorn/prevent-abbreviations in xo 0.28.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-23 16:53:12 -07:00
Anders Kaseorg
5deffa5022 changelog: Add missing changelog entry for v4.0.3.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-23 15:08:21 -07:00
Anders Kaseorg
6f01f1362a js: Declare 'use strict' on tests too.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-09 22:14:23 -07:00
Anders Kaseorg
9d2739f050 js: Declare 'use strict' on all scripts and no modules.
And enable the import/unambiguous ESLint rule as a check on our
partition between scripts and modules.  After this commit, if you add
a new file and get this error:

  ✖  1:1  This module could be parsed as a valid script.  import/unambiguous

* For a module, add an `import` or `export` declaration to make the
  file unambiguously a module (the empty `export {};` declaration
  suffices).
* For a script, add the file to the xo overrides section of
  package.json that marks it "sourceType": "script", and add a 'use
  strict' declaration.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-09 20:04:43 -07:00
Akash Nimare
01f6e77237 macOS: Fix undo redo not working on macOS.
The default API provided by Electron doesn't work
as expected. More info here -
https://github.com/electron/electron/issues/15728

Fixes: #866.
2020-03-10 00:32:05 +05:30
Manav Mehta
7ac35cc087 macOS: Replace deprecated isDarkMode() with shouldUseDarkColors.
Fixes: #891.
2020-03-08 14:55:43 +05:30
Anders Kaseorg
32e6b3054f xo: Use more xo defaults.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 21:46:18 -08:00
Anders Kaseorg
40bf2a1f20 xo: Lint *.js too.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 21:22:04 -08:00
Anders Kaseorg
dee2f05ac0 locale-helper: Move supported-locales.js to supported-locales.json.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 20:45:57 -08:00
Anders Kaseorg
ca5de73155 xo: Reenable several easy rules.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 20:15:01 -08:00
Anders Kaseorg
d0f8c040c7 package.json: Add tsc to test script.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 18:26:06 -08:00
Anders Kaseorg
7cf40f1e08 typescript: One more switch to ES export syntax.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 18:15:42 -08:00
Anders Kaseorg
598c0df60b package.json: Reformat to match npm generated output.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 17:52:10 -08:00
Anders Kaseorg
d3bcd7306a typescript: Switch to ES import/export syntax.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 17:21:03 -08:00
Anders Kaseorg
b3261bcdff js: Explode more singleton classes to modules.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 16:27:44 -08:00
Manav Mehta
20c6f487c4 typescript: Implement some TODOs. 2020-03-04 14:21:25 -08:00
Anders Kaseorg
340797ca10 typescript: Refine some type annotations to avoid any.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 12:12:31 -08:00
Anders Kaseorg
220aac2d54 js: Explode singleton classes to modules.
Singleton classes may have a purpose.  This was not that purpose.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 11:54:45 -08:00
Akash Nimare
d9f6cf4cc9 docs: Update recommended node version for development. 2020-03-04 16:59:21 +05:30
Anders Kaseorg
dc3e5d4930 package.json: Sort xo overrides.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 21:50:02 -08:00
Anders Kaseorg
fc2b80c36a main: Fix realm icon updating.
Commit c937317ecf (#605) should have
updated this, but didn’t.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 21:50:02 -08:00
Anders Kaseorg
ab667d8053 checkCertError: Fix showMessageBox usage.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 21:50:02 -08:00
Manav Mehta
8b9a10a23d Update report issue placeholder.
Fixes: #873.
2020-03-04 11:16:31 +05:30
Anders Kaseorg
15af3e732f sentry-util: Hard-code the Sentry DSN.
Commit 088ddf9c62 (#755) does not work,
because neither the .env file nor the environment variables it
provides are available to normal users at runtime.  This silently
broke Sentry data collection.  When we upgraded @sentry/electron in
commit 107e522914, the silent failure
became an error that prevented the app from starting.

The Sentry DSN is not a secret, so we should just commit it to the
repository.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-04 11:07:00 +05:30
am2505
534f4c1463 Convert Promise to async-await.
Fixes #878.
2020-03-03 20:40:10 -08:00
Anders Kaseorg
063324550e run-dev: Fix for one package.json structure.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 20:00:36 -08:00
Anders Kaseorg
268471df6b typings: Remove redundant ZulipWebWindow.$.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 18:17:13 -08:00
Anders Kaseorg
ca6b2312be typings: Get Window.Notification from upstream.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 18:16:01 -08:00
Anders Kaseorg
ff026e5763 typings: Get NotificationOptions.silent from upstream.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 18:14:13 -08:00
Anders Kaseorg
8f810481e3 typings: Get requestIdleCallback from DefinitelyTyped.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 18:13:26 -08:00
Anders Kaseorg
f91e95647a typings: Use type declarations from DefinitelyTyped.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 18:01:24 -08:00
Anders Kaseorg
e9536f247b dependencies: Use one package.json structure.
The two package.json structure is no longer needed.
https://www.electron.build/tutorials/two-package-structure

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 11:02:59 -08:00
Anders Kaseorg
067cbf32a1 build: Fix cld sources exclusion.
As of commit 107e522914, @paulcbetts/cld
was renamed to cld.

docs is not being included anyway since it’s outside of app.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-03 10:59:41 -08:00
Anders Kaseorg
6824978114 dependencies: Remove @types/dotenv compatibility stub.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 20:31:20 -08:00
Anders Kaseorg
fa86f1ca25 dependencies: Upgrade everything to latest.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 20:31:19 -08:00
Anders Kaseorg
a63b3873ae dependencies: Remove electron-debug.
We already implement all of its functionality.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 20:15:43 -08:00
Anders Kaseorg
5064ea4b47 dependencies: Upgrade node-json-db from 0.9.2 to 1.0.3.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 19:52:39 -08:00
Anders Kaseorg
6b0d8520c5 dependencies: Remove unused dependencies assert, cp-file, is-ci.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 19:19:37 -08:00
Anders Kaseorg
4b16164155 cleanup: Remove unused tests/e2e directory.
It is not used even by test-e2e.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 19:18:25 -08:00
Anders Kaseorg
6036a44fb2 new-server-form: Remove useless .server-save-action wrappers.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 19:08:45 -08:00
Anders Kaseorg
598b96b6e8 webview: Wait for dom-ready before sending messages.
Fixes tests/test-add-organization.js.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 18:52:37 -08:00
Anders Kaseorg
675bc2f06c appveyor.yml, .travis.yml: Test current Node.js releases.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-03-02 16:30:12 -08:00
Tim Abbott
eb2988a5e4 dependencies: Update typescript and typescript-eslint.
The changes are mostly done via `xo --fix`; the other changes are
either trivial or disabling new linter rules that we plan to address
in future commits.
2020-02-29 23:39:55 -08:00
Tim Abbott
39ea18228c dependencies: Update gulp testing packages. 2020-02-29 22:54:50 -08:00
Tim Abbott
909e0f07e3 dependencies: Upgrade linters and fix linter errors.
The changes here are mostly straightforward; the one exception is
removing a zulipdev.org hack.

We disable some lint rules we'll want to address later (E.g. we want
to switch to using async/await rather than .then()).  But those are
out of scope for this commit.
2020-02-29 22:47:42 -08:00
Tim Abbott
31af6596bf dependencies: Upgrade Electron to version 8.
This is the latest Electron release, which means we're now getting
nearly modern Chrome (hopefully with fewer rendering bugs and better
performance).
2020-02-29 21:49:19 -08:00
Tim Abbott
3c9914542f badge: Clear badge counts on Linux as well.
My Linux desktop environment doesn't display unread badges, it seems,
but this is clearly how this code should read.
2020-02-29 21:39:56 -08:00
Tim Abbott
f4b9605742 electron: Update some setter/getters to user newer properties.
This removes a few deprecation warnings on app startup.
2020-02-29 21:39:56 -08:00
Tim Abbott
e2fc9241fa dependencies: Upgrade to Electron 7.
This works without any other changes, thanks to Electron's deprecation
process being done over multiple releases.
2020-02-29 21:39:56 -08:00
Tim Abbott
3b18357c74 download: Use removeListener for removing the updated listener.
This is slightly cleaner code, and also fixes a typescript error (that
might be a bug) we'll get when we upgrade to Electron 7.
2020-02-29 21:39:56 -08:00
Tim Abbott
c4beedf740 proxy: Migration to use async/await.
This is required for the upgrade to Electron 7, which removes the old
callback-based form of these APIs.
2020-02-29 21:39:56 -08:00
Anders Kaseorg
b34bf7236f .travis.yml: Fix npm ci invocation for app directory.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-29 18:49:01 -08:00
Tim Abbott
e32968b2f3 preferences: Convert one more dialog to use async/await.
This should have been in the main version update commit.
2020-02-29 18:32:09 -08:00
Anders Kaseorg
747fbb5ab0 .travis.yml: Run npm ci, not npm install.
This enforces that package-lock.json is up to date in Git.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-29 18:30:05 -08:00
vsvipul
107e522914 deps: Update Electron and related packages to Electron v6.
This updates most of our direct dependencies to much newer versions
(Electron v6, with compatible versions of related packages like
Spectron).

Further, it updates all of our recursive dependencies with `npm update
--depth=999`.

Modified by tabbott to migrate to async/await for dialogs rather than
the old synchronous API.
2020-02-29 18:28:42 -08:00
Tim Abbott
c83bc08359 i18n: Add additional automatically output strings.
I'm skeptical of the setup that these are generated dynamically while
working in the development environment; that seems not robust.
2020-02-29 17:38:23 -08:00
Anders Kaseorg
ef0b056437 package.json: Fix clean-ts-files script to spare app/node_modules.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-29 17:03:53 -08:00
Tim Abbott
9370f783ba i18n: Update generated translation data.
I feel like this should probably not be in Git.
2020-02-29 16:14:00 -08:00
Tim Abbott
227e59fee2 gitattributes: Disable binary handling of lock files.
This makes it possible to see what packages change after npm
operations.
2020-02-29 13:09:29 -08:00
Akash Nimare
b7147d0b29 webview: Update web security preference.
Electron docs suggests that we should not use
`disablewebsecurity` thus removing the same.
2020-02-27 16:55:47 +05:30
Akash Nimare
f93053eb20 release: New release 4.1.0-beta. 2020-02-27 10:37:55 +05:30
ViPuL
49b29bfed6 auth: Move social login process to browser.
Moves the social login to browser since there
was no way to verify the authencity of the
auth process for a custom server and to
prevent phishing attacks.

Fixes #849.

Co-authored-by: Kanishk Kakar <kanishk.kakar@gmail.com>
2020-02-25 20:05:27 +05:30
Akash Nimare
0fb610f858 macOS: Add colorless tray icon for macOS.
Fixes: #825.
2020-02-05 21:41:10 +05:30
ViPuL
1d9a923245 startup: Use inbuilt electron API for autostartup. (#859)
Switch from using the external auto-launch module to
inbuilt setLoginItemSettings for windows and macOS,
as some users reported issues on windows.

Fixes: #851.
2020-01-28 12:26:41 +05:30
Ross Brunton
9582d32de8 Added option to select download locations.
Added an option that, when enabled, will mean any file downloads that
would normally go to ~/Downloads (or wherever), in fact prompt.
2020-01-21 16:41:56 +05:30
ViPuL
a2a21631f2 Decode server name in Window menu. 2020-01-08 11:58:44 +05:30
Akash Nimare
9490265a03 dock: Toggle app on clicking the dock icon. 2019-12-29 00:49:46 +05:30
Brandon Liu
70bd619fa8 Fix size of macOS dock icon.
Fixes: #845.
2019-11-30 17:07:31 +05:30
Akash Nimare
c5797e4edb release: New release v4.0.3. 2019-11-21 12:47:29 +05:30
Kanishk Kakar
32321daef2 docs: Add release notes for v4.0.2-beta. (#841) 2019-11-17 04:27:55 +05:30
Akash Nimare
95a9568ece beta-release: New release v4.0.2-beta. 2019-11-13 14:56:34 +05:30
Kanishk Kakar
e7a885a1fb macos: Enable notarization for macOS Catalina.
This fixes the issue for Catalina users.
2019-11-13 14:55:11 +05:30
Akash Nimare
17d4d97e2e certificate: Make certificate location dynamic.
* certificate: Make certificate location dynamic.

* Update certificate location for old servers.
2019-11-01 19:56:22 +05:30
Akash Nimare
3b14684058 Add docs for translation 2019-10-30 02:10:38 +05:30
Akash Nimare
7d592a0a1c validation: Add SSL troubleshooting guide in error message. 2019-10-25 21:03:28 +05:30
Ross Brunton
eb1be7106b Added option to quit on closing the window.
This adds a configuration option to quit the app (rather than going to
the tray) when the "close" button is clicked.
2019-10-14 01:05:18 +05:30
Tim Abbott
1da6e5d51d README: Fix typo in link to server/webapp repository. 2019-10-07 11:51:57 -07:00
Tim Abbott
dae7089c7e README: Explicitly address where to report bugs. 2019-10-01 15:41:39 -07:00
Muskan Khedia
30b40e2ff2 network: Prompts Desktop App to ask for network setting in add-org page.
Fixes: #540.
2019-09-28 17:44:34 +05:30
Akash Nimare
b76f01349a docs: Update electron tutorial guide.
Fixes: #826.
2019-09-27 23:02:25 +05:30
Kanishk Kakar
8446deb673 sidebar: Improve UX for notification settings.
* sidebar: Disable notif settings if not logged in.

* sidebar: Activate relevant tab for notif settings.
2019-09-25 18:25:00 +05:30
Kanishk Kakar
d4b9663257 network: Tackle network issues independently.
Few changes -
* webview: Show connection failure per server.
* network: Try to reconnect diff servers.
* Fixes concern that some proxy networks may allow only specific servers
to be reachable.
* domains: Show network error on server invalidation.
* webview: Handle network errors in preload script.
Fixes: #591, #312.
2019-09-24 18:22:19 +05:30
Akash Nimare
77044fd9fa enterprise: Document the enterprise feature. 2019-09-18 23:26:44 +05:30
Kanishk Kakar
177b77f0b5 sidebar: Add option to open notification setting from the context menu. 2019-09-15 21:20:08 +05:30
Kanishk Kakar
99b154b8ae system-util: Set User-Agent from main process.
* Sets user-agent config item when the app's DOM is ready.
* App sends the right User-Agent to the server-settings API.
 
Fixes #817.
2019-09-11 16:39:56 +05:30
Kanishk Kakar
3fd8aedf81 network: Reactivate network.js script.
Fixes an issue introduced during TS migration that rendered network.ts
ineffective because exports were not defined.
2019-09-02 00:25:17 +05:30
Rhythm Sharma
b4d2e55c6f linux: Fix broken icon issue for snap package. 2019-08-29 02:20:12 +05:30
Kanishk Kakar
3c701ff518 sidebar: Load last active server before others.
Fixes: #551.
2019-08-28 14:12:01 +05:30
ViPuL
1f79a97b05 system-presence: Pass system active status to webapp.
We check user status every 15 seconds and update the status accordingly
to every organization connected. The webapp then uses this system presence data
we send to set the user status based on system activity.

Fixes #352.
2019-08-19 20:09:27 -04:00
Nikita
90e8e9a806 il8n: Fix translations for ru locales. 2019-08-17 10:52:36 -04:00
Kanishk Kakar
59ef505efd settings: Fix trailing brackets.
This accidentally slipped by in the translation work we recently did.
2019-08-17 10:49:24 -04:00
Akash Nimare
8d0a111c91 version: Update app to v4.0.1. 2019-08-17 01:41:15 +05:30
Akash Nimare
a10fa8f3ad badge-count: Show badge-count on Linux.
Added support for showing the badge counts in the Unity launcher.
This should work on elementary OS and Ubuntu.

More info -
https://github.com/electron/electron/issues/16001
https://github.com/signalapp/Signal-Desktop/issues/3387
2019-08-17 01:32:50 +05:30
Akash Nimare
39427091f5 linux: Add desktopName config for Linux.
This will help in creating .desktop file for Linux.
2019-08-17 01:31:31 +05:30
ViPuL
f8d93cf397 build: Add MSI installer support.
This PR adds MSI in the target option. MSI is useful for sysadmins. There is no support for auto-updates since it often manages by the admins. 
More info -
https://github.com/electron-userland/electron-builder/releases/tag/v19.41.0
https://github.com/electron-userland/electron-builder/issues/3322

Fixes: #641.
2019-08-15 00:35:00 +05:30
Priyank Patel
ab62b8b5bb Remove accidently commited translation-util.js file.
This file was accidently committed in 77a1fc0bd3
when migrating old PR to use typescript. This commit removes it.
2019-08-14 12:50:47 -04:00
Kanishk Kakar
ce071dcac9 docs: Add release notes for v4.0.0. 2019-08-11 01:13:37 +05:30
Akash Nimare
04c1109d43 release: New release v4.0.0. 2019-08-07 22:17:34 +05:30
Prakhar Uniyal
26f321e7f9 preference: Fix visually broken delete custom css button.
There were couple of issue solved by this commit:
  - The button had extra 10px margin to right; this comes from .action class
  - The "Delete" text and icon were not aligned
  - The button looks bigger than other buttons

Both of which made the button look broken. We fix first by changing
.css-delete-action to #css-delete-action in css file; git grep shows
this is only used once and is used as id not a class. Then we use
display flex to fix the alinging of icon and text. We add box-sizing border-box
which solves the bigger width issue. We remove extra 10px margin that comes from
.action.

Fixes: #801.
2019-08-07 20:57:44 +05:30
Kanishk Kakar
47b729cbc2 i18n: Use English as fallback language.
In case app.getLocale() returns a falsey value, English is used as the
app language.
2019-08-02 00:45:12 +05:30
Kanishk Kakar
6d34da5953 i18n: Add translations to the new server form. 2019-08-02 00:45:12 +05:30
Kanishk Kakar
cfd1ebc643 i18n: Add translations to settings sidebar. 2019-08-02 00:45:12 +05:30
Kanishk Kakar
dab30dfb7a i18n: Add translations for shortcuts pane. 2019-08-02 00:45:12 +05:30
Kanishk Kakar
e2bb28c738 i18n: Add translations for connected orgs. 2019-08-02 00:45:12 +05:30
Kanishk Kakar
ebd4d9a69b i18n: Add translations for network settings. 2019-08-02 00:45:12 +05:30
Kanishk Kakar
ab80315846 i18n: Add translations for general settings. 2019-08-02 00:45:12 +05:30
Kanishk Kakar
c78ea51ec9 i18n: Change menu according to system locale. 2019-08-02 00:45:12 +05:30
Kanishk Kakar
d521f533f0 i18n: Add new values to translations 2019-08-02 00:45:12 +05:30
Kanishk Kakar
67a69f0dc2 i18n: Add translations to app menu
* Uses new keys added to locale-template.json
* Disregards org names
2019-08-02 00:45:12 +05:30
Kanishk Kakar
d99cc0d49c locales: Change translate API to handle rate limit
Also add catch block to log possible errors.
2019-08-02 00:45:12 +05:30
Kanishk Kakar
77a1fc0bd3 i18n: Setup module and add translation-util.js
* Add i18n package.
* Use system locale for TranslationUtil.
2019-08-02 00:45:12 +05:30
Akash Nimare
cf96e94470 zoom: Fix zoom-in shortcut symbol.
Use symbol instead of symbol name in accelerators in menu items.

This is a hackish solution for the following upstream issue -
https://github.com/electron/electron/issues/10371

Fixes: #718.
2019-07-31 18:37:03 +05:30
Akash Nimare
4395edc3de macos-shortcut: Add meta key for cmd. 2019-07-31 18:37:03 +05:30
Akash Nimare
66479e0d88 docs: Update auto-update docs for preset orgs. 2019-07-31 04:43:58 +05:30
Kanishk Kakar
7c44ef5e41 enterprise: Restrict EnterpriseUtil method calls.
* Add a boolean flag to EnterpriseUtil so unnecessary function calls
don't take place for non-admin users.
2019-07-31 04:32:10 +05:30
Kanishk Kakar
296c83949f typescript: Define DialogBoxError in messages.ts. 2019-07-31 04:32:10 +05:30
Kanishk Kakar
ac6f14f5b4 settings: Show tooltip when setting disabled.
If setting has been locked by an admin, then a tooltip is shown in the
settings page.
2019-07-31 04:32:10 +05:30
Kanishk Kakar
20ac6bf921 enterprise: Silently fail config updates.
If a config item exists in enterprise config and we're not trying to
override, then setConfigItem returns.
2019-07-31 04:32:10 +05:30
Kanishk Kakar
5e9f9aa9a2 enterprise: Disable autoUpdate on admin setting. 2019-07-31 04:32:10 +05:30
Kanishk Kakar
4efa98118b enterprise: Init enterpriseSettings from file.
Read enterprise settings while loading the app and give preference to
global config.
2019-07-31 04:32:10 +05:30
Kanishk Kakar
a7887211ac enterprise: Raise error when removing orgs.
When a user tries to remove a preset org, we raise an error asking them
to contact their sys admin.
2019-07-31 04:32:10 +05:30
Kanishk Kakar
33fadcd876 enterprise: Stop loading gif if no domains added.
If no domains have been added, stop the loading gif and open the new
server form.
2019-07-31 04:32:10 +05:30
Kanishk Kakar
e7e92ebecc enterprise: Show error when all preset orgs fail.
On app load, if all the preset orgs fail to get added to the server,
then we show an errorBox.
2019-07-31 04:32:10 +05:30
Kanishk Kakar
6c29af6766 docs: Add enterprise docs for preset orgs. 2019-07-31 04:32:10 +05:30
Kanishk Kakar
eabac3ae81 enterprise: Show loading gif view.
When no other orgs have been added and presetOrgs have been specified in
enterprise config, loading gif will appear.
2019-07-31 04:32:10 +05:30
Kanishk Kakar
fc0385433e enterprise: Detect changes in orgs config.
Allow app to identify if presetOrgs have changed in enterprise config
and change domains based on user response.
2019-07-31 04:32:10 +05:30
Kanishk Kakar
400c02f264 enterprise: Add support for preset orgs.
Enable configuration of preset organizations in the enterprise config
file and load them when app loads.
Partially valid array of URLs accepted while loading app.
2019-07-31 04:32:10 +05:30
Kanishk Kakar
be841cff34 enterpise: Add enterprise-util for custom configs. 2019-07-31 04:32:10 +05:30
Kanishk Kakar
7d426c4791 sidebar: Revert to fallback icon only when needed.
Fix an issue where server icon was replaced with character icon on
network issues.
2019-07-30 02:14:40 +05:30
Akash Nimare
939aa50e9b menu: Disable window sub-menu.
Disable switch to next-org and previous org when the
server length is less than one.

Fixes: #799.
2019-07-26 05:14:45 +05:30
Akash Nimare
dc9b9a1dc3 zoom-in: Fix zoom-in shortcut. 2019-07-24 17:28:59 +05:30
Kanishk Kakar
b0da718bfc sidebar: Sync loading indicator with loading GIF.
* Disable loading indicator (same as going back in settings)
* Show loading indicator on reload
2019-07-24 02:34:05 +05:30
Priyank Patel
7ea6255e65 darwin-notifications: Use electron_bridge notification reply api if supported.
To avoid using the brittle code we have for inline notification reply
currently, we added more future proof in webapp via the electron_bridge,
we use the newer API if it is supported.

Fixes: #794.
2019-07-23 22:08:24 +05:30
127 changed files with 12775 additions and 10829 deletions

2
.gitattributes vendored
View File

@@ -1,7 +1,5 @@
* text=auto eol=lf * text=auto eol=lf
package-lock.json binary
app/package-lock.json binary
*.gif binary *.gif binary
*.jpg binary *.jpg binary
*.jpeg binary *.jpeg binary

4
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# Dependency directories # Dependency directory
node_modules/ /node_modules/
# npm cache directory # npm cache directory
.npm .npm

View File

@@ -15,17 +15,17 @@ addons:
language: node_js language: node_js
node_js: node_js:
- '8' - '10'
- '12'
before_install: before_install:
- ./scripts/travis-xvfb.sh - ./scripts/travis-xvfb.sh
- npm install -g gulp - npm install -g gulp
- npm install - npm ci
cache: cache:
directories: directories:
- node_modules - node_modules
- app/node_modules
script: script:
- npm run travis - npm run travis

View File

@@ -6,7 +6,7 @@ The following is a set of guidelines for contributing to Zulip's desktop Client.
## Getting Started ## Getting Started
Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If you are new to Electron, please head over to [this](https://jlord.dev/blog/essential-electron) great article. Zulip-Desktop app is built on top of [Electron](http://electron.atom.io/). If you are new to Electron, please head over to [this](https://jlord.us/essential-electron) great article.
## Community ## Community

View File

@@ -18,6 +18,15 @@ Please see the [installation guide](https://zulipchat.com/help/desktop-app-insta
* Multi-language spell checker * Multi-language spell checker
* Automatic updates * Automatic updates
# Reporting issues
This desktop client shares most of its code with the Zulip webapp.
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
issues in the desktop app and its settings should be reported [in this
project](https://github.com/zulip/zulip-desktop/issues/new).
# Contribute # Contribute
First, join us on the [Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html)! First, join us on the [Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html)!

View File

@@ -1,11 +1,11 @@
'use strict'; import { app, dialog } from 'electron';
import { app, dialog, shell } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import { linuxUpdateNotification } from './linuxupdater'; // Required only in case of linux import { linuxUpdateNotification } from './linuxupdater'; // Required only in case of linux
import log = require('electron-log'); import log from 'electron-log';
import isDev = require('electron-is-dev'); import isDev from 'electron-is-dev';
import ConfigUtil = require('../renderer/js/utils/config-util'); import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as LinkUtil from '../renderer/js/utils/link-util';
export function appUpdater(updateFromMenu = false): void { export function appUpdater(updateFromMenu = false): void {
// Don't initiate auto-updates in development // Don't initiate auto-updates in development
@@ -62,20 +62,19 @@ export function appUpdater(updateFromMenu = false): void {
} }
}); });
autoUpdater.on('error', error => { autoUpdater.on('error', async error => {
if (updateFromMenu) { if (updateFromMenu) {
const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates'); const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates');
dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'error', type: 'error',
buttons: ['Manual Download', 'Cancel'], buttons: ['Manual Download', 'Cancel'],
message: messageText, message: messageText,
detail: (error).toString() + `\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulipchat.com/apps/.\n detail: (error).toString() + `\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulipchat.com/apps/.\n
Current Version: ${app.getVersion()}` Current Version: ${app.getVersion()}`
}, response => {
if (response === 0) {
shell.openExternal('https://zulipchat.com/apps/');
}
}); });
if (response === 0) {
LinkUtil.openBrowser(new URL('https://zulipchat.com/apps/'));
}
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these // Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times. // listeners don't trigger multiple times.
autoUpdater.removeAllListeners(); autoUpdater.removeAllListeners();
@@ -83,23 +82,22 @@ Current Version: ${app.getVersion()}`
}); });
// Ask the user if update is available // Ask the user if update is available
autoUpdater.on('update-downloaded', event => { autoUpdater.on('update-downloaded', async event => {
// Ask user to update the app // Ask user to update the app
dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'question', type: 'question',
buttons: ['Install and Relaunch', 'Install Later'], buttons: ['Install and Relaunch', 'Install Later'],
defaultId: 0, defaultId: 0,
message: `A new update ${event.version} has been downloaded`, message: `A new update ${event.version} has been downloaded`,
detail: 'It will be installed the next time you restart the application' detail: 'It will be installed the next time you restart the application'
}, response => {
if (response === 0) {
setTimeout(() => {
autoUpdater.quitAndInstall();
// force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit();
}, 1000);
}
}); });
if (response === 0) {
setTimeout(() => {
autoUpdater.quitAndInstall();
// force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit();
}, 1000);
}
}); });
// Init for updates // Init for updates
autoUpdater.checkForUpdates(); autoUpdater.checkForUpdates();

View File

@@ -1,19 +1,16 @@
'use strict';
import { sentryInit } from '../renderer/js/utils/sentry-util'; import { sentryInit } from '../renderer/js/utils/sentry-util';
import { appUpdater } from './autoupdater'; import { appUpdater } from './autoupdater';
import { setAutoLaunch } from './startup'; import { setAutoLaunch } from './startup';
import windowStateKeeper = require('electron-window-state'); import windowStateKeeper from 'electron-window-state';
import path = require('path'); import path from 'path';
import fs = require('fs'); import fs from 'fs';
import isDev = require('electron-is-dev'); import electron, { app, ipcMain, session, dialog } from 'electron';
import electron = require('electron');
const { app, ipcMain } = electron;
import AppMenu = require('./menu'); import * as AppMenu from './menu';
import BadgeSettings = require('../renderer/js/pages/preference/badge-settings'); import * as BadgeSettings from '../renderer/js/pages/preference/badge-settings';
import ConfigUtil = require('../renderer/js/utils/config-util'); import * as ConfigUtil from '../renderer/js/utils/config-util';
import ProxyUtil = require('../renderer/js/utils/proxy-util'); import * as ProxyUtil from '../renderer/js/utils/proxy-util';
interface PatchedGlobal extends NodeJS.Global { interface PatchedGlobal extends NodeJS.Global {
mainWindowState: windowStateKeeper.State; mainWindowState: windowStateKeeper.State;
@@ -21,12 +18,6 @@ interface PatchedGlobal extends NodeJS.Global {
const globalPatched = global as PatchedGlobal; const globalPatched = global as PatchedGlobal;
// Adds debug features like hotkeys for triggering dev tools and reload
// in development mode
if (isDev) {
require('electron-debug')();
}
// Prevent window being garbage collected // Prevent window being garbage collected
let mainWindow: Electron.BrowserWindow; let mainWindow: Electron.BrowserWindow;
let badgeCount: number; let badgeCount: number;
@@ -57,6 +48,15 @@ const iconPath = (): string => {
return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png'); return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
}; };
// toggle the app window
const toggleApp = (): void => {
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
mainWindow.show();
} else {
mainWindow.hide();
}
};
function createMainWindow(): Electron.BrowserWindow { function createMainWindow(): Electron.BrowserWindow {
// Load the previous state with fallback to defaults // Load the previous state with fallback to defaults
const mainWindowState: windowStateKeeper.State = windowStateKeeper({ const mainWindowState: windowStateKeeper.State = windowStateKeeper({
@@ -81,7 +81,8 @@ function createMainWindow(): Electron.BrowserWindow {
webPreferences: { webPreferences: {
plugins: true, plugins: true,
nodeIntegration: true, nodeIntegration: true,
partition: 'persist:webviewsession' partition: 'persist:webviewsession',
webviewTag: true
}, },
show: false show: false
}); });
@@ -93,9 +94,12 @@ function createMainWindow(): Electron.BrowserWindow {
win.loadURL(mainURL); win.loadURL(mainURL);
// Keep the app running in background on close event // Keep the app running in background on close event
win.on('close', e => { win.on('close', event => {
if (ConfigUtil.getConfigItem('quitOnClose')) {
app.quit();
}
if (!isQuitting) { if (!isQuitting) {
e.preventDefault(); event.preventDefault();
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
app.hide(); app.hide();
@@ -116,8 +120,8 @@ function createMainWindow(): Electron.BrowserWindow {
}); });
// To destroy tray icon when navigate to a new URL // To destroy tray icon when navigate to a new URL
win.webContents.on('will-navigate', e => { win.webContents.on('will-navigate', event => {
if (e) { if (event) {
win.webContents.send('destroytray'); win.webContents.send('destroytray');
} }
}); });
@@ -143,13 +147,20 @@ app.on('certificate-error', (event: Event, _webContents: Electron.WebContents, _
callback(true); callback(true);
}); });
// this event is only available on macOS. Triggers when you click on the dock icon.
app.on('activate', () => { app.on('activate', () => {
if (!mainWindow) { if (mainWindow) {
// if there is already a window show it
mainWindow.show();
} else {
mainWindow = createMainWindow(); mainWindow = createMainWindow();
} }
}); });
app.on('ready', () => { app.on('ready', () => {
const ses = session.fromPartition('persist:webviewsession');
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);
AppMenu.setMenu({ AppMenu.setMenu({
tabs: [] tabs: []
}); });
@@ -158,7 +169,7 @@ app.on('ready', () => {
// Auto-hide menu bar on Windows + Linux // Auto-hide menu bar on Windows + Linux
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
const shouldHideMenu = ConfigUtil.getConfigItem('autoHideMenubar') || false; const shouldHideMenu = ConfigUtil.getConfigItem('autoHideMenubar') || false;
mainWindow.setAutoHideMenuBar(shouldHideMenu); mainWindow.autoHideMenuBar = shouldHideMenu;
mainWindow.setMenuBarVisibility(!shouldHideMenu); mainWindow.setMenuBarVisibility(!shouldHideMenu);
} }
@@ -184,6 +195,10 @@ app.on('ready', () => {
} }
}); });
ipcMain.on('fetch-user-agent', event => {
event.returnValue = session.fromPartition('persist:webviewsession').getUserAgent();
});
page.once('did-frame-finish-load', () => { page.once('did-frame-finish-load', () => {
// Initiate auto-updates on MacOS and Windows // Initiate auto-updates on MacOS and Windows
if (ConfigUtil.getConfigItem('autoUpdate')) { if (ConfigUtil.getConfigItem('autoUpdate')) {
@@ -191,6 +206,27 @@ app.on('ready', () => {
} }
}); });
const permissionCallbacks = new Map();
let nextPermissionId = 0;
page.session.setPermissionRequestHandler((webContents, permission, callback, details) => {
const {origin} = new URL(details.requestingUrl);
permissionCallbacks.set(nextPermissionId, callback);
page.send('permission-request', nextPermissionId, {
webContentsId: webContents.id === mainWindow.webContents.id ?
null :
webContents.id,
origin,
permission
});
nextPermissionId++;
});
ipcMain.on('permission-response', (event: Event, permissionId: number, grant: boolean) => {
permissionCallbacks.get(permissionId)(grant);
permissionCallbacks.delete(permissionId);
});
// Temporarily remove this event // Temporarily remove this event
// electron.powerMonitor.on('resume', () => { // electron.powerMonitor.on('resume', () => {
// mainWindow.reload(); // mainWindow.reload();
@@ -244,38 +280,34 @@ app.on('ready', () => {
}); });
ipcMain.on('toggle-app', () => { ipcMain.on('toggle-app', () => {
if (!mainWindow.isVisible() || mainWindow.isMinimized()) { toggleApp();
mainWindow.show();
} else {
mainWindow.hide();
}
}); });
ipcMain.on('toggle-badge-option', () => { ipcMain.on('toggle-badge-option', () => {
BadgeSettings.updateBadge(badgeCount, mainWindow); BadgeSettings.updateBadge(badgeCount, mainWindow);
}); });
ipcMain.on('toggle-menubar', (_event: Electron.IpcMessageEvent, showMenubar: boolean) => { ipcMain.on('toggle-menubar', (_event: Electron.IpcMainEvent, showMenubar: boolean) => {
mainWindow.setAutoHideMenuBar(showMenubar); mainWindow.autoHideMenuBar = showMenubar;
mainWindow.setMenuBarVisibility(!showMenubar); mainWindow.setMenuBarVisibility(!showMenubar);
page.send('toggle-autohide-menubar', showMenubar, true); page.send('toggle-autohide-menubar', showMenubar, true);
}); });
ipcMain.on('update-badge', (_event: Electron.IpcMessageEvent, messageCount: number) => { ipcMain.on('update-badge', (_event: Electron.IpcMainEvent, messageCount: number) => {
badgeCount = messageCount; badgeCount = messageCount;
BadgeSettings.updateBadge(badgeCount, mainWindow); BadgeSettings.updateBadge(badgeCount, mainWindow);
page.send('tray', messageCount); page.send('tray', messageCount);
}); });
ipcMain.on('update-taskbar-icon', (_event: Electron.IpcMessageEvent, data: any, text: string) => { ipcMain.on('update-taskbar-icon', (_event: Electron.IpcMainEvent, data: string, text: string) => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow); BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
}); });
ipcMain.on('forward-message', (_event: Electron.IpcMessageEvent, listener: any, ...params: any[]) => { ipcMain.on('forward-message', (_event: Electron.IpcMainEvent, listener: string, ...parameters: any[]) => {
page.send(listener, ...params); page.send(listener, ...parameters);
}); });
ipcMain.on('update-menu', (_event: Electron.IpcMessageEvent, props: any) => { ipcMain.on('update-menu', (_event: Electron.IpcMainEvent, props: any) => {
AppMenu.setMenu(props); AppMenu.setMenu(props);
const activeTab = props.tabs[props.activeTabIndex]; const activeTab = props.tabs[props.activeTabIndex];
if (activeTab) { if (activeTab) {
@@ -283,35 +315,50 @@ app.on('ready', () => {
} }
}); });
ipcMain.on('toggleAutoLauncher', (_event: Electron.IpcMessageEvent, AutoLaunchValue: boolean) => { ipcMain.on('toggleAutoLauncher', (_event: Electron.IpcMainEvent, AutoLaunchValue: boolean) => {
setAutoLaunch(AutoLaunchValue); setAutoLaunch(AutoLaunchValue);
}); });
ipcMain.on('downloadFile', (_event: Electron.IpcMessageEvent, url: string, downloadPath: string) => { ipcMain.on('downloadFile', (_event: Electron.IpcMainEvent, url: string, downloadPath: string) => {
page.downloadURL(url); page.downloadURL(url);
page.session.once('will-download', (_event: Event, item) => { page.session.once('will-download', async (_event: Event, item) => {
const filePath = path.join(downloadPath, item.getFilename()); let setFilePath: string;
let shortFileName: string;
if (ConfigUtil.getConfigItem('promptDownload', false)) {
const showDialogOptions: object = {
defaultPath: path.join(downloadPath, item.getFilename())
};
const getTimeStamp = (): any => { const result = await dialog.showSaveDialog(mainWindow, showDialogOptions);
const date = new Date(); if (result.canceled) {
return date.getTime(); item.cancel();
}; return;
}
setFilePath = result.filePath;
shortFileName = path.basename(setFilePath);
} else {
const getTimeStamp = (): number => {
const date = new Date();
return date.getTime();
};
const formatFile = (filePath: string): string => { const formatFile = (filePath: string): string => {
const fileExtension = path.extname(filePath); const fileExtension = path.extname(filePath);
const baseName = path.basename(filePath, fileExtension); const baseName = path.basename(filePath, fileExtension);
return `${baseName}-${getTimeStamp()}${fileExtension}`; return `${baseName}-${getTimeStamp()}${fileExtension}`;
}; };
// Update the name and path of the file if it already exists const filePath = path.join(downloadPath, item.getFilename());
const updatedFilePath = path.join(downloadPath, formatFile(filePath)); // Update the name and path of the file if it already exists
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
const setFilePath = fs.existsSync(filePath) ? updatedFilePath : filePath; setFilePath = fs.existsSync(filePath) ? updatedFilePath : filePath;
shortFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename();
}
item.setSavePath(setFilePath); item.setSavePath(setFilePath);
item.on('updated', (_event: Event, state) => { const updatedListener = (_event: Event, state: string): void => {
switch (state) { switch (state) {
case 'interrupted': { case 'interrupted': {
// Can interrupted to due to network error, cancel download then // Can interrupted to due to network error, cancel download then
@@ -330,38 +377,51 @@ app.on('ready', () => {
console.info('Unknown updated state of download item'); console.info('Unknown updated state of download item');
} }
} }
}); };
item.on('updated', updatedListener);
item.once('done', (_event: Event, state) => { item.once('done', (_event: Event, state) => {
const getFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename();
if (state === 'completed') { if (state === 'completed') {
page.send('downloadFileCompleted', item.getSavePath(), getFileName); page.send('downloadFileCompleted', item.getSavePath(), shortFileName);
} else { } else {
console.log('Download failed state: ', state); console.log('Download failed state:', state);
page.send('downloadFileFailed'); page.send('downloadFileFailed');
} }
// To stop item for listening to updated events of this file // To stop item for listening to updated events of this file
item.removeAllListeners('updated'); item.removeListener('updated', updatedListener);
}); });
}); });
}); });
ipcMain.on('realm-name-changed', (_event: Electron.IpcMessageEvent, serverURL: string, realmName: string) => { ipcMain.on('realm-name-changed', (_event: Electron.IpcMainEvent, serverURL: string, realmName: string) => {
page.send('update-realm-name', serverURL, realmName); page.send('update-realm-name', serverURL, realmName);
}); });
ipcMain.on('realm-icon-changed', (_event: Electron.IpcMessageEvent, serverURL: string, iconURL: string) => { ipcMain.on('realm-icon-changed', (_event: Electron.IpcMainEvent, serverURL: string, iconURL: string) => {
page.send('update-realm-icon', serverURL, iconURL); page.send('update-realm-icon', serverURL, iconURL);
}); });
// Using event.sender.send instead of page.send here to // Using event.sender.send instead of page.send here to
// make sure the value of errorReporting is sent only once on load. // make sure the value of errorReporting is sent only once on load.
ipcMain.on('error-reporting', (event: Electron.IpcMessageEvent) => { ipcMain.on('error-reporting', (event: Electron.IpcMainEvent) => {
event.sender.send('error-reporting-val', errorReporting); event.sender.send('error-reporting-val', errorReporting);
}); });
ipcMain.on('save-last-tab', (_event: Electron.IpcMessageEvent, index: number) => { ipcMain.on('save-last-tab', (_event: Electron.IpcMainEvent, index: number) => {
ConfigUtil.setConfigItem('lastActiveTab', index); 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', () => { app.on('before-quit', () => {

View File

@@ -1,11 +1,11 @@
import { app, Notification } from 'electron'; import { app, Notification } from 'electron';
import request = require('request'); import request from 'request';
import semver = require('semver'); import semver from 'semver';
import ConfigUtil = require('../renderer/js/utils/config-util'); import * as ConfigUtil from '../renderer/js/utils/config-util';
import ProxyUtil = require('../renderer/js/utils/proxy-util'); import * as ProxyUtil from '../renderer/js/utils/proxy-util';
import LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util'); import * as LinuxUpdateUtil from '../renderer/js/utils/linux-update-util';
import Logger = require('../renderer/js/utils/logger-util'); import Logger from '../renderer/js/utils/logger-util';
const logger = new Logger({ const logger = new Logger({
file: 'linux-update-util.log', file: 'linux-update-util.log',

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,8 @@
'use strict';
import { app } from 'electron'; import { app } from 'electron';
import AutoLaunch = require('auto-launch'); import AutoLaunch from 'auto-launch';
import isDev = require('electron-is-dev'); import isDev from 'electron-is-dev';
import ConfigUtil = require('../renderer/js/utils/config-util'); import * as ConfigUtil from '../renderer/js/utils/config-util';
export const setAutoLaunch = (AutoLaunchValue: boolean): void => { export const setAutoLaunch = (AutoLaunchValue: boolean): void => {
// Don't run this in development // Don't run this in development
@@ -11,21 +10,23 @@ export const setAutoLaunch = (AutoLaunchValue: boolean): void => {
return; return;
} }
// On Mac, work around a bug in auto-launch where it opens a Terminal window
// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
const appPath = process.platform === 'darwin' ? app.getPath('exe').replace(/\.app\/Content.*/, '.app') : undefined; // Use the default
const ZulipAutoLauncher = new AutoLaunch({
name: 'Zulip',
path: appPath,
isHidden: false
});
const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue); const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue);
if (autoLaunchOption) { // setLoginItemSettings doesn't support linux
ZulipAutoLauncher.enable(); if (process.platform === 'linux') {
const ZulipAutoLauncher = new AutoLaunch({
name: 'Zulip',
isHidden: false
});
if (autoLaunchOption) {
ZulipAutoLauncher.enable();
} else {
ZulipAutoLauncher.disable();
}
} else { } else {
ZulipAutoLauncher.disable(); app.setLoginItemSettings({
openAtLogin: autoLaunchOption,
openAsHidden: false
});
} }
}; };

1681
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +0,0 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "3.1.0-beta",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
"copyright": "Kandra Labs, Inc.",
"author": {
"name": "Kandra Labs, Inc.",
"email": "support@zulipchat.com"
},
"repository": {
"type": "git",
"url": "https://github.com/zulip/zulip-desktop.git"
},
"bugs": {
"url": "https://github.com/zulip/zulip-desktop/issues"
},
"main": "main/index.js",
"keywords": [
"Zulip",
"Group Chat app",
"electron-app",
"electron",
"Desktop app",
"InstantMessaging"
],
"dependencies": {
"@electron-elements/send-feedback": "1.0.8",
"@sentry/electron": "0.14.0",
"adm-zip": "0.4.11",
"auto-launch": "5.0.5",
"dotenv": "8.0.0",
"electron-is-dev": "0.3.0",
"electron-log": "2.2.14",
"electron-spellchecker": "1.1.2",
"electron-updater": "4.0.6",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"is-online": "7.0.0",
"node-json-db": "0.9.2",
"request": "2.85.0",
"semver": "5.4.1",
"wurl": "2.5.0"
},
"optionalDependencies": {
"node-mac-notifier": "1.1.0"
}
}

View File

@@ -14,33 +14,19 @@
<div class="maintenance-info"> <div class="maintenance-info">
<p class="detail maintainer"> <p class="detail maintainer">
Maintained by Maintained by
<a onclick="linkInBrowser('website')">Zulip</a> <a href="https://zulipchat.com" target="_blank" rel="noopener noreferrer">Zulip</a>
</p> </p>
<p class="detail license"> <p class="detail license">
Available under the Available under the
<a onclick="linkInBrowser('license')">Apache 2.0 License</a> <a href="https://github.com/zulip/zulip-desktop/blob/master/LICENSE" target="_blank" rel="noopener noreferrer">Apache 2.0 License</a>
</p> </p>
</div> </div>
</div> </div>
<script> <script>
const { app } = require('electron').remote; const { app } = require('electron').remote;
const { shell } = require('electron');
const version_tag = document.querySelector('#version'); const version_tag = document.querySelector('#version');
version_tag.innerHTML = 'v' + app.getVersion(); version_tag.innerHTML = 'v' + app.getVersion();
function linkInBrowser(type) {
let url;
switch (type) {
case 'website':
url = "https://zulipchat.com";
break;
case 'license':
url = "https://github.com/zulip/zulip-desktop/blob/master/LICENSE";
break;
}
shell.openExternal(url);
}
</script> </script>
<script>require('./js/shared/preventdrag.js')</script> <script>require('./js/shared/preventdrag.js')</script>
</body> </body>

View File

@@ -17,27 +17,43 @@ body {
} }
#title { #title {
text-align: left;
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
margin: 20px 0; margin: 20px 0;
} }
#subtitle {
font-size: 20px;
text-align: left;
margin: 12px 0;
}
#description { #description {
text-align: left;
font-size: 16px; font-size: 16px;
list-style-position: inside;
} }
#reconnect { #reconnect {
float: left;
}
#settings {
margin-left: 116px;
}
.button {
font-size: 16px; font-size: 16px;
background: rgba(0, 150, 136, 1.000); background: rgba(0, 150, 136, 1.000);
color: rgba(255, 255, 255, 1.000); color: rgba(255, 255, 255, 1.000);
width: 84px; width: 96px;
height: 32px; height: 32px;
border-radius: 5px; border-radius: 5px;
line-height: 32px; line-height: 32px;
margin: 20px auto 0;
cursor: pointer; cursor: pointer;
} }
#reconnect:hover { .button:hover {
opacity: 0.8; opacity: 0.8;
} }

View File

@@ -399,8 +399,17 @@ i.open-tab-button {
cursor: pointer; cursor: pointer;
} }
.css-delete-action { #css-delete-action {
margin-bottom: 10px; display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
/*
.action class will add extra margin to right which
we don't want for a button; the extra margin is intended for radio buttons
*/
margin-right: 0px;
} }
.selected-css-path, .selected-css-path,
@@ -453,6 +462,11 @@ i.open-tab-button {
user-select: none; user-select: none;
} }
.disallowed:hover {
background-color: rgba(241, 241, 241, 1.000);
cursor: not-allowed;
}
input.toggle-round + label { input.toggle-round + label {
padding: 2px; padding: 2px;
width: 50px; width: 50px;
@@ -628,6 +642,26 @@ input.toggle-round:checked + label::after {
cursor: pointer; cursor: pointer;
} }
.server-network-option {
font-weight: bold;
font-size: 1.1rem;
margin-top: 10px;
padding-top: 15px;
align-items: center;
text-align: center;
color: rgb(78, 191, 172);
width: 98%;
height: 46px;
cursor: pointer;
}
i.open-network-button {
font-size: 16px;
cursor: pointer;
padding-left: 5px;
position: absolute;
}
/* responsive grid */ /* responsive grid */
@media (max-width: 650px) { @media (max-width: 650px) {
@@ -690,3 +724,13 @@ input.toggle-round:checked + label::after {
margin-top: 10px; margin-top: 10px;
} }
} }
.lang-menu {
font-size: 13px;
font-weight: bold;
background: rgba(78, 191, 172, 1.000);
width: 100px;
height: 38px;
color: rgba(255, 255, 255, 1.000);
border-color: rgba(0, 0, 0, 0);
}

View File

@@ -1,11 +1,7 @@
'use strict'; export default class BaseComponent {
class BaseComponent {
generateNodeFromTemplate(template: string): Element | null { generateNodeFromTemplate(template: string): Element | null {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.innerHTML = template; wrapper.innerHTML = template;
return wrapper.firstElementChild; return wrapper.firstElementChild;
} }
} }
export = BaseComponent;

View File

@@ -1,8 +1,6 @@
'use strict'; import Tab, { TabProps } from './tab';
import Tab = require('./tab'); export default class FunctionalTab extends Tab {
class FunctionalTab extends Tab {
$closeButton: Element; $closeButton: Element;
template(): string { template(): string {
return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}"> return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
@@ -15,8 +13,7 @@ class FunctionalTab extends Tab {
</div>`; </div>`;
} }
// TODO: Typescript - This type for props should be TabProps constructor(props: TabProps) {
constructor(props: any) {
super(props); super(props);
this.init(); this.init();
} }
@@ -41,11 +38,9 @@ class FunctionalTab extends Tab {
this.$closeButton.classList.remove('active'); this.$closeButton.classList.remove('active');
}); });
this.$closeButton.addEventListener('click', (e: Event) => { this.$closeButton.addEventListener('click', (event: Event) => {
this.props.onDestroy(); this.props.onDestroy();
e.stopPropagation(); event.stopPropagation();
}); });
} }
} }
export = FunctionalTab;

View File

@@ -1,83 +1,56 @@
import { ipcRenderer, remote } from 'electron'; import { ipcRenderer, remote } from 'electron';
import LinkUtil = require('../utils/link-util'); import * as LinkUtil from '../utils/link-util';
import DomainUtil = require('../utils/domain-util'); import * as ConfigUtil from '../utils/config-util';
import ConfigUtil = require('../utils/config-util'); import type WebView from './webview';
const { shell, app } = remote; const { shell, app } = remote;
const dingSound = new Audio('../resources/sounds/ding.ogg'); const dingSound = new Audio('../resources/sounds/ding.ogg');
// TODO: TypeScript - Figure out a way to pass correct type here. export default function handleExternalLink(this: WebView, event: Electron.NewWindowEvent): void {
function handleExternalLink(this: any, event: any): void { event.preventDefault();
const { url } = event;
const domainPrefix = DomainUtil.getDomain(this.props.index).url; const url = new URL(event.url);
const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`); const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`);
const shouldShowInFolder = ConfigUtil.getConfigItem('showDownloadFolder', false);
// Whitelist URLs which are allowed to be opened in the app if (LinkUtil.isUploadsUrl(this.props.url, url)) {
const { ipcRenderer.send('downloadFile', url.href, downloadPath);
isInternalUrl: isWhiteListURL, ipcRenderer.once('downloadFileCompleted', (_event: Event, filePath: string, fileName: string) => {
isUploadsUrl: isUploadsURL const downloadNotification = new Notification('Download Complete', {
} = LinkUtil.isInternal(domainPrefix, url); body: `Click to show ${fileName} in folder`,
silent: true // We'll play our own sound - ding.ogg
if (isWhiteListURL) {
event.preventDefault();
// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
// Show pdf attachments in a new window
// if (LinkUtil.isPDF(url) && isUploadsURL) {
// ipcRenderer.send('pdf-view', url);
// return;
// }
// download txt, mp3, mp4 etc.. by using downloadURL in the
// main process which allows the user to save the files to their desktop
// and not trigger webview reload while image in webview will
// do nothing and will not save it
// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
// if (!LinkUtil.isImage(url) && !LinkUtil.isPDF(url) && isUploadsURL) {
if (!LinkUtil.isImage(url) && isUploadsURL) {
ipcRenderer.send('downloadFile', url, downloadPath);
ipcRenderer.once('downloadFileCompleted', (_event: Event, filePath: string, fileName: string) => {
const downloadNotification = new Notification('Download Complete', {
body: shouldShowInFolder ? `Click to show ${fileName} in folder` : `Click to open ${fileName}`,
silent: true // We'll play our own sound - ding.ogg
});
// Play sound to indicate download complete
if (!ConfigUtil.getConfigItem('silent')) {
dingSound.play();
}
downloadNotification.addEventListener('click', () => {
if (shouldShowInFolder) {
// Reveal file in download folder
shell.showItemInFolder(filePath);
} else {
// Open file in the default native app
shell.openItem(filePath);
}
});
ipcRenderer.removeAllListeners('downloadFileFailed');
}); });
ipcRenderer.once('downloadFileFailed', () => { // Play sound to indicate download complete
// Automatic download failed, so show save dialog prompt and download if (!ConfigUtil.getConfigItem('silent')) {
// through webview dingSound.play();
this.$el.downloadURL(url); }
ipcRenderer.removeAllListeners('downloadFileCompleted');
});
return;
}
// open internal urls inside the current webview. downloadNotification.addEventListener('click', () => {
this.$el.loadURL(url); // Reveal file in download folder
shell.showItemInFolder(filePath);
});
ipcRenderer.removeAllListeners('downloadFileFailed');
});
ipcRenderer.once('downloadFileFailed', () => {
// 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)
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 { } else {
event.preventDefault(); LinkUtil.openBrowser(url);
shell.openExternal(url);
} }
} }
export = handleExternalLink;

View File

@@ -1,11 +1,9 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import Tab = require('./tab'); import Tab, { TabProps } from './tab';
import SystemUtil = require('../utils/system-util'); import * as SystemUtil from '../utils/system-util';
class ServerTab extends Tab { export default class ServerTab extends Tab {
$badge: Element; $badge: Element;
template(): string { template(): string {
@@ -19,8 +17,7 @@ class ServerTab extends Tab {
</div>`; </div>`;
} }
// TODO: Typescript - This type for props should be TabProps constructor(props: TabProps) {
constructor(props: any) {
super(props); super(props);
this.init(); this.init();
} }
@@ -64,5 +61,3 @@ class ServerTab extends Tab {
return shortcutText; return shortcutText;
} }
} }
export = ServerTab;

View File

@@ -1,14 +1,22 @@
'use strict'; import WebView from './webview';
import BaseComponent from './base';
import WebView = require('./webview'); export interface TabProps {
import BaseComponent = require('./base'); role: string;
icon?: string;
// TODO: TypeScript - Type annotate props name: string;
interface TabProps { $root: Element;
[key: string]: any; onClick: () => void;
index: number;
tabIndex: number;
onHover?: () => void;
onHoverOut?: () => void;
webview: WebView;
materialIcon?: string;
onDestroy?: () => void;
} }
class Tab extends BaseComponent { export default class Tab extends BaseComponent {
props: TabProps; props: TabProps;
webview: WebView; webview: WebView;
$el: Element; $el: Element;
@@ -25,6 +33,10 @@ class Tab extends BaseComponent {
this.$el.addEventListener('mouseout', this.props.onHoverOut); this.$el.addEventListener('mouseout', this.props.onHoverOut);
} }
showNetworkError(): void {
this.webview.forceLoad();
}
activate(): void { activate(): void {
this.$el.classList.add('active'); this.$el.classList.add('active');
this.webview.load(); this.webview.load();
@@ -40,5 +52,3 @@ class Tab extends BaseComponent {
this.webview.$el.parentNode.removeChild(this.webview.$el); this.webview.$el.parentNode.removeChild(this.webview.$el);
} }
} }
export = Tab;

View File

@@ -1,30 +1,41 @@
'use strict'; import { ipcRenderer, remote } from 'electron';
import { remote } from 'electron';
import path = require('path'); import path from 'path';
import fs = require('fs'); import fs from 'fs';
import ConfigUtil = require('../utils/config-util'); import * as ConfigUtil from '../utils/config-util';
import SystemUtil = require('../utils/system-util'); import * as SystemUtil from '../utils/system-util';
import BaseComponent = require('../components/base'); import BaseComponent from './base';
import handleExternalLink = require('../components/handle-external-link'); import handleExternalLink from './handle-external-link';
const { app, dialog } = remote; const { app, dialog } = remote;
const shouldSilentWebview = ConfigUtil.getConfigItem('silent'); const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
// TODO: TypeScript - Type annotate WebViewProps.
interface WebViewProps { interface WebViewProps {
[key: string]: any; $root: Element;
index: number;
tabIndex: number;
url: any;
role: string;
name: string;
isActive: () => boolean;
switchLoading: (loading: any, url: string) => void;
onNetworkError: (index: number) => void;
nodeIntegration: boolean;
preload: boolean;
onTitleChange: any;
hasPermission?: (origin: string, permission: string) => boolean;
} }
class WebView extends BaseComponent { export default class WebView extends BaseComponent {
props: any; props: WebViewProps;
zoomFactor: number; zoomFactor: number;
badgeCount: number; badgeCount: number;
loading: boolean; loading: boolean;
customCSS: string; customCSS: string;
$webviewsContainer: DOMTokenList; $webviewsContainer: DOMTokenList;
$el: Electron.WebviewTag; $el: Electron.WebviewTag;
domReady?: Promise<void>;
// This is required because in main.js we access WebView.method as // This is required because in main.js we access WebView.method as
// webview[method]. // webview[method].
@@ -34,7 +45,7 @@ class WebView extends BaseComponent {
super(); super();
this.props = props; this.props = props;
this.zoomFactor = 1.0; this.zoomFactor = 1;
this.loading = true; this.loading = true;
this.badgeCount = 0; this.badgeCount = 0;
this.customCSS = ConfigUtil.getConfigItem('customCSS'); this.customCSS = ConfigUtil.getConfigItem('customCSS');
@@ -47,16 +58,18 @@ class WebView extends BaseComponent {
data-tab-id="${this.props.tabIndex}" data-tab-id="${this.props.tabIndex}"
src="${this.props.url}" src="${this.props.url}"
${this.props.nodeIntegration ? 'nodeIntegration' : ''} ${this.props.nodeIntegration ? 'nodeIntegration' : ''}
disablewebsecurity
${this.props.preload ? 'preload="js/preload.js"' : ''} ${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession" partition="persist:webviewsession"
name="${this.props.name}" name="${this.props.name}"
webpreferences="allowRunningInsecureContent, javascript=yes"> webpreferences="${this.props.nodeIntegration ? '' : 'contextIsolation, '}javascript=yes">
</webview>`; </webview>`;
} }
init(): void { init(): void {
this.$el = this.generateNodeFromTemplate(this.template()) as Electron.WebviewTag; 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.props.$root.append(this.$el);
this.registerListeners(); this.registerListeners();
@@ -125,7 +138,9 @@ class WebView extends BaseComponent {
const hasConnectivityErr = SystemUtil.connectivityERR.includes(errorDescription); const hasConnectivityErr = SystemUtil.connectivityERR.includes(errorDescription);
if (hasConnectivityErr) { if (hasConnectivityErr) {
console.error('error', errorDescription); console.error('error', errorDescription);
this.props.onNetworkError(); if (!this.props.url.includes('network.html')) {
this.props.onNetworkError(this.props.index);
}
} }
}); });
@@ -134,12 +149,6 @@ class WebView extends BaseComponent {
if (!isSettingPage) { if (!isSettingPage) {
this.props.switchLoading(true, this.props.url); this.props.switchLoading(true, this.props.url);
} }
let userAgent = SystemUtil.getUserAgent();
if (!userAgent) {
SystemUtil.setUserAgent(this.$el.getUserAgent());
userAgent = SystemUtil.getUserAgent();
}
this.$el.setUserAgent(userAgent);
}); });
this.$el.addEventListener('did-stop-loading', () => { this.$el.addEventListener('did-stop-loading', () => {
@@ -152,6 +161,10 @@ class WebView extends BaseComponent {
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0; return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
} }
showNotificationSettings(): void {
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'show-notification-settings');
}
show(): void { show(): void {
// Do not show WebView if another tab was selected and this tab should be in background. // Do not show WebView if another tab was selected and this tab should be in background.
if (!this.props.isActive()) { if (!this.props.isActive()) {
@@ -184,8 +197,8 @@ class WebView extends BaseComponent {
this.customCSS = null; this.customCSS = null;
ConfigUtil.setConfigItem('customCSS', null); ConfigUtil.setConfigItem('customCSS', null);
const errMsg = 'The custom css previously set is deleted!'; const errorMessage = 'The custom css previously set is deleted!';
dialog.showErrorBox('custom css file deleted!', errMsg); dialog.showErrorBox('custom css file deleted!', errorMessage);
return; return;
} }
@@ -232,16 +245,16 @@ class WebView extends BaseComponent {
} }
zoomActualSize(): void { zoomActualSize(): void {
this.zoomFactor = 1.0; this.zoomFactor = 1;
this.$el.setZoomFactor(this.zoomFactor); this.$el.setZoomFactor(this.zoomFactor);
} }
logOut(): void { logOut(): void {
this.$el.executeJavaScript('logout()'); ipcRenderer.sendTo(this.$el.getWebContentsId(), 'logout');
} }
showShortcut(): void { showShortcut(): void {
this.$el.executeJavaScript('shortcut()'); ipcRenderer.sendTo(this.$el.getWebContentsId(), 'shortcut');
} }
openDevTools(): void { openDevTools(): void {
@@ -275,12 +288,16 @@ class WebView extends BaseComponent {
// Shows the loading indicator till the webview is reloaded // Shows the loading indicator till the webview is reloaded
this.$webviewsContainer.remove('loaded'); this.$webviewsContainer.remove('loaded');
this.loading = true; this.loading = true;
this.props.switchLoading(true, this.props.url);
this.$el.reload(); this.$el.reload();
} }
send(channel: string, ...param: any[]): void { forceLoad(): void {
this.$el.send(channel, ...param); this.init();
}
async send(channel: string, ...parameters: any[]): Promise<void> {
await this.domReady;
this.$el.send(channel, ...parameters);
} }
} }
export = WebView;

View File

@@ -1,21 +1,51 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import events = require('events'); import { EventEmitter } from 'events';
import { NotificationData, newNotification } from './notification';
type ListenerType = ((...args: any[]) => void); type ListenerType = ((...args: any[]) => void);
// we have and will have some non camelcase stuff class ElectronBridge extends EventEmitter {
// while working with zulip so just turning the rule off send_notification_reply_message_supported: boolean;
// for the whole file. idle_on_system: boolean;
/* eslint-disable @typescript-eslint/camelcase */ last_active_on_system: number;
class ElectronBridge extends events {
send_event(eventName: string | symbol, ...args: any[]): void { constructor() {
this.emit(eventName, ...args); super();
this.send_notification_reply_message_supported = false;
// Indicates if the user is idle or not
this.idle_on_system = false;
// Indicates the time at which user was last active
this.last_active_on_system = Date.now();
} }
on_event(eventName: string, listener: ListenerType): void { send_event = (eventName: string | symbol, ...args: any[]): void => {
this.emit(eventName, ...args);
};
on_event = (eventName: string, listener: ListenerType): void => {
this.on(eventName, listener); this.on(eventName, listener);
} };
new_notification = (
title: string,
options: NotificationOptions | undefined,
dispatch: (type: string, eventInit: EventInit) => boolean
): NotificationData =>
newNotification(title, options, dispatch);
get_idle_on_system = (): boolean => this.idle_on_system;
get_last_active_on_system = (): number => this.last_active_on_system;
get_send_notification_reply_message_supported = (): boolean =>
this.send_notification_reply_message_supported;
set_send_notification_reply_message_supported = (value: boolean): void => {
this.send_notification_reply_message_supported = value;
};
} }
const electron_bridge = new ElectronBridge(); const electron_bridge = new ElectronBridge();
@@ -40,6 +70,4 @@ electron_bridge.on('realm_icon_url', iconURL => {
// functions zulip side will emit event using ElectronBrigde.send_event // functions zulip side will emit event using ElectronBrigde.send_event
// which is alias of .emit and on this side we can handle the data by adding // which is alias of .emit and on this side we can handle the data by adding
// a listener for the event. // a listener for the event.
export = electron_bridge; export default electron_bridge;
/* eslint-enable @typescript-eslint/camelcase */

View File

@@ -1,8 +1,8 @@
import { remote } from 'electron'; import { remote } from 'electron';
import SendFeedback from '@electron-elements/send-feedback'; import SendFeedback from '@electron-elements/send-feedback';
import path = require('path'); import path from 'path';
import fs = require('fs'); import fs from 'fs';
const { app } = remote; const { app } = remote;
@@ -34,12 +34,26 @@ customElements.define('send-feedback', SendFeedback);
export const sendFeedback: SendFeedbackType = document.querySelector('send-feedback'); export const sendFeedback: SendFeedbackType = document.querySelector('send-feedback');
export const feedbackHolder = sendFeedback.parentElement; export const feedbackHolder = sendFeedback.parentElement;
/* eslint-disable no-multi-str */
// customize the fields of custom elements // customize the fields of custom elements
sendFeedback.title = 'Report Issue'; sendFeedback.title = 'Report Issue';
sendFeedback.titleLabel = 'Issue title:'; sendFeedback.titleLabel = 'Issue title:';
sendFeedback.titlePlaceholder = 'Enter issue title'; sendFeedback.titlePlaceholder = 'Enter issue title';
sendFeedback.textareaLabel = 'Describe the issue:'; sendFeedback.textareaLabel = 'Describe the issue:';
sendFeedback.textareaPlaceholder = 'Succinctly describe your issue and steps to reproduce it...'; sendFeedback.textareaPlaceholder = 'Succinctly describe your issue and steps to reproduce it...\n\n\
---\n\
<!-- Please Include: -->\n\
- **Operating System**:\n\
- [ ] Windows\n\
- [ ] Linux/Ubuntu\n\
- [ ] macOS\n\
- **Clear steps to reproduce the issue**:\n\
- **Relevant error messages and/or screenshots**:\n\
';
/* eslint-enable no-multi-str */
sendFeedback.buttonLabel = 'Report Issue'; sendFeedback.buttonLabel = 'Report Issue';
sendFeedback.loaderSuccessText = ''; sendFeedback.loaderSuccessText = '';
@@ -47,10 +61,10 @@ sendFeedback.useReporter('emailReporter', {
email: 'akash@zulipchat.com' email: 'akash@zulipchat.com'
}); });
feedbackHolder.addEventListener('click', (e: Event) => { feedbackHolder.addEventListener('click', (event: Event) => {
// only remove the class if the grey out faded // only remove the class if the grey out faded
// part is clicked and not the feedback element itself // part is clicked and not the feedback element itself
if (e.target === e.currentTarget) { if (event.target === event.currentTarget) {
feedbackHolder.classList.remove('show'); feedbackHolder.classList.remove('show');
} }
}); });

122
app/renderer/js/injected.ts Normal file
View File

@@ -0,0 +1,122 @@
'use strict';
(() => {
const zulipWindow = window as typeof window & {
electron_bridge: any;
narrow: any;
page_params: any;
raw_electron_bridge: any;
};
const electron_bridge = {
...zulipWindow.raw_electron_bridge,
get idle_on_system() {
return this.get_idle_on_system();
},
get last_active_on_system() {
return this.get_last_active_on_system();
},
get send_notification_reply_message_supported() {
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', {
serverLanguage: page_params.default_language
});
}
})();
electron_bridge.on_event('narrow-by-topic', (id: string) => {
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;
})();

View File

@@ -1,30 +1,26 @@
'use strict'; import { ipcRenderer, remote, clipboard } from 'electron';
import { ipcRenderer, remote, clipboard, shell } from 'electron';
import { feedbackHolder } from './feedback'; import { feedbackHolder } from './feedback';
import path = require('path'); import path from 'path';
import escape = require('escape-html'); import escape from 'escape-html';
import isDev = require('electron-is-dev'); import isDev from 'electron-is-dev';
const { session, app, Menu, dialog } = remote; const { session, app, Menu, dialog } = remote;
// eslint-disable-next-line import/no-unassigned-import // eslint-disable-next-line import/no-unassigned-import
require('./tray'); import './tray';
import DomainUtil = require('./utils/domain-util'); import * as DomainUtil from './utils/domain-util';
import WebView = require('./components/webview'); import WebView from './components/webview';
import ServerTab = require('./components/server-tab'); import ServerTab from './components/server-tab';
import FunctionalTab = require('./components/functional-tab'); import FunctionalTab from './components/functional-tab';
import ConfigUtil = require('./utils/config-util'); import * as ConfigUtil from './utils/config-util';
import DNDUtil = require('./utils/dnd-util'); import * as DNDUtil from './utils/dnd-util';
import ReconnectUtil = require('./utils/reconnect-util'); import ReconnectUtil from './utils/reconnect-util';
import Logger = require('./utils/logger-util'); import Logger from './utils/logger-util';
import CommonUtil = require('./utils/common-util'); import * as CommonUtil from './utils/common-util';
import * as EnterpriseUtil from './utils/enterprise-util';
const logger = new Logger({ import * as LinkUtil from './utils/link-util';
file: 'errors.log', import * as Messages from '../../resources/messages';
timestamp: true
});
interface FunctionalTabProps { interface FunctionalTabProps {
name: string; name: string;
@@ -60,12 +56,18 @@ interface SettingsOptions {
flashTaskbarOnMessage?: boolean; flashTaskbarOnMessage?: boolean;
}; };
downloadsPath: string; downloadsPath: string;
showDownloadFolder: boolean; quitOnClose: boolean;
promptDownload: boolean;
flashTaskbarOnMessage?: boolean; flashTaskbarOnMessage?: boolean;
dockBouncing?: boolean; dockBouncing?: boolean;
loading?: AnyObject; loading?: AnyObject;
} }
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
const rendererDirectory = path.resolve(__dirname, '..'); const rendererDirectory = path.resolve(__dirname, '..');
type ServerOrFunctionalTab = ServerTab | FunctionalTab; type ServerOrFunctionalTab = ServerTab | FunctionalTab;
@@ -93,6 +95,7 @@ class ServerManagerView {
tabs: ServerOrFunctionalTab[]; tabs: ServerOrFunctionalTab[];
functionalTabs: AnyObject; functionalTabs: AnyObject;
tabIndex: number; tabIndex: number;
presetOrgs: string[];
constructor() { constructor() {
this.$addServerButton = document.querySelector('#add-tab'); this.$addServerButton = document.querySelector('#add-tab');
this.$tabsContainer = document.querySelector('#tabs-container'); this.$tabsContainer = document.querySelector('#tabs-container');
@@ -128,47 +131,49 @@ class ServerManagerView {
this.loading = {}; this.loading = {};
this.activeTabIndex = -1; this.activeTabIndex = -1;
this.tabs = []; this.tabs = [];
this.presetOrgs = [];
this.functionalTabs = {}; this.functionalTabs = {};
this.tabIndex = 0; this.tabIndex = 0;
} }
init(): void { async init(): Promise<void> {
this.loadProxy().then(() => { await this.loadProxy();
this.initSidebar(); this.initDefaultSettings();
this.initTabs(); this.initSidebar();
this.initActions(); this.removeUAfromDisk();
this.registerIpcs(); if (EnterpriseUtil.configFile) {
this.initDefaultSettings(); this.initPresetOrgs();
}); }
this.initTabs();
this.initActions();
this.registerIpcs();
} }
loadProxy(): Promise<boolean> { async loadProxy(): Promise<void> {
return new Promise(resolve => { // To change proxyEnable to useManualProxy in older versions
// To change proxyEnable to useManualProxy in older versions const proxyEnabledOld = ConfigUtil.isConfigItemExists('useProxy');
const proxyEnabledOld = ConfigUtil.isConfigItemExists('useProxy'); if (proxyEnabledOld) {
if (proxyEnabledOld) { const proxyEnableOldState = ConfigUtil.getConfigItem('useProxy');
const proxyEnableOldState = ConfigUtil.getConfigItem('useProxy'); if (proxyEnableOldState) {
if (proxyEnableOldState) { ConfigUtil.setConfigItem('useManualProxy', true);
ConfigUtil.setConfigItem('useManualProxy', true);
}
ConfigUtil.removeConfigItem('useProxy');
} }
ConfigUtil.removeConfigItem('useProxy');
}
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy'); const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
if (proxyEnabled) { if (proxyEnabled) {
session.fromPartition('persist:webviewsession').setProxy({ await session.fromPartition('persist:webviewsession').setProxy({
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''), pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
proxyRules: ConfigUtil.getConfigItem('proxyRules', ''), proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '') proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
}, resolve); });
} else { } else {
session.fromPartition('persist:webviewsession').setProxy({ await session.fromPartition('persist:webviewsession').setProxy({
pacScript: '', pacScript: '',
proxyRules: '', proxyRules: '',
proxyBypassRules: '' proxyBypassRules: ''
}, resolve); });
} }
});
} }
// Settings are initialized only when user clicks on General/Server/Network section settings // Settings are initialized only when user clicks on General/Server/Network section settings
@@ -199,7 +204,8 @@ class ServerManagerView {
silent: false silent: false
}, },
downloadsPath: `${app.getPath('downloads')}`, downloadsPath: `${app.getPath('downloads')}`,
showDownloadFolder: false quitOnClose: false,
promptDownload: false
}; };
// Platform specific settings // Platform specific settings
@@ -219,10 +225,12 @@ class ServerManagerView {
settingOptions.autoHideMenubar = false; settingOptions.autoHideMenubar = false;
} }
for (const i in settingOptions) { for (const [setting, value] of Object.entries(settingOptions)) {
const setting = i as keyof SettingsOptions; // give preference to defaults defined in global_config.json
if (ConfigUtil.getConfigItem(i) === null) { if (EnterpriseUtil.configItemExists(setting)) {
ConfigUtil.setConfigItem(i, settingOptions[setting]); ConfigUtil.setConfigItem(setting, EnterpriseUtil.getConfigItem(setting), true);
} else if (ConfigUtil.getConfigItem(setting) === null) {
ConfigUtil.setConfigItem(setting, value);
} }
} }
} }
@@ -232,20 +240,106 @@ class ServerManagerView {
this.toggleSidebar(showSidebar); this.toggleSidebar(showSidebar);
} }
// Remove the stale UA string from the disk if the app is not freshly
// installed. This should be removed in a further release.
removeUAfromDisk(): void {
ConfigUtil.removeConfigItem('userAgent');
}
async queueDomain(domain: any): Promise<boolean> {
// allows us to start adding multiple domains to the app simultaneously
// promise of addition resolves in both cases, but we consider it rejected
// if the resolved value is false
try {
const serverConf = await DomainUtil.checkDomain(domain);
await DomainUtil.addDomain(serverConf);
return true;
} catch (err) {
logger.error(err);
logger.error('Could not add ' + domain + '. Please contact your system administrator.');
return false;
}
}
async initPresetOrgs(): Promise<void> {
// read preset organizations from global_config.json and queues them
// for addition to the app's domains
const preAddedDomains = DomainUtil.getDomains();
this.presetOrgs = EnterpriseUtil.getConfigItem('presetOrganizations', []);
// set to true if at least one new domain is added
const domainPromises = [];
for (const url of this.presetOrgs) {
if (DomainUtil.duplicateDomain(url)) {
continue;
}
domainPromises.push(this.queueDomain(url));
}
const domainsAdded = await Promise.all(domainPromises);
if (domainsAdded.includes(true)) {
// at least one domain was resolved
if (preAddedDomains.length > 0) {
// user already has servers added
// ask them before reloading the app
const { response } = await dialog.showMessageBox({
type: 'question',
buttons: ['Yes', 'Later'],
defaultId: 0,
message: 'New server' + (domainsAdded.length > 1 ? 's' : '') + ' added. Reload app now?'
});
if (response === 0) {
ipcRenderer.send('reload-full-app');
}
} else {
ipcRenderer.send('reload-full-app');
}
} else if (domainsAdded.length > 0) {
// find all orgs that failed
const failedDomains: string[] = [];
for (const org of this.presetOrgs) {
if (DomainUtil.duplicateDomain(org)) {
continue;
}
failedDomains.push(org);
}
const { title, content } = Messages.enterpriseOrgError(domainsAdded.length, failedDomains);
dialog.showErrorBox(title, content);
if (DomainUtil.getDomains().length === 0) {
// no orgs present, stop showing loading gif
await this.openSettings('AddServer');
}
}
}
initTabs(): void { initTabs(): void {
const servers = DomainUtil.getDomains(); const servers = DomainUtil.getDomains();
if (servers.length > 0) { if (servers.length > 0) {
for (let i = 0; i < servers.length; i++) { for (const [i, server] of servers.entries()) {
this.initServer(servers[i], i); this.initServer(server, i);
DomainUtil.updateSavedServer(servers[i].url, i);
this.activateTab(i);
} }
// Open last active tab // Open last active tab
this.activateTab(ConfigUtil.getConfigItem('lastActiveTab')); let lastActiveTab = ConfigUtil.getConfigItem('lastActiveTab');
if (lastActiveTab >= servers.length) {
lastActiveTab = 0;
}
// checkDomain() and webview.load() for lastActiveTab before the others
DomainUtil.updateSavedServer(servers[lastActiveTab].url, lastActiveTab);
this.activateTab(lastActiveTab);
for (const [i, server] of servers.entries()) {
// after the lastActiveTab is activated, we load the others in the background
// without activating them, to prevent flashing of server icons
if (i === lastActiveTab) {
continue;
}
DomainUtil.updateSavedServer(server.url, i);
this.tabs[i].webview.load();
}
// Remove focus from the settings icon at sidebar bottom // Remove focus from the settings icon at sidebar bottom
this.$settingsButton.classList.remove('active'); this.$settingsButton.classList.remove('active');
} else { } else if (this.presetOrgs.length === 0) {
// not attempting to add organisations in the background
this.openSettings('AddServer'); this.openSettings('AddServer');
} else {
this.showLoading(true);
} }
} }
@@ -254,7 +348,7 @@ class ServerManagerView {
this.tabs.push(new ServerTab({ this.tabs.push(new ServerTab({
role: 'server', role: 'server',
icon: server.icon, icon: server.icon,
name: server.alias, name: CommonUtil.decodeString(server.alias),
$root: this.$tabsContainer, $root: this.$tabsContainer,
onClick: this.activateLastTab.bind(this, index), onClick: this.activateLastTab.bind(this, index),
index, index,
@@ -268,6 +362,8 @@ class ServerManagerView {
url: server.url, url: server.url,
role: 'server', role: 'server',
name: CommonUtil.decodeString(server.alias), name: CommonUtil.decodeString(server.alias),
hasPermission: (origin: string, permission: string) =>
origin === server.url && permission === 'notifications',
isActive: () => { isActive: () => {
return index === this.activeTabIndex; return index === this.activeTabIndex;
}, },
@@ -279,7 +375,7 @@ class ServerManagerView {
} }
this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]); this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]);
}, },
onNetworkError: this.openNetworkTroubleshooting.bind(this), onNetworkError: (index: number) => this.openNetworkTroubleshooting(index),
onTitleChange: this.updateBadge.bind(this), onTitleChange: this.updateBadge.bind(this),
nodeIntegration: false, nodeIntegration: false,
preload: true preload: true
@@ -370,7 +466,7 @@ class ServerManagerView {
$altIcon.classList.add('server-icon'); $altIcon.classList.add('server-icon');
$altIcon.classList.add('alt-icon'); $altIcon.classList.add('alt-icon');
$parent.removeChild($img); $img.remove();
$parent.append($altIcon); $parent.append($altIcon);
this.addContextMenu($altIcon as HTMLImageElement, index); this.addContextMenu($altIcon as HTMLImageElement, index);
@@ -445,7 +541,7 @@ class ServerManagerView {
} }
this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]); this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]);
}, },
onNetworkError: this.openNetworkTroubleshooting.bind(this), onNetworkError: (index: number) => this.openNetworkTroubleshooting(index),
onTitleChange: this.updateBadge.bind(this), onTitleChange: this.updateBadge.bind(this),
nodeIntegration: true, nodeIntegration: true,
preload: false preload: false
@@ -459,14 +555,14 @@ class ServerManagerView {
this.activateTab(this.functionalTabs[tabProps.name]); this.activateTab(this.functionalTabs[tabProps.name]);
} }
openSettings(nav = 'General'): void { async openSettings(nav = 'General'): Promise<void> {
this.openFunctionalTab({ this.openFunctionalTab({
name: 'Settings', name: 'Settings',
materialIcon: 'settings', materialIcon: 'settings',
url: `file://${rendererDirectory}/preference.html#${nav}` url: `file://${rendererDirectory}/preference.html#${nav}`
}); });
this.$settingsButton.classList.add('active'); this.$settingsButton.classList.add('active');
this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav); await this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav);
} }
openAbout(): void { openAbout(): void {
@@ -477,12 +573,11 @@ class ServerManagerView {
}); });
} }
openNetworkTroubleshooting(): void { openNetworkTroubleshooting(index: number): void {
this.openFunctionalTab({ const reconnectUtil = new ReconnectUtil(this.tabs[index].webview);
name: 'Network Troubleshooting', reconnectUtil.pollInternetAndReload();
materialIcon: 'network_check', this.tabs[index].webview.props.url = `file://${rendererDirectory}/network.html`;
url: `file://${rendererDirectory}/network.html` this.tabs[index].showNetworkError();
});
} }
activateLastTab(index: number): void { activateLastTab(index: number): void {
@@ -619,10 +714,8 @@ class ServerManagerView {
} }
updateGeneralSettings(setting: string, value: any): void { updateGeneralSettings(setting: string, value: any): void {
const selector = 'webview:not([class*=disabled])'; if (this.getActiveWebview()) {
const webview: Electron.WebviewTag = document.querySelector(selector); const webContents = this.getActiveWebview().getWebContents();
if (webview) {
const webContents = webview.getWebContents();
webContents.send(setting, value); webContents.send(setting, value);
} }
} }
@@ -641,24 +734,47 @@ class ServerManagerView {
this.$dndButton.querySelector('i').textContent = alert ? 'notifications_off' : 'notifications'; this.$dndButton.querySelector('i').textContent = alert ? 'notifications_off' : 'notifications';
} }
isLoggedIn(tabIndex: number): boolean {
const url = this.tabs[tabIndex].webview.$el.src;
return !(url.endsWith('/login/') || this.tabs[tabIndex].webview.loading);
}
getActiveWebview(): Electron.WebviewTag {
const selector = 'webview:not(.disabled)';
const webview: Electron.WebviewTag = document.querySelector(selector);
return webview;
}
addContextMenu($serverImg: HTMLImageElement, index: number): void { addContextMenu($serverImg: HTMLImageElement, index: number): void {
$serverImg.addEventListener('contextmenu', e => { $serverImg.addEventListener('contextmenu', event => {
e.preventDefault(); event.preventDefault();
const template = [ const template = [
{ {
label: 'Disconnect organization', label: 'Disconnect organization',
click: () => { click: async () => {
dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'warning', type: 'warning',
buttons: ['YES', 'NO'], buttons: ['YES', 'NO'],
defaultId: 0, defaultId: 0,
message: 'Are you sure you want to disconnect this organization?' message: 'Are you sure you want to disconnect this organization?'
}, response => {
if (response === 0) {
DomainUtil.removeDomain(index);
ipcRenderer.send('reload-full-app');
}
}); });
if (response === 0) {
if (DomainUtil.removeDomain(index)) {
ipcRenderer.send('reload-full-app');
} else {
const { title, content } = Messages.orgRemovalError(DomainUtil.getDomain(index).url);
dialog.showErrorBox(title, content);
}
}
}
},
{
label: 'Notification settings',
enabled: this.isLoggedIn(index),
click: () => {
// switch to tab whose icon was right-clicked
this.activateTab(index);
this.tabs[index].webview.showNotificationSettings();
} }
}, },
{ {
@@ -687,15 +803,42 @@ class ServerManagerView {
'tab-devtools': 'openDevTools' 'tab-devtools': 'openDevTools'
}; };
for (const key in webviewListeners) { for (const [key, method] of Object.entries(webviewListeners)) {
ipcRenderer.on(key, () => { ipcRenderer.on(key, () => {
const activeWebview = this.tabs[this.activeTabIndex].webview; const activeWebview = this.tabs[this.activeTabIndex].webview;
if (activeWebview) { if (activeWebview) {
activeWebview[webviewListeners[key] as string](); activeWebview[method]();
} }
}); });
} }
ipcRenderer.on('permission-request', (
event: Event,
permissionId: number,
{webContentsId, origin, permission}: {
webContentsId: number | null;
origin: string;
permission: string;
}
) => {
const grant = webContentsId === null ?
origin === 'null' && permission === 'notifications' :
this.tabs.some(
({webview}) =>
webview.$el.getWebContentsId() === webContentsId &&
webview.props.hasPermission?.(origin, permission)
);
console.log(
grant ? 'Granted' : 'Denied', 'permissions request for',
permission, 'from', origin
);
ipcRenderer.send('permission-response', permissionId, grant);
});
ipcRenderer.on('show-network-error', (event: Event, index: number) => {
this.openNetworkTroubleshooting(index);
});
ipcRenderer.on('open-settings', (event: Event, settingNav: string) => { ipcRenderer.on('open-settings', (event: Event, settingNav: string) => {
this.openSettings(settingNav); this.openSettings(settingNav);
}); });
@@ -704,8 +847,7 @@ class ServerManagerView {
ipcRenderer.on('open-help', () => { ipcRenderer.on('open-help', () => {
// Open help page of current active server // Open help page of current active server
const helpPage = this.getCurrentActiveServer() + '/help'; LinkUtil.openBrowser(new URL('/help', this.getCurrentActiveServer()));
shell.openExternal(helpPage);
}); });
ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index)); ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index));
@@ -728,13 +870,12 @@ class ServerManagerView {
this.openSettings('AddServer'); this.openSettings('AddServer');
}); });
ipcRenderer.on('reload-proxy', (event: Event, showAlert: boolean) => { ipcRenderer.on('reload-proxy', async (event: Event, showAlert: boolean) => {
this.loadProxy().then(() => { await this.loadProxy();
if (showAlert) { if (showAlert) {
alert('Proxy settings saved!'); alert('Proxy settings saved!');
ipcRenderer.send('reload-full-app'); ipcRenderer.send('reload-full-app');
} }
});
}); });
ipcRenderer.on('toggle-sidebar', (event: Event, show: boolean) => { ipcRenderer.on('toggle-sidebar', (event: Event, show: boolean) => {
@@ -773,17 +914,14 @@ class ServerManagerView {
ipcRenderer.on('toggle-dnd', (event: Event, state: boolean, newSettings: SettingsOptions) => { ipcRenderer.on('toggle-dnd', (event: Event, state: boolean, newSettings: SettingsOptions) => {
this.toggleDNDButton(state); this.toggleDNDButton(state);
ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent); ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent);
const selector = 'webview:not([class*=disabled])'; const webContents = this.getActiveWebview().getWebContents();
const webview: Electron.WebviewTag = document.querySelector(selector);
const webContents = webview.getWebContents();
webContents.send('toggle-dnd', state, newSettings); webContents.send('toggle-dnd', state, newSettings);
}); });
ipcRenderer.on('update-realm-name', (event: Event, serverURL: string, realmName: string) => { ipcRenderer.on('update-realm-name', (event: Event, serverURL: string, realmName: string) => {
// TODO: TypeScript - Type annotate getDomains() or this domain paramter. DomainUtil.getDomains().forEach((domain: DomainUtil.ServerConf, index: number) => {
DomainUtil.getDomains().forEach((domain: any, index: number) => {
if (domain.url.includes(serverURL)) { if (domain.url.includes(serverURL)) {
const serverTooltipSelector = `.tab .server-tooltip`; const serverTooltipSelector = '.tab .server-tooltip';
const serverTooltips = document.querySelectorAll(serverTooltipSelector); const serverTooltips = document.querySelectorAll(serverTooltipSelector);
serverTooltips[index].innerHTML = escape(realmName); serverTooltips[index].innerHTML = escape(realmName);
this.tabs[index].props.name = escape(realmName); this.tabs[index].props.name = escape(realmName);
@@ -802,18 +940,18 @@ class ServerManagerView {
}); });
ipcRenderer.on('update-realm-icon', (event: Event, serverURL: string, iconURL: string) => { ipcRenderer.on('update-realm-icon', (event: Event, serverURL: string, iconURL: string) => {
// TODO: TypeScript - Type annotate getDomains() or this domain paramter. DomainUtil.getDomains().forEach(async (domain, index) => {
DomainUtil.getDomains().forEach((domain: any, index: number) => {
if (domain.url.includes(serverURL)) { if (domain.url.includes(serverURL)) {
DomainUtil.saveServerIcon(iconURL).then((localIconUrl: string) => { const localIconUrl: string = await DomainUtil.saveServerIcon({
const serverImgsSelector = `.tab .server-icons`; url: serverURL,
const serverImgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(serverImgsSelector); icon: iconURL
serverImgs[index].src = localIconUrl;
domain.icon = localIconUrl;
DomainUtil.db.push(`/domains[${index}]`, domain, true);
DomainUtil.reloadDB();
}); });
const serverImgsSelector = '.tab .server-icons';
const serverImgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(serverImgsSelector);
serverImgs[index].src = localIconUrl;
domain.icon = localIconUrl;
DomainUtil.db.push(`/domains[${index}]`, domain, true);
DomainUtil.reloadDB();
} }
}); });
}); });
@@ -879,22 +1017,39 @@ class ServerManagerView {
ipcRenderer.on('new-server', () => { ipcRenderer.on('new-server', () => {
this.openSettings('AddServer'); this.openSettings('AddServer');
}); });
// Redo and undo functionality since the default API doesn't work on macOS
ipcRenderer.on('undo', () => {
return this.getActiveWebview().undo();
});
ipcRenderer.on('redo', () => {
return this.getActiveWebview().redo();
});
ipcRenderer.on('set-active', () => {
const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => {
webview.send('set-active');
});
});
ipcRenderer.on('set-idle', () => {
const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => {
webview.send('set-idle');
});
});
ipcRenderer.on('open-network-settings', () => {
this.openSettings('Network');
});
} }
} }
window.addEventListener('load', () => { window.addEventListener('load', () => {
const serverManagerView = new ServerManagerView(); const serverManagerView = new ServerManagerView();
const reconnectUtil = new ReconnectUtil(serverManagerView);
serverManagerView.init(); serverManagerView.init();
window.addEventListener('online', () => {
reconnectUtil.pollInternetAndReload();
});
window.addEventListener('offline', () => {
reconnectUtil.clearState();
logger.log('No internet connection, you are offline.');
});
// only start electron-connect (auto reload on change) when its ran // only start electron-connect (auto reload on change) when its ran
// from `npm run dev` or `gulp dev` and not from `npm start` when // from `npm run dev` or `gulp dev` and not from `npm start` when
// app is started `npm start` main process's proces.argv will have // app is started `npm start` main process's proces.argv will have
@@ -905,4 +1060,4 @@ window.addEventListener('load', () => {
} }
}); });
export = new ServerManagerView(); export {};

View File

@@ -1,12 +1,11 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { import {
appId, customReply, focusCurrentServer, parseReply, setupReply appId, customReply, focusCurrentServer, parseReply
} from './helpers'; } from './helpers';
import url = require('url'); import MacNotifier from 'node-mac-notifier';
import MacNotifier = require('node-mac-notifier'); import * as ConfigUtil from '../utils/config-util';
import ConfigUtil = require('../utils/config-util'); import electron_bridge from '../electron-bridge';
type ReplyHandler = (response: string) => void; type ReplyHandler = (response: string) => void;
type ClickHandler = () => void; type ClickHandler = () => void;
@@ -20,14 +19,13 @@ interface NotificationHandlerArgs {
class DarwinNotification { class DarwinNotification {
tag: string; tag: string;
constructor(title: string, opts: NotificationOptions) { constructor(title: string, options: NotificationOptions) {
const silent: boolean = ConfigUtil.getConfigItem('silent') || false; const silent: boolean = ConfigUtil.getConfigItem('silent') || false;
const { host, protocol } = location; const { icon } = options;
const { icon } = opts; const profilePic = new URL(icon, location.href).href;
const profilePic = url.resolve(`${protocol}//${host}`, icon);
this.tag = opts.tag; this.tag = options.tag;
const notification = new MacNotifier(title, Object.assign(opts, { const notification = new MacNotifier(title, Object.assign(options, {
bundleId: appId, bundleId: appId,
canReply: true, canReply: true,
silent, silent,
@@ -57,22 +55,22 @@ class DarwinNotification {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied'; return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
} }
set onreply(handler: ReplyHandler) {
replyHandler = handler;
}
get onreply(): ReplyHandler { get onreply(): ReplyHandler {
return replyHandler; return replyHandler;
} }
set onclick(handler: ClickHandler) { set onreply(handler: ReplyHandler) {
clickHandler = handler; replyHandler = handler;
} }
get onclick(): ClickHandler { get onclick(): ClickHandler {
return clickHandler; return clickHandler;
} }
set onclick(handler: ClickHandler) {
clickHandler = handler;
}
// not something that is common or // not something that is common or
// used by zulip server but added to be // used by zulip server but added to be
// future proff. // future proff.
@@ -86,11 +84,15 @@ class DarwinNotification {
} }
} }
notificationHandler({ response }: NotificationHandlerArgs): void { async notificationHandler({ response }: NotificationHandlerArgs): Promise<void> {
response = parseReply(response); response = await parseReply(response);
focusCurrentServer(); focusCurrentServer();
setupReply(this.tag); 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) { if (replyHandler) {
replyHandler(response); replyHandler(response);
return; return;

View File

@@ -1,15 +1,13 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { focusCurrentServer } from './helpers'; import { focusCurrentServer } from './helpers';
import ConfigUtil = require('../utils/config-util'); import * as ConfigUtil from '../utils/config-util';
const NativeNotification = window.Notification; const NativeNotification = window.Notification;
class BaseNotification extends NativeNotification { export default class BaseNotification extends NativeNotification {
constructor(title: string, opts: NotificationOptions) { constructor(title: string, options: NotificationOptions) {
opts.silent = true; options.silent = true;
super(title, opts); super(title, options);
this.addEventListener('click', () => { this.addEventListener('click', () => {
// focus to the server who sent the // focus to the server who sent the
@@ -19,8 +17,8 @@ class BaseNotification extends NativeNotification {
}); });
} }
static requestPermission(): void { static async requestPermission(): Promise<NotificationPermission> {
return; // eslint-disable-line no-useless-return return this.permission;
} }
// Override default Notification permission // Override default Notification permission
@@ -28,5 +26,3 @@ class BaseNotification extends NativeNotification {
return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied'; return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
} }
} }
export = BaseNotification;

View File

@@ -1,6 +1,6 @@
import { remote } from 'electron'; import { remote } from 'electron';
import Logger = require('../utils/logger-util'); import Logger from '../utils/logger-util';
const logger = new Logger({ const logger = new Logger({
file: 'errors.log', file: 'errors.log',
@@ -15,32 +15,23 @@ const botsList: BotListItem[] = [];
let botsListLoaded = false; let botsListLoaded = false;
// this function load list of bots from the server // this function load list of bots from the server
// sync=True for a synchronous getJSON request
// in case botsList isn't already completely loaded when required in parseRely // in case botsList isn't already completely loaded when required in parseRely
export function loadBots(sync = false): void { export async function loadBots(): Promise<void> {
const { $ } = window;
botsList.length = 0; botsList.length = 0;
if (sync) { const response = await fetch('/json/users');
$.ajaxSetup({async: false}); if (response.ok) {
} const { members } = await response.json();
$.getJSON('/json/users') members.forEach((membersRow: any) => {
.done((data: any) => { if (membersRow.is_bot) {
const { members } = data; const bot = `@${membersRow.full_name}`;
members.forEach((membersRow: any) => { const mention = `@**${bot.replace(/^@/, '')}**`;
if (membersRow.is_bot) { botsList.push([bot, mention]);
const bot = `@${membersRow.full_name}`; }
const mention = `@**${bot.replace(/^@/, '')}**`;
botsList.push([bot, mention]);
}
});
botsListLoaded = true;
})
.fail((error: any) => {
logger.log('Load bots request failed: ', error.responseText);
logger.log('Load bots request status: ', error.statusText);
}); });
if (sync) { botsListLoaded = true;
$.ajaxSetup({async: true}); } else {
logger.log('Load bots request failed: ', await response.text());
logger.log('Load bots request status: ', response.status);
} }
} }
@@ -80,16 +71,14 @@ const webContentsId = webContents.id;
// this function will focus the server that sent // this function will focus the server that sent
// the notification. Main function implemented in main.js // the notification. Main function implemented in main.js
export function focusCurrentServer(): void { export function focusCurrentServer(): void {
// TODO: TypeScript: currentWindow of type BrowserWindow doesn't currentWindow.webContents.send('focus-webview-with-id', webContentsId);
// have a .send() property per typescript.
(currentWindow as any).send('focus-webview-with-id', webContentsId);
} }
// this function parses the reply from to notification // this function parses the reply from to notification
// making it easier to reply from notification eg // making it easier to reply from notification eg
// @username in reply will be converted to @**username** // @username in reply will be converted to @**username**
// #stream in reply will be converted to #**stream** // #stream in reply will be converted to #**stream**
// bot mentions are not yet supported // bot mentions are not yet supported
export function parseReply(reply: string): string { export async function parseReply(reply: string): Promise<string> {
const usersDiv = document.querySelectorAll('#user_presences li'); const usersDiv = document.querySelectorAll('#user_presences li');
const streamHolder = document.querySelectorAll('#stream_filters li'); const streamHolder = document.querySelectorAll('#stream_filters li');
@@ -128,9 +117,9 @@ export function parseReply(reply: string): string {
reply = reply.replace(regex, streamMention); reply = reply.replace(regex, streamMention);
}); });
// If botsList isn't completely loaded yet, make a synchronous getJSON request for list // If botsList isn't completely loaded yet, make a request for list
if (botsListLoaded === false) { if (!botsListLoaded) {
loadBots(true); await loadBots();
} }
// Iterate for every bot name and replace in reply // Iterate for every bot name and replace in reply
@@ -145,9 +134,3 @@ export function parseReply(reply: string): string {
reply = reply.replace(/\\n/, '\n'); reply = reply.replace(/\\n/, '\n');
return reply; return reply;
} }
export function setupReply(id: string): void {
const { narrow } = window;
const narrowByTopic = narrow.by_topic || narrow.by_subject;
narrowByTopic(id, { trigger: 'notification' });
}

View File

@@ -1,25 +1,72 @@
'use strict';
import { remote } from 'electron'; import { remote } from 'electron';
import * as params from '../utils/params-util'; import electron_bridge from '../electron-bridge';
import { appId, loadBots } from './helpers'; import { appId, loadBots } from './helpers';
import DefaultNotification = require('./default-notification'); import DefaultNotification from './default-notification';
const { app } = remote; const { app } = remote;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid // From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work. // On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
app.setAppUserModelId(appId); app.setAppUserModelId(appId);
window.Notification = DefaultNotification; let Notification = DefaultNotification;
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
window.Notification = require('./darwin-notifications'); Notification = require('./darwin-notifications');
} }
window.addEventListener('load', () => { export interface NotificationData {
// eslint-disable-next-line no-undef, @typescript-eslint/camelcase close(): void;
if (params.isPageParams() && page_params.realm_uri) { title: string;
loadBots(); dir: NotificationDirection;
lang: string;
body: string;
tag: string;
image: string;
icon: string;
badge: string;
vibrate: readonly number[];
timestamp: number;
renotify: boolean;
silent: boolean;
requireInteraction: boolean;
data: unknown;
actions: readonly NotificationAction[];
}
export function newNotification(
title: string,
options: NotificationOptions | undefined,
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();
}
});
} }
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
};
}
electron_bridge.once('zulip-loaded', () => {
loadBots();
}); });

View File

@@ -1,21 +1,10 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
class NetworkTroubleshootingView { export function init($reconnectButton: Element, $settingsButton: Element): void {
$reconnectButton: Element; $reconnectButton.addEventListener('click', () => {
constructor() { ipcRenderer.send('forward-message', 'reload-viewer');
this.$reconnectButton = document.querySelector('#reconnect'); });
} $settingsButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'open-settings');
init(): void { });
this.$reconnectButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
}
} }
window.addEventListener('load', () => {
const networkTroubleshootingView = new NetworkTroubleshootingView();
networkTroubleshootingView.init();
});

View File

@@ -2,21 +2,25 @@
import { remote, OpenDialogOptions } from 'electron'; import { remote, OpenDialogOptions } from 'electron';
import path = require('path'); import path from 'path';
import BaseComponent = require('../../components/base'); import BaseComponent from '../../components/base';
import CertificateUtil = require('../../utils/certificate-util'); import * as CertificateUtil from '../../utils/certificate-util';
import DomainUtil = require('../../utils/domain-util'); import * as DomainUtil from '../../utils/domain-util';
import * as t from '../../utils/translation-util';
interface AddCertificateProps {
$root: Element;
}
const { dialog } = remote; const { dialog } = remote;
class AddCertificate extends BaseComponent { export default class AddCertificate extends BaseComponent {
// TODO: TypeScript - Here props should be object type props: AddCertificateProps;
props: any;
_certFile: string; _certFile: string;
$addCertificate: Element | null; $addCertificate: Element | null;
addCertificateButton: Element | null; addCertificateButton: Element | null;
serverUrl: HTMLInputElement | null; serverUrl: HTMLInputElement | null;
constructor(props: any) { constructor(props: AddCertificateProps) {
super(); super();
this.props = props; this.props = props;
this._certFile = ''; this._certFile = '';
@@ -26,12 +30,12 @@ class AddCertificate extends BaseComponent {
return ` return `
<div class="settings-card certificates-card"> <div class="settings-card certificates-card">
<div class="certificate-input"> <div class="certificate-input">
<div>Organization URL</div> <div>${t.__('Organization URL')}</div>
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/> <input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
</div> </div>
<div class="certificate-input"> <div class="certificate-input">
<div>Certificate file</div> <div>${t.__('Certificate file')}</div>
<button class="green" id="add-certificate-button">Upload</button> <button class="green" id="add-certificate-button">${t.__('Upload')}</button>
</div> </div>
</div> </div>
`; `;
@@ -58,7 +62,7 @@ class AddCertificate extends BaseComponent {
CertificateUtil.setCertificate(server, fileName); CertificateUtil.setCertificate(server, fileName);
dialog.showMessageBox({ dialog.showMessageBox({
title: 'Success', title: 'Success',
message: `Certificate saved!` message: 'Certificate saved!'
}); });
this.serverUrl.value = ''; this.serverUrl.value = '';
} else { } else {
@@ -67,18 +71,17 @@ class AddCertificate extends BaseComponent {
} }
} }
addHandler(): void { async addHandler(): Promise<void> {
const showDialogOptions: OpenDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: 'Select file', title: 'Select file',
properties: ['openFile'], properties: ['openFile'],
filters: [{ name: 'crt, pem', extensions: ['crt', 'pem'] }] filters: [{ name: 'crt, pem', extensions: ['crt', 'pem'] }]
}; };
dialog.showOpenDialog(showDialogOptions, selectedFile => { const { filePaths, canceled } = await dialog.showOpenDialog(showDialogOptions);
if (selectedFile) { if (!canceled) {
this._certFile = selectedFile[0] || ''; this._certFile = filePaths[0] || '';
this.validateAndAdd(); this.validateAndAdd();
} }
});
} }
initListeners(): void { initListeners(): void {
@@ -87,13 +90,9 @@ class AddCertificate extends BaseComponent {
}); });
this.serverUrl.addEventListener('keypress', event => { this.serverUrl.addEventListener('keypress', event => {
const EnterkeyCode = event.keyCode; if (event.key === 'Enter') {
if (EnterkeyCode === 13) {
this.addHandler(); this.addHandler();
} }
}); });
} }
} }
export = AddCertificate;

View File

@@ -1,63 +1,43 @@
'use strict'; import electron, { app } from 'electron';
import { app } from 'electron';
import electron = require('electron'); import * as ConfigUtil from '../../utils/config-util';
import ConfigUtil = require('../../utils/config-util');
let instance: BadgeSettings | any = null; function showBadgeCount(messageCount: number, mainWindow: electron.BrowserWindow): void {
if (process.platform === 'win32') {
class BadgeSettings { updateOverlayIcon(messageCount, mainWindow);
constructor() { } else {
if (instance) { app.badgeCount = messageCount;
return instance;
} else {
instance = this;
}
return instance;
}
showBadgeCount(messageCount: number, mainWindow: electron.BrowserWindow): void {
if (process.platform === 'darwin') {
app.setBadgeCount(messageCount);
}
if (process.platform === 'win32') {
this.updateOverlayIcon(messageCount, mainWindow);
}
}
hideBadgeCount(mainWindow: electron.BrowserWindow): void {
if (process.platform === 'darwin') {
app.setBadgeCount(0);
}
if (process.platform === 'win32') {
mainWindow.setOverlayIcon(null, '');
}
}
updateBadge(badgeCount: number, mainWindow: electron.BrowserWindow): void {
if (ConfigUtil.getConfigItem('badgeOption', true)) {
this.showBadgeCount(badgeCount, mainWindow);
} else {
this.hideBadgeCount(mainWindow);
}
}
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);
}
}
updateTaskbarIcon(data: string, text: string, mainWindow: electron.BrowserWindow): void {
const img = electron.nativeImage.createFromDataURL(data);
mainWindow.setOverlayIcon(img, text);
} }
} }
export = new BadgeSettings(); 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,29 +1,37 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import escape from 'escape-html';
import BaseComponent = require('../../components/base'); import BaseComponent from '../../components/base';
class BaseSection extends BaseComponent { interface BaseSectionProps {
// TODO: TypeScript - Here props should be object type $element: HTMLElement;
generateSettingOption(props: any): void { disabled?: boolean;
const {$element, value, clickHandler} = props; value: boolean;
clickHandler: () => void;
}
export default class BaseSection extends BaseComponent {
generateSettingOption(props: BaseSectionProps): void {
const {$element, disabled, value, clickHandler} = props;
$element.innerHTML = ''; $element.innerHTML = '';
const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value)); const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value, disabled));
$element.append($optionControl); $element.append($optionControl);
$optionControl.addEventListener('click', clickHandler); if (!disabled) {
$optionControl.addEventListener('click', clickHandler);
}
} }
generateOptionTemplate(settingOption: boolean): string { generateOptionTemplate(settingOption: boolean, disabled?: boolean): string {
const label = disabled ? '<label class="disallowed" title="Setting locked by system administrator."/>' : '<label/>';
if (settingOption) { if (settingOption) {
return ` return `
<div class="action"> <div class="action">
<div class="switch"> <div class="switch">
<input class="toggle toggle-round" type="checkbox" checked> <input class="toggle toggle-round" type="checkbox" checked disabled>
<label></label> ${label}
</div> </div>
</div> </div>
`; `;
@@ -32,16 +40,26 @@ class BaseSection extends BaseComponent {
<div class="action"> <div class="action">
<div class="switch"> <div class="switch">
<input class="toggle toggle-round" type="checkbox"> <input class="toggle toggle-round" type="checkbox">
<label></label> ${label}
</div> </div>
</div> </div>
`; `;
} }
} }
/* a method that in future can be used to create dropdown menus using <select> <option> tags.
it needs an object which has ``key: value`` pairs and will return a string that can be appended to HTML
*/
generateSelectTemplate(options: {[key: string]: string}, className?: string, idName?: string): string {
let select = `<select class="${escape(className)}" id="${escape(idName)}">\n`;
Object.keys(options).forEach(key => {
select += `<option name="${escape(key)}" value="${escape(key)}">${escape(options[key])}</option>\n`;
});
select += '</select>';
return select;
}
reloadApp(): void { reloadApp(): void {
ipcRenderer.send('forward-message', 'reload-viewer'); ipcRenderer.send('forward-message', 'reload-viewer');
} }
} }
export = BaseSection;

View File

@@ -1,22 +1,24 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import BaseSection = require('./base-section'); import BaseSection from './base-section';
import DomainUtil = require('../../utils/domain-util'); import * as DomainUtil from '../../utils/domain-util';
import ServerInfoForm = require('./server-info-form'); import ServerInfoForm from './server-info-form';
import AddCertificate = require('./add-certificate'); import AddCertificate from './add-certificate';
import FindAccounts = require('./find-accounts'); import FindAccounts from './find-accounts';
import * as t from '../../utils/translation-util';
class ConnectedOrgSection extends BaseSection { interface ConnectedOrgSectionProps {
// TODO: TypeScript - Here props should be object type $root: Element;
props: any; }
export default class ConnectedOrgSection extends BaseSection {
props: ConnectedOrgSectionProps;
$serverInfoContainer: Element | null; $serverInfoContainer: Element | null;
$existingServers: Element | null; $existingServers: Element | null;
$newOrgButton: HTMLButtonElement | null; $newOrgButton: HTMLButtonElement | null;
$addCertificateContainer: Element | null; $addCertificateContainer: Element | null;
$findAccountsContainer: Element | null; $findAccountsContainer: Element | null;
constructor(props: any) { constructor(props: ConnectedOrgSectionProps) {
super(); super();
this.props = props; this.props = props;
} }
@@ -24,13 +26,13 @@ class ConnectedOrgSection extends BaseSection {
template(): string { template(): string {
return ` return `
<div class="settings-pane" id="server-settings-pane"> <div class="settings-pane" id="server-settings-pane">
<div class="page-title">Connected organizations</div> <div class="page-title">${t.__('Connected organizations')}</div>
<div class="title" id="existing-servers">All the connected orgnizations will appear here.</div> <div class="title" id="existing-servers">${t.__('All the connected orgnizations will appear here.')}</div>
<div id="server-info-container"></div> <div id="server-info-container"></div>
<div id="new-org-button"><button class="green sea w-250">Connect to another organization</button></div> <div id="new-org-button"><button class="green sea w-250">${t.__('Connect to another organization')}</button></div>
<div class="page-title">Add Custom Certificates</div> <div class="page-title">${t.__('Add Custom Certificates')}</div>
<div id="add-certificate-container"></div> <div id="add-certificate-container"></div>
<div class="page-title">Find accounts by email</div> <div class="page-title">${t.__('Find accounts by email')}</div>
<div id="find-accounts-container"></div> <div id="find-accounts-container"></div>
</div> </div>
`; `;
@@ -52,14 +54,14 @@ class ConnectedOrgSection extends BaseSection {
this.$addCertificateContainer = document.querySelector('#add-certificate-container'); this.$addCertificateContainer = document.querySelector('#add-certificate-container');
this.$findAccountsContainer = document.querySelector('#find-accounts-container'); this.$findAccountsContainer = document.querySelector('#find-accounts-container');
const noServerText = 'All the connected orgnizations will appear here'; const noServerText = t.__('All the connected orgnizations will appear here');
// Show noServerText if no servers are there otherwise hide it // Show noServerText if no servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? noServerText : ''; this.$existingServers.innerHTML = servers.length === 0 ? noServerText : '';
for (let i = 0; i < servers.length; i++) { for (const [i, server] of servers.entries()) {
new ServerInfoForm({ new ServerInfoForm({
$root: this.$serverInfoContainer, $root: this.$serverInfoContainer,
server: servers[i], server,
index: i, index: i,
onChange: this.reloadApp onChange: this.reloadApp
}).init(); }).init();
@@ -85,5 +87,3 @@ class ConnectedOrgSection extends BaseSection {
}).init(); }).init();
} }
} }
export = ConnectedOrgSection;

View File

@@ -1,16 +1,19 @@
'use-strict'; 'use-strict';
import { shell } from 'electron'; import BaseComponent from '../../components/base';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
import BaseComponent = require('../../components/base'); interface FindAccountsProps {
$root: Element;
}
class FindAccounts extends BaseComponent { export default class FindAccounts extends BaseComponent {
// TODO: TypeScript - Here props should be object type props: FindAccountsProps;
props: any;
$findAccounts: Element | null; $findAccounts: Element | null;
$findAccountsButton: Element | null; $findAccountsButton: Element | null;
$serverUrlField: HTMLInputElement | null; $serverUrlField: HTMLInputElement | null;
constructor(props: any) { constructor(props: FindAccountsProps) {
super(); super();
this.props = props; this.props = props;
} }
@@ -19,11 +22,11 @@ class FindAccounts extends BaseComponent {
return ` return `
<div class="settings-card certificate-card"> <div class="settings-card certificate-card">
<div class="certificate-input"> <div class="certificate-input">
<div>Organization URL</div> <div>${t.__('Organization URL')}</div>
<input class="setting-input-value" value="zulipchat.com"/> <input class="setting-input-value" value="zulipchat.com"/>
</div> </div>
<div class="certificate-input"> <div class="certificate-input">
<button class="green w-150" id="find-accounts-button">Find accounts</button> <button class="green w-150" id="find-accounts-button">${t.__('Find accounts')}</button>
</div> </div>
</div> </div>
`; `;
@@ -44,7 +47,7 @@ class FindAccounts extends BaseComponent {
if (!url.startsWith('http')) { if (!url.startsWith('http')) {
url = 'https://' + url; url = 'https://' + url;
} }
shell.openExternal(url + '/accounts/find'); LinkUtil.openBrowser(new URL('/accounts/find', url));
} }
initListeners(): void { initListeners(): void {
@@ -59,7 +62,7 @@ class FindAccounts extends BaseComponent {
}); });
this.$serverUrlField.addEventListener('keypress', event => { this.$serverUrlField.addEventListener('keypress', event => {
if (event.keyCode === 13) { if (event.key === 'Enter') {
this.findAccounts(this.$serverUrlField.value); this.findAccounts(this.$serverUrlField.value);
} }
}); });
@@ -73,5 +76,3 @@ class FindAccounts extends BaseComponent {
}); });
} }
} }
export = FindAccounts;

View File

@@ -1,19 +1,24 @@
'use strict';
import { ipcRenderer, remote, OpenDialogOptions } from 'electron'; import { ipcRenderer, remote, OpenDialogOptions } from 'electron';
import path = require('path'); import path from 'path';
import fs = require('fs-extra'); import fs from 'fs-extra';
const { app, dialog } = remote; const { app, dialog } = remote;
const currentBrowserWindow = remote.getCurrentWindow(); const currentBrowserWindow = remote.getCurrentWindow();
import BaseSection = require('./base-section'); import BaseSection from './base-section';
import ConfigUtil = require('../../utils/config-util'); import * as ConfigUtil from '../../utils/config-util';
import * as EnterpriseUtil from '../../utils/enterprise-util';
import * as t from '../../utils/translation-util';
import supportedLocales from '../../../../translations/supported-locales.json';
class GeneralSection extends BaseSection { interface GeneralSectionProps {
// TODO: TypeScript - Here props should be object type $root: Element;
props: any; }
constructor(props: any) {
export default class GeneralSection extends BaseSection {
props: GeneralSectionProps;
constructor(props: GeneralSectionProps) {
super(); super();
this.props = props; this.props = props;
} }
@@ -21,117 +26,127 @@ class GeneralSection extends BaseSection {
template(): string { template(): string {
return ` return `
<div class="settings-pane"> <div class="settings-pane">
<div class="title">Appearance</div> <div class="title">${t.__('Appearance')}</div>
<div id="appearance-option-settings" class="settings-card"> <div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="tray-option"> <div class="setting-row" id="tray-option">
<div class="setting-description">Show app icon in system tray</div> <div class="setting-description">${t.__('Show app icon in system tray')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="menubar-option" style= "display:${process.platform === 'darwin' ? 'none' : ''}"> <div class="setting-row" id="menubar-option" style= "display:${process.platform === 'darwin' ? 'none' : ''}">
<div class="setting-description">Auto hide menu bar (Press Alt key to display)</div> <div class="setting-description">${t.__('Auto hide menu bar (Press Alt key to display)')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="sidebar-option"> <div class="setting-row" id="sidebar-option">
<div class="setting-description">Show sidebar (<span class="code">${process.platform === 'darwin' ? 'Cmd+Shift+S' : 'Ctrl+Shift+S'}</span>)</div> <div class="setting-description">${t.__('Show sidebar')} (<span class="code">${process.platform === 'darwin' ? 'Cmd+Shift+S' : 'Ctrl+Shift+S'}</span>)</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="badge-option"> <div class="setting-row" id="badge-option">
<div class="setting-description">Show app unread badge</div> <div class="setting-description">${t.__('Show app unread badge')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="dock-bounce-option" style= "display:${process.platform === 'darwin' ? '' : 'none'}"> <div class="setting-row" id="dock-bounce-option" style= "display:${process.platform === 'darwin' ? '' : 'none'}">
<div class="setting-description">Bounce dock on new private message</div> <div class="setting-description">${t.__('Bounce dock on new private message')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="flash-taskbar-option" style= "display:${process.platform === 'win32' ? '' : 'none'}"> <div class="setting-row" id="flash-taskbar-option" style= "display:${process.platform === 'win32' ? '' : 'none'}">
<div class="setting-description">Flash taskbar on new message</div> <div class="setting-description">${t.__('Flash taskbar on new message')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
</div> </div>
<div class="title">Desktop Notifications</div> <div class="title">${t.__('Desktop Notifications')}</div>
<div class="settings-card"> <div class="settings-card">
<div class="setting-row" id="show-notification-option"> <div class="setting-row" id="show-notification-option">
<div class="setting-description">Show desktop notifications</div> <div class="setting-description">${t.__('Show desktop notifications')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="silent-option"> <div class="setting-row" id="silent-option">
<div class="setting-description">Mute all sounds from Zulip</div> <div class="setting-description">${t.__('Mute all sounds from Zulip')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
</div> </div>
<div class="title">App Updates</div> <div class="title">${t.__('App Updates')}</div>
<div class="settings-card"> <div class="settings-card">
<div class="setting-row" id="autoupdate-option"> <div class="setting-row" id="autoupdate-option">
<div class="setting-description">Enable auto updates</div> <div class="setting-description">${t.__('Enable auto updates')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="betaupdate-option"> <div class="setting-row" id="betaupdate-option">
<div class="setting-description">Get beta updates</div> <div class="setting-description">${t.__('Get beta updates')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
</div> </div>
<div class="title">Functionality</div> <div class="title">${t.__('Functionality')}</div>
<div class="settings-card"> <div class="settings-card">
<div class="setting-row" id="startAtLogin-option"> <div class="setting-row" id="startAtLogin-option">
<div class="setting-description">Start app at login</div> <div class="setting-description">${t.__('Start app at login')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="start-minimize-option"> <div class="setting-row" id="start-minimize-option">
<div class="setting-description">Always start minimized</div> <div class="setting-description">${t.__('Always start minimized')}</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="quitOnClose-option">
<div class="setting-description">${t.__('Quit when the window is closed')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="enable-spellchecker-option"> <div class="setting-row" id="enable-spellchecker-option">
<div class="setting-description">Enable spellchecker (requires restart)</div> <div class="setting-description">${t.__('Enable spellchecker (requires restart)')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
</div> </div>
<div class="title">Advanced</div> <div class="title">${t.__('Advanced')}</div>
<div class="settings-card"> <div class="settings-card">
<div class="setting-row" id="enable-error-reporting">
<div class="setting-description">Enable error reporting (requires restart)</div> <div class="setting-row" id="enable-error-reporting">
<div class="setting-control"></div> <div class="setting-description">${t.__('Enable error reporting (requires restart)')}</div>
</div> <div class="setting-control"></div>
<div class="setting-row" id="show-download-folder"> </div>
<div class="setting-description">Show downloaded files in file manager</div>
<div class="setting-control"></div> <div class="setting-row" id="app-language">
</div> <div class="setting-description">${t.__('App language (requires restart)')}</div>
<div class="setting-row" id="add-custom-css"> <div id="lang-div" class="lang-div"></div>
<div class="setting-description"> </div>
Add custom CSS
</div> <div class="setting-row" id="add-custom-css">
<button class="custom-css-button green">Upload</button> <div class="setting-description">
</div> ${t.__('Add custom CSS')}
<div class="setting-row" id="remove-custom-css"> </div>
<div class="setting-description"> <button class="custom-css-button green">${t.__('Upload')}</button>
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div> </div>
</div> <div class="setting-row" id="remove-custom-css">
<div class="action red" id="css-delete-action"> <div class="setting-description">
<i class="material-icons">indeterminate_check_box</i> <div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div>
<span>Delete</span> </div>
</div> <div class="action red" id="css-delete-action">
</div> <i class="material-icons">indeterminate_check_box</i>
<span>${t.__('Delete')}</span>
</div>
</div>
<div class="setting-row" id="download-folder"> <div class="setting-row" id="download-folder">
<div class="setting-description"> <div class="setting-description">
Default download location ${t.__('Default download location')}
</div> </div>
<button class="download-folder-button green">Change</button> <button class="download-folder-button green">${t.__('Change')}</button>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="setting-description"> <div class="setting-description">
<div class="download-folder-path">${ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`)}</div> <div class="download-folder-path">${ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`)}</div>
</div> </div>
</div> </div>
<div class="setting-row" id="prompt-download">
</div> <div class="setting-description">${t.__('Ask where to save files before downloading')}</div>
<div class="title">Reset Application Data</div> <div class="setting-control"></div>
<div class="settings-card">
<div class="setting-row" id="resetdata-option">
<div class="setting-description">This will delete all application data including all added accounts and preferences
</div>
<button class="reset-data-button red w-150">Reset App Data</button>
</div> </div>
</div> </div>
</div> <div class="title">${t.__('Reset Application Data')}</div>
<div class="settings-card">
<div class="setting-row" id="resetdata-option">
<div class="setting-description">${t.__('This will delete all application data including all added accounts and preferences')}
</div>
<button class="reset-data-button red w-150">${t.__('Reset App Data')}</button>
</div>
</div>
</div>
`; `;
} }
@@ -152,8 +167,10 @@ class GeneralSection extends BaseSection {
this.showCustomCSSPath(); this.showCustomCSSPath();
this.removeCustomCSS(); this.removeCustomCSS();
this.downloadFolder(); this.downloadFolder();
this.showDownloadFolder(); this.updateQuitOnCloseOption();
this.updatePromptDownloadOption();
this.enableErrorReporting(); this.enableErrorReporting();
this.setLocale();
// Platform specific settings // Platform specific settings
@@ -238,6 +255,7 @@ class GeneralSection extends BaseSection {
autoUpdateOption(): void { autoUpdateOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#autoupdate-option .setting-control'), $element: document.querySelector('#autoupdate-option .setting-control'),
disabled: EnterpriseUtil.configItemExists('autoUpdate'),
value: ConfigUtil.getConfigItem('autoUpdate', true), value: ConfigUtil.getConfigItem('autoUpdate', true),
clickHandler: () => { clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('autoUpdate'); const newValue = !ConfigUtil.getConfigItem('autoUpdate');
@@ -273,9 +291,7 @@ class GeneralSection extends BaseSection {
const newValue = !ConfigUtil.getConfigItem('silent', true); const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue); ConfigUtil.setConfigItem('silent', newValue);
this.updateSilentOption(); this.updateSilentOption();
// TODO: TypeScript: currentWindow of type BrowserWindow doesn't currentBrowserWindow.webContents.send('toggle-silent', newValue);
// have a .send() property per typescript.
(currentBrowserWindow as any).send('toggle-silent', newValue);
} }
}); });
} }
@@ -318,6 +334,18 @@ class GeneralSection extends BaseSection {
}); });
} }
updateQuitOnCloseOption(): void {
this.generateSettingOption({
$element: document.querySelector('#quitOnClose-option .setting-control'),
value: ConfigUtil.getConfigItem('quitOnClose', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('quitOnClose');
ConfigUtil.setConfigItem('quitOnClose', newValue);
this.updateQuitOnCloseOption();
}
});
}
enableSpellchecker(): void { enableSpellchecker(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#enable-spellchecker-option .setting-control'), $element: document.querySelector('#enable-spellchecker-option .setting-control'),
@@ -342,37 +370,35 @@ class GeneralSection extends BaseSection {
}); });
} }
clearAppDataDialog(): void { async clearAppDataDialog(): Promise<void> {
const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.'; const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.';
const getAppPath = path.join(app.getPath('appData'), app.getName()); const getAppPath = path.join(app.getPath('appData'), app.name);
dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'warning', type: 'warning',
buttons: ['YES', 'NO'], buttons: ['YES', 'NO'],
defaultId: 0, defaultId: 0,
message: 'Are you sure', message: 'Are you sure',
detail: clearAppDataMessage detail: clearAppDataMessage
}, response => {
if (response === 0) {
fs.remove(getAppPath);
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000);
}
}); });
if (response === 0) {
fs.remove(getAppPath);
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000);
}
} }
customCssDialog(): void { async customCssDialog(): Promise<void> {
const showDialogOptions: OpenDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: 'Select file', title: 'Select file',
properties: ['openFile'], properties: ['openFile'],
filters: [{ name: 'CSS file', extensions: ['css'] }] filters: [{ name: 'CSS file', extensions: ['css'] }]
}; };
dialog.showOpenDialog(showDialogOptions, selectedFile => { const { filePaths, canceled } = await dialog.showOpenDialog(showDialogOptions);
if (selectedFile) { if (!canceled) {
ConfigUtil.setConfigItem('customCSS', selectedFile[0]); ConfigUtil.setConfigItem('customCSS', filePaths[0]);
ipcRenderer.send('forward-message', 'hard-reload'); ipcRenderer.send('forward-message', 'hard-reload');
} }
});
} }
updateResetDataOption(): void { updateResetDataOption(): void {
@@ -382,6 +408,24 @@ class GeneralSection extends BaseSection {
}); });
} }
setLocale(): void {
const langDiv: HTMLSelectElement = document.querySelector('.lang-div');
// This path is for the JSON file that stores key: value pairs for supported locales
const langList = this.generateSelectTemplate(supportedLocales, 'lang-menu');
langDiv.innerHTML += langList;
// langMenu is the select-option dropdown menu formed after executing the previous command
const langMenu: HTMLSelectElement = document.querySelector('.lang-menu');
// The next three lines set the selected language visible on the dropdown button
let language = ConfigUtil.getConfigItem('appLanguage');
language = language && langMenu.options.namedItem(language) ? language : 'en-US';
langMenu.options.namedItem(language).selected = true;
langMenu.addEventListener('change', (event: Event) => {
ConfigUtil.setConfigItem('appLanguage', (event.target as HTMLSelectElement).value);
});
}
minimizeOnStart(): void { minimizeOnStart(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#start-minimize-option .setting-control'), $element: document.querySelector('#start-minimize-option .setting-control'),
@@ -411,25 +455,25 @@ class GeneralSection extends BaseSection {
removeCustomCSS(): void { removeCustomCSS(): void {
const removeCSSButton = document.querySelector('#css-delete-action'); const removeCSSButton = document.querySelector('#css-delete-action');
removeCSSButton.addEventListener('click', () => { removeCSSButton.addEventListener('click', () => {
ConfigUtil.setConfigItem('customCSS', ""); ConfigUtil.setConfigItem('customCSS', '');
ipcRenderer.send('forward-message', 'hard-reload'); ipcRenderer.send('forward-message', 'hard-reload');
}); });
} }
downloadFolderDialog(): void { async downloadFolderDialog(): Promise<void> {
const showDialogOptions: OpenDialogOptions = { const showDialogOptions: OpenDialogOptions = {
title: 'Select Download Location', title: 'Select Download Location',
properties: ['openDirectory'] properties: ['openDirectory']
}; };
dialog.showOpenDialog(showDialogOptions, selectedFolder => { const { filePaths, canceled } = await dialog.showOpenDialog(showDialogOptions);
if (selectedFolder) { if (!canceled) {
ConfigUtil.setConfigItem('downloadsPath', selectedFolder[0]); ConfigUtil.setConfigItem('downloadsPath', filePaths[0]);
const downloadFolderPath: HTMLElement = document.querySelector('.download-folder-path'); const downloadFolderPath: HTMLElement = document.querySelector('.download-folder-path');
downloadFolderPath.innerText = selectedFolder[0]; downloadFolderPath.textContent = filePaths[0];
} }
});
} }
downloadFolder(): void { downloadFolder(): void {
const downloadFolder = document.querySelector('#download-folder .download-folder-button'); const downloadFolder = document.querySelector('#download-folder .download-folder-button');
downloadFolder.addEventListener('click', () => { downloadFolder.addEventListener('click', () => {
@@ -437,17 +481,15 @@ class GeneralSection extends BaseSection {
}); });
} }
showDownloadFolder(): void { updatePromptDownloadOption(): void {
this.generateSettingOption({ this.generateSettingOption({
$element: document.querySelector('#show-download-folder .setting-control'), $element: document.querySelector('#prompt-download .setting-control'),
value: ConfigUtil.getConfigItem('showDownloadFolder', false), value: ConfigUtil.getConfigItem('promptDownload', false),
clickHandler: () => { clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('showDownloadFolder'); const newValue = !ConfigUtil.getConfigItem('promptDownload');
ConfigUtil.setConfigItem('showDownloadFolder', newValue); ConfigUtil.setConfigItem('promptDownload', newValue);
this.showDownloadFolder(); this.updatePromptDownloadOption();
} }
}); });
} }
} }
export = GeneralSection;

View File

@@ -1,13 +1,16 @@
'use strict'; import BaseComponent from '../../components/base';
import * as t from '../../utils/translation-util';
import BaseComponent = require('../../components/base'); interface PreferenceNavProps {
$root: Element;
onItemSelected: (navItem: string) => void;
}
class PreferenceNav extends BaseComponent { export default class PreferenceNav extends BaseComponent {
// TODO: TypeScript - Here props should be object type props: PreferenceNavProps;
props: any;
navItems: string[]; navItems: string[];
$el: Element; $el: Element;
constructor(props: any) { constructor(props: PreferenceNavProps) {
super(); super();
this.props = props; this.props = props;
this.navItems = ['General', 'Network', 'AddServer', 'Organizations', 'Shortcuts']; this.navItems = ['General', 'Network', 'AddServer', 'Organizations', 'Shortcuts'];
@@ -17,12 +20,12 @@ class PreferenceNav extends BaseComponent {
template(): string { template(): string {
let navItemsTemplate = ''; let navItemsTemplate = '';
for (const navItem of this.navItems) { for (const navItem of this.navItems) {
navItemsTemplate += `<div class="nav" id="nav-${navItem}">${navItem}</div>`; navItemsTemplate += `<div class="nav" id="nav-${navItem}">${t.__(navItem)}</div>`;
} }
return ` return `
<div> <div>
<div id="settings-header">Settings</div> <div id="settings-header">${t.__('Settings')}</div>
<div id="nav-container">${navItemsTemplate}</div> <div id="nav-container">${navItemsTemplate}</div>
</div> </div>
`; `;
@@ -63,5 +66,3 @@ class PreferenceNav extends BaseComponent {
$item.classList.remove('active'); $item.classList.remove('active');
} }
} }
export = PreferenceNav;

View File

@@ -1,19 +1,21 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import BaseSection = require('./base-section'); import BaseSection from './base-section';
import ConfigUtil = require('../../utils/config-util'); import * as ConfigUtil from '../../utils/config-util';
import * as t from '../../utils/translation-util';
class NetworkSection extends BaseSection { interface NetworkSectionProps {
// TODO: TypeScript - Here props should be object type $root: Element;
props: any; }
export default class NetworkSection extends BaseSection {
props: NetworkSectionProps;
$proxyPAC: HTMLInputElement; $proxyPAC: HTMLInputElement;
$proxyRules: HTMLInputElement; $proxyRules: HTMLInputElement;
$proxyBypass: HTMLInputElement; $proxyBypass: HTMLInputElement;
$proxySaveAction: Element; $proxySaveAction: Element;
$manualProxyBlock: Element; $manualProxyBlock: Element;
constructor(props: any) { constructor(props: NetworkSectionProps) {
super(); super();
this.props = props; this.props = props;
} }
@@ -21,32 +23,32 @@ class NetworkSection extends BaseSection {
template(): string { template(): string {
return ` return `
<div class="settings-pane"> <div class="settings-pane">
<div class="title">Proxy</div> <div class="title">${t.__('Proxy')}</div>
<div id="appearance-option-settings" class="settings-card"> <div id="appearance-option-settings" class="settings-card">
<div class="setting-row" id="use-system-settings"> <div class="setting-row" id="use-system-settings">
<div class="setting-description">Use system proxy settings (requires restart)</div> <div class="setting-description">${t.__('Use system proxy settings (requires restart)')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="setting-row" id="use-manual-settings"> <div class="setting-row" id="use-manual-settings">
<div class="setting-description">Manual proxy configuration</div> <div class="setting-description">${t.__('Manual proxy configuration')}</div>
<div class="setting-control"></div> <div class="setting-control"></div>
</div> </div>
<div class="manual-proxy-block"> <div class="manual-proxy-block">
<div class="setting-row" id="proxy-pac-option"> <div class="setting-row" id="proxy-pac-option">
<span class="setting-input-key">PAC script</span> <span class="setting-input-key">PAC ${t.__('script')}</span>
<input class="setting-input-value" placeholder="e.g. foobar.com/pacfile.js"/> <input class="setting-input-value" placeholder="e.g. foobar.com/pacfile.js"/>
</div> </div>
<div class="setting-row" id="proxy-rules-option"> <div class="setting-row" id="proxy-rules-option">
<span class="setting-input-key">Proxy rules</span> <span class="setting-input-key">${t.__('Proxy rules')}</span>
<input class="setting-input-value" placeholder="e.g. http=foopy:80;ftp=foopy2"/> <input class="setting-input-value" placeholder="e.g. http=foopy:80;ftp=foopy2"/>
</div> </div>
<div class="setting-row" id="proxy-bypass-option"> <div class="setting-row" id="proxy-bypass-option">
<span class="setting-input-key">Proxy bypass rules</span> <span class="setting-input-key">${t.__('Proxy bypass rules')}</span>
<input class="setting-input-value" placeholder="e.g. foobar.com"/> <input class="setting-input-value" placeholder="e.g. foobar.com"/>
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div class="action green" id="proxy-save-action"> <div class="action green" id="proxy-save-action">
<span>Save</span> <span>${t.__('Save')}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -103,7 +105,7 @@ class NetworkSection extends BaseSection {
ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue); ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue);
this.toggleManualProxySettings(!manualProxyValue); this.toggleManualProxySettings(!manualProxyValue);
} }
if (newValue === false) { if (!newValue) {
// Remove proxy system proxy settings // Remove proxy system proxy settings
ConfigUtil.setConfigItem('proxyRules', ''); ConfigUtil.setConfigItem('proxyRules', '');
ipcRenderer.send('forward-message', 'reload-proxy', false); ipcRenderer.send('forward-message', 'reload-proxy', false);
@@ -131,5 +133,3 @@ class NetworkSection extends BaseSection {
}); });
} }
} }
export = NetworkSection;

View File

@@ -1,17 +1,21 @@
'use strict'; import { ipcRenderer } from 'electron';
import { shell } from 'electron'; 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 BaseComponent = require('../../components/base'); interface NewServerFormProps {
import DomainUtil = require('../../utils/domain-util'); $root: Element;
onChange: () => void;
}
class NewServerForm extends BaseComponent { export default class NewServerForm extends BaseComponent {
// TODO: TypeScript - Here props should be object type props: NewServerFormProps;
props: any;
$newServerForm: Element; $newServerForm: Element;
$saveServerButton: HTMLButtonElement; $saveServerButton: HTMLButtonElement;
$newServerUrl: HTMLInputElement; $newServerUrl: HTMLInputElement;
constructor(props: any) { constructor(props: NewServerFormProps) {
super(); super();
this.props = props; this.props = props;
} }
@@ -19,25 +23,27 @@ class NewServerForm extends BaseComponent {
template(): string { template(): string {
return ` return `
<div class="server-input-container"> <div class="server-input-container">
<div class="title">Organization URL</div> <div class="title">${t.__('Organization URL')}</div>
<div class="add-server-info-row"> <div class="add-server-info-row">
<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/> <input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
</div> </div>
<div class="server-center"> <div class="server-center">
<div class="server-save-action"> <button id="connect">${t.__('Connect')}</button>
<button id="connect">Connect</button> </div>
<div class="server-center">
<div class="divider">
<hr class="left"/>${t.__('OR')}<hr class="right" />
</div> </div>
</div> </div>
<div class="server-center"> <div class="server-center">
<div class="divider"> <button id="open-create-org-link">${t.__('Create a new organization')}</button>
<hr class="left"/>OR<hr class="right" />
</div>
</div> </div>
<div class="server-center"> <div class="server-center">
<div class="server-save-action"> <div class="server-network-option">
<button id="open-create-org-link">Create a new organization</button> <span id="open-network-settings">${t.__('Network and Proxy Settings')}</span>
</div> <i class="material-icons open-network-button">open_in_new</i>
</div> </div>
</div>
</div> </div>
`; `;
} }
@@ -49,46 +55,50 @@ class NewServerForm extends BaseComponent {
initForm(): void { initForm(): void {
this.$newServerForm = this.generateNodeFromTemplate(this.template()); this.$newServerForm = this.generateNodeFromTemplate(this.template());
this.$saveServerButton = this.$newServerForm.querySelectorAll('.server-save-action')[0] as HTMLButtonElement; this.$saveServerButton = this.$newServerForm.querySelector('#connect');
this.props.$root.innerHTML = ''; this.props.$root.innerHTML = '';
this.props.$root.append(this.$newServerForm); this.props.$root.append(this.$newServerForm);
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement; this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
} }
submitFormHandler(): void { async submitFormHandler(): Promise<void> {
this.$saveServerButton.children[0].innerHTML = 'Connecting...'; this.$saveServerButton.innerHTML = 'Connecting...';
DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => { let serverConf;
DomainUtil.addDomain(serverConf).then(() => { try {
this.props.onChange(this.props.index); serverConf = await DomainUtil.checkDomain(this.$newServerUrl.value);
}); } catch (errorMessage) {
}, errorMessage => { this.$saveServerButton.innerHTML = 'Connect';
this.$saveServerButton.children[0].innerHTML = 'Connect';
alert(errorMessage); alert(errorMessage);
}); return;
}
await DomainUtil.addDomain(serverConf);
this.props.onChange();
} }
openCreateNewOrgExternalLink(): void { openCreateNewOrgExternalLink(): void {
const link = 'https://zulipchat.com/new/'; const link = 'https://zulipchat.com/new/';
const externalCreateNewOrgEl = document.querySelector('#open-create-org-link'); const externalCreateNewOrgElement = document.querySelector('#open-create-org-link');
externalCreateNewOrgEl.addEventListener('click', () => { externalCreateNewOrgElement.addEventListener('click', () => {
shell.openExternal(link); 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 { initActions(): void {
this.$saveServerButton.addEventListener('click', () => { this.$saveServerButton.addEventListener('click', () => {
this.submitFormHandler(); this.submitFormHandler();
}); });
this.$newServerUrl.addEventListener('keypress', event => { this.$newServerUrl.addEventListener('keypress', event => {
const EnterkeyCode = event.keyCode; if (event.key === 'Enter') {
// Submit form when Enter key is pressed
if (EnterkeyCode === 13) {
this.submitFormHandler(); this.submitFormHandler();
} }
}); });
// open create new org link in default browser // open create new org link in default browser
this.openCreateNewOrgExternalLink(); this.openCreateNewOrgExternalLink();
this.networkSettingsLink();
} }
} }
export = NewServerForm;

View File

@@ -1,18 +1,16 @@
'use strict';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import BaseComponent = require('../../components/base'); import BaseComponent from '../../components/base';
import Nav = require('./nav'); import Nav from './nav';
import ServersSection = require('./servers-section'); import ServersSection from './servers-section';
import GeneralSection = require('./general-section'); import GeneralSection from './general-section';
import NetworkSection = require('./network-section'); import NetworkSection from './network-section';
import ConnectedOrgSection = require('./connected-org-section'); import ConnectedOrgSection from './connected-org-section';
import ShortcutsSection = require('./shortcuts-section'); import ShortcutsSection from './shortcuts-section';
type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection; type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection;
class PreferenceView extends BaseComponent { export default class PreferenceView extends BaseComponent {
$sidebarContainer: Element; $sidebarContainer: Element;
$settingsContainer: Element; $settingsContainer: Element;
nav: Nav; nav: Nav;
@@ -37,7 +35,7 @@ class PreferenceView extends BaseComponent {
let nav = 'General'; let nav = 'General';
const hasTag = window.location.hash; const hasTag = window.location.hash;
if (hasTag) { if (hasTag) {
nav = hasTag.substring(1); nav = hasTag.slice(1);
} }
this.handleNavigation(nav); this.handleNavigation(nav);
} }
@@ -122,5 +120,3 @@ window.addEventListener('load', () => {
const preferenceView = new PreferenceView(); const preferenceView = new PreferenceView();
preferenceView.init(); preferenceView.init();
}); });
export = PreferenceView;

View File

@@ -1,21 +1,27 @@
'use strict';
import { remote, ipcRenderer } from 'electron'; import { remote, ipcRenderer } from 'electron';
import BaseComponent = require('../../components/base'); import BaseComponent from '../../components/base';
import DomainUtil = require('../../utils/domain-util'); import * as DomainUtil from '../../utils/domain-util';
import * as Messages from '../../../../resources/messages';
import * as t from '../../utils/translation-util';
const { dialog } = remote; const { dialog } = remote;
class ServerInfoForm extends BaseComponent { interface ServerInfoFormProps {
// TODO: TypeScript - Here props should be object type $root: Element;
props: any; server: DomainUtil.ServerConf;
index: number;
onChange: () => void;
}
export default class ServerInfoForm extends BaseComponent {
props: ServerInfoFormProps;
$serverInfoForm: Element; $serverInfoForm: Element;
$serverInfoAlias: Element; $serverInfoAlias: Element;
$serverIcon: Element; $serverIcon: Element;
$deleteServerButton: Element; $deleteServerButton: Element;
$openServerButton: Element; $openServerButton: Element;
constructor(props: any) { constructor(props: ServerInfoFormProps) {
super(); super();
this.props = props; this.props = props;
} }
@@ -36,7 +42,7 @@ class ServerInfoForm extends BaseComponent {
</div> </div>
<div class="server-info-row"> <div class="server-info-row">
<div class="action red server-delete-action"> <div class="action red server-delete-action">
<span>Disconnect</span> <span>${t.__('Disconnect')}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -59,18 +65,21 @@ class ServerInfoForm extends BaseComponent {
} }
initActions(): void { initActions(): void {
this.$deleteServerButton.addEventListener('click', () => { this.$deleteServerButton.addEventListener('click', async () => {
dialog.showMessageBox({ const { response } = await dialog.showMessageBox({
type: 'warning', type: 'warning',
buttons: ['YES', 'NO'], buttons: [t.__('YES'), t.__('NO')],
defaultId: 0, defaultId: 0,
message: 'Are you sure you want to disconnect this organization?' message: t.__('Are you sure you want to disconnect this organization?')
}, response => {
if (response === 0) {
DomainUtil.removeDomain(this.props.index);
this.props.onChange(this.props.index);
}
}); });
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', () => { this.$openServerButton.addEventListener('click', () => {
@@ -86,5 +95,3 @@ class ServerInfoForm extends BaseComponent {
}); });
} }
} }
export = ServerInfoForm;

View File

@@ -1,13 +1,15 @@
'use strict'; import BaseSection from './base-section';
import NewServerForm from './new-server-form';
import * as t from '../../utils/translation-util';
import BaseSection = require('./base-section'); interface ServersSectionProps {
import NewServerForm = require('./new-server-form'); $root: Element;
}
class ServersSection extends BaseSection { export default class ServersSection extends BaseSection {
// TODO: TypeScript - Here props should be object type props: ServersSectionProps;
props: any;
$newServerContainer: Element; $newServerContainer: Element;
constructor(props: any) { constructor(props: ServersSectionProps) {
super(); super();
this.props = props; this.props = props;
} }
@@ -17,7 +19,7 @@ class ServersSection extends BaseSection {
<div class="add-server-modal"> <div class="add-server-modal">
<div class="modal-container"> <div class="modal-container">
<div class="settings-pane" id="server-settings-pane"> <div class="settings-pane" id="server-settings-pane">
<div class="page-title">Add a Zulip organization</div> <div class="page-title">${t.__('Add a Zulip organization')}</div>
<div id="new-server-container"></div> <div id="new-server-container"></div>
</div> </div>
</div> </div>
@@ -45,5 +47,3 @@ class ServersSection extends BaseSection {
}).init(); }).init();
} }
} }
export = ServersSection;

View File

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

View File

@@ -1,39 +1,31 @@
'use strict'; import { contextBridge, ipcRenderer, webFrame } from 'electron';
import fs from 'fs';
import * as SetupSpellChecker from './spellchecker';
import { ipcRenderer, shell } from 'electron'; import isDev from 'electron-is-dev';
import SetupSpellChecker from './spellchecker';
import LinkUtil = require('./utils/link-util'); import * as NetworkError from './pages/network';
import params = require('./utils/params-util');
interface PatchedGlobal extends NodeJS.Global {
logout: () => void;
shortcut: () => void;
}
const globalPatched = global as PatchedGlobal;
// eslint-disable-next-line import/no-unassigned-import // eslint-disable-next-line import/no-unassigned-import
require('./notification'); import './notification';
// Prevent drag and drop event in main process which prevents remote code executaion // Prevent drag and drop event in main process which prevents remote code executaion
require(__dirname + '/shared/preventdrag.js'); // eslint-disable-next-line import/no-unassigned-import
import './shared/preventdrag';
declare let window: ZulipWebWindow; import electron_bridge from './electron-bridge';
contextBridge.exposeInMainWorld('raw_electron_bridge', electron_bridge);
// eslint-disable-next-line @typescript-eslint/camelcase ipcRenderer.on('logout', () => {
window.electron_bridge = require('./electron-bridge');
const logout = (): void => {
// Create the menu for the below // Create the menu for the below
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle'); const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click(); dropdown.click();
const nodes: NodeListOf<HTMLElement> = document.querySelectorAll('.dropdown-menu li:last-child a'); const nodes: NodeListOf<HTMLElement> = document.querySelectorAll('.dropdown-menu li:last-child a');
nodes[nodes.length - 1].click(); nodes[nodes.length - 1].click();
}; });
const shortcut = (): void => { ipcRenderer.on('shortcut', () => {
// Create the menu for the below // Create the menu for the below
const node: HTMLElement = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]'); const node: HTMLElement = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
// Additional check // Additional check
@@ -44,49 +36,35 @@ const shortcut = (): void => {
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle'); const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click(); dropdown.click();
} }
};
process.once('loaded', (): void => {
globalPatched.logout = logout;
globalPatched.shortcut = shortcut;
}); });
// To prevent failing this script on linux we need to load it after the document loaded ipcRenderer.on('show-notification-settings', () => {
document.addEventListener('DOMContentLoaded', (): void => { // Create the menu for the below
if (params.isPageParams()) { 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);
});
electron_bridge.once('zulip-loaded', ({ serverLanguage }) => {
// Get the default language of the server // Get the default language of the server
const serverLanguage = page_params.default_language; // eslint-disable-line no-undef, @typescript-eslint/camelcase if (serverLanguage) {
if (serverLanguage) { // Init spellchecker
// Init spellchecker SetupSpellChecker.init(serverLanguage);
SetupSpellChecker.init(serverLanguage); }
} // redirect users to network troubleshooting page
// redirect users to network troubleshooting page const getRestartButton = document.querySelector('.restart_get_events_button');
const getRestartButton = document.querySelector('.restart_get_events_button'); if (getRestartButton) {
if (getRestartButton) { getRestartButton.addEventListener('click', () => {
getRestartButton.addEventListener('click', () => { ipcRenderer.send('forward-message', 'reload-viewer');
ipcRenderer.send('forward-message', 'reload-viewer');
});
}
// Open image attachment link in the lightbox instead of opening in the default browser
const { $, lightbox } = window;
$('#main_div').on('click', '.message_content p a', function (this: HTMLElement, e: Event) {
const url = $(this).attr('href');
if (LinkUtil.isImage(url)) {
const $img = $(this).parent().siblings('.message_inline_image').find('img');
// prevent the image link from opening in a new page.
e.preventDefault();
// prevent the message compose dialog from happening.
e.stopPropagation();
// Open image in the default browser if image preview is unavailable
if (!$img[0]) {
shell.openExternal(window.location.origin + url);
}
// Open image in lightbox
lightbox.open($img);
}
}); });
} }
}); });
@@ -97,17 +75,48 @@ window.addEventListener('beforeunload', (): void => {
SetupSpellChecker.unsubscribeSpellChecker(); SetupSpellChecker.unsubscribeSpellChecker();
}); });
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 // electron's globalShortcut can cause unexpected results
// so adding the reload shortcut in the old-school way // so adding the reload shortcut in the old-school way
// Zoom from numpad keys is not supported by electron, so adding it through listeners. // Zoom from numpad keys is not supported by electron, so adding it through listeners.
document.addEventListener('keydown', event => { document.addEventListener('keydown', event => {
const cmdOrCtrl = event.ctrlKey || event.metaKey;
if (event.code === 'F5') { if (event.code === 'F5') {
ipcRenderer.send('forward-message', 'hard-reload'); ipcRenderer.send('forward-message', 'hard-reload');
} else if (event.ctrlKey && event.code === 'NumpadAdd') { } else if (cmdOrCtrl && (event.code === 'NumpadAdd' || event.code === 'Equal')) {
ipcRenderer.send('forward-message', 'zoomIn'); ipcRenderer.send('forward-message', 'zoomIn');
} else if (event.ctrlKey && event.code === 'NumpadSubtract') { } else if (cmdOrCtrl && event.code === 'NumpadSubtract') {
ipcRenderer.send('forward-message', 'zoomOut'); ipcRenderer.send('forward-message', 'zoomOut');
} else if (event.ctrlKey && event.code === 'Numpad0') { } else if (cmdOrCtrl && event.code === 'Numpad0') {
ipcRenderer.send('forward-message', 'zoomActualSize'); 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;
});
webFrame.executeJavaScript(
fs.readFileSync(require.resolve('./injected'), 'utf8')
);

View File

@@ -1,5 +1,3 @@
'use strict';
// This is a security fix. Following function prevents drag and drop event in the app // 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 // so that attackers can't execute any remote code within the app
// It doesn't affect the compose box so that users can still // It doesn't affect the compose box so that users can still
@@ -15,3 +13,5 @@ const preventDragAndDrop = (): void => {
}; };
preventDragAndDrop(); preventDragAndDrop();
export {};

View File

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

View File

@@ -1,13 +1,20 @@
'use strict';
import { ipcRenderer, remote, WebviewTag, NativeImage } from 'electron'; import { ipcRenderer, remote, WebviewTag, NativeImage } from 'electron';
import path = require('path'); import path from 'path';
import ConfigUtil = require('./utils/config-util.js'); import * as ConfigUtil from './utils/config-util';
const { Tray, Menu, nativeImage, BrowserWindow } = remote;
const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray'); const { Tray, Menu, nativeImage, BrowserWindow, nativeTheme } = remote;
declare let window: ZulipWebWindow; let tray: Electron.Tray;
// get the theme on macOS
const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
const ICON_DIR = process.platform === 'darwin' ? `../../resources/tray/${theme}` : '../../resources/tray';
const TRAY_SUFFIX = 'tray';
const APP_ICON = path.join(__dirname, ICON_DIR, TRAY_SUFFIX);
const iconPath = (): string => { const iconPath = (): string => {
if (process.platform === 'linux') { if (process.platform === 'linux') {
@@ -43,69 +50,59 @@ const config = {
thick: process.platform === 'win32' thick: process.platform === 'win32'
}; };
const renderCanvas = function (arg: number): Promise<HTMLCanvasElement> { const renderCanvas = function (arg: number): HTMLCanvasElement {
config.unreadCount = arg; config.unreadCount = arg;
return new Promise(resolve => { const SIZE = config.size * config.pixelRatio;
const SIZE = config.size * config.pixelRatio; const PADDING = SIZE * 0.05;
const PADDING = SIZE * 0.05; const CENTER = SIZE / 2;
const CENTER = SIZE / 2; const HAS_COUNT = config.showUnreadCount && config.unreadCount;
const HAS_COUNT = config.showUnreadCount && config.unreadCount; const color = config.unreadCount ? config.unreadColor : config.readColor;
const color = config.unreadCount ? config.unreadColor : config.readColor; const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor;
const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = SIZE; canvas.width = SIZE;
canvas.height = SIZE; canvas.height = SIZE;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
// Circle // Circle
// If (!config.thick || config.thick && HAS_COUNT) { // If (!config.thick || config.thick && HAS_COUNT) {
ctx.beginPath(); ctx.beginPath();
ctx.arc(CENTER, CENTER, (SIZE / 2) - PADDING, 0, 2 * Math.PI, false); ctx.arc(CENTER, CENTER, (SIZE / 2) - PADDING, 0, 2 * Math.PI, false);
ctx.fillStyle = backgroundColor; ctx.fillStyle = backgroundColor;
ctx.fill(); ctx.fill();
ctx.lineWidth = SIZE / (config.thick ? 10 : 20); ctx.lineWidth = SIZE / (config.thick ? 10 : 20);
ctx.strokeStyle = backgroundColor; ctx.strokeStyle = backgroundColor;
ctx.stroke(); ctx.stroke();
// Count or Icon // Count or Icon
if (HAS_COUNT) { if (HAS_COUNT) {
ctx.fillStyle = color; ctx.fillStyle = color;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
if (config.unreadCount > 99) { if (config.unreadCount > 99) {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`; ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`;
ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15)); ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15));
} else if (config.unreadCount < 10) { } else if (config.unreadCount < 10) {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.20)); ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.2));
} else { } else {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`; ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.15)); ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.15));
}
resolve(canvas);
} }
}); }
return canvas;
}; };
/** /**
* Renders the tray icon as a native image * Renders the tray icon as a native image
* @param arg: Unread count * @param arg: Unread count
* @return the native image * @return the native image
*/ */
const renderNativeImage = function (arg: number): Promise<NativeImage> { const renderNativeImage = function (arg: number): NativeImage {
return Promise.resolve() const canvas = renderCanvas(arg);
.then(() => renderCanvas(arg)) const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
.then(canvas => { return nativeImage.createFromBuffer(pngData, {
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG(); scaleFactor: config.pixelRatio
});
// TODO: Fix the function to correctly use Promise correctly.
// the Promise.resolve().then(...) above is useless we should
// start with renderCanvas(arg).then
// eslint-disable-next-line promise/no-return-wrap
return Promise.resolve(nativeImage.createFromBuffer(pngData, {
scaleFactor: config.pixelRatio
}));
});
}; };
function sendAction(action: string): void { function sendAction(action: string): void {
@@ -119,7 +116,6 @@ function sendAction(action: string): void {
} }
const createTray = function (): void { const createTray = function (): void {
window.tray = new Tray(iconPath());
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
label: 'Zulip', label: 'Zulip',
@@ -144,22 +140,23 @@ const createTray = function (): void {
} }
} }
]); ]);
window.tray.setContextMenu(contextMenu); tray = new Tray(iconPath());
tray.setContextMenu(contextMenu);
if (process.platform === 'linux' || process.platform === 'win32') { if (process.platform === 'linux' || process.platform === 'win32') {
window.tray.on('click', () => { tray.on('click', () => {
ipcRenderer.send('toggle-app'); ipcRenderer.send('toggle-app');
}); });
} }
}; };
ipcRenderer.on('destroytray', (event: Event): Event => { ipcRenderer.on('destroytray', (event: Event): Event => {
if (!window.tray) { if (!tray) {
return undefined; return undefined;
} }
window.tray.destroy(); tray.destroy();
if (window.tray.isDestroyed()) { if (tray.isDestroyed()) {
window.tray = null; tray = null;
} else { } else {
throw new Error('Tray icon not properly destroyed.'); throw new Error('Tray icon not properly destroyed.');
} }
@@ -168,42 +165,40 @@ ipcRenderer.on('destroytray', (event: Event): Event => {
}); });
ipcRenderer.on('tray', (_event: Event, arg: number): void => { ipcRenderer.on('tray', (_event: Event, arg: number): void => {
if (!window.tray) { if (!tray) {
return; return;
} }
// We don't want to create tray from unread messages on macOS since it already has dock badges. // We don't want to create tray from unread messages on macOS since it already has dock badges.
if (process.platform === 'linux' || process.platform === 'win32') { if (process.platform === 'linux' || process.platform === 'win32') {
if (arg === 0) { if (arg === 0) {
unread = arg; unread = arg;
window.tray.setImage(iconPath()); tray.setImage(iconPath());
window.tray.setToolTip('No unread messages'); tray.setToolTip('No unread messages');
} else { } else {
unread = arg; unread = arg;
renderNativeImage(arg).then(image => { const image = renderNativeImage(arg);
window.tray.setImage(image); tray.setImage(image);
window.tray.setToolTip(arg + ' unread messages'); tray.setToolTip(arg + ' unread messages');
});
} }
} }
}); });
function toggleTray(): void { function toggleTray(): void {
let state; let state;
if (window.tray) { if (tray) {
state = false; state = false;
window.tray.destroy(); tray.destroy();
if (window.tray.isDestroyed()) { if (tray.isDestroyed()) {
window.tray = null; tray = null;
} }
ConfigUtil.setConfigItem('trayIcon', false); ConfigUtil.setConfigItem('trayIcon', false);
} else { } else {
state = true; state = true;
createTray(); createTray();
if (process.platform === 'linux' || process.platform === 'win32') { if (process.platform === 'linux' || process.platform === 'win32') {
renderNativeImage(unread).then(image => { const image = renderNativeImage(unread);
window.tray.setImage(image); tray.setImage(image);
window.tray.setToolTip(unread + ' unread messages'); tray.setToolTip(unread + ' unread messages');
});
} }
ConfigUtil.setConfigItem('trayIcon', true); ConfigUtil.setConfigItem('trayIcon', true);
} }
@@ -218,3 +213,5 @@ ipcRenderer.on('toggletray', toggleTray);
if (ConfigUtil.getConfigItem('trayIcon', true)) { if (ConfigUtil.getConfigItem('trayIcon', true)) {
createTray(); createTray();
} }
export {};

View File

@@ -1,96 +1,80 @@
'use strict';
import { remote } from 'electron'; import { remote } from 'electron';
import JsonDB from 'node-json-db'; import { JsonDB } from 'node-json-db';
import { initSetUp } from './default-util'; import { initSetUp } from './default-util';
import fs = require('fs'); import fs from 'fs';
import path = require('path'); import path from 'path';
import Logger = require('./logger-util'); import Logger from './logger-util';
const { app, dialog } = remote; const { app, dialog } = remote;
initSetUp(); initSetUp();
const logger = new Logger({ const logger = new Logger({
file: `certificate-util.log`, file: 'certificate-util.log',
timestamp: true timestamp: true
}); });
let instance: null | CertificateUtil = null;
const certificatesDir = `${app.getPath('userData')}/certificates`; const certificatesDir = `${app.getPath('userData')}/certificates`;
class CertificateUtil { let db: JsonDB;
db: JsonDB;
constructor() { reloadDB();
if (instance) {
return instance;
} else {
instance = this;
}
this.reloadDB(); export function getCertificate(server: string, defaultValue: any = null): any {
return instance; reloadDB();
} const value = db.getData('/')[server];
if (value === undefined) {
getCertificate(server: string, defaultValue: any = null): any { return defaultValue;
this.reloadDB(); } else {
const value = this.db.getData('/')[server]; return value;
if (value === undefined) {
return defaultValue;
} else {
return value;
}
}
// Function to copy the certificate to userData folder
copyCertificate(_server: string, location: string, fileName: string): boolean {
let copied = false;
const filePath = `${certificatesDir}/${fileName}`;
try {
fs.copyFileSync(location, filePath);
copied = true;
} catch (err) {
dialog.showErrorBox(
'Error saving certificate',
'We encountered error while saving the certificate.'
);
logger.error('Error while copying the certificate to certificates folder.');
logger.error(err);
}
return copied;
}
setCertificate(server: string, fileName: string): void {
const filePath = `${certificatesDir}/${fileName}`;
this.db.push(`/${server}`, filePath, true);
this.reloadDB();
}
removeCertificate(server: string): void {
this.db.delete(`/${server}`);
this.reloadDB();
}
reloadDB(): void {
const settingsJsonPath = path.join(app.getPath('userData'), '/config/certificates.json');
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
'Error saving settings',
'We encountered error while saving the certificate.'
);
logger.error('Error while JSON parsing certificates.json: ');
logger.error(err);
}
}
this.db = new JsonDB(settingsJsonPath, true, true);
} }
} }
export = new CertificateUtil(); // 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 (err) {
dialog.showErrorBox(
'Error saving certificate',
'We encountered error while saving the certificate.'
);
logger.error('Error while copying the certificate to certificates folder.');
logger.error(err);
}
return copied;
}
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 (err) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
'Error saving settings',
'We encountered error while saving the certificate.'
);
logger.error('Error while JSON parsing certificates.json: ');
logger.error(err);
}
}
db = new JsonDB(settingsJsonPath, true, true);
}

View File

@@ -1,25 +1,8 @@
'use strict'; // unescape already encoded/escaped strings
export function decodeString(stringInput: string): string {
let instance: null | CommonUtil = null; const parser = new DOMParser();
const dom = parser.parseFromString(
class CommonUtil { '<!doctype html><body>' + stringInput,
constructor() { 'text/html');
if (instance) { return dom.body.textContent;
return instance;
} else {
instance = this;
}
return instance;
}
// unescape already encoded/escaped strings
decodeString(stringInput: string): string {
const parser = new DOMParser();
const dom = parser.parseFromString(
'<!doctype html><body>' + stringInput,
'text/html');
return dom.body.textContent;
}
} }
export = new CommonUtil();

View File

@@ -1,17 +1,16 @@
'use strict'; import { JsonDB } from 'node-json-db';
import JsonDB from 'node-json-db';
import fs = require('fs'); import fs from 'fs';
import path = require('path'); import path from 'path';
import electron = require('electron'); import electron from 'electron';
import Logger = require('./logger-util'); import Logger from './logger-util';
import * as EnterpriseUtil from './enterprise-util';
const logger = new Logger({ const logger = new Logger({
file: 'config-util.log', file: 'config-util.log',
timestamp: true timestamp: true
}); });
let instance: null | ConfigUtil = null;
let dialog: Electron.Dialog = null; let dialog: Electron.Dialog = null;
let app: Electron.App = null; let app: Electron.App = null;
@@ -25,77 +24,68 @@ if (process.type === 'renderer') {
app = electron.app; app = electron.app;
} }
class ConfigUtil { let db: JsonDB;
db: JsonDB;
constructor() { reloadDB();
if (instance) {
return instance;
} else {
instance = this;
}
this.reloadDB(); export function getConfigItem(key: string, defaultValue: any = null): any {
return instance; try {
db.reload();
} catch (err) {
logger.error('Error while reloading settings.json: ');
logger.error(err);
} }
const value = db.getData('/')[key];
getConfigItem(key: string, defaultValue: any = null): any { if (value === undefined) {
try { setConfigItem(key, defaultValue);
this.db.reload(); return defaultValue;
} catch (err) { } else {
logger.error('Error while reloading settings.json: '); return value;
logger.error(err);
}
const value = this.db.getData('/')[key];
if (value === undefined) {
this.setConfigItem(key, defaultValue);
return defaultValue;
} else {
return value;
}
}
// This function returns whether a key exists in the configuration file (settings.json)
isConfigItemExists(key: string): boolean {
try {
this.db.reload();
} catch (err) {
logger.error('Error while reloading settings.json: ');
logger.error(err);
}
const value = this.db.getData('/')[key];
return (value !== undefined);
}
setConfigItem(key: string, value: any): void {
this.db.push(`/${key}`, value, true);
this.db.save();
}
removeConfigItem(key: string): void {
this.db.delete(`/${key}`);
this.db.save();
}
reloadDB(): void {
const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.json');
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
'Error saving settings',
'We encountered an error while saving the settings.'
);
logger.error('Error while JSON parsing settings.json: ');
logger.error(err);
logger.reportSentry(err);
}
}
this.db = new JsonDB(settingsJsonPath, true, true);
} }
} }
export = new ConfigUtil(); // This function returns whether a key exists in the configuration file (settings.json)
export function isConfigItemExists(key: string): boolean {
try {
db.reload();
} catch (err) {
logger.error('Error while reloading settings.json: ');
logger.error(err);
}
const value = db.getData('/')[key];
return (value !== undefined);
}
export function setConfigItem(key: string, value: any, 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 (err) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
'Error saving settings',
'We encountered an error while saving the settings.'
);
logger.error('Error while JSON parsing settings.json: ');
logger.error(err);
logger.reportSentry(err);
}
}
db = new JsonDB(settingsJsonPath, true, true);
}

View File

@@ -1,11 +1,12 @@
import fs = require('fs'); import electron from 'electron';
import fs from 'fs';
let app: Electron.App = null; let app: Electron.App = null;
let setupCompleted = false; let setupCompleted = false;
if (process.type === 'renderer') { if (process.type === 'renderer') {
app = require('electron').remote.app; app = electron.remote.app;
} else { } else {
app = require('electron').app; app = electron.app;
} }
const zulipDir = app.getPath('userData'); const zulipDir = app.getPath('userData');
@@ -41,19 +42,19 @@ export const initSetUp = (): void => {
const configData = [ const configData = [
{ {
path: domainJson, path: domainJson,
fileName: `domain.json` fileName: 'domain.json'
}, },
{ {
path: certificatesJson, path: certificatesJson,
fileName: `certificates.json` fileName: 'certificates.json'
}, },
{ {
path: settingsJson, path: settingsJson,
fileName: `settings.json` fileName: 'settings.json'
}, },
{ {
path: updatesJson, path: updatesJson,
fileName: `updates.json` fileName: 'updates.json'
} }
]; ];
configData.forEach(data => { configData.forEach(data => {

View File

@@ -1,13 +1,11 @@
'use strict'; import * as ConfigUtil from './config-util';
import ConfigUtil = require('./config-util');
// TODO: TypeScript - add to Setting interface // TODO: TypeScript - add to Setting interface
// the list of settings since we have fixed amount of them // the list of settings since we have fixed amount of them
// We want to do this by creating a new module that exports // We want to do this by creating a new module that exports
// this interface // this interface
interface Setting { interface Setting {
[key: string]: any; [key: string]: boolean;
} }
interface Toggle { interface Toggle {

View File

@@ -1,310 +1,309 @@
'use strict'; import { JsonDB } from 'node-json-db';
import JsonDB from 'node-json-db';
import escape = require('escape-html'); import escape from 'escape-html';
import request = require('request'); import request from 'request';
import fs = require('fs'); import fs from 'fs';
import path = require('path'); import path from 'path';
import Logger = require('./logger-util'); import Logger from './logger-util';
import electron = require('electron'); import { remote } from 'electron';
import RequestUtil = require('./request-util'); import * as RequestUtil from './request-util';
import Messages = require('../../../resources/messages'); import * as EnterpriseUtil from './enterprise-util';
import * as Messages from '../../../resources/messages';
const { app, dialog } = electron.remote; const { app, dialog } = remote;
export interface ServerConf {
url: string;
alias?: string;
icon?: string;
ignoreCerts?: boolean;
}
const logger = new Logger({ const logger = new Logger({
file: `domain-util.log`, file: 'domain-util.log',
timestamp: true timestamp: true
}); });
let instance: null | DomainUtil = null;
const defaultIconUrl = '../renderer/img/icon.png'; const defaultIconUrl = '../renderer/img/icon.png';
class DomainUtil { export let db: JsonDB;
db: JsonDB;
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
this.reloadDB(); reloadDB();
// Migrate from old schema // Migrate from old schema
if (this.db.getData('/').domain) { if (db.getData('/').domain) {
this.addDomain({ addDomain({
alias: 'Zulip', alias: 'Zulip',
url: this.db.getData('/domain') url: db.getData('/domain')
}); });
this.db.delete('/domain'); db.delete('/domain');
} }
return instance; export function getDomains(): ServerConf[] {
reloadDB();
if (db.getData('/').domains === undefined) {
return [];
} else {
return db.getData('/domains');
} }
}
getDomains(): any { export function getDomain(index: number): ServerConf {
this.reloadDB(); reloadDB();
if (this.db.getData('/').domains === undefined) { return db.getData(`/domains[${index}]`);
return []; }
} else {
return this.db.getData('/domains'); export function shouldIgnoreCerts(url: string): boolean {
const domains = getDomains();
for (const domain of domains) {
if (domain.url === url) {
return domain.ignoreCerts;
} }
} }
return null;
}
getDomain(index: number): any { function updateDomain(index: number, server: ServerConf): void {
this.reloadDB(); reloadDB();
return this.db.getData(`/domains[${index}]`); db.push(`/domains[${index}]`, server, true);
}
export async function addDomain(server: ServerConf): Promise<void> {
const { ignoreCerts } = server;
if (server.icon) {
const localIconUrl = await saveServerIcon(server, ignoreCerts);
server.icon = localIconUrl;
db.push('/domains[]', server, true);
reloadDB();
} else {
server.icon = defaultIconUrl;
db.push('/domains[]', server, true);
reloadDB();
} }
}
updateDomain(index: number, server: object): void { export function removeDomains(): void {
this.reloadDB(); db.delete('/domains');
this.db.push(`/domains[${index}]`, server, true); reloadDB();
} }
addDomain(server: any): Promise<void> { export function removeDomain(index: number): boolean {
const { ignoreCerts } = server; if (EnterpriseUtil.isPresetOrg(getDomain(index).url)) {
return new Promise(resolve => {
if (server.icon) {
this.saveServerIcon(server, ignoreCerts).then(localIconUrl => {
server.icon = localIconUrl;
this.db.push('/domains[]', server, true);
this.reloadDB();
resolve();
});
} else {
server.icon = defaultIconUrl;
this.db.push('/domains[]', server, true);
this.reloadDB();
resolve();
}
});
}
removeDomains(): void {
this.db.delete('/domains');
this.reloadDB();
}
removeDomain(index: number): void {
this.db.delete(`/domains[${index}]`);
this.reloadDB();
}
// Check if domain is already added
duplicateDomain(domain: any): boolean {
domain = this.formatUrl(domain);
const servers = this.getDomains();
for (const i in servers) {
if (servers[i].url === domain) {
return true;
}
}
return false; return false;
} }
db.delete(`/domains[${index}]`);
reloadDB();
return true;
}
async checkCertError(domain: any, serverConf: any, error: string, silent: boolean): Promise<string | object> { // Check if domain is already added
if (silent) { export function duplicateDomain(domain: string): boolean {
// since getting server settings has already failed domain = formatUrl(domain);
return serverConf; return getDomains().some(server => server.url === domain);
} else { }
// Report error to sentry to get idea of possible certificate errors
// users get when adding the servers
logger.reportSentry(new Error(error).toString());
const certErrorMessage = Messages.certErrorMessage(domain, error);
const certErrorDetail = Messages.certErrorDetail();
const response = await (dialog.showMessageBox({ async function checkCertError(domain: string, serverConf: ServerConf, error: string, silent: boolean): Promise<ServerConf> {
type: 'warning', if (silent) {
buttons: ['Yes', 'No'], // since getting server settings has already failed
defaultId: 1, return serverConf;
message: certErrorMessage, } else {
detail: certErrorDetail // Report error to sentry to get idea of possible certificate errors
}) as any); // TODO: TypeScript - Figure this out // users get when adding the servers
if (response === 0) { logger.reportSentry(new Error(error).toString());
// set ignoreCerts parameter to true in case user responds with yes const certErrorMessage = Messages.certErrorMessage(domain, error);
serverConf.ignoreCerts = true; const certErrorDetail = Messages.certErrorDetail();
try {
return await this.getServerSettings(domain, serverConf.ignoreCerts);
} catch (_) {
if (error === Messages.noOrgsError(domain)) {
throw new Error(error);
}
return serverConf;
}
} else {
throw new Error('Untrusted certificate.');
}
}
}
// ignoreCerts parameter helps in fetching server icon and const { response } = await dialog.showMessageBox({
// other server details when user chooses to ignore certificate warnings type: 'warning',
async checkDomain(domain: any, ignoreCerts = false, silent = false): Promise<any> { buttons: ['Yes', 'No'],
if (!silent && this.duplicateDomain(domain)) { defaultId: 1,
// Do not check duplicate in silent mode message: certErrorMessage,
throw new Error('This server has been added.'); detail: certErrorDetail
}
domain = this.formatUrl(domain);
const serverConf = {
icon: defaultIconUrl,
url: domain,
alias: domain,
ignoreCerts
};
try {
return await this.getServerSettings(domain, serverConf.ignoreCerts);
} catch (err) {
// If the domain contains following strings we just bypass the server
const whitelistDomains = [
'zulipdev.org'
];
// make sure that error is an error or string not undefined
// so validation does not throw error.
const error = err || '';
const certsError = error.toString().includes('certificate');
if (domain.indexOf(whitelistDomains) >= 0 || certsError) {
return this.checkCertError(domain, serverConf, error, silent);
} else {
throw Messages.invalidZulipServerError(domain);
}
}
}
getServerSettings(domain: any, ignoreCerts = false): Promise<object | string> {
const serverSettingsOptions = {
url: domain + '/api/v1/server_settings',
...RequestUtil.requestOptions(domain, ignoreCerts)
};
return new Promise((resolve, reject) => {
request(serverSettingsOptions, (error: string, response: any) => {
if (!error && response.statusCode === 200) {
const data = JSON.parse(response.body);
if (data.hasOwnProperty('realm_icon') && data.realm_icon) {
resolve({
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
url: data.realm_uri,
alias: escape(data.realm_name),
ignoreCerts
});
} else {
reject(Messages.noOrgsError(domain));
}
} else {
reject(response);
}
});
}); });
if (response === 0) {
// set ignoreCerts parameter to true in case user responds with yes
serverConf.ignoreCerts = true;
try {
return await getServerSettings(domain, serverConf.ignoreCerts);
} catch (_) {
if (error === Messages.noOrgsError(domain)) {
throw new Error(error);
}
return serverConf;
}
} else {
throw new Error('Untrusted certificate.');
}
}
}
// ignoreCerts parameter helps in fetching server icon and
// other server details when user chooses to ignore certificate warnings
export async function checkDomain(domain: string, ignoreCerts = false, silent = false): Promise<ServerConf> {
if (!silent && duplicateDomain(domain)) {
// Do not check duplicate in silent mode
throw new Error('This server has been added.');
} }
saveServerIcon(server: any, ignoreCerts = false): Promise<string> { domain = formatUrl(domain);
const url = server.icon;
const domain = server.url;
const serverIconOptions = { const serverConf = {
url, icon: defaultIconUrl,
...RequestUtil.requestOptions(domain, ignoreCerts) url: domain,
}; alias: domain,
ignoreCerts
};
// The save will always succeed. If url is invalid, downgrade to default icon. try {
return new Promise(resolve => { return await getServerSettings(domain, serverConf.ignoreCerts);
const filePath = this.generateFilePath(url); } catch (err) {
const file = fs.createWriteStream(filePath); // Make sure that error is an error or string not undefined
try { // so validation does not throw error.
request(serverIconOptions).on('response', (response: any) => { const error = err || '';
response.on('error', (err: string) => {
logger.log('Could not get server icon.'); const certsError = error.toString().includes('certificate');
logger.log(err); if (certsError) {
logger.reportSentry(err); const result = await checkCertError(domain, serverConf, error, silent);
resolve(defaultIconUrl); return result;
} else {
throw new Error(Messages.invalidZulipServerError(domain));
}
}
}
async function getServerSettings(domain: string, ignoreCerts = false): Promise<ServerConf> {
const serverSettingsOptions = {
url: domain + '/api/v1/server_settings',
...RequestUtil.requestOptions(domain, ignoreCerts)
};
return new Promise((resolve, reject) => {
request(serverSettingsOptions, (error: string, response: any) => {
if (!error && response.statusCode === 200) {
const data = JSON.parse(response.body);
if (Object.prototype.hasOwnProperty.call(data, 'realm_icon') && data.realm_icon) {
resolve({
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
url: data.realm_uri,
alias: escape(data.realm_name),
ignoreCerts
}); });
response.pipe(file).on('finish', () => { } else {
resolve(filePath); reject(Messages.noOrgsError(domain));
}); }
}).on('error', (err: string) => { } else {
reject(response);
}
});
});
}
export async function saveServerIcon(server: ServerConf, ignoreCerts = false): Promise<string> {
const url = server.icon;
const domain = server.url;
const serverIconOptions = {
url,
...RequestUtil.requestOptions(domain, ignoreCerts)
};
// The save will always succeed. If url is invalid, downgrade to default icon.
return new Promise(resolve => {
const filePath = generateFilePath(url);
const file = fs.createWriteStream(filePath);
try {
request(serverIconOptions).on('response', (response: any) => {
response.on('error', (err: string) => {
logger.log('Could not get server icon.'); logger.log('Could not get server icon.');
logger.log(err); logger.log(err);
logger.reportSentry(err); logger.reportSentry(err);
resolve(defaultIconUrl); resolve(defaultIconUrl);
}); });
} catch (err) { response.pipe(file).on('finish', () => {
resolve(filePath);
});
}).on('error', (err: string) => {
logger.log('Could not get server icon.'); logger.log('Could not get server icon.');
logger.log(err); logger.log(err);
logger.reportSentry(err); logger.reportSentry(err);
resolve(defaultIconUrl); resolve(defaultIconUrl);
}
});
}
updateSavedServer(url: string, index: number): void {
// Does not promise successful update
const { ignoreCerts } = this.getDomain(index);
this.checkDomain(url, ignoreCerts, true).then(newServerConf => {
this.saveServerIcon(newServerConf, ignoreCerts).then(localIconUrl => {
newServerConf.icon = localIconUrl;
this.updateDomain(index, newServerConf);
this.reloadDB();
}); });
});
}
reloadDB(): void {
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
try {
const file = fs.readFileSync(domainJsonPath, 'utf8');
JSON.parse(file);
} catch (err) { } catch (err) {
if (fs.existsSync(domainJsonPath)) { logger.log('Could not get server icon.');
fs.unlinkSync(domainJsonPath); logger.log(err);
dialog.showErrorBox( logger.reportSentry(err);
'Error saving new organization', resolve(defaultIconUrl);
'There seems to be error while saving new organization, ' +
'you may have to re-add your previous organizations back.'
);
logger.error('Error while JSON parsing domain.json: ');
logger.error(err);
logger.reportSentry(err);
}
} }
this.db = new JsonDB(domainJsonPath, true, true); });
} }
generateFilePath(url: string): string { export async function updateSavedServer(url: string, index: number): Promise<void> {
const dir = `${app.getPath('userData')}/server-icons`; // Does not promise successful update
const extension = path.extname(url).split('?')[0]; const oldIcon = getDomain(index).icon;
const { ignoreCerts } = getDomain(index);
let hash = 5381; try {
let len = url.length; const newServerConf = await checkDomain(url, ignoreCerts, true);
const localIconUrl = await saveServerIcon(newServerConf, ignoreCerts);
while (len) { if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') {
hash = (hash * 33) ^ url.charCodeAt(--len); newServerConf.icon = localIconUrl;
} updateDomain(index, newServerConf);
reloadDB();
// Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return `${dir}/${hash >>> 0}${extension}`;
}
formatUrl(domain: any): string {
const hasPrefix = (domain.indexOf('http') === 0);
if (hasPrefix) {
return domain;
} else {
return (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`;
} }
} catch (err) {
logger.log('Could not update server icon.');
logger.log(err);
logger.reportSentry(err);
} }
} }
export = new DomainUtil(); export function reloadDB(): void {
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
try {
const file = fs.readFileSync(domainJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(domainJsonPath)) {
fs.unlinkSync(domainJsonPath);
dialog.showErrorBox(
'Error saving new organization',
'There seems to be error while saving new organization, ' +
'you may have to re-add your previous organizations back.'
);
logger.error('Error while JSON parsing domain.json: ');
logger.error(err);
logger.reportSentry(err);
}
}
db = new JsonDB(domainJsonPath, true, true);
}
function generateFilePath(url: string): string {
const dir = `${app.getPath('userData')}/server-icons`;
const extension = path.extname(url).split('?')[0];
let hash = 5381;
let { length } = url;
while (length) {
hash = (hash * 33) ^ url.charCodeAt(--length);
}
// Create 'server-icons' directory if not existed
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
return `${dir}/${hash >>> 0}${extension}`;
}
export function formatUrl(domain: string): string {
if (domain.startsWith('http://') || domain.startsWith('https://')) {
return domain;
}
if (domain.startsWith('localhost:')) {
return `http://${domain}`;
}
return `https://${domain}`;
}

View File

@@ -0,0 +1,68 @@
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
export let enterpriseSettings: any;
export 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 (err) {
logger.log('Error while JSON parsing global_config.json: ');
logger.log(err);
}
} else {
configFile = false;
}
}
export function getConfigItem(key: string, defaultValue?: any): 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;
for (const org of presetOrgs) {
if (url.includes(org)) {
return true;
}
}
return false;
}

View File

@@ -1,51 +1,45 @@
'use strict'; import { shell } from 'electron';
import escape from 'escape-html';
import fs from 'fs';
import os from 'os';
import path from 'path';
// TODO: TypeScript - Add @types/ export function isUploadsUrl(server: string, url: URL): boolean {
import wurl = require('wurl'); return url.origin === server && url.pathname.startsWith('/user_uploads/');
let instance: null | LinkUtil = null;
interface IsInternalResponse {
isInternalUrl: boolean;
isUploadsUrl: boolean;
} }
class LinkUtil { export function openBrowser(url: URL): void {
constructor() { if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
if (instance) { shell.openExternal(url.href);
return instance; } else {
} else { // For security, indirect links to non-whitelisted protocols
instance = this; // through a real web browser via a local HTML file.
} const dir = fs.mkdtempSync(
path.join(os.tmpdir(), 'zulip-redirect-')
return instance; );
} const file = path.join(dir, 'redirect.html');
fs.writeFileSync(file, `\
isInternal(currentUrl: string, newUrl: string): IsInternalResponse { <!DOCTYPE html>
const currentDomain = wurl('hostname', currentUrl); <html>
const newDomain = wurl('hostname', newUrl); <head>
<meta charset="UTF-8" />
const sameDomainUrl = (currentDomain === newDomain || newUrl === currentUrl + '/'); <meta http-equiv="Refresh" content="0; url=${escape(url.href)}" />
const isUploadsUrl = newUrl.includes(currentUrl + '/user_uploads/'); <title>Redirecting</title>
const isInternalUrl = newUrl.includes('/#narrow') || isUploadsUrl; <style>
html {
return { font-family: menu, "Helvetica Neue", sans-serif;
isInternalUrl: sameDomainUrl && isInternalUrl, }
isUploadsUrl </style>
}; </head>
} <body>
<p>Opening <a href="${escape(url.href)}">${escape(url.href)}</a>…</p>
isImage(url: string): boolean { </body>
// test for images extension as well as urls like .png?s=100 </html>
const isImageUrl = /\.(bmp|gif|jpg|jpeg|png|webp)\?*.*$/i; `);
return isImageUrl.test(url); shell.openItem(file);
} setTimeout(() => {
fs.unlinkSync(file);
isPDF(url: string): boolean { fs.rmdirSync(dir);
// test for pdf extension }, 15000);
const isPDFUrl = /\.(pdf)\?*.*$/i;
return isPDFUrl.test(url);
} }
} }
export = new LinkUtil();

View File

@@ -1,10 +1,9 @@
'use strict'; import { JsonDB } from 'node-json-db';
import JsonDB from 'node-json-db';
import fs = require('fs'); import fs from 'fs';
import path = require('path'); import path from 'path';
import electron = require('electron'); import electron from 'electron';
import Logger = require('./logger-util'); import Logger from './logger-util';
const remote = const remote =
process.type === 'renderer' ? electron.remote : electron; process.type === 'renderer' ? electron.remote : electron;
@@ -16,61 +15,47 @@ const logger = new Logger({
/* To make the util runnable in both main and renderer process */ /* To make the util runnable in both main and renderer process */
const { dialog, app } = remote; const { dialog, app } = remote;
let instance: null | LinuxUpdateUtil = null;
class LinuxUpdateUtil { let db: JsonDB;
db: JsonDB;
constructor() { reloadDB();
if (instance) {
return instance;
} else {
instance = this;
}
this.reloadDB(); export function getUpdateItem(key: string, defaultValue: any = null): any {
return instance; reloadDB();
} const value = db.getData('/')[key];
if (value === undefined) {
getUpdateItem(key: string, defaultValue: any = null): any { setUpdateItem(key, defaultValue);
this.reloadDB(); return defaultValue;
const value = this.db.getData('/')[key]; } else {
if (value === undefined) { return value;
this.setUpdateItem(key, defaultValue);
return defaultValue;
} else {
return value;
}
}
setUpdateItem(key: string, value: any): void {
this.db.push(`/${key}`, value, true);
this.reloadDB();
}
removeUpdateItem(key: string): void {
this.db.delete(`/${key}`);
this.reloadDB();
}
reloadDB(): void {
const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/config/updates.json');
try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
'Error saving update notifications.',
'We encountered an error while saving the update notifications.'
);
logger.error('Error while JSON parsing updates.json: ');
logger.error(err);
}
}
this.db = new JsonDB(linuxUpdateJsonPath, true, true);
} }
} }
export = new LinuxUpdateUtil(); export function setUpdateItem(key: string, value: any): 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 (err) {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
'Error saving update notifications.',
'We encountered an error while saving the update notifications.'
);
logger.error('Error while JSON parsing updates.json: ');
logger.error(err);
}
}
db = new JsonDB(linuxUpdateJsonPath, true, true);
}

View File

@@ -2,10 +2,10 @@ import { Console as NodeConsole } from 'console'; // eslint-disable-line node/pr
import { initSetUp } from './default-util'; import { initSetUp } from './default-util';
import { sentryInit, captureException } from './sentry-util'; import { sentryInit, captureException } from './sentry-util';
import fs = require('fs'); import fs from 'fs';
import os = require('os'); import os from 'os';
import isDev = require('electron-is-dev'); import isDev from 'electron-is-dev';
import electron = require('electron'); import electron from 'electron';
// this interface adds [key: string]: any so // this interface adds [key: string]: any so
// we can do console[type] later on in the code // we can do console[type] later on in the code
interface PatchedConsole extends Console { interface PatchedConsole extends Console {
@@ -13,7 +13,7 @@ interface PatchedConsole extends Console {
} }
interface LoggerOptions { interface LoggerOptions {
timestamp?: any; timestamp?: true | (() => string);
file?: string; file?: string;
level?: boolean; level?: boolean;
logInDevMode?: boolean; logInDevMode?: boolean;
@@ -44,20 +44,20 @@ if (process.type === 'renderer') {
const browserConsole: PatchedConsole = console; const browserConsole: PatchedConsole = console;
const logDir = `${app.getPath('userData')}/Logs`; const logDir = `${app.getPath('userData')}/Logs`;
class Logger { export default class Logger {
nodeConsole: PatchedConsole; nodeConsole: PatchedConsole;
timestamp: any; // TODO: TypeScript - Figure out how to make this work with string | Function. timestamp?: () => string;
level: boolean; level: boolean;
logInDevMode: boolean; logInDevMode: boolean;
[key: string]: any; [key: string]: any;
constructor(opts: LoggerOptions = {}) { constructor(options: LoggerOptions = {}) {
let { let {
timestamp = true, timestamp = true,
file = 'console.log', file = 'console.log',
level = true, level = true,
logInDevMode = false logInDevMode = false
} = opts; } = options;
file = `${logDir}/${file}`; file = `${logDir}/${file}`;
if (timestamp === true) { if (timestamp === true) {
@@ -92,7 +92,7 @@ class Logger {
case typeof timestamp === 'function': case typeof timestamp === 'function':
args.unshift(timestamp() + ' |\t'); args.unshift(timestamp() + ' |\t');
case (level !== false): case (level):
args.unshift(type.toUpperCase() + ' |'); args.unshift(type.toUpperCase() + ' |');
case isDev || logInDevMode: case isDev || logInDevMode:
@@ -107,7 +107,7 @@ class Logger {
} }
setUpConsole(): void { setUpConsole(): void {
for (const type in browserConsole) { for (const type of Object.keys(browserConsole)) {
this.setupConsoleMethod(type); this.setupConsoleMethod(type);
} }
} }
@@ -151,5 +151,3 @@ class Logger {
}); });
} }
} }
export = Logger;

View File

@@ -1,11 +0,0 @@
// This util function returns the page params if they're present else returns null
export function isPageParams(): null | object {
let webpageParams = null;
try {
// eslint-disable-next-line no-undef, @typescript-eslint/camelcase
webpageParams = page_params;
} catch (_) {
webpageParams = null;
}
return webpageParams;
}

View File

@@ -1,142 +1,118 @@
'use strict'; import * as ConfigUtil from './config-util';
import url = require('url'); export interface ProxyRule {
import ConfigUtil = require('./config-util');
let instance: null | ProxyUtil = null;
interface ProxyRule {
hostname?: string; hostname?: string;
port?: number; port?: number;
} }
class ProxyUtil { // Return proxy to be used for a particular uri, to be used for request
constructor() { export function getProxy(_uri: string): ProxyRule | void {
if (instance) { let uri;
return instance; try {
} else { uri = new URL(_uri);
instance = this; } catch (err) {
} return;
return instance;
} }
// Return proxy to be used for a particular uri, to be used for request const proxyRules = ConfigUtil.getConfigItem('proxyRules', '').split(';');
getProxy(_uri: string): ProxyRule | void { // If SPS is on and system uses no proxy then request should not try to use proxy from
const parsedUri = url.parse(_uri); // environment. NO_PROXY = '*' makes request ignore all environment proxy variables.
if (parsedUri === null) { if (proxyRules[0] === '') {
return; process.env.NO_PROXY = '*';
} return;
const uri = parsedUri;
const proxyRules = ConfigUtil.getConfigItem('proxyRules', '').split(';');
// If SPS is on and system uses no proxy then request should not try to use proxy from
// environment. NO_PROXY = '*' makes request ignore all environment proxy variables.
if (proxyRules[0] === '') {
process.env.NO_PROXY = '*';
return;
}
const proxyRule: any = {};
if (uri.protocol === 'http:') {
proxyRules.forEach((proxy: string) => {
if (proxy.includes('http=')) {
proxyRule.hostname = proxy.split('http=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('http=')[1].trim().split(':')[1];
}
});
return proxyRule;
}
if (uri.protocol === 'https:') {
proxyRules.forEach((proxy: string) => {
if (proxy.includes('https=')) {
proxyRule.hostname = proxy.split('https=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('https=')[1].trim().split(':')[1];
}
});
return proxyRule;
}
} }
// TODO: Refactor to async function const proxyRule: any = {};
resolveSystemProxy(mainWindow: Electron.BrowserWindow): void { if (uri.protocol === 'http:') {
const page = mainWindow.webContents; proxyRules.forEach((proxy: string) => {
const ses = page.session; if (proxy.includes('http=')) {
const resolveProxyUrl = 'www.example.com'; proxyRule.hostname = proxy.split('http=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('http=')[1].trim().split(':')[1];
// Check HTTP Proxy
const httpProxy = new Promise(resolve => {
ses.resolveProxy('http://' + resolveProxyUrl, (proxy: string) => {
let httpString = '';
if (proxy !== 'DIRECT') {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
httpString = 'http=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(httpString);
});
});
// Check HTTPS Proxy
const httpsProxy = new Promise(resolve => {
ses.resolveProxy('https://' + resolveProxyUrl, (proxy: string) => {
let httpsString = '';
if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
httpsString += 'https=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(httpsString);
});
});
// Check FTP Proxy
const ftpProxy = new Promise(resolve => {
ses.resolveProxy('ftp://' + resolveProxyUrl, (proxy: string) => {
let ftpString = '';
if (proxy !== 'DIRECT') {
if (proxy.includes('PROXY')) {
ftpString += 'ftp=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(ftpString);
});
});
// Check SOCKS Proxy
const socksProxy = new Promise(resolve => {
ses.resolveProxy('socks4://' + resolveProxyUrl, (proxy: string) => {
let socksString = '';
if (proxy !== 'DIRECT') {
if (proxy.includes('SOCKS5')) {
socksString += 'socks=' + proxy.split('SOCKS5')[1] + ';';
} else if (proxy.includes('SOCKS4')) {
socksString += 'socks=' + proxy.split('SOCKS4')[1] + ';';
} else if (proxy.includes('PROXY')) {
socksString += 'socks=' + proxy.split('PROXY')[1] + ';';
}
}
resolve(socksString);
});
});
Promise.all([httpProxy, httpsProxy, ftpProxy, socksProxy]).then(values => {
let proxyString = '';
values.forEach(proxy => {
proxyString += proxy;
});
ConfigUtil.setConfigItem('systemProxyRules', proxyString);
const useSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
if (useSystemProxy) {
ConfigUtil.setConfigItem('proxyRules', proxyString);
} }
}); });
return proxyRule;
}
if (uri.protocol === 'https:') {
proxyRules.forEach((proxy: string) => {
if (proxy.includes('https=')) {
proxyRule.hostname = proxy.split('https=')[1].trim().split(':')[0];
proxyRule.port = proxy.split('https=')[1].trim().split(':')[1];
}
});
return proxyRule;
} }
} }
export = new ProxyUtil(); // 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,67 +1,88 @@
import isOnline = require('is-online'); import { ipcRenderer } from 'electron';
import Logger = require('./logger-util');
import type WebView from '../components/webview';
import backoff from 'backoff';
import request from 'request';
import Logger from './logger-util';
import * as RequestUtil from './request-util';
import * as DomainUtil from './domain-util';
const logger = new Logger({ const logger = new Logger({
file: `domain-util.log`, file: 'domain-util.log',
timestamp: true timestamp: true
}); });
class ReconnectUtil { export default class ReconnectUtil {
// TODO: TypeScript - Figure out how to annotate serverManagerView webview: WebView;
// it should be ServerManagerView; maybe make it a generic so we can url: string;
// pass the class from main.js
serverManagerView: any;
alreadyReloaded: boolean; alreadyReloaded: boolean;
fibonacciBackoff: backoff.Backoff;
constructor(serverManagerView: any) { constructor(webview: WebView) {
this.serverManagerView = serverManagerView; this.webview = webview;
this.url = webview.props.url;
this.alreadyReloaded = false; this.alreadyReloaded = false;
this.clearState();
} }
clearState(): void { clearState(): void {
this.alreadyReloaded = false; this.fibonacciBackoff = backoff.fibonacci({
initialDelay: 5000,
maxDelay: 300000
});
} }
pollInternetAndReload(): void { async isOnline(): Promise<boolean> {
const pollInterval = setInterval(() => {
this._checkAndReload()
.then(status => {
if (status) {
this.alreadyReloaded = true;
clearInterval(pollInterval);
}
});
}, 1500);
}
// TODO: Make this a async function
_checkAndReload(): Promise<boolean> {
return new Promise(resolve => { return new Promise(resolve => {
if (!this.alreadyReloaded) { // eslint-disable-line no-negated-condition try {
isOnline() const ignoreCerts = DomainUtil.shouldIgnoreCerts(this.url);
.then((online: boolean) => { if (ignoreCerts === null) {
if (online) { return;
if (!this.alreadyReloaded) { }
this.serverManagerView.reloadCurrentView(); request(
} {
logger.log('You\'re back online.'); url: `${this.url}/static/favicon.ico`,
return resolve(true); ...RequestUtil.requestOptions(this.url, ignoreCerts)
} },
(error: Error, response: any) => {
logger.log('There is no internet connection, try checking network cables, modem and router.'); const isValidResponse =
const errMsgHolder = document.querySelector('#description'); !error && response.statusCode >= 200 && response.statusCode < 400;
if (errMsgHolder) { resolve(isValidResponse);
errMsgHolder.innerHTML = ` }
<div>Your internet connection doesn't seem to work properly!</div> );
<div>Verify that it works and then click try again.</div>`; } catch (err) {
} logger.log(err);
return resolve(false);
});
} else {
return resolve(true);
} }
}); });
} }
}
export = ReconnectUtil; 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;
}
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 = `
<div>Your internet connection doesn't seem to work properly!</div>
<div>Verify that it works and then click try again.</div>`;
}
return false;
}
}

View File

@@ -1,73 +1,70 @@
import fs = require('fs'); import { remote } from 'electron';
import ConfigUtil = require('./config-util');
import Logger = require('./logger-util'); import fs from 'fs';
import ProxyUtil = require('./proxy-util'); import path from 'path';
import CertificateUtil = require('./certificate-util'); import * as ConfigUtil from './config-util';
import SystemUtil = require('./system-util'); import Logger from './logger-util';
import * as ProxyUtil from './proxy-util';
import * as CertificateUtil from './certificate-util';
import * as SystemUtil from './system-util';
const { app } = remote;
const logger = new Logger({ const logger = new Logger({
file: `request-util.log`, file: 'request-util.log',
timestamp: true timestamp: true
}); });
let instance: null | RequestUtil = null;
// TODO: TypeScript - Use ProxyRule for the proxy property
// we can do this now since we use export = ProxyUtil syntax
interface RequestUtilResponse { interface RequestUtilResponse {
ca: string; ca: string;
proxy: string | void | object; proxy: string | void | ProxyUtil.ProxyRule;
ecdhCurve: 'auto'; ecdhCurve: 'auto';
headers: { 'User-Agent': string }; headers: { 'User-Agent': string };
rejectUnauthorized: boolean; rejectUnauthorized: boolean;
} }
class RequestUtil { // ignoreCerts parameter helps in fetching server icon and
constructor() { // other server details when user chooses to ignore certificate warnings
if (!instance) { export function requestOptions(domain: string, ignoreCerts: boolean): RequestUtilResponse {
instance = this; domain = formatUrl(domain);
} const certificate = CertificateUtil.getCertificate(
encodeURIComponent(domain)
);
return instance; let certificateFile = null;
if (certificate?.includes('/')) {
// certificate saved using old app version
certificateFile = certificate;
} else if (certificate) {
certificateFile = path.join(`${app.getPath('userData')}/certificates`, certificate);
} }
// ignoreCerts parameter helps in fetching server icon and let certificateLocation = '';
// other server details when user chooses to ignore certificate warnings if (certificate) {
requestOptions(domain: string, ignoreCerts: boolean): RequestUtilResponse { // To handle case where certificate has been moved from the location in certificates.json
domain = this.formatUrl(domain); try {
const certificate = CertificateUtil.getCertificate( certificateLocation = fs.readFileSync(certificateFile, 'utf8');
encodeURIComponent(domain) } catch (err) {
); logger.warn(`Error while trying to get certificate: ${err}`);
let certificateLocation = '';
if (certificate) {
// To handle case where certificate has been moved from the location in certificates.json
try {
certificateLocation = fs.readFileSync(certificate, 'utf8');
} catch (err) {
logger.warn(`Error while trying to get certificate: ${err}`);
}
}
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
// If certificate for the domain exists add it as a ca key in the request's parameter else consider only domain as the parameter for request
// Add proxy as a parameter if it is being used.
return {
ca: certificateLocation ? certificateLocation : '',
proxy: proxyEnabled ? ProxyUtil.getProxy(domain) : '',
ecdhCurve: 'auto',
headers: { 'User-Agent': SystemUtil.getUserAgent() },
rejectUnauthorized: !ignoreCerts
};
}
formatUrl(domain: string): string {
const hasPrefix = domain.startsWith('http', 0);
if (hasPrefix) {
return domain;
} else {
return domain.includes('localhost:') ? `http://${domain}` : `https://${domain}`;
} }
} }
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
// If certificate for the domain exists add it as a ca key in the request's parameter else consider only domain as the parameter for request
// Add proxy as a parameter if it is being used.
return {
ca: certificateLocation ? certificateLocation : '',
proxy: proxyEnabled ? ProxyUtil.getProxy(domain) : '',
ecdhCurve: 'auto',
headers: { 'User-Agent': SystemUtil.getUserAgent() },
rejectUnauthorized: !ignoreCerts
};
} }
const requestUtil = new RequestUtil(); function formatUrl(domain: string): string {
export = requestUtil; const hasPrefix = domain.startsWith('http', 0);
if (hasPrefix) {
return domain;
} else {
return domain.includes('localhost:') ? `http://${domain}` : `https://${domain}`;
}
}

View File

@@ -1,14 +1,11 @@
import { init } from '@sentry/electron'; import { init } from '@sentry/electron';
import isDev = require('electron-is-dev'); import isDev from 'electron-is-dev';
import path = require('path');
import dotenv = require('dotenv');
dotenv.config({ path: path.resolve(__dirname, '/../../../../.env') });
export const sentryInit = (): void => { export const sentryInit = (): void => {
if (!isDev) { if (!isDev) {
init({ init({
dsn: process.env.SENTRY_DSN, dsn: 'https://628dc2f2864243a08ead72e63f94c7b1@sentry.io/204668',
// We should ignore this error since it's harmless and we know the reason behind this // 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 error mainly comes from the console logs.
// This is a temp solution until Sentry supports disabling the console logs // This is a temp solution until Sentry supports disabling the console logs

View File

@@ -1,60 +1,34 @@
'use strict'; import { ipcRenderer } from 'electron';
import { remote } from 'electron';
import os = require('os'); import os from 'os';
const { app } = remote; export const connectivityERR: string[] = [
let instance: null | SystemUtil = null; 'ERR_INTERNET_DISCONNECTED',
'ERR_PROXY_CONNECTION_FAILED',
'ERR_CONNECTION_RESET',
'ERR_NOT_CONNECTED',
'ERR_NAME_NOT_RESOLVED',
'ERR_NETWORK_CHANGED'
];
class SystemUtil { const userAgent = ipcRenderer.sendSync('fetch-user-agent');
connectivityERR: string[];
userAgent: string | null; export function getOS(): string {
const platform = os.platform();
constructor() { if (platform === 'darwin') {
if (instance) { return 'Mac';
return instance; } else if (platform === 'linux') {
return 'Linux';
} else if (platform === 'win32') {
if (parseFloat(os.release()) < 6.2) {
return 'Windows 7';
} else { } else {
instance = this; return 'Windows 10';
} }
} else {
this.connectivityERR = [ return '';
'ERR_INTERNET_DISCONNECTED',
'ERR_PROXY_CONNECTION_FAILED',
'ERR_CONNECTION_RESET',
'ERR_NOT_CONNECTED',
'ERR_NAME_NOT_RESOLVED',
'ERR_NETWORK_CHANGED'
];
this.userAgent = null;
return instance;
}
getOS(): string {
const platform = os.platform();
if (platform === 'darwin') {
return 'Mac';
} else if (platform === 'linux') {
return 'Linux';
} else if (platform === 'win32') {
if (parseFloat(os.release()) < 6.2) {
return 'Windows 7';
} else {
return 'Windows 10';
}
} else {
return '';
}
}
setUserAgent(webViewUserAgent: string): void {
this.userAgent = `ZulipElectron/${app.getVersion()} ${webViewUserAgent}`;
}
getUserAgent(): string | null {
return this.userAgent;
} }
} }
export function getUserAgent(): string {
export = new SystemUtil(); return userAgent;
}

View File

@@ -0,0 +1,15 @@
import path from 'path';
import i18n from 'i18n';
import * as ConfigUtil from './config-util';
i18n.configure({
directory: path.join(__dirname, '../../../translations/')
});
/* fetches the current appLocale from settings.json */
const appLocale = ConfigUtil.getConfigItem('appLanguage');
/* if no locale present in the json, en is set default */
export function __(phrase: string): string {
return i18n.__({ phrase, locale: appLocale ? appLocale : 'en' });
}

View File

@@ -32,7 +32,7 @@
<i class="material-icons md-48">refresh</i> <i class="material-icons md-48">refresh</i>
<span id="reload-tooltip" style="display:none">Reload</span> <span id="reload-tooltip" style="display:none">Reload</span>
</div> </div>
<div class="action-button" id="loading-action"> <div class="action-button disable" id="loading-action">
<i class="refresh material-icons md-48">loop</i> <i class="refresh material-icons md-48">loop</i>
<span id="loading-tooltip" style="display:none">Loading</span> <span id="loading-tooltip" style="display:none">Loading</span>
</div> </div>

View File

@@ -9,14 +9,18 @@
<body> <body>
<div id="content"> <div id="content">
<div id="picture"><img src="img/zulip_network.png"></div> <div id="picture"><img src="img/zulip_network.png"></div>
<div id="title">Zulip can't connect</div> <div id="title">We can't connect to this organization</div>
<div id="description"> <div id="subtitle">This could be because</div>
<div>Your computer seems to be offline.</div> <ul id="description">
<div>We will keep trying to reconnect, or you can try now.</div> <li>You're not online or your proxy is misconfigured.</li>
<li>There is no Zulip organization hosted at this URL.</li>
<li>This Zulip organization is temporarily unavailable.</li>
<li>This Zulip organization has been moved or deleted.</li>
</ul>
<div id="buttons">
<div id="reconnect" class="button">Reconnect</div>
<div id="settings" class="button">Settings</div>
</div> </div>
<div id="reconnect">Try now</div>
</div> </div>
</body> </body>
<script src="js/pages/network.js"></script>
<script>require('./js/shared/preventdrag.js')</script>
</html> </html>

Binary file not shown.

View File

@@ -1,27 +1,48 @@
class Messages { interface DialogBoxError {
invalidZulipServerError(domain: string): string { title: string;
return `${domain} does not appear to be a valid Zulip server. Make sure that content: string;
\n • You can connect to that URL in a web browser.\
\n • If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.\
\n • It's a Zulip server. (The oldest supported version is 1.6).\
\n • The server has a valid certificate. (You can add custom certificates in Settings > Organizations).`;
}
noOrgsError(domain: string): string {
return `${domain} does not have any organizations added.\
\nPlease contact your server administrator.`;
}
certErrorMessage(domain: string, error: string): string {
return `Do you trust certificate from ${domain}? \n ${error}`;
}
certErrorDetail(): string {
return `The organization you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
\nIf you have a valid certificate please add it from Settings>Organizations and try to add the organization again.
\nUnless you have a good reason to believe otherwise, you should not proceed.
\nYou can click here if you'd like to proceed with the connection.`;
}
} }
export = new Messages(); export function invalidZulipServerError(domain: string): string {
return `${domain} does not appear to be a valid Zulip server. Make sure that
\n • You can connect to that URL in a web browser.\
\n • If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.\
\n • It's a Zulip server. (The oldest supported version is 1.6).\
\n • The server has a valid certificate. (You can add custom certificates in Settings > Organizations). \
\n • The SSL is correctly configured for the certificate. Check out the SSL troubleshooting guide -
\n https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`;
}
export function noOrgsError(domain: string): string {
return `${domain} does not have any organizations added.\
\nPlease contact your server administrator.`;
}
export function certErrorMessage(domain: string, error: string): string {
return `Do you trust certificate from ${domain}? \n ${error}`;
}
export function certErrorDetail(): string {
return `The organization you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
\nIf you have a valid certificate please add it from Settings>Organizations and try to add the organization again.
\nUnless you have a good reason to believe otherwise, you should not proceed.
\nYou can click here if you'd like to proceed with the connection.`;
}
export function enterpriseOrgError(length: number, domains: string[]): DialogBoxError {
let domainList = '';
for (const domain of domains) {
domainList += `${domain}\n`;
}
return {
title: `Could not add the following ${length === 1 ? 'organization' : 'organizations'}`,
content: `${domainList}\nPlease contact your system administrator.`
};
}
export function orgRemovalError(url: string): DialogBoxError {
return {
title: `Removing ${url} is a restricted operation.`,
content: 'Please contact your system administrator.'
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 737 B

After

Width:  |  Height:  |  Size: 737 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

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

View File

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

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "O společnosti Zulip", "About Zulip": "O uživateli Zulip",
"Actual Size": "Aktuální velikost", "Actual Size": "Aktuální velikost",
"Add Custom Certificates": "Přidat vlastní certifikáty",
"Add Organization": "Přidat organizaci",
"Add a Zulip organization": "Přidejte organizaci Zulip",
"Add custom CSS": "Přidat vlastní CSS",
"Advanced": "Pokročilý",
"All the connected organizations will appear here": "Zde se zobrazí všechny připojené organizace",
"Always start minimized": "Vždy začněte minimalizovat",
"App Updates": "Aktualizace aplikací",
"Appearance": "Vzhled",
"Application Shortcuts": "Zkratky aplikací",
"Are you sure you want to disconnect this organization?": "Opravdu chcete tuto organizaci odpojit?",
"Auto hide Menu bar": "Auto hide Menu bar",
"Auto hide menu bar (Press Alt key to display)": "Automaticky skrýt lištu menu (stiskněte klávesu Alt pro zobrazení)",
"Back": "Zadní", "Back": "Zadní",
"Bounce dock on new private message": "Bounce dock na nové soukromé zprávy",
"Certificate file": "Soubor certifikátu",
"Change": "Změna",
"Check for Updates": "Kontrola aktualizací",
"Close": "Zavřít", "Close": "Zavřít",
"Connect": "Připojit",
"Connect to another organization": "Připojte se k jiné organizaci",
"Connected organizations": "Připojené organizace",
"Copy": "kopírovat", "Copy": "kopírovat",
"Copy Zulip URL": "Kopírovat adresu URL aplikace Zulip",
"Create a new organization": "Vytvořit novou organizaci",
"Cut": "Střih", "Cut": "Střih",
"Delete": "Vymazat", "Default download location": "Výchozí umístění stahování",
"Delete": "Smazat",
"Desktop App Settings": "Nastavení aplikace Desktop", "Desktop App Settings": "Nastavení aplikace Desktop",
"Desktop Notifications": "Oznámení o pracovní ploše",
"Desktop Settings": "Nastavení plochy",
"Disconnect": "Odpojit",
"Download App Logs": "Stáhnout App Logs",
"Edit": "Upravit", "Edit": "Upravit",
"Edit Shortcuts": "Upravit zkratky",
"Enable auto updates": "Povolit automatické aktualizace",
"Enable error reporting (requires restart)": "Povolit hlášení chyb (vyžaduje restart)",
"Enable spellchecker (requires restart)": "Povolit kontrolu pravopisu (vyžaduje restartování)",
"Factory Reset": "Tovární nastavení",
"File": "Soubor", "File": "Soubor",
"Find accounts": "Najít účty",
"Find accounts by email": "Najít účty e-mailem",
"Flash taskbar on new message": "Hlavní panel Flash na nové zprávě",
"Forward": "Vpřed", "Forward": "Vpřed",
"Functionality": "Funkčnost",
"General": "Všeobecné",
"Get beta updates": "Získejte aktualizace beta",
"Hard Reload": "Hard Reload", "Hard Reload": "Hard Reload",
"Help": "Pomoc", "Help": "Pomoc",
"Help Center": "Centrum nápovědy",
"History": "Dějiny", "History": "Dějiny",
"History Shortcuts": "Zkratky historie",
"Keyboard Shortcuts": "Klávesové zkratky", "Keyboard Shortcuts": "Klávesové zkratky",
"Log Out": "Odhlásit se", "Log Out": "Odhlásit se",
"Minimize": "Minimalizujte", "Log Out of Organization": "Odhlásit se z organizace",
"Manual proxy configuration": "Ruční konfigurace proxy",
"Minimize": "Minimalizovat",
"Mute all sounds from Zulip": "Vypněte všechny zvuky ze Zulipu",
"NO": "NE",
"Network": "Síť",
"OR": "NEBO",
"Organization URL": "URL organizace",
"Organizations": "Organizace",
"Paste": "Vložit", "Paste": "Vložit",
"Paste and Match Style": "Vložit a přizpůsobit styl", "Paste and Match Style": "Vložit a shodit styl",
"Proxy": "Proxy",
"Proxy bypass rules": "Proxy bypass pravidla",
"Proxy rules": "Proxy pravidla",
"Quit": "Přestat", "Quit": "Přestat",
"Quit Zulip": "Ukončete Zulip",
"Redo": "Předělat", "Redo": "Předělat",
"Release Notes": "Poznámky k vydání",
"Reload": "Znovu načíst", "Reload": "Znovu načíst",
"Report an issue...": "Nahlásit problém...", "Report an Issue": "Nahlásit problém",
"Reset App Data": "Resetovat data aplikace",
"Reset App Settings": "Obnovit nastavení aplikace", "Reset App Settings": "Obnovit nastavení aplikace",
"Reset Application Data": "Resetovat data aplikace",
"Save": "Uložit",
"Select All": "Vybrat vše", "Select All": "Vybrat vše",
"Show App Logs": "Zobrazit protokoly aplikace", "Settings": "Nastavení",
"Toggle DevTools for Active Tab": "Přepínač DevTools pro aktivní kartu", "Shortcuts": "Zkratky",
"Toggle DevTools for Zulip App": "Přepnout nástroj DevTools pro aplikaci Zulip App", "Show App Logs": "Zobrazit protokoly aplikací",
"Show app icon in system tray": "Zobrazit ikonu aplikace v systémové liště",
"Show app unread badge": "Zobrazit nepřečtený odznak aplikace",
"Show desktop notifications": "Zobrazit oznámení na ploše",
"Show downloaded files in file manager": "Zobrazit stažené soubory ve správci souborů",
"Show sidebar": "Zobrazit postranní panel",
"Start app at login": "Spuštění aplikace při přihlášení",
"Switch to Next Organization": "Přepnout na další organizaci",
"Switch to Previous Organization": "Přepněte na předchozí organizaci",
"These desktop app shortcuts extend the Zulip webapp's": "Tyto klávesové zkratky pro aplikaci na ploše rozšiřují aplikaci Zulip webapp",
"This will delete all application data including all added accounts and preferences": "Tím odstraníte všechna data aplikace včetně všech přidaných účtů a předvoleb",
"Tip": "Tip",
"Toggle DevTools for Active Tab": "Přepnout DevTools pro Active Tab",
"Toggle DevTools for Zulip App": "Přepnout DevTools pro Zulip App",
"Toggle Do Not Disturb": "Přepnout Do Nerušit",
"Toggle Full Screen": "Přepnout na celou obrazovku", "Toggle Full Screen": "Přepnout na celou obrazovku",
"Toggle Sidebar": "Přepnout postranní panel", "Toggle Sidebar": "Přepnout postranní panel",
"Toggle Tray Icon": "Přepnout ikonu zásobníku", "Toggle Tray Icon": "Přepnout ikonu zásobníku",
"Tools": "Nástroje",
"Undo": "vrátit", "Undo": "vrátit",
"Upload": "nahrát",
"Use system proxy settings (requires restart)": "Použít nastavení proxy serveru (vyžaduje restart)",
"View": "Pohled", "View": "Pohled",
"View Shortcuts": "Zobrazit zástupce",
"Window": "Okno", "Window": "Okno",
"Window Shortcuts": "Klávesové zkratky",
"YES": "ANO",
"Zoom In": "Přiblížit", "Zoom In": "Přiblížit",
"Zoom Out": "Oddálit", "Zoom Out": "Oddálit",
"Zulip Help": "Zulip Nápověda" "Zulip Help": "Nápověda Zulip",
"keyboard shortcuts": "klávesové zkratky",
"script": "skript"
} }

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "Über Zulip", "About Zulip": "Über Zulip",
"Actual Size": "Tatsächliche Größe", "Actual Size": "Tatsächliche Größe",
"Add Custom Certificates": "Benutzerdefinierte Zertifikate hinzufügen",
"Add Organization": "Organisation hinzufügen",
"Add a Zulip organization": "Fügen Sie eine Zulip-Organisation hinzu",
"Add custom CSS": "Fügen Sie benutzerdefiniertes CSS hinzu",
"Advanced": "Fortgeschritten",
"All the connected organizations will appear here": "Alle verbundenen Organisationen werden hier angezeigt",
"Always start minimized": "Immer minimiert anfangen",
"App Updates": "App-Updates",
"Appearance": "Aussehen",
"Application Shortcuts": "Anwendungsverknüpfungen",
"Are you sure you want to disconnect this organization?": "Möchten Sie diese Organisation wirklich trennen?",
"Auto hide Menu bar": "Menüleiste automatisch ausblenden",
"Auto hide menu bar (Press Alt key to display)": "Menüleiste automatisch ausblenden (Drücken Sie die Alt-Taste, um anzuzeigen)",
"Back": "Zurück", "Back": "Zurück",
"Bounce dock on new private message": "Bounce-Dock für neue private Nachricht",
"Certificate file": "Zertifikatsdatei",
"Change": "Veränderung",
"Check for Updates": "Auf Updates prüfen",
"Close": "Schließen", "Close": "Schließen",
"Connect": "Verbinden",
"Connect to another organization": "Stellen Sie eine Verbindung zu einer anderen Organisation her",
"Connected organizations": "Verbundene Organisationen",
"Copy": "Kopieren", "Copy": "Kopieren",
"Copy Zulip URL": "Zulip-URL kopieren",
"Create a new organization": "Erstellen Sie eine neue Organisation",
"Cut": "Schnitt", "Cut": "Schnitt",
"Default download location": "Standard-Download-Speicherort",
"Delete": "Löschen", "Delete": "Löschen",
"Desktop App Settings": "Desktop App Einstellungen", "Desktop App Settings": "Desktop App-Einstellungen",
"Desktop Notifications": "Desktop-Benachrichtigungen",
"Desktop Settings": "Desktop-Einstellungen",
"Disconnect": "Trennen",
"Download App Logs": "App-Protokolle herunterladen",
"Edit": "Bearbeiten", "Edit": "Bearbeiten",
"Edit Shortcuts": "Verknüpfungen bearbeiten",
"Enable auto updates": "Automatische Updates aktivieren",
"Enable error reporting (requires restart)": "Fehlerberichterstattung aktivieren (Neustart erforderlich)",
"Enable spellchecker (requires restart)": "Rechtschreibprüfung aktivieren (Neustart erforderlich)",
"Factory Reset": "Werkseinstellungen zurückgesetzt",
"File": "Datei", "File": "Datei",
"Find accounts": "Konten suchen",
"Find accounts by email": "Finden Sie Konten per E-Mail",
"Flash taskbar on new message": "Flash-Taskleiste bei neuer Nachricht",
"Forward": "Nach vorne", "Forward": "Nach vorne",
"Hard Reload": "Hard Reload", "Functionality": "Funktionalität",
"General": "Allgemeines",
"Get beta updates": "Erhalten Sie Beta-Updates",
"Hard Reload": "Festes Nachladen",
"Help": "Hilfe", "Help": "Hilfe",
"Help Center": "Hilfezentrum",
"History": "Geschichte", "History": "Geschichte",
"History Shortcuts": "Verlaufsverknüpfungen",
"Keyboard Shortcuts": "Tastatürkürzel", "Keyboard Shortcuts": "Tastatürkürzel",
"Log Out": "Ausloggen", "Log Out": "Ausloggen",
"Log Out of Organization": "Aus Organisation ausloggen",
"Manual proxy configuration": "Manuelle Proxy-Konfiguration",
"Minimize": "Minimieren", "Minimize": "Minimieren",
"Mute all sounds from Zulip": "Schaltet alle Sounds von Zulip stumm",
"NO": "NEIN",
"Network": "Netzwerk",
"OR": "ODER",
"Organization URL": "Organisations-URL",
"Organizations": "Organisationen",
"Paste": "Einfügen", "Paste": "Einfügen",
"Paste and Match Style": "Einfügen und Anpassen von Stilen", "Paste and Match Style": "Einfügen und Format anpassen",
"Proxy": "Proxy",
"Proxy bypass rules": "Proxy-Umgehungsregeln",
"Proxy rules": "Proxy-Regeln",
"Quit": "Verlassen", "Quit": "Verlassen",
"Redo": "Wiederholen", "Quit Zulip": "Beenden Sie Zulip",
"Redo": "Redo",
"Release Notes": "Versionshinweise",
"Reload": "Neu laden", "Reload": "Neu laden",
"Report an issue...": "Ein Problem melden...", "Report an Issue": "Ein Problem melden",
"Reset App Data": "App-Daten zurücksetzen",
"Reset App Settings": "App-Einstellungen zurücksetzen", "Reset App Settings": "App-Einstellungen zurücksetzen",
"Reset Application Data": "Anwendungsdaten zurücksetzen",
"Save": "sparen",
"Select All": "Wählen Sie Alle", "Select All": "Wählen Sie Alle",
"Show App Logs": "App-Logs anzeigen", "Settings": "die Einstellungen",
"Toggle DevTools for Active Tab": "Schalten Sie DevTools für Active Tab ein", "Shortcuts": "Verknüpfungen",
"Toggle DevTools for Zulip App": "Umschalt DevTools für Zulip App", "Show App Logs": "App-Protokolle anzeigen",
"Show app icon in system tray": "App-Symbol in der Taskleiste anzeigen",
"Show app unread badge": "Zeige App ungelesenen Badge",
"Show desktop notifications": "Desktop-Benachrichtigungen anzeigen",
"Show downloaded files in file manager": "Heruntergeladene Dateien im Dateimanager anzeigen",
"Show sidebar": "Seitenleiste anzeigen",
"Start app at login": "App beim Login starten",
"Switch to Next Organization": "Wechseln Sie zur nächsten Organisation",
"Switch to Previous Organization": "Zur vorherigen Organisation wechseln",
"These desktop app shortcuts extend the Zulip webapp's": "Diese Desktop-App-Verknüpfungen erweitern die Zulip-Webanwendungen",
"This will delete all application data including all added accounts and preferences": "Dadurch werden alle Anwendungsdaten einschließlich aller hinzugefügten Konten und Einstellungen gelöscht",
"Tip": "Spitze",
"Toggle DevTools for Active Tab": "DevTools für aktive Registerkarte umschalten",
"Toggle DevTools for Zulip App": "DevTools für Zulip App umschalten",
"Toggle Do Not Disturb": "Nicht stören umschalten",
"Toggle Full Screen": "Vollbild umschalten", "Toggle Full Screen": "Vollbild umschalten",
"Toggle Sidebar": "Toggle Seitenleiste", "Toggle Sidebar": "Seitenleiste umschalten",
"Toggle Tray Icon": "Toggle Tray-Symbol", "Toggle Tray Icon": "Taskleistensymbol umschalten",
"Tools": "Werkzeuge",
"Undo": "Rückgängig machen", "Undo": "Rückgängig machen",
"Upload": "Hochladen",
"Use system proxy settings (requires restart)": "System-Proxy-Einstellungen verwenden (Neustart erforderlich)",
"View": "Aussicht", "View": "Aussicht",
"View Shortcuts": "Verknüpfungen anzeigen",
"Window": "Fenster", "Window": "Fenster",
"Window Shortcuts": "Fensterverknüpfungen",
"YES": "JA",
"Zoom In": "Hineinzoomen", "Zoom In": "Hineinzoomen",
"Zoom Out": "Rauszoomen", "Zoom Out": "Rauszoomen",
"Zulip Help": "Zulip Hilfe" "Zulip Help": "Zulip-Hilfe",
"keyboard shortcuts": "Tastatürkürzel",
"script": "Skript"
} }

122
app/translations/en-GB.json Normal file
View File

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

119
app/translations/en-US.json Normal file
View File

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

View File

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

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "Acerca de Zulip", "About Zulip": "Acerca de Zulip",
"Actual Size": "Tamaño real", "Actual Size": "Tamaño real",
"Add Custom Certificates": "Añadir certificados personalizados",
"Add Organization": "Añadir organización",
"Add a Zulip organization": "Añadir una organización Zulip",
"Add custom CSS": "Añadir CSS personalizado",
"Advanced": "Avanzado",
"All the connected organizations will appear here": "Todas las organizaciones conectadas aparecerán aquí.",
"Always start minimized": "Siempre empezar minimizado",
"App Updates": "Actualizaciones de la aplicación",
"Appearance": "Apariencia",
"Application Shortcuts": "Atajos de aplicación",
"Are you sure you want to disconnect this organization?": "¿Estás seguro de que quieres desconectar esta organización?",
"Auto hide Menu bar": "Ocultar automáticamente la barra de menú",
"Auto hide menu bar (Press Alt key to display)": "Ocultar automáticamente la barra de menú (presionar la tecla Alt para mostrar)",
"Back": "Espalda", "Back": "Espalda",
"Close": "Cerca", "Bounce dock on new private message": "Bounce dock en nuevo mensaje privado",
"Certificate file": "Archivo de certificado",
"Change": "Cambio",
"Check for Updates": "Buscar actualizaciones",
"Close": "Cerrar",
"Connect": "Conectar",
"Connect to another organization": "Conectarse a otra organización",
"Connected organizations": "Organizaciones conectadas",
"Copy": "Dupdo", "Copy": "Dupdo",
"Copy Zulip URL": "Copiar URL de Zulip",
"Create a new organization": "Crear una nueva organización",
"Cut": "Cortar", "Cut": "Cortar",
"Default download location": "Ubicación de descarga predeterminada",
"Delete": "Borrar", "Delete": "Borrar",
"Desktop App Settings": "Configuración de la aplicación de escritorio", "Desktop App Settings": "Configuración de la aplicación de escritorio",
"Desktop Notifications": "Notificaciones de escritorio",
"Desktop Settings": "Configuraciones de escritorio",
"Disconnect": "Desconectar",
"Download App Logs": "Descargar App Logs",
"Edit": "Editar", "Edit": "Editar",
"File": "Archivo", "Edit Shortcuts": "Editar accesos directos",
"Enable auto updates": "Habilitar actualizaciones automáticas",
"Enable error reporting (requires restart)": "Habilitar informes de errores (requiere reinicio)",
"Enable spellchecker (requires restart)": "Habilitar el corrector ortográfico (requiere reinicio)",
"Factory Reset": "Restablecimiento de fábrica",
"File": "Expediente",
"Find accounts": "Encontrar cuentas",
"Find accounts by email": "Encuentra cuentas por correo electrónico",
"Flash taskbar on new message": "Flash barra de tareas en nuevo mensaje",
"Forward": "Adelante", "Forward": "Adelante",
"Hard Reload": "Recargar duro", "Functionality": "Funcionalidad",
"General": "General",
"Get beta updates": "Recibe actualizaciones beta",
"Hard Reload": "Recarga dura",
"Help": "Ayuda", "Help": "Ayuda",
"Help Center": "Centro de ayuda",
"History": "Historia", "History": "Historia",
"History Shortcuts": "Atajos de historia",
"Keyboard Shortcuts": "Atajos de teclado", "Keyboard Shortcuts": "Atajos de teclado",
"Log Out": "Cerrar sesión", "Log Out": "Cerrar sesión",
"Log Out of Organization": "Salir de la organización",
"Manual proxy configuration": "Configuración de proxy manual",
"Minimize": "Minimizar", "Minimize": "Minimizar",
"Mute all sounds from Zulip": "Silencia todos los sonidos de Zulip",
"NO": "NO",
"Network": "Red",
"OR": "O",
"Organization URL": "URL de la organización",
"Organizations": "Organizaciones",
"Paste": "Pegar", "Paste": "Pegar",
"Paste and Match Style": "Pegar y combinar estilo", "Paste and Match Style": "Pegar y combinar estilo",
"Proxy": "Apoderado",
"Proxy bypass rules": "Reglas de omisión de proxy",
"Proxy rules": "Reglas de proxy",
"Quit": "Dejar", "Quit": "Dejar",
"Quit Zulip": "Dejar Zulip",
"Redo": "Rehacer", "Redo": "Rehacer",
"Release Notes": "Notas de lanzamiento",
"Reload": "Recargar", "Reload": "Recargar",
"Report an issue...": "Reportar un problema...", "Report an Issue": "Reportar un problema",
"Reset App Settings": "Restablecer configuraciones", "Reset App Data": "Restablecer datos de la aplicación",
"Reset App Settings": "Restablecer la configuración de la aplicación",
"Reset Application Data": "Restablecer datos de aplicación",
"Save": "Salvar",
"Select All": "Seleccionar todo", "Select All": "Seleccionar todo",
"Settings": "Ajustes",
"Shortcuts": "Atajos",
"Show App Logs": "Mostrar registros de aplicaciones", "Show App Logs": "Mostrar registros de aplicaciones",
"Show app icon in system tray": "Mostrar icono de la aplicación en la bandeja del sistema",
"Show app unread badge": "Mostrar la aplicación de placa sin leer",
"Show desktop notifications": "Mostrar notificaciones de escritorio",
"Show downloaded files in file manager": "Mostrar los archivos descargados en el administrador de archivos",
"Show sidebar": "Mostrar barra lateral",
"Start app at login": "Iniciar la aplicación al iniciar sesión",
"Switch to Next Organization": "Cambiar a la siguiente organización",
"Switch to Previous Organization": "Cambiar a la organización anterior",
"These desktop app shortcuts extend the Zulip webapp's": "Estos accesos directos de aplicaciones de escritorio extienden las aplicaciones web de Zulip.",
"This will delete all application data including all added accounts and preferences": "Esto eliminará todos los datos de la aplicación, incluidas todas las cuentas y preferencias agregadas.",
"Tip": "Propina",
"Toggle DevTools for Active Tab": "Alternar DevTools para Active Tab", "Toggle DevTools for Active Tab": "Alternar DevTools para Active Tab",
"Toggle DevTools for Zulip App": "Alternar DevTools para la aplicación Zulip", "Toggle DevTools for Zulip App": "Alternar DevTools para Zulip App",
"Toggle Do Not Disturb": "Alternar No molestar",
"Toggle Full Screen": "Alternar pantalla completa", "Toggle Full Screen": "Alternar pantalla completa",
"Toggle Sidebar": "Alternar barra lateral", "Toggle Sidebar": "Alternar barra lateral",
"Toggle Tray Icon": "Alternar el icono de la bandeja", "Toggle Tray Icon": "Icono de bandeja de palanca",
"Tools": "Herramientas",
"Undo": "Deshacer", "Undo": "Deshacer",
"Upload": "Subir",
"Use system proxy settings (requires restart)": "Usar la configuración del proxy del sistema (requiere reinicio)",
"View": "Ver", "View": "Ver",
"View Shortcuts": "Ver accesos directos",
"Window": "Ventana", "Window": "Ventana",
"Window Shortcuts": "Atajos de ventana",
"YES": "SÍ",
"Zoom In": "Acercarse", "Zoom In": "Acercarse",
"Zoom Out": "Disminuir el zoom", "Zoom Out": "Disminuir el zoom",
"Zulip Help": "Ayuda de Zulip" "Zulip Help": "Ayuda de Zulip",
"keyboard shortcuts": "atajos de teclado",
"script": "guión"
} }

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "À propos de Zulip", "About Zulip": "À propos de Zulip",
"Actual Size": "Taille actuelle", "Actual Size": "Taille actuelle",
"Back": "Arrière", "Add Custom Certificates": "Ajouter des certificats personnalisés",
"Add Organization": "Ajouter une organisation",
"Add a Zulip organization": "Ajouter une organisation Zulip",
"Add custom CSS": "Ajouter un CSS personnalisé",
"Advanced": "Avancée",
"All the connected organizations will appear here": "Toutes les organisations connectées apparaîtront ici",
"Always start minimized": "Toujours commencer minimisé",
"App Updates": "Mises à jour de l'application",
"Appearance": "Apparence",
"Application Shortcuts": "Raccourcis d'application",
"Are you sure you want to disconnect this organization?": "Êtes-vous sûr de vouloir déconnecter cette organisation?",
"Auto hide Menu bar": "Masquer automatiquement la barre de menus",
"Auto hide menu bar (Press Alt key to display)": "Masquer automatiquement la barre de menu (appuyez sur la touche Alt pour afficher)",
"Back": "Retour",
"Bounce dock on new private message": "Bounce Dock sur un nouveau message privé",
"Certificate file": "Dossier de certificat",
"Change": "Changement",
"Check for Updates": "Vérifier les mises à jour",
"Close": "Fermer", "Close": "Fermer",
"Connect": "Relier",
"Connect to another organization": "Se connecter à une autre organisation",
"Connected organizations": "Organisations connectées",
"Copy": "Copie", "Copy": "Copie",
"Copy Zulip URL": "Copier l'URL Zulip",
"Create a new organization": "Créer une nouvelle organisation",
"Cut": "Couper", "Cut": "Couper",
"Default download location": "Emplacement de téléchargement par défaut",
"Delete": "Effacer", "Delete": "Effacer",
"Desktop App Settings": "Paramètres de l'application de bureau", "Desktop App Settings": "Paramètres de l'application de bureau",
"Desktop Notifications": "Notifications de bureau",
"Desktop Settings": "Paramètres du bureau",
"Disconnect": "Déconnecter",
"Download App Logs": "Télécharger les journaux d'application",
"Edit": "modifier", "Edit": "modifier",
"Edit Shortcuts": "Modifier les raccourcis",
"Enable auto updates": "Activer les mises à jour automatiques",
"Enable error reporting (requires restart)": "Activer le signalement des erreurs (nécessite un redémarrage)",
"Enable spellchecker (requires restart)": "Activer le correcteur orthographique (nécessite un redémarrage)",
"Factory Reset": "Retour aux paramètres d'usine",
"File": "Fichier", "File": "Fichier",
"Find accounts": "Trouver des comptes",
"Find accounts by email": "Trouver des comptes par email",
"Flash taskbar on new message": "Barre de tâches Flash sur un nouveau message",
"Forward": "Vers l'avant", "Forward": "Vers l'avant",
"Functionality": "La fonctionnalité",
"General": "Général",
"Get beta updates": "Obtenir les mises à jour bêta",
"Hard Reload": "Rechargement dur", "Hard Reload": "Rechargement dur",
"Help": "Aidez-moi", "Help": "Aidez-moi",
"History": "Histoire", "Help Center": "Centre d'aide",
"History": "L'histoire",
"History Shortcuts": "Raccourcis Histoire",
"Keyboard Shortcuts": "Raccourcis clavier", "Keyboard Shortcuts": "Raccourcis clavier",
"Log Out": "Connectez - Out", "Log Out": "Connectez - Out",
"Log Out of Organization": "Déconnexion de l'organisation",
"Manual proxy configuration": "Configuration manuelle du proxy",
"Minimize": "Minimiser", "Minimize": "Minimiser",
"Mute all sounds from Zulip": "Couper tous les sons de Zulip",
"NO": "NON",
"Network": "Réseau",
"OR": "OU",
"Organization URL": "URL de l'organisation",
"Organizations": "Les organisations",
"Paste": "Coller", "Paste": "Coller",
"Paste and Match Style": "Coller et assortir le style", "Paste and Match Style": "Coller et assortir le style",
"Proxy": "Procuration",
"Proxy bypass rules": "Règles de contournement des procurations",
"Proxy rules": "Règles de procuration",
"Quit": "Quitter", "Quit": "Quitter",
"Quit Zulip": "Quittez Zulip",
"Redo": "Refaire", "Redo": "Refaire",
"Release Notes": "Notes de version",
"Reload": "Recharger", "Reload": "Recharger",
"Report an issue...": "Signaler un problème...", "Report an Issue": "Signaler un problème",
"Reset App Settings": "Réinitialiser les paramètres", "Reset App Data": "Réinitialiser les données de l'application",
"Reset App Settings": "Réinitialiser les paramètres de l'application",
"Reset Application Data": "Réinitialiser les données d'application",
"Save": "sauvegarder",
"Select All": "Tout sélectionner", "Select All": "Tout sélectionner",
"Show App Logs": "Afficher les journaux d'applications", "Settings": "Réglages",
"Shortcuts": "Raccourcis",
"Show App Logs": "Afficher les journaux d'application",
"Show app icon in system tray": "Afficher l'icône de l'application dans la barre d'état système",
"Show app unread badge": "Afficher le badge non lu de l'application",
"Show desktop notifications": "Afficher les notifications du bureau",
"Show downloaded files in file manager": "Afficher les fichiers téléchargés dans le gestionnaire de fichiers",
"Show sidebar": "Afficher la barre latérale",
"Start app at login": "Lancer l'application à la connexion",
"Switch to Next Organization": "Passer à l'organisation suivante",
"Switch to Previous Organization": "Passer à l'organisation précédente",
"These desktop app shortcuts extend the Zulip webapp's": "Ces raccourcis dapplications de bureau étendent les applications Web de Zulip.",
"This will delete all application data including all added accounts and preferences": "Cela supprimera toutes les données de l'application, y compris tous les comptes et préférences ajoutés.",
"Tip": "Pointe",
"Toggle DevTools for Active Tab": "Basculer DevTools pour l'onglet actif", "Toggle DevTools for Active Tab": "Basculer DevTools pour l'onglet actif",
"Toggle DevTools for Zulip App": "Basculer DevTools pour l'application Zulip", "Toggle DevTools for Zulip App": "Basculer DevTools pour Zulip App",
"Toggle Do Not Disturb": "Basculer ne pas déranger",
"Toggle Full Screen": "Basculer en plein écran", "Toggle Full Screen": "Basculer en plein écran",
"Toggle Sidebar": "Basculer la barre latérale", "Toggle Sidebar": "Basculer la barre latérale",
"Toggle Tray Icon": "Toggle Tray Icône", "Toggle Tray Icon": "Basculer l'icône du plateau",
"Tools": "Outils",
"Undo": "annuler", "Undo": "annuler",
"Upload": "Télécharger",
"Use system proxy settings (requires restart)": "Utiliser les paramètres proxy du système (nécessite un redémarrage)",
"View": "Vue", "View": "Vue",
"Window": "Fenêtre", "View Shortcuts": "Afficher les raccourcis",
"Window": "La fenêtre",
"Window Shortcuts": "Raccourcis de la fenêtre",
"YES": "OUI",
"Zoom In": "Agrandir", "Zoom In": "Agrandir",
"Zoom Out": "Dézoomer", "Zoom Out": "Dézoomer",
"Zulip Help": "Aide Zulip" "Zulip Help": "Aide Zulip",
"keyboard shortcuts": "Raccourcis clavier",
"script": "scénario"
} }

View File

@@ -1,39 +1,118 @@
{ {
"About Zulip": "Zulip के बारे में", "About Zulip": "जूलिप के बारे में",
"Actual Size": "वास्तविक आकार", "Actual Size": "वास्तविक आकार",
"Add Custom Certificates": "कस्टम प्रमाण पत्र जोड़ें",
"Add Organization": "संगठन जोड़ें",
"Add a Zulip organization": "एक जूलिप संगठन जोड़ें",
"Add custom CSS": "कस्टम CSS जोड़ें",
"Advanced": "उन्नत",
"All the connected organizations will appear here": "सभी जुड़े संगठन यहां दिखाई देंगे",
"Always start minimized": "हमेशा कम से कम शुरू करें",
"App Updates": "ऐप अपडेट",
"Appearance": "दिखावट",
"Application Shortcuts": "आवेदन शॉर्टकट",
"Are you sure you want to disconnect this organization?": "क्या आप वाकई इस संगठन को डिस्कनेक्ट करना चाहते हैं?",
"Auto hide Menu bar": "ऑटो मेनू मेनू छुपाएँ",
"Auto hide menu bar (Press Alt key to display)": "ऑटो छिपाने मेनू बार (प्रेस Alt कुंजी प्रदर्शित करने के लिए)",
"Back": "वापस", "Back": "वापस",
"Bounce dock on new private message": "नए निजी संदेश पर बाउंस डॉक",
"Certificate file": "प्रमाणपत्र फ़ाइल",
"Change": "परिवर्तन",
"Check for Updates": "अद्यतन के लिए जाँच",
"Close": "बंद करे", "Close": "बंद करे",
"Connect": "जुडिये",
"Connect to another organization": "किसी अन्य संगठन से कनेक्ट करें",
"Connected organizations": "जुड़े हुए संगठन",
"Copy": "प्रतिलिपि", "Copy": "प्रतिलिपि",
"Copy Zulip URL": "Zulip URL को कॉपी करें",
"Create a new organization": "एक नया संगठन बनाएं",
"Cut": "कट गया", "Cut": "कट गया",
"Default download location": "डिफ़ॉल्ट डाउनलोड स्थान",
"Delete": "हटाना", "Delete": "हटाना",
"Desktop App Settings": "डेस्कटॉप ऐप सेटिंग्स", "Desktop App Settings": "डेस्कटॉप ऐप सेटिंग्स",
"Desktop Notifications": "डेस्कटॉप सूचनाएं",
"Desktop Settings": "डेस्कटॉप सेटिंग्स",
"Disconnect": "डिस्कनेक्ट",
"Download App Logs": "ऐप लॉग डाउनलोड करें",
"Edit": "संपादित करें", "Edit": "संपादित करें",
"Edit Shortcuts": "शॉर्टकट संपादित करें",
"Enable auto updates": "ऑटो अपडेट सक्षम करें",
"Enable error reporting (requires restart)": "त्रुटि रिपोर्टिंग सक्षम करें (पुनरारंभ की आवश्यकता है)",
"Enable spellchecker (requires restart)": "वर्तनी जाँचक सक्षम करें (पुनः आरंभ करने की आवश्यकता है)",
"Factory Reset": "नए यंत्र जैसी सेटिंग",
"File": "फ़ाइल", "File": "फ़ाइल",
"Find accounts": "खाते ढूंढे",
"Find accounts by email": "ईमेल द्वारा खाते ढूंढें",
"Flash taskbar on new message": "नए संदेश पर फ्लैश टास्कबार",
"Forward": "आगे", "Forward": "आगे",
"Hard Reload": "कठिन पुन: लोड करें", "Functionality": "कार्यक्षमता",
"General": "सामान्य",
"Get beta updates": "बीटा अपडेट प्राप्त करें",
"Hard Reload": "हार्ड रीलोड",
"Help": "मदद", "Help": "मदद",
"Help Center": "सहायता केंद्र",
"History": "इतिहास", "History": "इतिहास",
"History Shortcuts": "इतिहास शॉर्टकट",
"Keyboard Shortcuts": "कुंजीपटल अल्प मार्ग", "Keyboard Shortcuts": "कुंजीपटल अल्प मार्ग",
"Log Out": "लोग आउट", "Log Out": "लोग आउट",
"Log Out of Organization": "संगठन से बाहर प्रवेश करें",
"Manual proxy configuration": "मैनुअल प्रॉक्सी कॉन्फ़िगरेशन",
"Minimize": "छोटा करना", "Minimize": "छोटा करना",
"Mute all sounds from Zulip": "ज़ूलिप से सभी ध्वनियों को म्यूट करें",
"NO": "नहीं",
"Network": "नेटवर्क",
"OR": "या",
"Organization URL": "संगठन का URL",
"Organizations": "संगठन",
"Paste": "चिपकाएं", "Paste": "चिपकाएं",
"Paste and Match Style": "चिपकाएं और शैली का मिलान करें", "Paste and Match Style": "पेस्ट और मैच स्टाइल",
"Proxy": "प्रतिनिधि",
"Proxy bypass rules": "प्रॉक्सी बायपास नियम",
"Proxy rules": "प्रॉक्सी नियम",
"Quit": "छोड़ना", "Quit": "छोड़ना",
"Quit Zulip": "ज़ुल्फ़ छोड़ो",
"Redo": "फिर से करना", "Redo": "फिर से करना",
"Release Notes": "रिलीज नोट्स",
"Reload": "सीमा से अधिक लादना", "Reload": "सीमा से अधिक लादना",
"Report an issue...": "मामले की रिपोर्ट करें...", "Report an Issue": "मामले की रिपोर्ट करें",
"Reset App Settings": "ऐप ेटिंग्स रीसेट करें", "Reset App Data": "ऐप ेट रीसेट करें",
"Reset App Settings": "ऐप सेटिंग रीसेट करें",
"Reset Application Data": "एप्लिकेशन डेटा रीसेट करें",
"Save": "बचाना",
"Select All": "सभी का चयन करे", "Select All": "सभी का चयन करे",
"Settings": "सेटिंग्स",
"Shortcuts": "शॉर्टकट",
"Show App Logs": "ऐप लॉग दिखाएं", "Show App Logs": "ऐप लॉग दिखाएं",
"Show app icon in system tray": "सिस्टम ट्रे में ऐप आइकन दिखाएं",
"Show app unread badge": "ऐप अपठित बैज दिखाएं",
"Show desktop notifications": "डेस्कटॉप सूचनाएं दिखाएं",
"Show downloaded files in file manager": "फ़ाइल प्रबंधक में डाउनलोड की गई फ़ाइलें दिखाएं",
"Show sidebar": "साइडबार दिखाओ",
"Start app at login": "लॉगिन पर ऐप शुरू करें",
"Switch to Next Organization": "अगला संगठन पर स्विच करें",
"Switch to Previous Organization": "पिछले संगठन पर स्विच करें",
"These desktop app shortcuts extend the Zulip webapp's": "ये डेस्कटॉप ऐप शॉर्टकट Zulip webapp's का विस्तार करते हैं",
"This will delete all application data including all added accounts and preferences": "यह सभी जोड़े गए खातों और वरीयताओं सहित सभी एप्लिकेशन डेटा को हटा देगा",
"Tip": "टिप",
"Toggle DevTools for Active Tab": "सक्रिय टैब के लिए DevTools टॉगल करें", "Toggle DevTools for Active Tab": "सक्रिय टैब के लिए DevTools टॉगल करें",
"Toggle DevTools for Zulip App": "Zulip ऐप के लिए DevTools टॉगल करें", "Toggle DevTools for Zulip App": "Zulip App के लिए DevTools टॉगल करें",
"Toggle Do Not Disturb": "टॉगल डू नॉट डिस्टर्ब",
"Toggle Full Screen": "पूर्णस्क्रीन चालू करें", "Toggle Full Screen": "पूर्णस्क्रीन चालू करें",
"Toggle Sidebar": "साइडबार टॉगल करें", "Toggle Sidebar": "टॉगल साइडबार",
"Toggle Tray Icon": "ट्रे आइकन टॉगल करें", "Toggle Tray Icon": "टॉगल ट्रे आइकन",
"Tools": "उपकरण",
"Undo": "पूर्ववत करें", "Undo": "पूर्ववत करें",
"Upload": "अपलोड",
"Use system proxy settings (requires restart)": "सिस्टम प्रॉक्सी सेटिंग्स का उपयोग करें (पुनः आरंभ करने की आवश्यकता है)",
"View": "राय", "View": "राय",
"View Shortcuts": "शॉर्टकट देखें",
"Window": "खिड़की", "Window": "खिड़की",
"Window Shortcuts": "विंडो शॉर्टकट",
"YES": "हाँ",
"Zoom In": "ज़ूम इन", "Zoom In": "ज़ूम इन",
"Zoom Out": "ज़ूम आउट", "Zoom Out": "ज़ूम आउट",
"Zulip Help": "Zulip सहायता" "Zulip Help": "Zulip मदद",
} "keyboard shortcuts": "कुंजीपटल अल्प मार्ग",
"script": "लिपि",
"AddServer": "AddServer"
}

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "A Zulipról", "About Zulip": "Zulipról",
"Actual Size": "Valódi méret", "Actual Size": "Valódi méret",
"Add Custom Certificates": "Egyéni tanúsítványok hozzáadása",
"Add Organization": "Szervezet hozzáadása",
"Add a Zulip organization": "Adjon hozzá egy Zulip szervezetet",
"Add custom CSS": "Egyéni CSS hozzáadása",
"Advanced": "Fejlett",
"All the connected organizations will appear here": "Itt megjelenik az összes kapcsolódó szervezet",
"Always start minimized": "Mindig kezdjen minimálisra",
"App Updates": "Alkalmazásfrissítések",
"Appearance": "Megjelenés",
"Application Shortcuts": "Alkalmazás parancsikonok",
"Are you sure you want to disconnect this organization?": "Biztosan le szeretné szüntetni ezt a szervezetet?",
"Auto hide Menu bar": "Automatikus elrejtése Menüsor",
"Auto hide menu bar (Press Alt key to display)": "Automatikus elrejtés a menüsorban (nyomja meg az Alt billentyűt a megjelenítéshez)",
"Back": "Hát", "Back": "Hát",
"Bounce dock on new private message": "Ugrás a dokkolót az új privát üzenetre",
"Certificate file": "Tanúsítványfájl",
"Change": "változás",
"Check for Updates": "Frissítések keresése",
"Close": "Bezárás", "Close": "Bezárás",
"Connect": "Csatlakozás",
"Connect to another organization": "Csatlakozás egy másik szervezethez",
"Connected organizations": "Kapcsolódó szervezetek",
"Copy": "Másolat", "Copy": "Másolat",
"Copy Zulip URL": "Zulip URL másolása",
"Create a new organization": "Hozzon létre egy új szervezetet",
"Cut": "Vágott", "Cut": "Vágott",
"Default download location": "Alapértelmezett letöltési hely",
"Delete": "Töröl", "Delete": "Töröl",
"Desktop App Settings": "Asztali alkalmazások beállításai", "Desktop App Settings": "Asztali alkalmazás beállításai",
"Desktop Notifications": "Asztali értesítések",
"Desktop Settings": "Asztali beállítások",
"Disconnect": "szétkapcsol",
"Download App Logs": "Alkalmazásnaplók letöltése",
"Edit": "szerkesztése", "Edit": "szerkesztése",
"Edit Shortcuts": "Gyorsbillentyűk szerkesztése",
"Enable auto updates": "Automatikus frissítések engedélyezése",
"Enable error reporting (requires restart)": "Hibajelentés engedélyezése (újraindítás szükséges)",
"Enable spellchecker (requires restart)": "Helyesírás-ellenőrző engedélyezése (újraindítás szükséges)",
"Factory Reset": "Gyári beállítások visszaállítása",
"File": "fájl", "File": "fájl",
"Find accounts": "Fiókok keresése",
"Find accounts by email": "Fiókok keresése e-mailben",
"Flash taskbar on new message": "Flash-tálca az új üzeneten",
"Forward": "Előre", "Forward": "Előre",
"Functionality": "funkcionalitás",
"General": "Tábornok",
"Get beta updates": "Béta frissítéseket kap",
"Hard Reload": "Hard Reload", "Hard Reload": "Hard Reload",
"Help": "Segítség", "Help": "Segítség",
"Help Center": "Segítség Központ",
"History": "Történelem", "History": "Történelem",
"History Shortcuts": "Előzmények parancsikonjai",
"Keyboard Shortcuts": "Gyorsbillentyűket", "Keyboard Shortcuts": "Gyorsbillentyűket",
"Log Out": "Kijelentkezés", "Log Out": "Kijelentkezés",
"Log Out of Organization": "Jelentkezzen ki a szervezetből",
"Manual proxy configuration": "Manuális proxy konfiguráció",
"Minimize": "Kis méret", "Minimize": "Kis méret",
"Mute all sounds from Zulip": "A Zulip összes hangjának elnémítása",
"NO": "NEM",
"Network": "Hálózat",
"OR": "VAGY",
"Organization URL": "A szervezet URL-címe",
"Organizations": "szervezetek",
"Paste": "Paszta", "Paste": "Paszta",
"Paste and Match Style": "Beillesztés és stílusok illesztése", "Paste and Match Style": "Beillesztés és egyezés stílusa",
"Proxy": "Meghatalmazott",
"Proxy bypass rules": "Proxy bypass szabályok",
"Proxy rules": "Proxy szabályok",
"Quit": "quit", "Quit": "quit",
"Quit Zulip": "Lépjen ki Zulipból",
"Redo": "Újra", "Redo": "Újra",
"Release Notes": "Kiadási megjegyzések",
"Reload": "Reload", "Reload": "Reload",
"Report an issue...": "Hibajelentés ...", "Report an Issue": "Jelentés bejelentése",
"Reset App Settings": "Az alkalmazás beállításainak visszaállítása", "Reset App Data": "Az App Data visszaállítása",
"Reset App Settings": "Alkalmazásbeállítások visszaállítása",
"Reset Application Data": "Alkalmazásadatok visszaállítása",
"Save": "Mentés",
"Select All": "Mindet kiválaszt", "Select All": "Mindet kiválaszt",
"Settings": "Beállítások",
"Shortcuts": "parancsikonok",
"Show App Logs": "Alkalmazásnaplók megjelenítése", "Show App Logs": "Alkalmazásnaplók megjelenítése",
"Toggle DevTools for Active Tab": "A DevTools aktiválása az Aktív laphoz", "Show app icon in system tray": "Az alkalmazás ikonjának megjelenítése a tálcán",
"Toggle DevTools for Zulip App": "Kapcsolja a DevTools-ot a Zulip App-hoz", "Show app unread badge": "Az alkalmazás olvasatlan jelvényének megjelenítése",
"Show desktop notifications": "Az asztali értesítések megjelenítése",
"Show downloaded files in file manager": "A letöltött fájlok megjelenítése a fájlkezelőben",
"Show sidebar": "Az oldalsáv megjelenítése",
"Start app at login": "Indítsa el az alkalmazást bejelentkezéskor",
"Switch to Next Organization": "Váltás a következő szervezetre",
"Switch to Previous Organization": "Váltás az előző szervezetre",
"These desktop app shortcuts extend the Zulip webapp's": "Ezek az asztali alkalmazás gyorsbillentyűk kiterjesztik a Zulip webapp-ot",
"This will delete all application data including all added accounts and preferences": "Ez törli az összes alkalmazásadatot, beleértve az összes hozzáadott fiókot és beállítást",
"Tip": "Tipp",
"Toggle DevTools for Active Tab": "Váltás DevTools az aktív fülre",
"Toggle DevTools for Zulip App": "A DevTools váltása a Zulip App számára",
"Toggle Do Not Disturb": "Váltás Ne zavarja",
"Toggle Full Screen": "Teljes képernyőre váltás", "Toggle Full Screen": "Teljes képernyőre váltás",
"Toggle Sidebar": "Oldalsáv átkapcsolása", "Toggle Sidebar": "Az oldalsáv átkapcsolása",
"Toggle Tray Icon": "Tálcaikon átkapcsolása", "Toggle Tray Icon": "Átváltás a tálca ikonjára",
"Tools": "Eszközök",
"Undo": "kibont", "Undo": "kibont",
"Upload": "Feltöltés",
"Use system proxy settings (requires restart)": "Rendszer proxy beállításainak használata (újraindítás szükséges)",
"View": "Kilátás", "View": "Kilátás",
"View Shortcuts": "Gyorsbillentyűk megtekintése",
"Window": "Ablak", "Window": "Ablak",
"Zoom In": "Nagyítás", "Window Shortcuts": "Ablak gyorsbillentyűk",
"YES": "IGEN",
"Zoom In": "Ráközelíteni",
"Zoom Out": "Kicsinyítés", "Zoom Out": "Kicsinyítés",
"Zulip Help": "Zulip Súgó" "Zulip Help": "Zulip Súgó",
"keyboard shortcuts": "gyorsbillentyűket",
"script": "forgatókönyv"
} }

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "Tentang Zulip", "About Zulip": "Tentang Zulip",
"Actual Size": "Ukuran sebenarnya", "Actual Size": "Ukuran sebenarnya",
"Add Custom Certificates": "Tambahkan Sertifikat Kustom",
"Add Organization": "Tambahkan Organisasi",
"Add a Zulip organization": "Tambahkan organisasi Zulip",
"Add custom CSS": "Tambahkan CSS khusus",
"Advanced": "Maju",
"All the connected organizations will appear here": "Semua organisasi yang terhubung akan muncul di sini",
"Always start minimized": "Selalu mulai diminimalkan",
"App Updates": "Pembaruan Aplikasi",
"Appearance": "Penampilan",
"Application Shortcuts": "Pintasan aplikasi",
"Are you sure you want to disconnect this organization?": "Apakah Anda yakin ingin memutuskan koneksi organisasi ini?",
"Auto hide Menu bar": "Sembunyikan bilah Menu secara otomatis",
"Auto hide menu bar (Press Alt key to display)": "Sembunyikan bilah menu secara otomatis (Tekan tombol Alt untuk menampilkan)",
"Back": "Kembali", "Back": "Kembali",
"Bounce dock on new private message": "Bounce dock pada pesan pribadi baru",
"Certificate file": "File sertifikat",
"Change": "Perubahan",
"Check for Updates": "Periksa Pembaruan",
"Close": "Dekat", "Close": "Dekat",
"Connect": "Menghubungkan",
"Connect to another organization": "Terhubung ke organisasi lain",
"Connected organizations": "Organisasi yang terhubung",
"Copy": "Salinan", "Copy": "Salinan",
"Copy Zulip URL": "Salin URL Zulip",
"Create a new organization": "Buat organisasi baru",
"Cut": "Memotong", "Cut": "Memotong",
"Default download location": "Lokasi unduhan default",
"Delete": "Menghapus", "Delete": "Menghapus",
"Desktop App Settings": "Setelan Aplikasi Desktop", "Desktop App Settings": "Pengaturan Aplikasi Desktop",
"Desktop Notifications": "Pemberitahuan Desktop",
"Desktop Settings": "Pengaturan Desktop",
"Disconnect": "Memutuskan",
"Download App Logs": "Unduh Log Aplikasi",
"Edit": "Edit", "Edit": "Edit",
"Edit Shortcuts": "Edit Pintasan",
"Enable auto updates": "Aktifkan pembaruan otomatis",
"Enable error reporting (requires restart)": "Aktifkan pelaporan kesalahan (membutuhkan restart)",
"Enable spellchecker (requires restart)": "Aktifkan pemeriksa ejaan (memerlukan mulai ulang)",
"Factory Reset": "Reset Pabrik",
"File": "Mengajukan", "File": "Mengajukan",
"Find accounts": "Temukan akun",
"Find accounts by email": "Temukan akun melalui email",
"Flash taskbar on new message": "Taskbar flash pada pesan baru",
"Forward": "Meneruskan", "Forward": "Meneruskan",
"Functionality": "Fungsionalitas",
"General": "Umum",
"Get beta updates": "Dapatkan pembaruan beta",
"Hard Reload": "Hard Reload", "Hard Reload": "Hard Reload",
"Help": "Membantu", "Help": "Membantu",
"Help Center": "Pusat Bantuan",
"History": "Sejarah", "History": "Sejarah",
"Keyboard Shortcuts": "Cara pintas keyboard", "History Shortcuts": "Pintasan Riwayat",
"Keyboard Shortcuts": "Pintasan keyboard",
"Log Out": "Keluar", "Log Out": "Keluar",
"Log Out of Organization": "Keluar dari Organisasi",
"Manual proxy configuration": "Konfigurasi proxy manual",
"Minimize": "Memperkecil", "Minimize": "Memperkecil",
"Mute all sounds from Zulip": "Bungkam semua suara dari Zulip",
"NO": "TIDAK",
"Network": "Jaringan",
"OR": "ATAU",
"Organization URL": "URL organisasi",
"Organizations": "Organisasi",
"Paste": "Pasta", "Paste": "Pasta",
"Paste and Match Style": "Tempel dan Cocokkan Gaya", "Paste and Match Style": "Tempel dan Cocokkan Gaya",
"Proxy": "Proksi",
"Proxy bypass rules": "Aturan memotong proxy",
"Proxy rules": "Aturan proxy",
"Quit": "Berhenti", "Quit": "Berhenti",
"Quit Zulip": "Berhenti Zulip",
"Redo": "Mengulangi", "Redo": "Mengulangi",
"Reload": "Reload", "Release Notes": "Catatan Rilis",
"Report an issue...": "Laporkan masalah ...", "Reload": "Muat ulang",
"Reset App Settings": "Setel ulang Pengaturan Aplikasi", "Report an Issue": "Laporkan sebuah Masalah",
"Reset App Data": "Setel Ulang Data Aplikasi",
"Reset App Settings": "Setel Ulang Pengaturan Aplikasi",
"Reset Application Data": "Reset Data Aplikasi",
"Save": "Menyimpan",
"Select All": "Pilih Semua", "Select All": "Pilih Semua",
"Show App Logs": "Tampilkan log aplikasi", "Settings": "Pengaturan",
"Toggle DevTools for Active Tab": "Toggle DevTools untuk Tab Aktif", "Shortcuts": "Pintasan",
"Toggle DevTools for Zulip App": "Toggle DevTools untuk Aplikasi Zulip", "Show App Logs": "Tampilkan Log Aplikasi",
"Toggle Full Screen": "Toggle Full Screen", "Show app icon in system tray": "Tampilkan ikon aplikasi di baki sistem",
"Toggle Sidebar": "Toggle Sidebar", "Show app unread badge": "Tampilkan lencana aplikasi yang belum dibaca",
"Toggle Tray Icon": "Toggle Tray Icon", "Show desktop notifications": "Tampilkan pemberitahuan desktop",
"Undo": "Membuka", "Show downloaded files in file manager": "Tampilkan file yang diunduh dalam pengelola file",
"Show sidebar": "Tampilkan bilah sisi",
"Start app at login": "Mulai aplikasi saat masuk",
"Switch to Next Organization": "Beralih ke Organisasi Selanjutnya",
"Switch to Previous Organization": "Beralih ke Organisasi Sebelumnya",
"These desktop app shortcuts extend the Zulip webapp's": "Pintasan aplikasi desktop ini memperpanjang aplikasi web Zulip",
"This will delete all application data including all added accounts and preferences": "Ini akan menghapus semua data aplikasi termasuk semua akun dan preferensi yang ditambahkan",
"Tip": "Tip",
"Toggle DevTools for Active Tab": "Beralih DevTools untuk Tab Aktif",
"Toggle DevTools for Zulip App": "Beralih DevTools untuk Aplikasi Zulip",
"Toggle Do Not Disturb": "Toggle Jangan Ganggu",
"Toggle Full Screen": "Matikan Layar Penuh",
"Toggle Sidebar": "Beralih Bilah Samping",
"Toggle Tray Icon": "Beralih Ikon Baki",
"Tools": "Alat",
"Undo": "Batalkan",
"Upload": "Unggah",
"Use system proxy settings (requires restart)": "Gunakan pengaturan proxy sistem (perlu dihidupkan ulang)",
"View": "Melihat", "View": "Melihat",
"View Shortcuts": "Lihat Pintasan",
"Window": "Jendela", "Window": "Jendela",
"Window Shortcuts": "Pintasan Jendela",
"YES": "IYA NIH",
"Zoom In": "Perbesar", "Zoom In": "Perbesar",
"Zoom Out": "Zoom Out", "Zoom Out": "Perkecil",
"Zulip Help": "Zulip Help" "Zulip Help": "Bantuan Zulip",
"keyboard shortcuts": "pintasan keyboard",
"script": "naskah"
} }

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "チューリップについて", "About Zulip": "Zulipについて",
"Actual Size": "実際のサイズ", "Actual Size": "実",
"Add Custom Certificates": "カスタム証明書を追加する",
"Add Organization": "組織を追加",
"Add a Zulip organization": "Zulip組織を追加する",
"Add custom CSS": "カスタムCSSを追加する",
"Advanced": "高度な",
"All the connected organizations will appear here": "すべての関連組織がここに表示されます",
"Always start minimized": "常に最小化して起動",
"App Updates": "アプリのアップデート",
"Appearance": "外観",
"Application Shortcuts": "アプリケーションのショートカット",
"Are you sure you want to disconnect this organization?": "この組織を切断してもよろしいですか?",
"Auto hide Menu bar": "自動非表示メニューバー",
"Auto hide menu bar (Press Alt key to display)": "メニューバーを自動的に隠すAltキーを押して表示",
"Back": "バック", "Back": "バック",
"Bounce dock on new private message": "新しいプライベートメッセージでバウンスドック",
"Certificate file": "証明書ファイル",
"Change": "変化する",
"Check for Updates": "アップデートを確認",
"Close": "閉じる", "Close": "閉じる",
"Copy": "コピー", "Connect": "接続する",
"Connect to another organization": "他の組織に接続する",
"Connected organizations": "関連組織",
"Copy": "コピーする",
"Copy Zulip URL": "Zulip URLをコピー",
"Create a new organization": "新しい組織を作る",
"Cut": "カット", "Cut": "カット",
"Delete": "削除", "Default download location": "デフォルトのダウンロード場所",
"Desktop App Settings": "デスクトップアプリケーションの設定", "Delete": "削除する",
"Edit": "編集", "Desktop App Settings": "デスクトップアプリの設定",
"Desktop Notifications": "デスクトップ通知",
"Desktop Settings": "デスクトップ設定",
"Disconnect": "切断する",
"Download App Logs": "アプリログをダウンロードする",
"Edit": "編集する",
"Edit Shortcuts": "ショートカットを編集する",
"Enable auto updates": "自動更新を有効にする",
"Enable error reporting (requires restart)": "エラー報告を有効にする(再起動が必要)",
"Enable spellchecker (requires restart)": "スペルチェッカーを有効にする(再起動が必要)",
"Factory Reset": "工場リセット",
"File": "ファイル", "File": "ファイル",
"Forward": "フォワード", "Find accounts": "アカウントを探す",
"Find accounts by email": "メールでアカウントを探す",
"Flash taskbar on new message": "新しいメッセージのフラッシュタスクバー",
"Forward": "進む",
"Functionality": "機能性",
"General": "一般的な",
"Get beta updates": "ベータ版のアップデートを入手",
"Hard Reload": "ハードリロード", "Hard Reload": "ハードリロード",
"Help": "助けて", "Help": "助けて",
"Help Center": "ヘルプセンター",
"History": "歴史", "History": "歴史",
"History Shortcuts": "履歴ショートカット",
"Keyboard Shortcuts": "キーボードショートカット", "Keyboard Shortcuts": "キーボードショートカット",
"Log Out": "ログアウト", "Log Out": "ログアウト",
"Minimize": "最小化する", "Log Out of Organization": "組織からログアウトする",
"Manual proxy configuration": "手動プロキシ設定",
"Minimize": "最小化",
"Mute all sounds from Zulip": "Zulipからのすべてのサウンドをミュート",
"NO": "いいえ",
"Network": "ネットワーク",
"OR": "または",
"Organization URL": "組織のURL",
"Organizations": "組織",
"Paste": "ペースト", "Paste": "ペースト",
"Paste and Match Style": "スタイルの貼り付けと一致", "Paste and Match Style": "貼り付けスタイル",
"Proxy": "代理人",
"Proxy bypass rules": "プロキシバイパスルール",
"Proxy rules": "プロキシルール",
"Quit": "終了する", "Quit": "終了する",
"Quit Zulip": "Zulipを終了します",
"Redo": "やり直し", "Redo": "やり直し",
"Release Notes": "リリースノート",
"Reload": "リロード", "Reload": "リロード",
"Report an issue...": "問題を報告する...", "Report an Issue": "問題を報告する",
"Reset App Settings": "アプリの設定をリセットする", "Reset App Data": "アプリデータをリセット",
"Reset App Settings": "アプリ設定をリセット",
"Reset Application Data": "アプリケーションデータのリセット",
"Save": "保存する",
"Select All": "すべて選択", "Select All": "すべて選択",
"Show App Logs": "アプリケーションログを表示する", "Settings": "設定",
"Toggle DevTools for Active Tab": "DevTools for Activeタブを切り替える", "Shortcuts": "ショートカット",
"Toggle DevTools for Zulip App": "Zulip AppのDevToolsの切り替え", "Show App Logs": "アプリログを表示する",
"Show app icon in system tray": "システムトレイにアプリのアイコンを表示する",
"Show app unread badge": "アプリの未読バッジを表示する",
"Show desktop notifications": "デスクトップ通知を表示する",
"Show downloaded files in file manager": "ダウンロードしたファイルをファイルマネージャに表示する",
"Show sidebar": "サイドバーを表示",
"Start app at login": "ログイン時にアプリを起動する",
"Switch to Next Organization": "次の組織に切り替える",
"Switch to Previous Organization": "前の組織に切り替える",
"These desktop app shortcuts extend the Zulip webapp's": "これらのデスクトップアプリのショートカットはZulip Webアプリケーションのショートカットを拡張します。",
"This will delete all application data including all added accounts and preferences": "これにより、追加されたすべてのアカウントと設定を含むすべてのアプリケーションデータが削除されます",
"Tip": "先端",
"Toggle DevTools for Active Tab": "アクティブタブのDevToolsを切り替える",
"Toggle DevTools for Zulip App": "Zulip App用DevToolsの切り替え",
"Toggle Do Not Disturb": "邪魔しないでください",
"Toggle Full Screen": "フルスクリーン切り替え", "Toggle Full Screen": "フルスクリーン切り替え",
"Toggle Sidebar": "サイドバーの切り替え", "Toggle Sidebar": "サイドバーの切り替え",
"Toggle Tray Icon": "トレイアイコン切り替え", "Toggle Tray Icon": "トレイアイコン切り替え",
"Tools": "道具",
"Undo": "元に戻す", "Undo": "元に戻す",
"View": "ビュー", "Upload": "アップロードする",
"Use system proxy settings (requires restart)": "システムプロキシ設定を使用する(再起動が必要)",
"View": "見る",
"View Shortcuts": "ショートカットを見る",
"Window": "窓", "Window": "窓",
"Window Shortcuts": "ウィンドウショートカット",
"YES": "はい",
"Zoom In": "ズームイン", "Zoom In": "ズームイン",
"Zoom Out": "ズームアウトする", "Zoom Out": "ズームアウトする",
"Zulip Help": "チューリップヘルプ" "Zulip Help": "Zulipヘルプ",
"keyboard shortcuts": "キーボードショートカット",
"script": "スクリプト"
} }

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "튤립 소개", "About Zulip": "튤립에 관하여",
"Actual Size": "실제 크기", "Actual Size": "실제 크기",
"Add Custom Certificates": "사용자 지정 인증서 추가",
"Add Organization": "조직 추가",
"Add a Zulip organization": "튤립 조직 추가",
"Add custom CSS": "맞춤 CSS 추가",
"Advanced": "많은",
"All the connected organizations will appear here": "연결된 모든 조직이 여기에 표시됩니다.",
"Always start minimized": "항상 최소화 된 상태로 시작하십시오.",
"App Updates": "앱 업데이트",
"Appearance": "외관",
"Application Shortcuts": "애플리케이션 단축키",
"Are you sure you want to disconnect this organization?": "이 조직의 연결을 해제 하시겠습니까?",
"Auto hide Menu bar": "메뉴 바 자동 숨기기",
"Auto hide menu bar (Press Alt key to display)": "메뉴 바 자동 숨기기 (표시하려면 Alt 키를 누릅니다)",
"Back": "뒤로", "Back": "뒤로",
"Bounce dock on new private message": "새 개인 메시지에 도약",
"Certificate file": "인증서 파일",
"Change": "변화",
"Check for Updates": "업데이트 확인",
"Close": "닫기", "Close": "닫기",
"Connect": "잇다",
"Connect to another organization": "다른 조직에 연결",
"Connected organizations": "연결된 조직",
"Copy": "부", "Copy": "부",
"Copy Zulip URL": "튤립 URL 복사",
"Create a new organization": "새 조직 만들기",
"Cut": "절단", "Cut": "절단",
"Default download location": "기본 다운로드 위치",
"Delete": "지우다", "Delete": "지우다",
"Desktop App Settings": "데스크톱 앱 설정", "Desktop App Settings": "데스크톱 앱 설정",
"Desktop Notifications": "데스크톱 알림",
"Desktop Settings": "데스크톱 설정",
"Disconnect": "연결 끊기",
"Download App Logs": "앱 로그 다운로드",
"Edit": "편집하다", "Edit": "편집하다",
"Edit Shortcuts": "바로 가기 편집",
"Enable auto updates": "자동 업데이트 사용",
"Enable error reporting (requires restart)": "오류보고 사용 (재시작 필요)",
"Enable spellchecker (requires restart)": "맞춤법 검사기 사용 (재시작 필요)",
"Factory Reset": "초기화",
"File": "파일", "File": "파일",
"Find accounts": "계정 찾기",
"Find accounts by email": "이메일로 계정 찾기",
"Flash taskbar on new message": "새 메시지의 Flash 작업 표시 줄",
"Forward": "앞으로", "Forward": "앞으로",
"Functionality": "기능",
"General": "일반",
"Get beta updates": "베타 업데이트 받기",
"Hard Reload": "하드 다시로드", "Hard Reload": "하드 다시로드",
"Help": "도움", "Help": "도움",
"Help Center": "지원 센터",
"History": "역사", "History": "역사",
"History Shortcuts": "연혁 단축키",
"Keyboard Shortcuts": "키보드 단축키", "Keyboard Shortcuts": "키보드 단축키",
"Log Out": "로그 아웃", "Log Out": "로그 아웃",
"Log Out of Organization": "조직에서 로그 아웃",
"Manual proxy configuration": "수동 프록시 구성",
"Minimize": "최소화", "Minimize": "최소화",
"Mute all sounds from Zulip": "튤립에서 모든 소리를 음소거합니다.",
"NO": "아니",
"Network": "회로망",
"OR": "또는",
"Organization URL": "조직 URL",
"Organizations": "단체",
"Paste": "풀", "Paste": "풀",
"Paste and Match Style": "붙여 넣기 및 스타일 일치", "Paste and Match Style": "붙여 넣기 및 스타일 일치",
"Proxy": "대리",
"Proxy bypass rules": "프록시 우회 규칙",
"Proxy rules": "프록시 규칙",
"Quit": "떠나다", "Quit": "떠나다",
"Quit Zulip": "튤립을 종료하십시오.",
"Redo": "다시 하다", "Redo": "다시 하다",
"Release Notes": "릴리즈 노트",
"Reload": "다시로드", "Reload": "다시로드",
"Report an issue...": "문제 신고 ...", "Report an Issue": "문제 신고",
"Reset App Data": "앱 데이터 재설정",
"Reset App Settings": "앱 설정 재설정", "Reset App Settings": "앱 설정 재설정",
"Reset Application Data": "응용 프로그램 데이터 재설정",
"Save": "구하다",
"Select All": "모두 선택", "Select All": "모두 선택",
"Settings": "설정",
"Shortcuts": "바로 가기",
"Show App Logs": "앱 로그 표시", "Show App Logs": "앱 로그 표시",
"Show app icon in system tray": "시스템 트레이에 앱 아이콘 표시",
"Show app unread badge": "앱에 읽지 않은 배지 표시",
"Show desktop notifications": "바탕 화면 알림 표시",
"Show downloaded files in file manager": "파일 관리자에서 다운로드 한 파일 표시",
"Show sidebar": "사이드 바 표시",
"Start app at login": "로그인시 앱 시작",
"Switch to Next Organization": "다음 조직으로 전환",
"Switch to Previous Organization": "이전 조직으로 전환",
"These desktop app shortcuts extend the Zulip webapp's": "이러한 데스크톱 앱 바로 가기는 Zulip 웹 앱을 확장합니다.",
"This will delete all application data including all added accounts and preferences": "이렇게하면 추가 된 모든 계정 및 환경 설정을 포함한 모든 응용 프로그램 데이터가 삭제됩니다.",
"Tip": "팁",
"Toggle DevTools for Active Tab": "DevTools for Active Tab 토글", "Toggle DevTools for Active Tab": "DevTools for Active Tab 토글",
"Toggle DevTools for Zulip App": "Zulip App 용 DevTools 토글", "Toggle DevTools for Zulip App": "Zulip App 용 DevTools 토글",
"Toggle Do Not Disturb": "방해 금지 전환",
"Toggle Full Screen": "전체 화면 토글", "Toggle Full Screen": "전체 화면 토글",
"Toggle Sidebar": "사이드 바 전환", "Toggle Sidebar": "사이드 바 전환",
"Toggle Tray Icon": "트레이 아이콘 토글", "Toggle Tray Icon": "트레이 아이콘 토글",
"Tools": "도구들",
"Undo": "끄르다", "Undo": "끄르다",
"Upload": "업로드",
"Use system proxy settings (requires restart)": "시스템 프록시 설정 사용 (다시 시작해야 함)",
"View": "전망", "View": "전망",
"View Shortcuts": "바로 가기보기",
"Window": "창문", "Window": "창문",
"Window Shortcuts": "창 바로 가기",
"YES": "예",
"Zoom In": "확대", "Zoom In": "확대",
"Zoom Out": "축소", "Zoom Out": "축소",
"Zulip Help": "튤립 도움말" "Zulip Help": "튤립 도움말",
"keyboard shortcuts": "키보드 단축키",
"script": "스크립트"
} }

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "സുലിപ്", "About Zulip": "സുലിപ്പിനെക്കുറിച്ച്",
"Actual Size": "യഥാർത്ഥ വലുപ്പം", "Actual Size": "യഥാർത്ഥ വലുപ്പം",
"Add Custom Certificates": "ഇഷ്‌ടാനുസൃത സർട്ടിഫിക്കറ്റുകൾ ചേർക്കുക",
"Add Organization": "ഓർഗനൈസേഷൻ ചേർക്കുക",
"Add a Zulip organization": "ഒരു സുലിപ്പ് ഓർഗനൈസേഷൻ ചേർക്കുക",
"Add custom CSS": "ഇഷ്‌ടാനുസൃത CSS ചേർക്കുക",
"Advanced": "വിപുലമായത്",
"All the connected organizations will appear here": "ബന്ധിപ്പിച്ച എല്ലാ ഓർ‌ഗനൈസേഷനുകളും ഇവിടെ ദൃശ്യമാകും",
"Always start minimized": "എല്ലായ്പ്പോഴും ചെറുതാക്കാൻ ആരംഭിക്കുക",
"App Updates": "അപ്ലിക്കേഷൻ അപ്‌ഡേറ്റുകൾ",
"Appearance": "രൂപം",
"Application Shortcuts": "അപ്ലിക്കേഷൻ കുറുക്കുവഴികൾ",
"Are you sure you want to disconnect this organization?": "ഈ ഓർഗനൈസേഷൻ വിച്ഛേദിക്കാൻ നിങ്ങൾ ആഗ്രഹിക്കുന്നുണ്ടോ?",
"Auto hide Menu bar": "യാന്ത്രികമായി മറയ്‌ക്കുക മെനു ബാർ",
"Auto hide menu bar (Press Alt key to display)": "യാന്ത്രികമായി മറയ്‌ക്കുക മെനു ബാർ (പ്രദർശിപ്പിക്കുന്നതിന് Alt കീ അമർത്തുക)",
"Back": "തിരികെ", "Back": "തിരികെ",
"Close": "അടയ്ക്കുക", "Bounce dock on new private message": "പുതിയ സ്വകാര്യ സന്ദേശത്തിൽ ഡോക്ക് ബൗൺസ് ചെയ്യുക",
"Certificate file": "സർട്ടിഫിക്കറ്റ് ഫയൽ",
"Change": "മാറ്റുക",
"Check for Updates": "അപ്‌ഡേറ്റുകൾക്കായി പരിശോധിക്കുക",
"Close": "അടയ്‌ക്കുക",
"Connect": "ബന്ധിപ്പിക്കുക",
"Connect to another organization": "മറ്റൊരു ഓർഗനൈസേഷനിലേക്ക് കണക്റ്റുചെയ്യുക",
"Connected organizations": "ബന്ധിപ്പിച്ച ഓർഗനൈസേഷനുകൾ",
"Copy": "പകർത്തുക", "Copy": "പകർത്തുക",
"Copy Zulip URL": "Zulip URL പകർത്തുക",
"Create a new organization": "ഒരു പുതിയ ഓർഗനൈസേഷൻ സൃഷ്ടിക്കുക",
"Cut": "മുറിക്കുക", "Cut": "മുറിക്കുക",
"Default download location": "സ്ഥിരസ്ഥിതി ഡ download ൺ‌ലോഡ് സ്ഥാനം",
"Delete": "ഇല്ലാതാക്കുക", "Delete": "ഇല്ലാതാക്കുക",
"Desktop App Settings": "ഡെസ്ക്ടോപ്പ് അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങൾ", "Desktop App Settings": "ഡെസ്ക്ടോപ്പ് അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങൾ",
"Desktop Notifications": "ഡെസ്ക്ടോപ്പ് അറിയിപ്പുകൾ",
"Desktop Settings": "ഡെസ്ക്ടോപ്പ് ക്രമീകരണങ്ങൾ",
"Disconnect": "വിച്ഛേദിക്കുക",
"Download App Logs": "അപ്ലിക്കേഷൻ ലോഗുകൾ ഡൗൺലോഡുചെയ്യുക",
"Edit": "എഡിറ്റുചെയ്യുക", "Edit": "എഡിറ്റുചെയ്യുക",
"Edit Shortcuts": "കുറുക്കുവഴികൾ എഡിറ്റുചെയ്യുക",
"Enable auto updates": "യാന്ത്രിക അപ്‌ഡേറ്റുകൾ പ്രവർത്തനക്ഷമമാക്കുക",
"Enable error reporting (requires restart)": "പിശക് റിപ്പോർട്ടിംഗ് പ്രാപ്തമാക്കുക (പുനരാരംഭിക്കേണ്ടതുണ്ട്)",
"Enable spellchecker (requires restart)": "അക്ഷരത്തെറ്റ് പരിശോധന പ്രാപ്തമാക്കുക (പുനരാരംഭിക്കേണ്ടതുണ്ട്)",
"Factory Reset": "ഫാക്ടറി പുന .സജ്ജമാക്കുക",
"File": "ഫയൽ", "File": "ഫയൽ",
"Forward": "മുന്നോട്ട്", "Find accounts": "അക്കൗണ്ടുകൾ കണ്ടെത്തുക",
"Find accounts by email": "ഇമെയിൽ വഴി അക്കൗണ്ടുകൾ കണ്ടെത്തുക",
"Flash taskbar on new message": "പുതിയ സന്ദേശത്തിൽ ഫ്ലാഷ് ടാസ്‌ക്ബാർ",
"Forward": "ഫോർവേഡ് ചെയ്യുക",
"Functionality": "പ്രവർത്തനം",
"General": "ജനറൽ",
"Get beta updates": "ബീറ്റ അപ്‌ഡേറ്റുകൾ നേടുക",
"Hard Reload": "ഹാർഡ് റീലോഡ്", "Hard Reload": "ഹാർഡ് റീലോഡ്",
"Help": "സഹായിക്കൂ", "Help": "സഹായിക്കൂ",
"Help Center": "സഹായകേന്ദ്രം",
"History": "ചരിത്രം", "History": "ചരിത്രം",
"History Shortcuts": "ചരിത്രം കുറുക്കുവഴികൾ",
"Keyboard Shortcuts": "കീബോർഡ് കുറുക്കുവഴികൾ", "Keyboard Shortcuts": "കീബോർഡ് കുറുക്കുവഴികൾ",
"Log Out": "ലോഗ് ട്ട്", "Log Out": "ലോഗ് .ട്ട് ചെയ്യുക",
"Log Out of Organization": "ഓർഗനൈസേഷനിൽ നിന്ന് പുറത്തുകടക്കുക",
"Manual proxy configuration": "സ്വമേധയാലുള്ള പ്രോക്സി കോൺഫിഗറേഷൻ",
"Minimize": "ചെറുതാക്കുക", "Minimize": "ചെറുതാക്കുക",
"Mute all sounds from Zulip": "സുലിപ്പിൽ നിന്നുള്ള എല്ലാ ശബ്ദങ്ങളും നിശബ്ദമാക്കുക",
"NO": "ഇല്ല",
"Network": "നെറ്റ്‌വർക്ക്",
"OR": "അഥവാ",
"Organization URL": "ഓർ‌ഗനൈസേഷൻ‌ URL",
"Organizations": "ഓർഗനൈസേഷനുകൾ",
"Paste": "പേസ്റ്റ്", "Paste": "പേസ്റ്റ്",
"Paste and Match Style": "ശൈലി ഒട്ടിക്കുകയും പൊരുത്തപ്പെടുത്തുകയും ചെയ്യുക", "Paste and Match Style": "ഒട്ടിച്ച് പൊരുത്തപ്പെടുന്ന ശൈലി",
"Quit": "പുറത്തുകടക്കുക", "Proxy": "പ്രോക്സി",
"Proxy bypass rules": "പ്രോക്സി ബൈപാസ് നിയമങ്ങൾ",
"Proxy rules": "പ്രോക്സി നിയമങ്ങൾ",
"Quit": "ഉപേക്ഷിക്കുക",
"Quit Zulip": "സുലിപ്പ് ഉപേക്ഷിക്കുക",
"Redo": "വീണ്ടും ചെയ്യുക", "Redo": "വീണ്ടും ചെയ്യുക",
"Release Notes": "പ്രകാശന കുറിപ്പുകൾ",
"Reload": "വീണ്ടും ലോഡുചെയ്യുക", "Reload": "വീണ്ടും ലോഡുചെയ്യുക",
"Report an issue...": "ഒരു പ്രശ്നം റിപ്പോർട്ടചെയ്യുക ...", "Report an Issue": "ഒരു പ്രശ്നം റിപ്പോർട്ടചെയ്യുക",
"Reset App Settings": "അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങൾ പുനഃസജ്ജമാക്കുക", "Reset App Data": "അപ്ലിക്കേഷൻ ഡാറ്റ പുന et സജ്ജമാക്കുക",
"Reset App Settings": "അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങൾ പുന et സജ്ജമാക്കുക",
"Reset Application Data": "അപ്ലിക്കേഷൻ ഡാറ്റ പുന et സജ്ജമാക്കുക",
"Save": "രക്ഷിക്കും",
"Select All": "എല്ലാം തിരഞ്ഞെടുക്കുക", "Select All": "എല്ലാം തിരഞ്ഞെടുക്കുക",
"Settings": "ക്രമീകരണങ്ങൾ",
"Shortcuts": "കുറുക്കുവഴികൾ",
"Show App Logs": "അപ്ലിക്കേഷൻ ലോഗുകൾ കാണിക്കുക", "Show App Logs": "അപ്ലിക്കേഷൻ ലോഗുകൾ കാണിക്കുക",
"Show app icon in system tray": "സിസ്റ്റം ട്രേയിൽ അപ്ലിക്കേഷൻ ഐക്കൺ കാണിക്കുക",
"Show app unread badge": "അപ്ലിക്കേഷൻ വായിക്കാത്ത ബാഡ്ജ് കാണിക്കുക",
"Show desktop notifications": "ഡെസ്ക്ടോപ്പ് അറിയിപ്പുകൾ കാണിക്കുക",
"Show downloaded files in file manager": "ഫയൽ മാനേജറിൽ ഡ download ൺലോഡ് ചെയ്ത ഫയലുകൾ കാണിക്കുക",
"Show sidebar": "സൈഡ്‌ബാർ കാണിക്കുക",
"Start app at login": "ലോഗിൻ ചെയ്യുമ്പോൾ അപ്ലിക്കേഷൻ ആരംഭിക്കുക",
"Switch to Next Organization": "അടുത്ത ഓർഗനൈസേഷനിലേക്ക് മാറുക",
"Switch to Previous Organization": "മുമ്പത്തെ ഓർഗനൈസേഷനിലേക്ക് മാറുക",
"These desktop app shortcuts extend the Zulip webapp's": "ഈ ഡെസ്ക്ടോപ്പ് അപ്ലിക്കേഷൻ കുറുക്കുവഴികൾ സുലിപ് വെബ്‌അപ്പിനെ വിപുലീകരിക്കുന്നു",
"This will delete all application data including all added accounts and preferences": "ചേർത്ത എല്ലാ അക്കൗണ്ടുകളും മുൻ‌ഗണനകളും ഉൾപ്പെടെ എല്ലാ ആപ്ലിക്കേഷൻ ഡാറ്റയും ഇത് ഇല്ലാതാക്കും",
"Tip": "നുറുങ്ങ്",
"Toggle DevTools for Active Tab": "സജീവ ടാബിനായി DevTools ടോഗിൾ ചെയ്യുക", "Toggle DevTools for Active Tab": "സജീവ ടാബിനായി DevTools ടോഗിൾ ചെയ്യുക",
"Toggle DevTools for Zulip App": "സുലിപ്പ് ആപ്ലിക്കേഷനായി DevTools ടോഗിൾ ചെയ്യുക", "Toggle DevTools for Zulip App": "Zulip അപ്ലിക്കേഷനായി DevTools ടോഗിൾ ചെയ്യുക",
"Toggle Full Screen": "പൂർണ്ണ സ്ക്രീൻ ടോഗിൾ ചെയ്യുക", "Toggle Do Not Disturb": "ശല്യപ്പെടുത്തരുത് ടോഗിൾ ചെയ്യുക",
"Toggle Sidebar": "സൈഡ്ബാർ ടോഗിൾ ചെയ്യുക", "Toggle Full Screen": "പൂർണ്ണ സ്‌ക്രീൻ ടോഗിൾ ചെയ്യുക",
"Toggle Sidebar": "സൈഡ്‌ബാർ ടോഗിൾ ചെയ്യുക",
"Toggle Tray Icon": "ട്രേ ഐക്കൺ ടോഗിൾ ചെയ്യുക", "Toggle Tray Icon": "ട്രേ ഐക്കൺ ടോഗിൾ ചെയ്യുക",
"Tools": "ഉപകരണങ്ങൾ",
"Undo": "പഴയപടിയാക്കുക", "Undo": "പഴയപടിയാക്കുക",
"Upload": "അപ്‌ലോഡുചെയ്യുക",
"Use system proxy settings (requires restart)": "സിസ്റ്റം പ്രോക്സി ക്രമീകരണങ്ങൾ ഉപയോഗിക്കുക (പുനരാരംഭിക്കേണ്ടതുണ്ട്)",
"View": "കാണുക", "View": "കാണുക",
"View Shortcuts": "കുറുക്കുവഴികൾ കാണുക",
"Window": "ജാലകം", "Window": "ജാലകം",
"Window Shortcuts": "വിൻഡോ കുറുക്കുവഴികൾ",
"YES": "അതെ",
"Zoom In": "വലുതാക്കുക", "Zoom In": "വലുതാക്കുക",
"Zoom Out": "സൂം ട്ട്", "Zoom Out": "സൂം .ട്ട് ചെയ്യുക",
"Zulip Help": "സുലിപ്പ് സഹായം" "Zulip Help": "സുലിപ് സഹായം",
"keyboard shortcuts": "കീബോർഡ് കുറുക്കുവഴികൾ",
"script": "സ്ക്രിപ്റ്റ്"
} }

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "Over Zulip", "About Zulip": "Over Zulip",
"Actual Size": "Daadwerkelijke grootte", "Actual Size": "Ware grootte",
"Add Custom Certificates": "Aangepaste certificaten toevoegen",
"Add Organization": "Voeg organisatie toe",
"Add a Zulip organization": "Voeg een Zulip-organisatie toe",
"Add custom CSS": "Voeg aangepaste CSS toe",
"Advanced": "gevorderd",
"All the connected organizations will appear here": "Alle verbonden organisaties verschijnen hier",
"Always start minimized": "Begin altijd geminimaliseerd",
"App Updates": "App-updates",
"Appearance": "Verschijning",
"Application Shortcuts": "Applicatiesnelkoppelingen",
"Are you sure you want to disconnect this organization?": "Weet je zeker dat je deze organisatie wilt ontkoppelen?",
"Auto hide Menu bar": "Menubalk automatisch verbergen",
"Auto hide menu bar (Press Alt key to display)": "Menubalk automatisch verbergen (druk op de Alt-toets om weer te geven)",
"Back": "Terug", "Back": "Terug",
"Bounce dock on new private message": "Bounce dock op nieuw privébericht",
"Certificate file": "Certificaatbestand",
"Change": "Verandering",
"Check for Updates": "Controleer op updates",
"Close": "Dichtbij", "Close": "Dichtbij",
"Connect": "Aansluiten",
"Connect to another organization": "Maak verbinding met een andere organisatie",
"Connected organizations": "Verbonden organisaties",
"Copy": "Kopiëren", "Copy": "Kopiëren",
"Copy Zulip URL": "Kopieer Zulip-URL",
"Create a new organization": "Maak een nieuwe organisatie",
"Cut": "Besnoeiing", "Cut": "Besnoeiing",
"Default download location": "Standaard downloadlocatie",
"Delete": "Verwijder", "Delete": "Verwijder",
"Desktop App Settings": "Desktop-app-instellingen", "Desktop App Settings": "Desktop-app-instellingen",
"Desktop Notifications": "Bureaublad notificaties",
"Desktop Settings": "Desktop-instellingen",
"Disconnect": "Loskoppelen",
"Download App Logs": "Applogs downloaden",
"Edit": "Bewerk", "Edit": "Bewerk",
"Edit Shortcuts": "Bewerk snelkoppelingen",
"Enable auto updates": "Schakel automatische updates in",
"Enable error reporting (requires restart)": "Foutrapportage inschakelen (opnieuw opstarten vereist)",
"Enable spellchecker (requires restart)": "Spellingcontrole inschakelen (opnieuw opstarten vereist)",
"Factory Reset": "Fabrieksinstellingen",
"File": "het dossier", "File": "het dossier",
"Find accounts": "Vind accounts",
"Find accounts by email": "Vind accounts per e-mail",
"Flash taskbar on new message": "Flash-taakbalk voor nieuw bericht",
"Forward": "Vooruit", "Forward": "Vooruit",
"Hard Reload": "Moeilijke herladen", "Functionality": "functionaliteit",
"General": "Algemeen",
"Get beta updates": "Ontvang bèta-updates",
"Hard Reload": "Harde herladen",
"Help": "Helpen", "Help": "Helpen",
"Help Center": "Helpcentrum",
"History": "Geschiedenis", "History": "Geschiedenis",
"History Shortcuts": "Geschiedenis Sneltoetsen",
"Keyboard Shortcuts": "Toetsenbord sneltoetsen", "Keyboard Shortcuts": "Toetsenbord sneltoetsen",
"Log Out": "Uitloggen", "Log Out": "Uitloggen",
"Log Out of Organization": "Uitloggen van organisatie",
"Manual proxy configuration": "Handmatige proxyconfiguratie",
"Minimize": "verkleinen", "Minimize": "verkleinen",
"Paste": "Plakken", "Mute all sounds from Zulip": "Demp alle geluiden van Zulip",
"NO": "NEE",
"Network": "Netwerk",
"OR": "OF",
"Organization URL": "Organisatie-URL",
"Organizations": "organisaties",
"Paste": "Pasta",
"Paste and Match Style": "Plak en match stijl", "Paste and Match Style": "Plak en match stijl",
"Proxy": "volmacht",
"Proxy bypass rules": "Proxy-bypassregels",
"Proxy rules": "Proxy-regels",
"Quit": "ophouden", "Quit": "ophouden",
"Quit Zulip": "Sluit Zulip",
"Redo": "Opnieuw doen", "Redo": "Opnieuw doen",
"Release Notes": "Releaseopmerkingen",
"Reload": "vernieuwen", "Reload": "vernieuwen",
"Report an issue...": "Een probleem melden...", "Report an Issue": "Een probleem melden",
"Reset App Data": "App-gegevens opnieuw instellen",
"Reset App Settings": "App-instellingen opnieuw instellen", "Reset App Settings": "App-instellingen opnieuw instellen",
"Reset Application Data": "Herstel toepassingsgegevens",
"Save": "Opslaan",
"Select All": "Selecteer alles", "Select All": "Selecteer alles",
"Settings": "instellingen",
"Shortcuts": "shortcuts",
"Show App Logs": "App-logs weergeven", "Show App Logs": "App-logs weergeven",
"Show app icon in system tray": "App-pictogram weergeven in systeemvak",
"Show app unread badge": "App ongelezen badge weergeven",
"Show desktop notifications": "Toon bureaubladmeldingen",
"Show downloaded files in file manager": "Laat gedownloade bestanden zien in bestandsbeheer",
"Show sidebar": "Toon zijbalk",
"Start app at login": "Start de app bij inloggen",
"Switch to Next Organization": "Schakel over naar volgende organisatie",
"Switch to Previous Organization": "Schakel over naar vorige organisatie",
"These desktop app shortcuts extend the Zulip webapp's": "Deze sneltoetsen voor bureaubladapp breiden de Zulip-webapp's uit",
"This will delete all application data including all added accounts and preferences": "Hiermee worden alle applicatiegegevens verwijderd, inclusief alle toegevoegde accounts en voorkeuren",
"Tip": "Tip",
"Toggle DevTools for Active Tab": "DevTools voor actieve tabblad omschakelen", "Toggle DevTools for Active Tab": "DevTools voor actieve tabblad omschakelen",
"Toggle DevTools for Zulip App": "DevTools voor Zulip-app omschakelen", "Toggle DevTools for Zulip App": "DevTools voor Zulip-app omschakelen",
"Toggle Do Not Disturb": "Schakel Niet storen in",
"Toggle Full Screen": "Volledig scherm activeren", "Toggle Full Screen": "Volledig scherm activeren",
"Toggle Sidebar": "Zijbalk verschuiven", "Toggle Sidebar": "Zijbalk verschuiven",
"Toggle Tray Icon": "Pictogram Lade wisselen", "Toggle Tray Icon": "Pictogram Lade wisselen",
"Tools": "Hulpmiddelen",
"Undo": "ongedaan maken", "Undo": "ongedaan maken",
"Upload": "Uploaden",
"Use system proxy settings (requires restart)": "Systeem proxy-instellingen gebruiken (opnieuw opstarten vereist)",
"View": "Uitzicht", "View": "Uitzicht",
"View Shortcuts": "Bekijk snelkoppelingen",
"Window": "Venster", "Window": "Venster",
"Window Shortcuts": "Venster snelkoppelingen",
"YES": "JA",
"Zoom In": "In zoomen", "Zoom In": "In zoomen",
"Zoom Out": "Uitzoomen", "Zoom Out": "Uitzoomen",
"Zulip Help": "Zulip Help" "Zulip Help": "Zulip Help",
"keyboard shortcuts": "Toetsenbord sneltoetsen",
"script": "script"
} }

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "O Zulip", "About Zulip": "O Zulipie",
"Actual Size": "Rzeczywisty rozmiar", "Actual Size": "Rzeczywisty rozmiar",
"Back": "Plecy", "Add Custom Certificates": "Dodaj niestandardowe certyfikaty",
"Add Organization": "Dodaj organizację",
"Add a Zulip organization": "Dodaj organizację Zulip",
"Add custom CSS": "Dodaj niestandardowy CSS",
"Advanced": "zaawansowane",
"All the connected organizations will appear here": "Tutaj pojawią się wszystkie połączone organizacje",
"Always start minimized": "Zawsze zaczynaj zminimalizowany",
"App Updates": "Aktualizacje aplikacji",
"Appearance": "Wygląd",
"Application Shortcuts": "Skróty do aplikacji",
"Are you sure you want to disconnect this organization?": "Czy na pewno chcesz odłączyć tę organizację?",
"Auto hide Menu bar": "Auto hide Pasek menu",
"Auto hide menu bar (Press Alt key to display)": "Pasek menu automatycznego ukrywania (naciśnij klawisz Alt, aby wyświetlić)",
"Back": "Z powrotem",
"Bounce dock on new private message": "Dok odbijania na nowej prywatnej wiadomości",
"Certificate file": "Plik certyfikatu",
"Change": "Zmiana",
"Check for Updates": "Sprawdź aktualizacje",
"Close": "Blisko", "Close": "Blisko",
"Connect": "Połączyć",
"Connect to another organization": "Połącz się z inną organizacją",
"Connected organizations": "Związane organizacje",
"Copy": "Kopiuj", "Copy": "Kopiuj",
"Copy Zulip URL": "Skopiuj adres URL Zulip",
"Create a new organization": "Utwórz nową organizację",
"Cut": "Ciąć", "Cut": "Ciąć",
"Default download location": "Domyślna lokalizacja pobierania",
"Delete": "Kasować", "Delete": "Kasować",
"Desktop App Settings": "Ustawienia aplikacji na komputer", "Desktop App Settings": "Ustawienia aplikacji komputerowej",
"Desktop Notifications": "Powiadomienia na pulpicie",
"Desktop Settings": "Ustawienia pulpitu",
"Disconnect": "Rozłączyć się",
"Download App Logs": "Pobierz dzienniki aplikacji",
"Edit": "Edytować", "Edit": "Edytować",
"Edit Shortcuts": "Edytuj skróty",
"Enable auto updates": "Włącz automatyczne aktualizacje",
"Enable error reporting (requires restart)": "Włącz raportowanie błędów (wymaga ponownego uruchomienia)",
"Enable spellchecker (requires restart)": "Włącz sprawdzanie pisowni (wymaga ponownego uruchomienia)",
"Factory Reset": "Przywrócenie ustawień fabrycznych",
"File": "Plik", "File": "Plik",
"Find accounts": "Znajdź konta",
"Find accounts by email": "Znajdź konta pocztą e-mail",
"Flash taskbar on new message": "Pasek zadań Flash w nowej wiadomości",
"Forward": "Naprzód", "Forward": "Naprzód",
"Hard Reload": "Trudne przeładowanie", "Functionality": "Funkcjonalność",
"General": "Generał",
"Get beta updates": "Pobierz aktualizacje beta",
"Hard Reload": "Hard Reload",
"Help": "Wsparcie", "Help": "Wsparcie",
"Help Center": "Centrum pomocy",
"History": "Historia", "History": "Historia",
"History Shortcuts": "Skróty historii",
"Keyboard Shortcuts": "Skróty klawiszowe", "Keyboard Shortcuts": "Skróty klawiszowe",
"Log Out": "Wyloguj", "Log Out": "Wyloguj",
"Log Out of Organization": "Wyloguj się z organizacji",
"Manual proxy configuration": "Ręczna konfiguracja proxy",
"Minimize": "Zminimalizować", "Minimize": "Zminimalizować",
"Mute all sounds from Zulip": "Wycisz wszystkie dźwięki z Zulip",
"NO": "NIE",
"Network": "Sieć",
"OR": "LUB",
"Organization URL": "Adres URL organizacji",
"Organizations": "Organizacje",
"Paste": "Pasta", "Paste": "Pasta",
"Paste and Match Style": "Wklej i dopasuj styl", "Paste and Match Style": "Wklej i dopasuj styl",
"Proxy": "Pełnomocnik",
"Proxy bypass rules": "Zasady omijania proxy",
"Proxy rules": "Zasady proxy",
"Quit": "Porzucić", "Quit": "Porzucić",
"Quit Zulip": "Zamknij Zulipa",
"Redo": "Przerobić", "Redo": "Przerobić",
"Release Notes": "Informacje o wydaniu",
"Reload": "Przeładować", "Reload": "Przeładować",
"Report an issue...": "Zgłoś problem...", "Report an Issue": "Zgłoś problem",
"Reset App Data": "Zresetuj dane aplikacji",
"Reset App Settings": "Zresetuj ustawienia aplikacji", "Reset App Settings": "Zresetuj ustawienia aplikacji",
"Reset Application Data": "Zresetuj dane aplikacji",
"Save": "Zapisać",
"Select All": "Zaznacz wszystko", "Select All": "Zaznacz wszystko",
"Settings": "Ustawienia",
"Shortcuts": "Skróty",
"Show App Logs": "Pokaż dzienniki aplikacji", "Show App Logs": "Pokaż dzienniki aplikacji",
"Show app icon in system tray": "Pokaż ikonę aplikacji w zasobniku systemowym",
"Show app unread badge": "Pokaż nieprzeczytaną odznakę aplikacji",
"Show desktop notifications": "Pokaż powiadomienia na pulpicie",
"Show downloaded files in file manager": "Pokaż pobrane pliki w menedżerze plików",
"Show sidebar": "Pokaż pasek boczny",
"Start app at login": "Uruchom aplikację przy logowaniu",
"Switch to Next Organization": "Przełącz na następną organizację",
"Switch to Previous Organization": "Przełącz na poprzednią organizację",
"These desktop app shortcuts extend the Zulip webapp's": "Te skróty do aplikacji na komputery stacjonarne rozszerzają aplikacje Zulip Webapp",
"This will delete all application data including all added accounts and preferences": "Spowoduje to usunięcie wszystkich danych aplikacji, w tym wszystkich dodanych kont i preferencji",
"Tip": "Wskazówka",
"Toggle DevTools for Active Tab": "Przełącz DevTools dla Active Tab", "Toggle DevTools for Active Tab": "Przełącz DevTools dla Active Tab",
"Toggle DevTools for Zulip App": "Przełącz DevTools dla aplikacji Zulip", "Toggle DevTools for Zulip App": "Przełącz DevTools dla aplikacji Zulip",
"Toggle Do Not Disturb": "Przełącz nie przeszkadzać",
"Toggle Full Screen": "Przełącz tryb pełnoekranowy", "Toggle Full Screen": "Przełącz tryb pełnoekranowy",
"Toggle Sidebar": "Przełącz boczny pasek", "Toggle Sidebar": "Przełącz pasek boczny",
"Toggle Tray Icon": "Przełącz ikonę tacy", "Toggle Tray Icon": "Przełącz ikonę tacy",
"Tools": "Przybory",
"Undo": "Cofnij", "Undo": "Cofnij",
"Upload": "Przekazać plik",
"Use system proxy settings (requires restart)": "Użyj ustawień serwera proxy (wymaga ponownego uruchomienia)",
"View": "Widok", "View": "Widok",
"View Shortcuts": "Wyświetl skróty",
"Window": "Okno", "Window": "Okno",
"Window Shortcuts": "Skróty do okien",
"YES": "TAK",
"Zoom In": "Zbliżenie", "Zoom In": "Zbliżenie",
"Zoom Out": "Pomniejsz", "Zoom Out": "Pomniejsz",
"Zulip Help": "Zulip Help" "Zulip Help": "Pomoc Zulip",
"keyboard shortcuts": "Skróty klawiszowe",
"script": "scenariusz"
} }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
{
"bg": "Bulgarian",
"cs": "Czech",
"de": "Deutsch",
"en-GB": "English (UK)",
"en-US": "English (US)",
"en": "English",
"es": "Español",
"fr": "French",
"hi": "Hindi",
"hu": "Hungarian",
"id": "Indonesian",
"ja": "Japanese",
"ko": "Korean",
"ml": "Malayalam",
"nl": "Dutch",
"pl": "Polish",
"pt": "Portuguese",
"ru": "Russian",
"sr": "Macedonian",
"ta": "Tamil",
"tr": "Turkish"
}

View File

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

View File

@@ -1,39 +1,117 @@
{ {
"About Zulip": "Zulip Hakkinda", "About Zulip": "Zulip hakkında",
"Actual Size": "Gerçek boyutu", "Actual Size": "Gerçek boyutu",
"Add Custom Certificates": "Özel Sertifika Ekleme",
"Add Organization": "Organizasyon ekle",
"Add a Zulip organization": "Bir Zulip organizasyonu ekleyin",
"Add custom CSS": "Özel CSS ekle",
"Advanced": "ileri",
"All the connected organizations will appear here": "Tüm bağlı kuruluşlar burada görünecek",
"Always start minimized": "Her zaman simge durumuna küçültülmüş olarak başla",
"App Updates": "Uygulama Güncellemeleri",
"Appearance": "Görünüm",
"Application Shortcuts": "Uygulama Kısayolları",
"Are you sure you want to disconnect this organization?": "Bu kuruluşun bağlantısını kesmek istediğinize emin misiniz?",
"Auto hide Menu bar": "Menü çubuğunu otomatik gizle",
"Auto hide menu bar (Press Alt key to display)": "Menü çubuğunu otomatik gizle (görüntülemek için Alt tuşuna basın)",
"Back": "Geri", "Back": "Geri",
"Bounce dock on new private message": "Yeni özel mesaja dock atla",
"Certificate file": "Sertifika dosyası",
"Change": "Değişiklik",
"Check for Updates": "Güncellemeleri kontrol et",
"Close": "Kapat", "Close": "Kapat",
"Connect": "bağlamak",
"Connect to another organization": "Başka bir kuruma bağlan",
"Connected organizations": "Bağlı kuruluşlar",
"Copy": "kopya", "Copy": "kopya",
"Copy Zulip URL": "Zulip URL'sini kopyala",
"Create a new organization": "Yeni bir organizasyon oluştur",
"Cut": "Kesmek", "Cut": "Kesmek",
"Default download location": "Varsayılan indirme yeri",
"Delete": "silmek", "Delete": "silmek",
"Desktop App Settings": "Masaüstü Uygulama Ayarları", "Desktop App Settings": "Masaüstü Uygulaması Ayarları",
"Desktop Notifications": "Masaüstü bildirimleri",
"Desktop Settings": "Masaüstü Ayarları",
"Disconnect": "kesmek",
"Download App Logs": "Uygulama Günlüklerini İndir",
"Edit": "Düzenle", "Edit": "Düzenle",
"Edit Shortcuts": "Kısayolları Düzenle",
"Enable auto updates": "Otomatik güncellemeleri etkinleştir",
"Enable error reporting (requires restart)": "Hata raporlamayı etkinleştir (yeniden başlatma gerektirir)",
"Enable spellchecker (requires restart)": "Yazım denetleyicisini etkinleştir (yeniden başlatma gerektirir)",
"Factory Reset": "Fabrika ayarları",
"File": "Dosya", "File": "Dosya",
"Find accounts": "Hesapları bulun",
"Find accounts by email": "E-postayla hesap bulun",
"Flash taskbar on new message": "Yeni mesajda Flash görev çubuğu",
"Forward": "ileri", "Forward": "ileri",
"Hard Reload": "Sert Yeniden Yükle", "Functionality": "İşlevsellik",
"General": "Genel",
"Get beta updates": "Beta güncellemelerini alın",
"Hard Reload": "Sabit Yeniden Yükleme",
"Help": "yardım et", "Help": "yardım et",
"Help Center": "Yardım Merkezi",
"History": "Tarihçe", "History": "Tarihçe",
"History Shortcuts": "Tarihçe Kısayolları",
"Keyboard Shortcuts": "Klavye kısayolları", "Keyboard Shortcuts": "Klavye kısayolları",
"Log Out": ıkış Yap", "Log Out": ıkış Yap",
"Log Out of Organization": "Kuruluştan Çıkma",
"Manual proxy configuration": "Manuel proxy yapılandırması",
"Minimize": "küçültmek", "Minimize": "küçültmek",
"Mute all sounds from Zulip": "Zulip'teki tüm sesleri kapat",
"NO": "YOK HAYIR",
"Network": "Ağ",
"OR": "VEYA",
"Organization URL": "Kuruluş URLsi",
"Organizations": "Organizasyonlar",
"Paste": "Yapıştırmak", "Paste": "Yapıştırmak",
"Paste and Match Style": "Stili Yapıştır ve Eşleştir", "Paste and Match Style": "Yapıştır ve Stili Eşleştir",
"Proxy": "vekil",
"Proxy bypass rules": "Proxy atlama kuralları",
"Proxy rules": "Proxy kuralları",
"Quit": ıkmak", "Quit": ıkmak",
"Quit Zulip": "Zulip'i bırak",
"Redo": "yeniden yapmak", "Redo": "yeniden yapmak",
"Release Notes": "Sürüm notları",
"Reload": "Tekrar yükle", "Reload": "Tekrar yükle",
"Report an issue...": "Bir sorun bildir ...", "Report an Issue": "Bir Sorunu Bildirin",
"Reset App Data": "Uygulama Verilerini Sıfırla",
"Reset App Settings": "Uygulama Ayarlarını Sıfırla", "Reset App Settings": "Uygulama Ayarlarını Sıfırla",
"Reset Application Data": "Uygulama Verilerini Sıfırla",
"Save": "Kayıt etmek",
"Select All": "Hepsini seç", "Select All": "Hepsini seç",
"Show App Logs": "Uygulama Günlüğlerini Göster", "Settings": "Ayarlar",
"Toggle DevTools for Active Tab": "Aktif Sekme İçin DevTools'a Geçiş Yap", "Shortcuts": "Kısayollar",
"Toggle DevTools for Zulip App": "Zulip App için DevTools'a Geçiş Yap", "Show App Logs": "Uygulama Günlüklerini Göster",
"Show app icon in system tray": "Sistem tepsisinde uygulama simgesini göster",
"Show app unread badge": "Uygulamaya okunmamış rozeti göster",
"Show desktop notifications": "Masaüstü bildirimlerini göster",
"Show downloaded files in file manager": "İndirilen dosyaları dosya yöneticisinde göster",
"Show sidebar": "Kenar çubuğunu göster",
"Start app at login": "Giriş yaparken uygulamayı başlat",
"Switch to Next Organization": "Sonraki Organizasyona Geç",
"Switch to Previous Organization": "Önceki Organizasyona Geç",
"These desktop app shortcuts extend the Zulip webapp's": "Bu masaüstü uygulaması kısayolları, Zulip webappın",
"This will delete all application data including all added accounts and preferences": "Bu, eklenen tüm hesaplar ve tercihler dahil tüm uygulama verilerini siler",
"Tip": "Bahşiş",
"Toggle DevTools for Active Tab": "Etkin Sekme için DevTools'u değiştirin",
"Toggle DevTools for Zulip App": "Zulip App için DevTools Toggle",
"Toggle Do Not Disturb": "Geçiş Rahatsız Etmeyin",
"Toggle Full Screen": "Tam ekrana geç", "Toggle Full Screen": "Tam ekrana geç",
"Toggle Sidebar": "Kenar Çubuğunu Aç / Kapat", "Toggle Sidebar": "Kenar Çubuğunu Değiştir",
"Toggle Tray Icon": "Tepsi Simgesini Aç / Kapat", "Toggle Tray Icon": "Tepsi İkonunu Aç / Kapat",
"Tools": "Araçlar",
"Undo": "Geri alma", "Undo": "Geri alma",
"Upload": "Yükleme",
"Use system proxy settings (requires restart)": "Sistem proxy ayarlarını kullan (yeniden başlatmayı gerektirir)",
"View": "Görünüm", "View": "Görünüm",
"View Shortcuts": "Kısayolları Göster",
"Window": "pencere", "Window": "pencere",
"Window Shortcuts": "Pencere Kısayolları",
"YES": "EVET",
"Zoom In": "Yakınlaştır", "Zoom In": "Yakınlaştır",
"Zoom Out": "Uzaklaştır", "Zoom Out": "Uzaklaştırmak",
"Zulip Help": "Zulip Yardımı" "Zulip Help": "Zulip Yardım",
"keyboard shortcuts": "Klavye kısayolları",
"script": "senaryo"
} }

View File

@@ -5,10 +5,10 @@ platform:
os: Previous Visual Studio 2015 os: Previous Visual Studio 2015
cache: cache:
- node_modules - node_modules -> appveyor.yml
install: install:
- ps: Install-Product node 8 x64 - ps: Install-Product node 10 x64
- git reset --hard HEAD - git reset --hard HEAD
- npm install npm -g - npm install npm -g
- node --version - node --version

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