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
package-lock.json binary
app/package-lock.json binary
*.gif binary
*.jpg binary
*.jpeg binary

4
.gitignore vendored
View File

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

View File

@@ -15,17 +15,17 @@ addons:
language: node_js
node_js:
- '8'
- '10'
- '12'
before_install:
- ./scripts/travis-xvfb.sh
- npm install -g gulp
- npm install
- npm ci
cache:
directories:
- node_modules
- app/node_modules
script:
- 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
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

View File

@@ -18,6 +18,15 @@ Please see the [installation guide](https://zulipchat.com/help/desktop-app-insta
* Multi-language spell checker
* 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
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, shell } from 'electron';
import { app, dialog } from 'electron';
import { autoUpdater } from 'electron-updater';
import { linuxUpdateNotification } from './linuxupdater'; // Required only in case of linux
import log = require('electron-log');
import isDev = require('electron-is-dev');
import ConfigUtil = require('../renderer/js/utils/config-util');
import log from 'electron-log';
import isDev from 'electron-is-dev';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as LinkUtil from '../renderer/js/utils/link-util';
export function appUpdater(updateFromMenu = false): void {
// 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) {
const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates');
dialog.showMessageBox({
const { response } = await dialog.showMessageBox({
type: 'error',
buttons: ['Manual Download', 'Cancel'],
message: messageText,
detail: (error).toString() + `\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulipchat.com/apps/.\n
Current Version: ${app.getVersion()}`
}, response => {
if (response === 0) {
shell.openExternal('https://zulipchat.com/apps/');
}
Current Version: ${app.getVersion()}`
});
if (response === 0) {
LinkUtil.openBrowser(new URL('https://zulipchat.com/apps/'));
}
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
@@ -83,23 +82,22 @@ Current Version: ${app.getVersion()}`
});
// Ask the user if update is available
autoUpdater.on('update-downloaded', event => {
autoUpdater.on('update-downloaded', async event => {
// Ask user to update the app
dialog.showMessageBox({
const { response } = await dialog.showMessageBox({
type: 'question',
buttons: ['Install and Relaunch', 'Install Later'],
defaultId: 0,
message: `A new update ${event.version} has been downloaded`,
detail: 'It will be installed the next time you restart the application'
}, 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
autoUpdater.checkForUpdates();

View File

@@ -1,19 +1,16 @@
'use strict';
import { sentryInit } from '../renderer/js/utils/sentry-util';
import { appUpdater } from './autoupdater';
import { setAutoLaunch } from './startup';
import windowStateKeeper = require('electron-window-state');
import path = require('path');
import fs = require('fs');
import isDev = require('electron-is-dev');
import electron = require('electron');
const { app, ipcMain } = electron;
import windowStateKeeper from 'electron-window-state';
import path from 'path';
import fs from 'fs';
import electron, { app, ipcMain, session, dialog } from 'electron';
import AppMenu = require('./menu');
import BadgeSettings = require('../renderer/js/pages/preference/badge-settings');
import ConfigUtil = require('../renderer/js/utils/config-util');
import ProxyUtil = require('../renderer/js/utils/proxy-util');
import * as AppMenu from './menu';
import * as BadgeSettings from '../renderer/js/pages/preference/badge-settings';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as ProxyUtil from '../renderer/js/utils/proxy-util';
interface PatchedGlobal extends NodeJS.Global {
mainWindowState: windowStateKeeper.State;
@@ -21,12 +18,6 @@ interface PatchedGlobal extends NodeJS.Global {
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
let mainWindow: Electron.BrowserWindow;
let badgeCount: number;
@@ -57,6 +48,15 @@ const iconPath = (): string => {
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 {
// Load the previous state with fallback to defaults
const mainWindowState: windowStateKeeper.State = windowStateKeeper({
@@ -81,7 +81,8 @@ function createMainWindow(): Electron.BrowserWindow {
webPreferences: {
plugins: true,
nodeIntegration: true,
partition: 'persist:webviewsession'
partition: 'persist:webviewsession',
webviewTag: true
},
show: false
});
@@ -93,9 +94,12 @@ function createMainWindow(): Electron.BrowserWindow {
win.loadURL(mainURL);
// 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) {
e.preventDefault();
event.preventDefault();
if (process.platform === 'darwin') {
app.hide();
@@ -116,8 +120,8 @@ function createMainWindow(): Electron.BrowserWindow {
});
// To destroy tray icon when navigate to a new URL
win.webContents.on('will-navigate', e => {
if (e) {
win.webContents.on('will-navigate', event => {
if (event) {
win.webContents.send('destroytray');
}
});
@@ -143,13 +147,20 @@ app.on('certificate-error', (event: Event, _webContents: Electron.WebContents, _
callback(true);
});
// this event is only available on macOS. Triggers when you click on the dock icon.
app.on('activate', () => {
if (!mainWindow) {
if (mainWindow) {
// if there is already a window show it
mainWindow.show();
} else {
mainWindow = createMainWindow();
}
});
app.on('ready', () => {
const ses = session.fromPartition('persist:webviewsession');
ses.setUserAgent(`ZulipElectron/${app.getVersion()} ${ses.getUserAgent()}`);
AppMenu.setMenu({
tabs: []
});
@@ -158,7 +169,7 @@ app.on('ready', () => {
// Auto-hide menu bar on Windows + Linux
if (process.platform !== 'darwin') {
const shouldHideMenu = ConfigUtil.getConfigItem('autoHideMenubar') || false;
mainWindow.setAutoHideMenuBar(shouldHideMenu);
mainWindow.autoHideMenuBar = 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', () => {
// Initiate auto-updates on MacOS and Windows
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
// electron.powerMonitor.on('resume', () => {
// mainWindow.reload();
@@ -244,38 +280,34 @@ app.on('ready', () => {
});
ipcMain.on('toggle-app', () => {
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
mainWindow.show();
} else {
mainWindow.hide();
}
toggleApp();
});
ipcMain.on('toggle-badge-option', () => {
BadgeSettings.updateBadge(badgeCount, mainWindow);
});
ipcMain.on('toggle-menubar', (_event: Electron.IpcMessageEvent, showMenubar: boolean) => {
mainWindow.setAutoHideMenuBar(showMenubar);
ipcMain.on('toggle-menubar', (_event: Electron.IpcMainEvent, showMenubar: boolean) => {
mainWindow.autoHideMenuBar = showMenubar;
mainWindow.setMenuBarVisibility(!showMenubar);
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;
BadgeSettings.updateBadge(badgeCount, mainWindow);
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);
});
ipcMain.on('forward-message', (_event: Electron.IpcMessageEvent, listener: any, ...params: any[]) => {
page.send(listener, ...params);
ipcMain.on('forward-message', (_event: Electron.IpcMainEvent, listener: string, ...parameters: any[]) => {
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);
const activeTab = props.tabs[props.activeTabIndex];
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);
});
ipcMain.on('downloadFile', (_event: Electron.IpcMessageEvent, url: string, downloadPath: string) => {
ipcMain.on('downloadFile', (_event: Electron.IpcMainEvent, url: string, downloadPath: string) => {
page.downloadURL(url);
page.session.once('will-download', (_event: Event, item) => {
const filePath = path.join(downloadPath, item.getFilename());
page.session.once('will-download', async (_event: Event, item) => {
let setFilePath: string;
let shortFileName: string;
if (ConfigUtil.getConfigItem('promptDownload', false)) {
const showDialogOptions: object = {
defaultPath: path.join(downloadPath, item.getFilename())
};
const getTimeStamp = (): any => {
const date = new Date();
return date.getTime();
};
const result = await dialog.showSaveDialog(mainWindow, showDialogOptions);
if (result.canceled) {
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 fileExtension = path.extname(filePath);
const baseName = path.basename(filePath, fileExtension);
return `${baseName}-${getTimeStamp()}${fileExtension}`;
};
const formatFile = (filePath: string): string => {
const fileExtension = path.extname(filePath);
const baseName = path.basename(filePath, 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));
const setFilePath = fs.existsSync(filePath) ? updatedFilePath : filePath;
// Update the name and path of the file if it already exists
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
setFilePath = fs.existsSync(filePath) ? updatedFilePath : filePath;
shortFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename();
}
item.setSavePath(setFilePath);
item.on('updated', (_event: Event, state) => {
const updatedListener = (_event: Event, state: string): void => {
switch (state) {
case 'interrupted': {
// 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');
}
}
});
};
item.on('updated', updatedListener);
item.once('done', (_event: Event, state) => {
const getFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename();
if (state === 'completed') {
page.send('downloadFileCompleted', item.getSavePath(), getFileName);
page.send('downloadFileCompleted', item.getSavePath(), shortFileName);
} else {
console.log('Download failed state: ', state);
console.log('Download failed state:', state);
page.send('downloadFileFailed');
}
// 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);
});
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);
});
// Using event.sender.send instead of page.send here to
// make sure the value of errorReporting is sent only once on load.
ipcMain.on('error-reporting', (event: Electron.IpcMessageEvent) => {
ipcMain.on('error-reporting', (event: Electron.IpcMainEvent) => {
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);
});
// 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', () => {

View File

@@ -1,11 +1,11 @@
import { app, Notification } from 'electron';
import request = require('request');
import semver = require('semver');
import ConfigUtil = require('../renderer/js/utils/config-util');
import ProxyUtil = require('../renderer/js/utils/proxy-util');
import LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util');
import Logger = require('../renderer/js/utils/logger-util');
import request from 'request';
import semver from 'semver';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as ProxyUtil from '../renderer/js/utils/proxy-util';
import * as LinuxUpdateUtil from '../renderer/js/utils/linux-update-util';
import Logger from '../renderer/js/utils/logger-util';
const logger = new Logger({
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 AutoLaunch = require('auto-launch');
import isDev = require('electron-is-dev');
import ConfigUtil = require('../renderer/js/utils/config-util');
import AutoLaunch from 'auto-launch';
import isDev from 'electron-is-dev';
import * as ConfigUtil from '../renderer/js/utils/config-util';
export const setAutoLaunch = (AutoLaunchValue: boolean): void => {
// Don't run this in development
@@ -11,21 +10,23 @@ export const setAutoLaunch = (AutoLaunchValue: boolean): void => {
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);
if (autoLaunchOption) {
ZulipAutoLauncher.enable();
// setLoginItemSettings doesn't support linux
if (process.platform === 'linux') {
const ZulipAutoLauncher = new AutoLaunch({
name: 'Zulip',
isHidden: false
});
if (autoLaunchOption) {
ZulipAutoLauncher.enable();
} else {
ZulipAutoLauncher.disable();
}
} 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">
<p class="detail maintainer">
Maintained by
<a onclick="linkInBrowser('website')">Zulip</a>
<a href="https://zulipchat.com" target="_blank" rel="noopener noreferrer">Zulip</a>
</p>
<p class="detail license">
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>
</div>
</div>
<script>
const { app } = require('electron').remote;
const { shell } = require('electron');
const version_tag = document.querySelector('#version');
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>require('./js/shared/preventdrag.js')</script>
</body>

View File

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

View File

@@ -399,8 +399,17 @@ i.open-tab-button {
cursor: pointer;
}
.css-delete-action {
margin-bottom: 10px;
#css-delete-action {
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,
@@ -453,6 +462,11 @@ i.open-tab-button {
user-select: none;
}
.disallowed:hover {
background-color: rgba(241, 241, 241, 1.000);
cursor: not-allowed;
}
input.toggle-round + label {
padding: 2px;
width: 50px;
@@ -628,6 +642,26 @@ input.toggle-round:checked + label::after {
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 */
@media (max-width: 650px) {
@@ -690,3 +724,13 @@ input.toggle-round:checked + label::after {
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';
class BaseComponent {
export default class BaseComponent {
generateNodeFromTemplate(template: string): Element | null {
const wrapper = document.createElement('div');
wrapper.innerHTML = template;
return wrapper.firstElementChild;
}
}
export = BaseComponent;

View File

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

View File

@@ -1,83 +1,56 @@
import { ipcRenderer, remote } from 'electron';
import LinkUtil = require('../utils/link-util');
import DomainUtil = require('../utils/domain-util');
import ConfigUtil = require('../utils/config-util');
import * as LinkUtil from '../utils/link-util';
import * as ConfigUtil from '../utils/config-util';
import type WebView from './webview';
const { shell, app } = remote;
const dingSound = new Audio('../resources/sounds/ding.ogg');
// TODO: TypeScript - Figure out a way to pass correct type here.
function handleExternalLink(this: any, event: any): void {
const { url } = event;
const domainPrefix = DomainUtil.getDomain(this.props.index).url;
export default function handleExternalLink(this: WebView, event: Electron.NewWindowEvent): void {
event.preventDefault();
const url = new URL(event.url);
const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`);
const shouldShowInFolder = ConfigUtil.getConfigItem('showDownloadFolder', false);
// Whitelist URLs which are allowed to be opened in the app
const {
isInternalUrl: isWhiteListURL,
isUploadsUrl: isUploadsURL
} = LinkUtil.isInternal(domainPrefix, url);
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');
if (LinkUtil.isUploadsUrl(this.props.url, url)) {
ipcRenderer.send('downloadFile', url.href, downloadPath);
ipcRenderer.once('downloadFileCompleted', (_event: Event, filePath: string, fileName: string) => {
const downloadNotification = new Notification('Download Complete', {
body: `Click to show ${fileName} in folder`,
silent: true // We'll play our own sound - ding.ogg
});
ipcRenderer.once('downloadFileFailed', () => {
// Automatic download failed, so show save dialog prompt and download
// through webview
this.$el.downloadURL(url);
ipcRenderer.removeAllListeners('downloadFileCompleted');
});
return;
}
// Play sound to indicate download complete
if (!ConfigUtil.getConfigItem('silent')) {
dingSound.play();
}
// open internal urls inside the current webview.
this.$el.loadURL(url);
downloadNotification.addEventListener('click', () => {
// 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 {
event.preventDefault();
shell.openExternal(url);
LinkUtil.openBrowser(url);
}
}
export = handleExternalLink;

View File

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

View File

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

View File

@@ -1,30 +1,41 @@
'use strict';
import { remote } from 'electron';
import { ipcRenderer, remote } from 'electron';
import path = require('path');
import fs = require('fs');
import ConfigUtil = require('../utils/config-util');
import SystemUtil = require('../utils/system-util');
import BaseComponent = require('../components/base');
import handleExternalLink = require('../components/handle-external-link');
import path from 'path';
import fs from 'fs';
import * as ConfigUtil from '../utils/config-util';
import * as SystemUtil from '../utils/system-util';
import BaseComponent from './base';
import handleExternalLink from './handle-external-link';
const { app, dialog } = remote;
const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
// TODO: TypeScript - Type annotate 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 {
props: any;
export default class WebView extends BaseComponent {
props: WebViewProps;
zoomFactor: number;
badgeCount: number;
loading: boolean;
customCSS: string;
$webviewsContainer: DOMTokenList;
$el: Electron.WebviewTag;
domReady?: Promise<void>;
// This is required because in main.js we access WebView.method as
// webview[method].
@@ -34,7 +45,7 @@ class WebView extends BaseComponent {
super();
this.props = props;
this.zoomFactor = 1.0;
this.zoomFactor = 1;
this.loading = true;
this.badgeCount = 0;
this.customCSS = ConfigUtil.getConfigItem('customCSS');
@@ -47,16 +58,18 @@ class WebView extends BaseComponent {
data-tab-id="${this.props.tabIndex}"
src="${this.props.url}"
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
disablewebsecurity
${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession"
name="${this.props.name}"
webpreferences="allowRunningInsecureContent, javascript=yes">
webpreferences="${this.props.nodeIntegration ? '' : 'contextIsolation, '}javascript=yes">
</webview>`;
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template()) as Electron.WebviewTag;
this.domReady = new Promise(resolve => {
this.$el.addEventListener('dom-ready', () => resolve(), true);
});
this.props.$root.append(this.$el);
this.registerListeners();
@@ -125,7 +138,9 @@ class WebView extends BaseComponent {
const hasConnectivityErr = SystemUtil.connectivityERR.includes(errorDescription);
if (hasConnectivityErr) {
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) {
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', () => {
@@ -152,6 +161,10 @@ class WebView extends BaseComponent {
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
}
showNotificationSettings(): void {
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'show-notification-settings');
}
show(): void {
// Do not show WebView if another tab was selected and this tab should be in background.
if (!this.props.isActive()) {
@@ -184,8 +197,8 @@ class WebView extends BaseComponent {
this.customCSS = null;
ConfigUtil.setConfigItem('customCSS', null);
const errMsg = 'The custom css previously set is deleted!';
dialog.showErrorBox('custom css file deleted!', errMsg);
const errorMessage = 'The custom css previously set is deleted!';
dialog.showErrorBox('custom css file deleted!', errorMessage);
return;
}
@@ -232,16 +245,16 @@ class WebView extends BaseComponent {
}
zoomActualSize(): void {
this.zoomFactor = 1.0;
this.zoomFactor = 1;
this.$el.setZoomFactor(this.zoomFactor);
}
logOut(): void {
this.$el.executeJavaScript('logout()');
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'logout');
}
showShortcut(): void {
this.$el.executeJavaScript('shortcut()');
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'shortcut');
}
openDevTools(): void {
@@ -275,12 +288,16 @@ class WebView extends BaseComponent {
// Shows the loading indicator till the webview is reloaded
this.$webviewsContainer.remove('loaded');
this.loading = true;
this.props.switchLoading(true, this.props.url);
this.$el.reload();
}
send(channel: string, ...param: any[]): void {
this.$el.send(channel, ...param);
forceLoad(): void {
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 events = require('events');
import { EventEmitter } from 'events';
import { NotificationData, newNotification } from './notification';
type ListenerType = ((...args: any[]) => void);
// we have and will have some non camelcase stuff
// while working with zulip so just turning the rule off
// for the whole file.
/* eslint-disable @typescript-eslint/camelcase */
class ElectronBridge extends events {
send_event(eventName: string | symbol, ...args: any[]): void {
this.emit(eventName, ...args);
class ElectronBridge extends EventEmitter {
send_notification_reply_message_supported: boolean;
idle_on_system: boolean;
last_active_on_system: number;
constructor() {
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);
}
};
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();
@@ -40,6 +70,4 @@ electron_bridge.on('realm_icon_url', iconURL => {
// 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
// a listener for the event.
export = electron_bridge;
/* eslint-enable @typescript-eslint/camelcase */
export default electron_bridge;

View File

@@ -1,8 +1,8 @@
import { remote } from 'electron';
import SendFeedback from '@electron-elements/send-feedback';
import path = require('path');
import fs = require('fs');
import path from 'path';
import fs from 'fs';
const { app } = remote;
@@ -34,12 +34,26 @@ customElements.define('send-feedback', SendFeedback);
export const sendFeedback: SendFeedbackType = document.querySelector('send-feedback');
export const feedbackHolder = sendFeedback.parentElement;
/* eslint-disable no-multi-str */
// customize the fields of custom elements
sendFeedback.title = 'Report Issue';
sendFeedback.titleLabel = 'Issue title:';
sendFeedback.titlePlaceholder = 'Enter issue title';
sendFeedback.textareaLabel = 'Describe the issue:';
sendFeedback.textareaPlaceholder = 'Succinctly describe your issue and steps to reproduce it...';
sendFeedback.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.loaderSuccessText = '';
@@ -47,10 +61,10 @@ sendFeedback.useReporter('emailReporter', {
email: 'akash@zulipchat.com'
});
feedbackHolder.addEventListener('click', (e: Event) => {
feedbackHolder.addEventListener('click', (event: Event) => {
// only remove the class if the grey out faded
// part is clicked and not the feedback element itself
if (e.target === e.currentTarget) {
if (event.target === event.currentTarget) {
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, shell } from 'electron';
import { ipcRenderer, remote, clipboard } from 'electron';
import { feedbackHolder } from './feedback';
import path = require('path');
import escape = require('escape-html');
import isDev = require('electron-is-dev');
import path from 'path';
import escape from 'escape-html';
import isDev from 'electron-is-dev';
const { session, app, Menu, dialog } = remote;
// eslint-disable-next-line import/no-unassigned-import
require('./tray');
import './tray';
import DomainUtil = require('./utils/domain-util');
import WebView = require('./components/webview');
import ServerTab = require('./components/server-tab');
import FunctionalTab = require('./components/functional-tab');
import ConfigUtil = require('./utils/config-util');
import DNDUtil = require('./utils/dnd-util');
import ReconnectUtil = require('./utils/reconnect-util');
import Logger = require('./utils/logger-util');
import CommonUtil = require('./utils/common-util');
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
import * as DomainUtil from './utils/domain-util';
import WebView from './components/webview';
import ServerTab from './components/server-tab';
import FunctionalTab from './components/functional-tab';
import * as ConfigUtil from './utils/config-util';
import * as DNDUtil from './utils/dnd-util';
import ReconnectUtil from './utils/reconnect-util';
import Logger from './utils/logger-util';
import * as CommonUtil from './utils/common-util';
import * as EnterpriseUtil from './utils/enterprise-util';
import * as LinkUtil from './utils/link-util';
import * as Messages from '../../resources/messages';
interface FunctionalTabProps {
name: string;
@@ -60,12 +56,18 @@ interface SettingsOptions {
flashTaskbarOnMessage?: boolean;
};
downloadsPath: string;
showDownloadFolder: boolean;
quitOnClose: boolean;
promptDownload: boolean;
flashTaskbarOnMessage?: boolean;
dockBouncing?: boolean;
loading?: AnyObject;
}
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
const rendererDirectory = path.resolve(__dirname, '..');
type ServerOrFunctionalTab = ServerTab | FunctionalTab;
@@ -93,6 +95,7 @@ class ServerManagerView {
tabs: ServerOrFunctionalTab[];
functionalTabs: AnyObject;
tabIndex: number;
presetOrgs: string[];
constructor() {
this.$addServerButton = document.querySelector('#add-tab');
this.$tabsContainer = document.querySelector('#tabs-container');
@@ -128,47 +131,49 @@ class ServerManagerView {
this.loading = {};
this.activeTabIndex = -1;
this.tabs = [];
this.presetOrgs = [];
this.functionalTabs = {};
this.tabIndex = 0;
}
init(): void {
this.loadProxy().then(() => {
this.initSidebar();
this.initTabs();
this.initActions();
this.registerIpcs();
this.initDefaultSettings();
});
async init(): Promise<void> {
await this.loadProxy();
this.initDefaultSettings();
this.initSidebar();
this.removeUAfromDisk();
if (EnterpriseUtil.configFile) {
this.initPresetOrgs();
}
this.initTabs();
this.initActions();
this.registerIpcs();
}
loadProxy(): Promise<boolean> {
return new Promise(resolve => {
// To change proxyEnable to useManualProxy in older versions
const proxyEnabledOld = ConfigUtil.isConfigItemExists('useProxy');
if (proxyEnabledOld) {
const proxyEnableOldState = ConfigUtil.getConfigItem('useProxy');
if (proxyEnableOldState) {
ConfigUtil.setConfigItem('useManualProxy', true);
}
ConfigUtil.removeConfigItem('useProxy');
async loadProxy(): Promise<void> {
// To change proxyEnable to useManualProxy in older versions
const proxyEnabledOld = ConfigUtil.isConfigItemExists('useProxy');
if (proxyEnabledOld) {
const proxyEnableOldState = ConfigUtil.getConfigItem('useProxy');
if (proxyEnableOldState) {
ConfigUtil.setConfigItem('useManualProxy', true);
}
ConfigUtil.removeConfigItem('useProxy');
}
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
if (proxyEnabled) {
session.fromPartition('persist:webviewsession').setProxy({
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
}, resolve);
} else {
session.fromPartition('persist:webviewsession').setProxy({
pacScript: '',
proxyRules: '',
proxyBypassRules: ''
}, resolve);
}
});
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
if (proxyEnabled) {
await session.fromPartition('persist:webviewsession').setProxy({
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
});
} else {
await session.fromPartition('persist:webviewsession').setProxy({
pacScript: '',
proxyRules: '',
proxyBypassRules: ''
});
}
}
// Settings are initialized only when user clicks on General/Server/Network section settings
@@ -199,7 +204,8 @@ class ServerManagerView {
silent: false
},
downloadsPath: `${app.getPath('downloads')}`,
showDownloadFolder: false
quitOnClose: false,
promptDownload: false
};
// Platform specific settings
@@ -219,10 +225,12 @@ class ServerManagerView {
settingOptions.autoHideMenubar = false;
}
for (const i in settingOptions) {
const setting = i as keyof SettingsOptions;
if (ConfigUtil.getConfigItem(i) === null) {
ConfigUtil.setConfigItem(i, settingOptions[setting]);
for (const [setting, value] of Object.entries(settingOptions)) {
// give preference to defaults defined in global_config.json
if (EnterpriseUtil.configItemExists(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);
}
// 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 {
const servers = DomainUtil.getDomains();
if (servers.length > 0) {
for (let i = 0; i < servers.length; i++) {
this.initServer(servers[i], i);
DomainUtil.updateSavedServer(servers[i].url, i);
this.activateTab(i);
for (const [i, server] of servers.entries()) {
this.initServer(server, i);
}
// 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
this.$settingsButton.classList.remove('active');
} else {
} else if (this.presetOrgs.length === 0) {
// not attempting to add organisations in the background
this.openSettings('AddServer');
} else {
this.showLoading(true);
}
}
@@ -254,7 +348,7 @@ class ServerManagerView {
this.tabs.push(new ServerTab({
role: 'server',
icon: server.icon,
name: server.alias,
name: CommonUtil.decodeString(server.alias),
$root: this.$tabsContainer,
onClick: this.activateLastTab.bind(this, index),
index,
@@ -268,6 +362,8 @@ class ServerManagerView {
url: server.url,
role: 'server',
name: CommonUtil.decodeString(server.alias),
hasPermission: (origin: string, permission: string) =>
origin === server.url && permission === 'notifications',
isActive: () => {
return index === this.activeTabIndex;
},
@@ -279,7 +375,7 @@ class ServerManagerView {
}
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),
nodeIntegration: false,
preload: true
@@ -370,7 +466,7 @@ class ServerManagerView {
$altIcon.classList.add('server-icon');
$altIcon.classList.add('alt-icon');
$parent.removeChild($img);
$img.remove();
$parent.append($altIcon);
this.addContextMenu($altIcon as HTMLImageElement, index);
@@ -445,7 +541,7 @@ class ServerManagerView {
}
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),
nodeIntegration: true,
preload: false
@@ -459,14 +555,14 @@ class ServerManagerView {
this.activateTab(this.functionalTabs[tabProps.name]);
}
openSettings(nav = 'General'): void {
async openSettings(nav = 'General'): Promise<void> {
this.openFunctionalTab({
name: 'Settings',
materialIcon: 'settings',
url: `file://${rendererDirectory}/preference.html#${nav}`
});
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 {
@@ -477,12 +573,11 @@ class ServerManagerView {
});
}
openNetworkTroubleshooting(): void {
this.openFunctionalTab({
name: 'Network Troubleshooting',
materialIcon: 'network_check',
url: `file://${rendererDirectory}/network.html`
});
openNetworkTroubleshooting(index: number): void {
const reconnectUtil = new ReconnectUtil(this.tabs[index].webview);
reconnectUtil.pollInternetAndReload();
this.tabs[index].webview.props.url = `file://${rendererDirectory}/network.html`;
this.tabs[index].showNetworkError();
}
activateLastTab(index: number): void {
@@ -619,10 +714,8 @@ class ServerManagerView {
}
updateGeneralSettings(setting: string, value: any): void {
const selector = 'webview:not([class*=disabled])';
const webview: Electron.WebviewTag = document.querySelector(selector);
if (webview) {
const webContents = webview.getWebContents();
if (this.getActiveWebview()) {
const webContents = this.getActiveWebview().getWebContents();
webContents.send(setting, value);
}
}
@@ -641,24 +734,47 @@ class ServerManagerView {
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 {
$serverImg.addEventListener('contextmenu', e => {
e.preventDefault();
$serverImg.addEventListener('contextmenu', event => {
event.preventDefault();
const template = [
{
label: 'Disconnect organization',
click: () => {
dialog.showMessageBox({
click: async () => {
const { response } = await dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
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'
};
for (const key in webviewListeners) {
for (const [key, method] of Object.entries(webviewListeners)) {
ipcRenderer.on(key, () => {
const activeWebview = this.tabs[this.activeTabIndex].webview;
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) => {
this.openSettings(settingNav);
});
@@ -704,8 +847,7 @@ class ServerManagerView {
ipcRenderer.on('open-help', () => {
// Open help page of current active server
const helpPage = this.getCurrentActiveServer() + '/help';
shell.openExternal(helpPage);
LinkUtil.openBrowser(new URL('/help', this.getCurrentActiveServer()));
});
ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index));
@@ -728,13 +870,12 @@ class ServerManagerView {
this.openSettings('AddServer');
});
ipcRenderer.on('reload-proxy', (event: Event, showAlert: boolean) => {
this.loadProxy().then(() => {
if (showAlert) {
alert('Proxy settings saved!');
ipcRenderer.send('reload-full-app');
}
});
ipcRenderer.on('reload-proxy', async (event: Event, showAlert: boolean) => {
await this.loadProxy();
if (showAlert) {
alert('Proxy settings saved!');
ipcRenderer.send('reload-full-app');
}
});
ipcRenderer.on('toggle-sidebar', (event: Event, show: boolean) => {
@@ -773,17 +914,14 @@ class ServerManagerView {
ipcRenderer.on('toggle-dnd', (event: Event, state: boolean, newSettings: SettingsOptions) => {
this.toggleDNDButton(state);
ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent);
const selector = 'webview:not([class*=disabled])';
const webview: Electron.WebviewTag = document.querySelector(selector);
const webContents = webview.getWebContents();
const webContents = this.getActiveWebview().getWebContents();
webContents.send('toggle-dnd', state, newSettings);
});
ipcRenderer.on('update-realm-name', (event: Event, serverURL: string, realmName: string) => {
// TODO: TypeScript - Type annotate getDomains() or this domain paramter.
DomainUtil.getDomains().forEach((domain: any, index: number) => {
DomainUtil.getDomains().forEach((domain: DomainUtil.ServerConf, index: number) => {
if (domain.url.includes(serverURL)) {
const serverTooltipSelector = `.tab .server-tooltip`;
const serverTooltipSelector = '.tab .server-tooltip';
const serverTooltips = document.querySelectorAll(serverTooltipSelector);
serverTooltips[index].innerHTML = 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) => {
// TODO: TypeScript - Type annotate getDomains() or this domain paramter.
DomainUtil.getDomains().forEach((domain: any, index: number) => {
DomainUtil.getDomains().forEach(async (domain, index) => {
if (domain.url.includes(serverURL)) {
DomainUtil.saveServerIcon(iconURL).then((localIconUrl: string) => {
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();
const localIconUrl: string = await DomainUtil.saveServerIcon({
url: serverURL,
icon: iconURL
});
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', () => {
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', () => {
const serverManagerView = new ServerManagerView();
const reconnectUtil = new ReconnectUtil(serverManagerView);
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
// 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
@@ -905,4 +1060,4 @@ window.addEventListener('load', () => {
}
});
export = new ServerManagerView();
export {};

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { remote } from 'electron';
import Logger = require('../utils/logger-util');
import Logger from '../utils/logger-util';
const logger = new Logger({
file: 'errors.log',
@@ -15,32 +15,23 @@ const botsList: BotListItem[] = [];
let botsListLoaded = false;
// 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
export function loadBots(sync = false): void {
const { $ } = window;
export async function loadBots(): Promise<void> {
botsList.length = 0;
if (sync) {
$.ajaxSetup({async: false});
}
$.getJSON('/json/users')
.done((data: any) => {
const { members } = data;
members.forEach((membersRow: any) => {
if (membersRow.is_bot) {
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);
const response = await fetch('/json/users');
if (response.ok) {
const { members } = await response.json();
members.forEach((membersRow: any) => {
if (membersRow.is_bot) {
const bot = `@${membersRow.full_name}`;
const mention = `@**${bot.replace(/^@/, '')}**`;
botsList.push([bot, mention]);
}
});
if (sync) {
$.ajaxSetup({async: true});
botsListLoaded = 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
// the notification. Main function implemented in main.js
export function focusCurrentServer(): void {
// TODO: TypeScript: currentWindow of type BrowserWindow doesn't
// have a .send() property per typescript.
(currentWindow as any).send('focus-webview-with-id', webContentsId);
currentWindow.webContents.send('focus-webview-with-id', webContentsId);
}
// this function parses the reply from to notification
// making it easier to reply from notification eg
// @username in reply will be converted to @**username**
// #stream in reply will be converted to #**stream**
// bot mentions are not yet supported
export function parseReply(reply: string): string {
export async function parseReply(reply: string): Promise<string> {
const usersDiv = document.querySelectorAll('#user_presences li');
const streamHolder = document.querySelectorAll('#stream_filters li');
@@ -128,9 +117,9 @@ export function parseReply(reply: string): string {
reply = reply.replace(regex, streamMention);
});
// If botsList isn't completely loaded yet, make a synchronous getJSON request for list
if (botsListLoaded === false) {
loadBots(true);
// If botsList isn't completely loaded yet, make a request for list
if (!botsListLoaded) {
await loadBots();
}
// Iterate for every bot name and replace in reply
@@ -145,9 +134,3 @@ export function parseReply(reply: string): string {
reply = reply.replace(/\\n/, '\n');
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 * as params from '../utils/params-util';
import electron_bridge from '../electron-bridge';
import { appId, loadBots } from './helpers';
import DefaultNotification = require('./default-notification');
import DefaultNotification from './default-notification';
const { app } = remote;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
app.setAppUserModelId(appId);
window.Notification = DefaultNotification;
let Notification = DefaultNotification;
if (process.platform === 'darwin') {
window.Notification = require('./darwin-notifications');
Notification = require('./darwin-notifications');
}
window.addEventListener('load', () => {
// eslint-disable-next-line no-undef, @typescript-eslint/camelcase
if (params.isPageParams() && page_params.realm_uri) {
loadBots();
export interface NotificationData {
close(): void;
title: string;
dir: NotificationDirection;
lang: string;
body: string;
tag: string;
image: string;
icon: string;
badge: string;
vibrate: readonly number[];
timestamp: number;
renotify: boolean;
silent: boolean;
requireInteraction: boolean;
data: unknown;
actions: readonly NotificationAction[];
}
export 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';
class NetworkTroubleshootingView {
$reconnectButton: Element;
constructor() {
this.$reconnectButton = document.querySelector('#reconnect');
}
init(): void {
this.$reconnectButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
}
export function init($reconnectButton: Element, $settingsButton: Element): void {
$reconnectButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
$settingsButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'open-settings');
});
}
window.addEventListener('load', () => {
const networkTroubleshootingView = new NetworkTroubleshootingView();
networkTroubleshootingView.init();
});

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,19 @@
'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 {
// TODO: TypeScript - Here props should be object type
props: any;
export default class FindAccounts extends BaseComponent {
props: FindAccountsProps;
$findAccounts: Element | null;
$findAccountsButton: Element | null;
$serverUrlField: HTMLInputElement | null;
constructor(props: any) {
constructor(props: FindAccountsProps) {
super();
this.props = props;
}
@@ -19,11 +22,11 @@ class FindAccounts extends BaseComponent {
return `
<div class="settings-card certificate-card">
<div class="certificate-input">
<div>Organization URL</div>
<div>${t.__('Organization URL')}</div>
<input class="setting-input-value" value="zulipchat.com"/>
</div>
<div class="certificate-input">
<button class="green w-150" id="find-accounts-button">Find accounts</button>
<button class="green w-150" id="find-accounts-button">${t.__('Find accounts')}</button>
</div>
</div>
`;
@@ -44,7 +47,7 @@ class FindAccounts extends BaseComponent {
if (!url.startsWith('http')) {
url = 'https://' + url;
}
shell.openExternal(url + '/accounts/find');
LinkUtil.openBrowser(new URL('/accounts/find', url));
}
initListeners(): void {
@@ -59,7 +62,7 @@ class FindAccounts extends BaseComponent {
});
this.$serverUrlField.addEventListener('keypress', event => {
if (event.keyCode === 13) {
if (event.key === 'Enter') {
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 path = require('path');
import fs = require('fs-extra');
import path from 'path';
import fs from 'fs-extra';
const { app, dialog } = remote;
const currentBrowserWindow = remote.getCurrentWindow();
import BaseSection = require('./base-section');
import ConfigUtil = require('../../utils/config-util');
import BaseSection from './base-section';
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 {
// TODO: TypeScript - Here props should be object type
props: any;
constructor(props: any) {
interface GeneralSectionProps {
$root: Element;
}
export default class GeneralSection extends BaseSection {
props: GeneralSectionProps;
constructor(props: GeneralSectionProps) {
super();
this.props = props;
}
@@ -21,117 +26,127 @@ class GeneralSection extends BaseSection {
template(): string {
return `
<div class="settings-pane">
<div class="title">Appearance</div>
<div class="title">${t.__('Appearance')}</div>
<div id="appearance-option-settings" class="settings-card">
<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>
<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>
<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>
<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>
<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>
<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>
</div>
<div class="title">Desktop Notifications</div>
<div class="title">${t.__('Desktop Notifications')}</div>
<div class="settings-card">
<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>
<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>
</div>
<div class="title">App Updates</div>
<div class="title">${t.__('App Updates')}</div>
<div class="settings-card">
<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>
<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>
</div>
<div class="title">Functionality</div>
<div class="title">${t.__('Functionality')}</div>
<div class="settings-card">
<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>
<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>
<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>
</div>
<div class="title">Advanced</div>
<div class="title">${t.__('Advanced')}</div>
<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-control"></div>
</div>
<div class="setting-row" id="show-download-folder">
<div class="setting-description">Show downloaded files in file manager</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="add-custom-css">
<div class="setting-description">
Add custom CSS
</div>
<button class="custom-css-button green">Upload</button>
</div>
<div class="setting-row" id="remove-custom-css">
<div class="setting-description">
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div>
</div>
<div class="action red" id="css-delete-action">
<i class="material-icons">indeterminate_check_box</i>
<span>Delete</span>
</div>
</div>
<div class="setting-row" id="enable-error-reporting">
<div class="setting-description">${t.__('Enable error reporting (requires restart)')}</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="app-language">
<div class="setting-description">${t.__('App language (requires restart)')}</div>
<div id="lang-div" class="lang-div"></div>
</div>
<div class="setting-row" id="add-custom-css">
<div class="setting-description">
${t.__('Add custom CSS')}
</div>
<button class="custom-css-button green">${t.__('Upload')}</button>
</div>
<div class="setting-row" id="remove-custom-css">
<div class="setting-description">
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div>
</div>
<div class="action red" id="css-delete-action">
<i class="material-icons">indeterminate_check_box</i>
<span>${t.__('Delete')}</span>
</div>
</div>
<div class="setting-row" id="download-folder">
<div class="setting-description">
Default download location
${t.__('Default download location')}
</div>
<button class="download-folder-button green">Change</button>
<button class="download-folder-button green">${t.__('Change')}</button>
</div>
<div class="setting-row">
<div class="setting-description">
<div class="download-folder-path">${ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`)}</div>
</div>
</div>
</div>
<div class="title">Reset Application Data</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 class="setting-row" id="prompt-download">
<div class="setting-description">${t.__('Ask where to save files before downloading')}</div>
<div class="setting-control"></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.removeCustomCSS();
this.downloadFolder();
this.showDownloadFolder();
this.updateQuitOnCloseOption();
this.updatePromptDownloadOption();
this.enableErrorReporting();
this.setLocale();
// Platform specific settings
@@ -238,6 +255,7 @@ class GeneralSection extends BaseSection {
autoUpdateOption(): void {
this.generateSettingOption({
$element: document.querySelector('#autoupdate-option .setting-control'),
disabled: EnterpriseUtil.configItemExists('autoUpdate'),
value: ConfigUtil.getConfigItem('autoUpdate', true),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('autoUpdate');
@@ -273,9 +291,7 @@ class GeneralSection extends BaseSection {
const newValue = !ConfigUtil.getConfigItem('silent', true);
ConfigUtil.setConfigItem('silent', newValue);
this.updateSilentOption();
// TODO: TypeScript: currentWindow of type BrowserWindow doesn't
// have a .send() property per typescript.
(currentBrowserWindow as any).send('toggle-silent', newValue);
currentBrowserWindow.webContents.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 {
this.generateSettingOption({
$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 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',
buttons: ['YES', 'NO'],
defaultId: 0,
message: 'Are you sure',
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 = {
title: 'Select file',
properties: ['openFile'],
filters: [{ name: 'CSS file', extensions: ['css'] }]
};
dialog.showOpenDialog(showDialogOptions, selectedFile => {
if (selectedFile) {
ConfigUtil.setConfigItem('customCSS', selectedFile[0]);
ipcRenderer.send('forward-message', 'hard-reload');
}
});
const { filePaths, canceled } = await dialog.showOpenDialog(showDialogOptions);
if (!canceled) {
ConfigUtil.setConfigItem('customCSS', filePaths[0]);
ipcRenderer.send('forward-message', 'hard-reload');
}
}
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 {
this.generateSettingOption({
$element: document.querySelector('#start-minimize-option .setting-control'),
@@ -411,25 +455,25 @@ class GeneralSection extends BaseSection {
removeCustomCSS(): void {
const removeCSSButton = document.querySelector('#css-delete-action');
removeCSSButton.addEventListener('click', () => {
ConfigUtil.setConfigItem('customCSS', "");
ConfigUtil.setConfigItem('customCSS', '');
ipcRenderer.send('forward-message', 'hard-reload');
});
}
downloadFolderDialog(): void {
async downloadFolderDialog(): Promise<void> {
const showDialogOptions: OpenDialogOptions = {
title: 'Select Download Location',
properties: ['openDirectory']
};
dialog.showOpenDialog(showDialogOptions, selectedFolder => {
if (selectedFolder) {
ConfigUtil.setConfigItem('downloadsPath', selectedFolder[0]);
const downloadFolderPath: HTMLElement = document.querySelector('.download-folder-path');
downloadFolderPath.innerText = selectedFolder[0];
}
});
const { filePaths, canceled } = await dialog.showOpenDialog(showDialogOptions);
if (!canceled) {
ConfigUtil.setConfigItem('downloadsPath', filePaths[0]);
const downloadFolderPath: HTMLElement = document.querySelector('.download-folder-path');
downloadFolderPath.textContent = filePaths[0];
}
}
downloadFolder(): void {
const downloadFolder = document.querySelector('#download-folder .download-folder-button');
downloadFolder.addEventListener('click', () => {
@@ -437,17 +481,15 @@ class GeneralSection extends BaseSection {
});
}
showDownloadFolder(): void {
updatePromptDownloadOption(): void {
this.generateSettingOption({
$element: document.querySelector('#show-download-folder .setting-control'),
value: ConfigUtil.getConfigItem('showDownloadFolder', false),
$element: document.querySelector('#prompt-download .setting-control'),
value: ConfigUtil.getConfigItem('promptDownload', false),
clickHandler: () => {
const newValue = !ConfigUtil.getConfigItem('showDownloadFolder');
ConfigUtil.setConfigItem('showDownloadFolder', newValue);
this.showDownloadFolder();
const newValue = !ConfigUtil.getConfigItem('promptDownload');
ConfigUtil.setConfigItem('promptDownload', newValue);
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 {
// TODO: TypeScript - Here props should be object type
props: any;
export default class PreferenceNav extends BaseComponent {
props: PreferenceNavProps;
navItems: string[];
$el: Element;
constructor(props: any) {
constructor(props: PreferenceNavProps) {
super();
this.props = props;
this.navItems = ['General', 'Network', 'AddServer', 'Organizations', 'Shortcuts'];
@@ -17,12 +20,12 @@ class PreferenceNav extends BaseComponent {
template(): string {
let navItemsTemplate = '';
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 `
<div>
<div id="settings-header">Settings</div>
<div id="settings-header">${t.__('Settings')}</div>
<div id="nav-container">${navItemsTemplate}</div>
</div>
`;
@@ -63,5 +66,3 @@ class PreferenceNav extends BaseComponent {
$item.classList.remove('active');
}
}
export = PreferenceNav;

View File

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

View File

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

View File

@@ -1,21 +1,27 @@
'use strict';
import { remote, ipcRenderer } from 'electron';
import BaseComponent = require('../../components/base');
import DomainUtil = require('../../utils/domain-util');
import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util';
import * as Messages from '../../../../resources/messages';
import * as t from '../../utils/translation-util';
const { dialog } = remote;
class ServerInfoForm extends BaseComponent {
// TODO: TypeScript - Here props should be object type
props: any;
interface ServerInfoFormProps {
$root: Element;
server: DomainUtil.ServerConf;
index: number;
onChange: () => void;
}
export default class ServerInfoForm extends BaseComponent {
props: ServerInfoFormProps;
$serverInfoForm: Element;
$serverInfoAlias: Element;
$serverIcon: Element;
$deleteServerButton: Element;
$openServerButton: Element;
constructor(props: any) {
constructor(props: ServerInfoFormProps) {
super();
this.props = props;
}
@@ -36,7 +42,7 @@ class ServerInfoForm extends BaseComponent {
</div>
<div class="server-info-row">
<div class="action red server-delete-action">
<span>Disconnect</span>
<span>${t.__('Disconnect')}</span>
</div>
</div>
</div>
@@ -59,18 +65,21 @@ class ServerInfoForm extends BaseComponent {
}
initActions(): void {
this.$deleteServerButton.addEventListener('click', () => {
dialog.showMessageBox({
this.$deleteServerButton.addEventListener('click', async () => {
const { response } = await dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
buttons: [t.__('YES'), t.__('NO')],
defaultId: 0,
message: 'Are you sure you want to disconnect this organization?'
}, response => {
if (response === 0) {
DomainUtil.removeDomain(this.props.index);
this.props.onChange(this.props.index);
}
message: t.__('Are you sure you want to disconnect this organization?')
});
if (response === 0) {
if (DomainUtil.removeDomain(this.props.index)) {
ipcRenderer.send('reload-full-app');
} else {
const { title, content } = Messages.orgRemovalError(DomainUtil.getDomain(this.props.index).url);
dialog.showErrorBox(title, content);
}
}
});
this.$openServerButton.addEventListener('click', () => {
@@ -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');
import NewServerForm = require('./new-server-form');
interface ServersSectionProps {
$root: Element;
}
class ServersSection extends BaseSection {
// TODO: TypeScript - Here props should be object type
props: any;
export default class ServersSection extends BaseSection {
props: ServersSectionProps;
$newServerContainer: Element;
constructor(props: any) {
constructor(props: ServersSectionProps) {
super();
this.props = props;
}
@@ -17,7 +19,7 @@ class ServersSection extends BaseSection {
<div class="add-server-modal">
<div class="modal-container">
<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>
</div>
@@ -45,5 +47,3 @@ class ServersSection extends BaseSection {
}).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');
class ShortcutsSection extends BaseSection {
// TODO: TypeScript - Here props should be object type
props: any;
constructor(props: any) {
export default class ShortcutsSection extends BaseSection {
props: ShortcutsSectionProps;
constructor(props: ShortcutsSectionProps) {
super();
this.props = props;
}
// TODO - Deduplicate templateMac and templateWinLin functions. In theory
// they both should be the same the only thing different should be the userOSKey
// variable but there seems to be inconsistences between both function, one has more
@@ -20,143 +22,143 @@ class ShortcutsSection extends BaseSection {
return `
<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="title">Application Shortcuts</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">${t.__('Application Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>,</kbd></td>
<td>Settings</td>
<td>${t.__('Settings')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
<td>${t.__('Keyboard Shortcuts')}</td>
</tr>
<tr>
<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>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>D</kbd></td>
<td>Reset App Settings</td>
<td>${t.__('Reset App Settings')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>L</kbd></td>
<td>Log Out</td>
<td>${t.__('Log Out')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>Hide Zulip</td>
<td>${t.__('Hide Zulip')}</td>
</tr>
<tr>
<td><kbd>Option</kbd><kbd>${userOSKey}</kbd><kbd>H</kbd></td>
<td>Hide Others</td>
<td>${t.__('Hide Others')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Q</kbd></td>
<td>Quit Zulip</td>
<td>${t.__('Quit Zulip')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Edit Shortcuts</div>
<div class="title">${t.__('Edit Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>Undo</td>
<td>${t.__('Undo')}</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>Z</kbd></td>
<td>Redo</td>
<td>${t.__('Redo')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>X</kbd></td>
<td>Cut</td>
<td>${t.__('Cut')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>C</kbd></td>
<td>Copy</td>
<td>${t.__('Copy')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>V</kbd></td>
<td>Paste</td>
<td>${t.__('Paste')}</td>
</tr>
<tr>
<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>
<td><kbd>${userOSKey}</kbd><kbd>A</kbd></td>
<td>Select All</td>
<td>${t.__('Select All')}</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>Space</kbd></td>
<td>Emoji & Symbols</td>
<td>${t.__('Emoji & Symbols')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">View Shortcuts</div>
<div class="title">${t.__('View Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>Reload</td>
<td>${t.__('Reload')}</td>
</tr>
<tr>
<td><kbd>Shift</kbd><kbd>${userOSKey}</kbd><kbd>R</kbd></td>
<td>Hard Reload</td>
<td>${t.__('Hard Reload')}</td>
</tr>
<tr>
<td><kbd>Control</kbd><kbd>${userOSKey}</kbd><kbd>F</kbd></td>
<td>Enter Full Screen</td>
<td>${t.__('Enter Full Screen')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>+</kbd></td>
<td>Zoom In</td>
<td>${t.__('Zoom In')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>-</kbd></td>
<td>Zoom Out</td>
<td>${t.__('Zoom Out')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>0</kbd></td>
<td>Actual Size</td>
<td>${t.__('Actual Size')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
<td>Toggle Sidebar</td>
<td>${t.__('Toggle Sidebar')}</td>
</tr>
<tr>
<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>
<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>
<td><kbd>Ctrl</kbd> + <kbd>Tab</kbd></td>
<td>Switch to Next Organization</td>
<td>${t.__('Switch to Next Organization')}</td>
</tr>
<tr>
<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>
</table>
<div class="setting-control"></div>
</div>
<div class="title">History Shortcuts</div>
<div class="title">${t.__('History Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>←</kbd></td>
<td>Back</td>
<td>${t.__('Back')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>→</kbd></td>
<td>Forward</td>
<td>${t.__('Forward')}</td>
</tr>
</table>
<div class="setting-control"></div>
@@ -166,11 +168,11 @@ class ShortcutsSection extends BaseSection {
<table>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>M</kbd></td>
<td>Minimize</td>
<td>${t.__('Minimize')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd><kbd>W</kbd></td>
<td>Close</td>
<td>${t.__('Close')}</td>
</tr>
</table>
<div class="setting-control"></div>
@@ -184,141 +186,141 @@ class ShortcutsSection extends BaseSection {
return `
<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="title">Application Shortcuts</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">${t.__('Application Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>,</kbd></td>
<td>Settings</td>
<td>${t.__('Settings')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>K</kbd></td>
<td>Keyboard Shortcuts</td>
<td>${t.__('Keyboard Shortcuts')}</td>
</tr>
<tr>
<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>
<td><kbd>${userOSKey}</kbd> + <kbd>L</kbd></td>
<td>Log Out</td>
<td>${t.__('Log Out')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Q</kbd></td>
<td>Quit Zulip</td>
<td>${t.__('Quit Zulip')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Edit Shortcuts</div>
<div class="title">${t.__('Edit Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Z</kbd></td>
<td>Undo</td>
<td>${t.__('Undo')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Y</kbd></td>
<td>Redo</td>
<td>${t.__('Redo')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>X</kbd></td>
<td>Cut</td>
<td>${t.__('Cut')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>C</kbd></td>
<td>Copy</td>
<td>${t.__('Copy')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>V</kbd></td>
<td>Paste</td>
<td>${t.__('Paste')}</td>
</tr>
<tr>
<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>
<td><kbd>${userOSKey}</kbd> + <kbd>A</kbd></td>
<td>Select All</td>
<td>${t.__('Select All')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">View Shortcuts</div>
<div class="title">${t.__('View Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>R</kbd></td>
<td>Reload</td>
<td>${t.__('Reload')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd></td>
<td>Hard Reload</td>
<td>${t.__('Hard Reload')}</td>
</tr>
<tr>
<td><kbd>F11</kbd></td>
<td>Toggle Full Screen</td>
<td>${t.__('Toggle Full Screen')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>=</kbd></td>
<td>Zoom In</td>
<td>${t.__('Zoom In')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>-</kbd></td>
<td>Zoom Out</td>
<td>${t.__('Zoom Out')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>0</kbd></td>
<td>Actual Size</td>
<td>${t.__('Actual Size')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></td>
<td>Toggle Sidebar</td>
<td>${t.__('Toggle Sidebar')}</td>
</tr>
<tr>
<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>
<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>
<td><kbd>${userOSKey}</kbd> + <kbd>Tab</kbd></td>
<td>Switch to Next Organization</td>
<td>${t.__('Switch to Next Organization')}</td>
</tr>
<tr>
<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>
</table>
<div class="setting-control"></div>
</div>
<div class="title">History Shortcuts</div>
<div class="title">${t.__('History Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>Alt</kbd> + <kbd>←</kbd></td>
<td>Back</td>
<td>${t.__('Back')}</td>
</tr>
<tr>
<td><kbd>Alt</kbd> + <kbd>→</kbd></td>
<td>Forward</td>
<td>${t.__('Forward')}</td>
</tr>
</table>
<div class="setting-control"></div>
</div>
<div class="title">Window Shortcuts</div>
<div class="title">${t.__('Window Shortcuts')}</div>
<div class="settings-card">
<table>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>M</kbd></td>
<td>Minimize</td>
<td>${t.__('Minimize')}</td>
</tr>
<tr>
<td><kbd>${userOSKey}</kbd> + <kbd>W</kbd></td>
<td>Close</td>
<td>${t.__('Close')}</td>
</tr>
</table>
<div class="setting-control"></div>
@@ -329,16 +331,15 @@ class ShortcutsSection extends BaseSection {
openHotkeysExternalLink(): void {
const link = 'https://zulipchat.com/help/keyboard-shortcuts';
const externalCreateNewOrgEl = document.querySelector('#open-hotkeys-link');
externalCreateNewOrgEl.addEventListener('click', () => {
shell.openExternal(link);
const externalCreateNewOrgElement = document.querySelector('#open-hotkeys-link');
externalCreateNewOrgElement.addEventListener('click', () => {
LinkUtil.openBrowser(new URL(link));
});
}
init(): void {
this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin();
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 SetupSpellChecker from './spellchecker';
import isDev from 'electron-is-dev';
import LinkUtil = require('./utils/link-util');
import params = require('./utils/params-util');
interface PatchedGlobal extends NodeJS.Global {
logout: () => void;
shortcut: () => void;
}
const globalPatched = global as PatchedGlobal;
import * as NetworkError from './pages/network';
// 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
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
window.electron_bridge = require('./electron-bridge');
const logout = (): void => {
ipcRenderer.on('logout', () => {
// Create the menu for the below
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click();
const nodes: NodeListOf<HTMLElement> = document.querySelectorAll('.dropdown-menu li:last-child a');
nodes[nodes.length - 1].click();
};
});
const shortcut = (): void => {
ipcRenderer.on('shortcut', () => {
// Create the menu for the below
const node: HTMLElement = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
// Additional check
@@ -44,49 +36,35 @@ const shortcut = (): void => {
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
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
document.addEventListener('DOMContentLoaded', (): void => {
if (params.isPageParams()) {
ipcRenderer.on('show-notification-settings', () => {
// Create the menu for the below
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click();
const nodes: NodeListOf<HTMLElement> = document.querySelectorAll('.dropdown-menu li a');
nodes[2].click();
const notificationItem: NodeListOf<HTMLElement> = document.querySelectorAll('.normal-settings-list li div');
// wait until the notification dom element shows up
setTimeout(() => {
notificationItem[2].click();
}, 100);
});
electron_bridge.once('zulip-loaded', ({ serverLanguage }) => {
// Get the default language of the server
const serverLanguage = page_params.default_language; // eslint-disable-line no-undef, @typescript-eslint/camelcase
if (serverLanguage) {
// Init spellchecker
SetupSpellChecker.init(serverLanguage);
}
// redirect users to network troubleshooting page
const getRestartButton = document.querySelector('.restart_get_events_button');
if (getRestartButton) {
getRestartButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
}
// 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);
}
if (serverLanguage) {
// Init spellchecker
SetupSpellChecker.init(serverLanguage);
}
// redirect users to network troubleshooting page
const getRestartButton = document.querySelector('.restart_get_events_button');
if (getRestartButton) {
getRestartButton.addEventListener('click', () => {
ipcRenderer.send('forward-message', 'reload-viewer');
});
}
});
@@ -97,17 +75,48 @@ window.addEventListener('beforeunload', (): void => {
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
// so adding the reload shortcut in the old-school way
// Zoom from numpad keys is not supported by electron, so adding it through listeners.
document.addEventListener('keydown', event => {
const cmdOrCtrl = event.ctrlKey || event.metaKey;
if (event.code === 'F5') {
ipcRenderer.send('forward-message', 'hard-reload');
} else if (event.ctrlKey && event.code === 'NumpadAdd') {
} else if (cmdOrCtrl && (event.code === 'NumpadAdd' || event.code === 'Equal')) {
ipcRenderer.send('forward-message', 'zoomIn');
} else if (event.ctrlKey && event.code === 'NumpadSubtract') {
} else if (cmdOrCtrl && event.code === 'NumpadSubtract') {
ipcRenderer.send('forward-message', 'zoomOut');
} else if (event.ctrlKey && event.code === 'Numpad0') {
} else if (cmdOrCtrl && event.code === 'Numpad0') {
ipcRenderer.send('forward-message', 'zoomActualSize');
}
});
// Set user as active and update the time of last activity
ipcRenderer.on('set-active', () => {
if (isDev) {
console.log('active');
}
electron_bridge.idle_on_system = false;
electron_bridge.last_active_on_system = Date.now();
});
// Set user as idle and time of last activity is left unchanged
ipcRenderer.on('set-idle', () => {
if (isDev) {
console.log('idle');
}
electron_bridge.idle_on_system = true;
});
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
// so that attackers can't execute any remote code within the app
// It doesn't affect the compose box so that users can still
@@ -15,3 +13,5 @@ const preventDragAndDrop = (): void => {
};
preventDragAndDrop();
export {};

View File

@@ -1,56 +1,59 @@
'use strict';
import type { Subject } from 'rxjs';
import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker';
import ConfigUtil = require('./utils/config-util');
import Logger = require('./utils/logger-util');
import * as ConfigUtil from './utils/config-util';
import Logger from './utils/logger-util';
declare module 'electron-spellchecker' {
interface SpellCheckHandler {
currentSpellcheckerChanged: Subject<true>;
currentSpellcheckerLanguage: string;
}
}
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
class SetupSpellChecker {
SpellCheckHandler: typeof SpellCheckHandler;
contextMenuListener: typeof ContextMenuListener;
init(serverLanguage: string): void {
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
this.enableSpellChecker();
}
this.enableContextMenu(serverLanguage);
let spellCheckHandler: SpellCheckHandler;
let contextMenuListener: ContextMenuListener;
export function init(serverLanguage: string): void {
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
enableSpellChecker();
}
enableContextMenu(serverLanguage);
}
enableSpellChecker(): void {
try {
this.SpellCheckHandler = new SpellCheckHandler();
} catch (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();
}
function enableSpellChecker(): void {
try {
spellCheckHandler = new SpellCheckHandler();
} catch (err) {
logger.error(err);
}
}
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 path = require('path');
import ConfigUtil = require('./utils/config-util.js');
const { Tray, Menu, nativeImage, BrowserWindow } = remote;
import path from 'path';
import * as ConfigUtil from './utils/config-util';
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 => {
if (process.platform === 'linux') {
@@ -43,69 +50,59 @@ const config = {
thick: process.platform === 'win32'
};
const renderCanvas = function (arg: number): Promise<HTMLCanvasElement> {
const renderCanvas = function (arg: number): HTMLCanvasElement {
config.unreadCount = arg;
return new Promise(resolve => {
const SIZE = config.size * config.pixelRatio;
const PADDING = SIZE * 0.05;
const CENTER = SIZE / 2;
const HAS_COUNT = config.showUnreadCount && config.unreadCount;
const color = config.unreadCount ? config.unreadColor : config.readColor;
const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor;
const SIZE = config.size * config.pixelRatio;
const PADDING = SIZE * 0.05;
const CENTER = SIZE / 2;
const HAS_COUNT = config.showUnreadCount && config.unreadCount;
const color = config.unreadCount ? config.unreadColor : config.readColor;
const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor;
const canvas = document.createElement('canvas');
canvas.width = SIZE;
canvas.height = SIZE;
const ctx = canvas.getContext('2d');
const canvas = document.createElement('canvas');
canvas.width = SIZE;
canvas.height = SIZE;
const ctx = canvas.getContext('2d');
// Circle
// If (!config.thick || config.thick && HAS_COUNT) {
ctx.beginPath();
ctx.arc(CENTER, CENTER, (SIZE / 2) - PADDING, 0, 2 * Math.PI, false);
ctx.fillStyle = backgroundColor;
ctx.fill();
ctx.lineWidth = SIZE / (config.thick ? 10 : 20);
ctx.strokeStyle = backgroundColor;
ctx.stroke();
// Count or Icon
if (HAS_COUNT) {
ctx.fillStyle = color;
ctx.textAlign = 'center';
if (config.unreadCount > 99) {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`;
ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15));
} else if (config.unreadCount < 10) {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.20));
} else {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.15));
}
resolve(canvas);
// Circle
// If (!config.thick || config.thick && HAS_COUNT) {
ctx.beginPath();
ctx.arc(CENTER, CENTER, (SIZE / 2) - PADDING, 0, 2 * Math.PI, false);
ctx.fillStyle = backgroundColor;
ctx.fill();
ctx.lineWidth = SIZE / (config.thick ? 10 : 20);
ctx.strokeStyle = backgroundColor;
ctx.stroke();
// Count or Icon
if (HAS_COUNT) {
ctx.fillStyle = color;
ctx.textAlign = 'center';
if (config.unreadCount > 99) {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`;
ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15));
} else if (config.unreadCount < 10) {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.2));
} else {
ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
ctx.fillText(String(config.unreadCount), CENTER, CENTER + (SIZE * 0.15));
}
});
}
return canvas;
};
/**
* Renders the tray icon as a native image
* @param arg: Unread count
* @return the native image
*/
const renderNativeImage = function (arg: number): Promise<NativeImage> {
return Promise.resolve()
.then(() => renderCanvas(arg))
.then(canvas => {
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
// 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
}));
});
const renderNativeImage = function (arg: number): NativeImage {
const canvas = renderCanvas(arg);
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
return nativeImage.createFromBuffer(pngData, {
scaleFactor: config.pixelRatio
});
};
function sendAction(action: string): void {
@@ -119,7 +116,6 @@ function sendAction(action: string): void {
}
const createTray = function (): void {
window.tray = new Tray(iconPath());
const contextMenu = Menu.buildFromTemplate([
{
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') {
window.tray.on('click', () => {
tray.on('click', () => {
ipcRenderer.send('toggle-app');
});
}
};
ipcRenderer.on('destroytray', (event: Event): Event => {
if (!window.tray) {
if (!tray) {
return undefined;
}
window.tray.destroy();
if (window.tray.isDestroyed()) {
window.tray = null;
tray.destroy();
if (tray.isDestroyed()) {
tray = null;
} else {
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 => {
if (!window.tray) {
if (!tray) {
return;
}
// We don't want to create tray from unread messages on macOS since it already has dock badges.
if (process.platform === 'linux' || process.platform === 'win32') {
if (arg === 0) {
unread = arg;
window.tray.setImage(iconPath());
window.tray.setToolTip('No unread messages');
tray.setImage(iconPath());
tray.setToolTip('No unread messages');
} else {
unread = arg;
renderNativeImage(arg).then(image => {
window.tray.setImage(image);
window.tray.setToolTip(arg + ' unread messages');
});
const image = renderNativeImage(arg);
tray.setImage(image);
tray.setToolTip(arg + ' unread messages');
}
}
});
function toggleTray(): void {
let state;
if (window.tray) {
if (tray) {
state = false;
window.tray.destroy();
if (window.tray.isDestroyed()) {
window.tray = null;
tray.destroy();
if (tray.isDestroyed()) {
tray = null;
}
ConfigUtil.setConfigItem('trayIcon', false);
} else {
state = true;
createTray();
if (process.platform === 'linux' || process.platform === 'win32') {
renderNativeImage(unread).then(image => {
window.tray.setImage(image);
window.tray.setToolTip(unread + ' unread messages');
});
const image = renderNativeImage(unread);
tray.setImage(image);
tray.setToolTip(unread + ' unread messages');
}
ConfigUtil.setConfigItem('trayIcon', true);
}
@@ -218,3 +213,5 @@ ipcRenderer.on('toggletray', toggleTray);
if (ConfigUtil.getConfigItem('trayIcon', true)) {
createTray();
}
export {};

View File

@@ -1,96 +1,80 @@
'use strict';
import { remote } from 'electron';
import JsonDB from 'node-json-db';
import { JsonDB } from 'node-json-db';
import { initSetUp } from './default-util';
import fs = require('fs');
import path = require('path');
import Logger = require('./logger-util');
import fs from 'fs';
import path from 'path';
import Logger from './logger-util';
const { app, dialog } = remote;
initSetUp();
const logger = new Logger({
file: `certificate-util.log`,
file: 'certificate-util.log',
timestamp: true
});
let instance: null | CertificateUtil = null;
const certificatesDir = `${app.getPath('userData')}/certificates`;
class CertificateUtil {
db: JsonDB;
let db: JsonDB;
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
reloadDB();
this.reloadDB();
return instance;
}
getCertificate(server: string, defaultValue: any = null): any {
this.reloadDB();
const value = this.db.getData('/')[server];
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 function getCertificate(server: string, defaultValue: any = null): any {
reloadDB();
const value = db.getData('/')[server];
if (value === undefined) {
return defaultValue;
} else {
return value;
}
}
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';
let instance: null | CommonUtil = null;
class CommonUtil {
constructor() {
if (instance) {
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;
}
// unescape already encoded/escaped strings
export function decodeString(stringInput: string): string {
const parser = new DOMParser();
const dom = parser.parseFromString(
'<!doctype html><body>' + stringInput,
'text/html');
return dom.body.textContent;
}
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 path = require('path');
import electron = require('electron');
import Logger = require('./logger-util');
import fs from 'fs';
import path from 'path';
import electron from 'electron';
import Logger from './logger-util';
import * as EnterpriseUtil from './enterprise-util';
const logger = new Logger({
file: 'config-util.log',
timestamp: true
});
let instance: null | ConfigUtil = null;
let dialog: Electron.Dialog = null;
let app: Electron.App = null;
@@ -25,77 +24,68 @@ if (process.type === 'renderer') {
app = electron.app;
}
class ConfigUtil {
db: JsonDB;
let db: JsonDB;
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
reloadDB();
this.reloadDB();
return instance;
export function getConfigItem(key: string, defaultValue: any = null): any {
try {
db.reload();
} catch (err) {
logger.error('Error while reloading settings.json: ');
logger.error(err);
}
getConfigItem(key: string, defaultValue: any = null): any {
try {
this.db.reload();
} catch (err) {
logger.error('Error while reloading settings.json: ');
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);
const value = db.getData('/')[key];
if (value === undefined) {
setConfigItem(key, defaultValue);
return defaultValue;
} else {
return value;
}
}
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 setupCompleted = false;
if (process.type === 'renderer') {
app = require('electron').remote.app;
app = electron.remote.app;
} else {
app = require('electron').app;
app = electron.app;
}
const zulipDir = app.getPath('userData');
@@ -41,19 +42,19 @@ export const initSetUp = (): void => {
const configData = [
{
path: domainJson,
fileName: `domain.json`
fileName: 'domain.json'
},
{
path: certificatesJson,
fileName: `certificates.json`
fileName: 'certificates.json'
},
{
path: settingsJson,
fileName: `settings.json`
fileName: 'settings.json'
},
{
path: updatesJson,
fileName: `updates.json`
fileName: 'updates.json'
}
];
configData.forEach(data => {

View File

@@ -1,13 +1,11 @@
'use strict';
import ConfigUtil = require('./config-util');
import * as ConfigUtil from './config-util';
// TODO: TypeScript - add to Setting interface
// the list of settings since we have fixed amount of them
// We want to do this by creating a new module that exports
// this interface
interface Setting {
[key: string]: any;
[key: string]: boolean;
}
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 request = require('request');
import fs = require('fs');
import path = require('path');
import Logger = require('./logger-util');
import electron = require('electron');
import escape from 'escape-html';
import request from 'request';
import fs from 'fs';
import path from 'path';
import Logger from './logger-util';
import { remote } from 'electron';
import RequestUtil = require('./request-util');
import Messages = require('../../../resources/messages');
import * as RequestUtil from './request-util';
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({
file: `domain-util.log`,
file: 'domain-util.log',
timestamp: true
});
let instance: null | DomainUtil = null;
const defaultIconUrl = '../renderer/img/icon.png';
class DomainUtil {
db: JsonDB;
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
export let db: JsonDB;
this.reloadDB();
// Migrate from old schema
if (this.db.getData('/').domain) {
this.addDomain({
alias: 'Zulip',
url: this.db.getData('/domain')
});
this.db.delete('/domain');
}
reloadDB();
// Migrate from old schema
if (db.getData('/').domain) {
addDomain({
alias: 'Zulip',
url: db.getData('/domain')
});
db.delete('/domain');
}
return instance;
export function getDomains(): ServerConf[] {
reloadDB();
if (db.getData('/').domains === undefined) {
return [];
} else {
return db.getData('/domains');
}
}
getDomains(): any {
this.reloadDB();
if (this.db.getData('/').domains === undefined) {
return [];
} else {
return this.db.getData('/domains');
export function getDomain(index: number): ServerConf {
reloadDB();
return db.getData(`/domains[${index}]`);
}
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 {
this.reloadDB();
return this.db.getData(`/domains[${index}]`);
function updateDomain(index: number, server: ServerConf): void {
reloadDB();
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 {
this.reloadDB();
this.db.push(`/domains[${index}]`, server, true);
}
export function removeDomains(): void {
db.delete('/domains');
reloadDB();
}
addDomain(server: any): Promise<void> {
const { ignoreCerts } = server;
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;
}
}
export function removeDomain(index: number): boolean {
if (EnterpriseUtil.isPresetOrg(getDomain(index).url)) {
return false;
}
db.delete(`/domains[${index}]`);
reloadDB();
return true;
}
async checkCertError(domain: any, serverConf: any, error: string, silent: boolean): Promise<string | object> {
if (silent) {
// since getting server settings has already failed
return serverConf;
} 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();
// Check if domain is already added
export function duplicateDomain(domain: string): boolean {
domain = formatUrl(domain);
return getDomains().some(server => server.url === domain);
}
const response = await (dialog.showMessageBox({
type: 'warning',
buttons: ['Yes', 'No'],
defaultId: 1,
message: certErrorMessage,
detail: certErrorDetail
}) as any); // TODO: TypeScript - Figure this out
if (response === 0) {
// set ignoreCerts parameter to true in case user responds with yes
serverConf.ignoreCerts = true;
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.');
}
}
}
async function checkCertError(domain: string, serverConf: ServerConf, error: string, silent: boolean): Promise<ServerConf> {
if (silent) {
// since getting server settings has already failed
return serverConf;
} 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();
// ignoreCerts parameter helps in fetching server icon and
// other server details when user chooses to ignore certificate warnings
async checkDomain(domain: any, ignoreCerts = false, silent = false): Promise<any> {
if (!silent && this.duplicateDomain(domain)) {
// Do not check duplicate in silent mode
throw new Error('This server has been added.');
}
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);
}
});
const { response } = await dialog.showMessageBox({
type: 'warning',
buttons: ['Yes', 'No'],
defaultId: 1,
message: certErrorMessage,
detail: certErrorDetail
});
if (response === 0) {
// set ignoreCerts parameter to true in case user responds with yes
serverConf.ignoreCerts = true;
try {
return await getServerSettings(domain, serverConf.ignoreCerts);
} catch (_) {
if (error === Messages.noOrgsError(domain)) {
throw new Error(error);
}
return serverConf;
}
} else {
throw new Error('Untrusted certificate.');
}
}
}
// 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> {
const url = server.icon;
const domain = server.url;
domain = formatUrl(domain);
const serverIconOptions = {
url,
...RequestUtil.requestOptions(domain, ignoreCerts)
};
const serverConf = {
icon: defaultIconUrl,
url: domain,
alias: domain,
ignoreCerts
};
// The save will always succeed. If url is invalid, downgrade to default icon.
return new Promise(resolve => {
const filePath = this.generateFilePath(url);
const file = fs.createWriteStream(filePath);
try {
request(serverIconOptions).on('response', (response: any) => {
response.on('error', (err: string) => {
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
try {
return await getServerSettings(domain, serverConf.ignoreCerts);
} catch (err) {
// 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 (certsError) {
const result = await checkCertError(domain, serverConf, error, silent);
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', () => {
resolve(filePath);
});
}).on('error', (err: string) => {
} else {
reject(Messages.noOrgsError(domain));
}
} else {
reject(response);
}
});
});
}
export async function saveServerIcon(server: ServerConf, ignoreCerts = false): Promise<string> {
const url = server.icon;
const domain = server.url;
const serverIconOptions = {
url,
...RequestUtil.requestOptions(domain, ignoreCerts)
};
// The save will always succeed. If url is invalid, downgrade to default icon.
return new Promise(resolve => {
const filePath = generateFilePath(url);
const file = fs.createWriteStream(filePath);
try {
request(serverIconOptions).on('response', (response: any) => {
response.on('error', (err: string) => {
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
});
} catch (err) {
response.pipe(file).on('finish', () => {
resolve(filePath);
});
}).on('error', (err: string) => {
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
}
});
}
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) {
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);
}
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
}
this.db = new JsonDB(domainJsonPath, true, true);
}
});
}
generateFilePath(url: string): string {
const dir = `${app.getPath('userData')}/server-icons`;
const extension = path.extname(url).split('?')[0];
let hash = 5381;
let len = url.length;
while (len) {
hash = (hash * 33) ^ url.charCodeAt(--len);
}
// 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}`;
export async function updateSavedServer(url: string, index: number): Promise<void> {
// Does not promise successful update
const oldIcon = getDomain(index).icon;
const { ignoreCerts } = getDomain(index);
try {
const newServerConf = await checkDomain(url, ignoreCerts, true);
const localIconUrl = await saveServerIcon(newServerConf, ignoreCerts);
if (!oldIcon || localIconUrl !== '../renderer/img/icon.png') {
newServerConf.icon = localIconUrl;
updateDomain(index, newServerConf);
reloadDB();
}
} 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/
import wurl = require('wurl');
let instance: null | LinkUtil = null;
interface IsInternalResponse {
isInternalUrl: boolean;
isUploadsUrl: boolean;
export function isUploadsUrl(server: string, url: URL): boolean {
return url.origin === server && url.pathname.startsWith('/user_uploads/');
}
class LinkUtil {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
return instance;
}
isInternal(currentUrl: string, newUrl: string): IsInternalResponse {
const currentDomain = wurl('hostname', currentUrl);
const newDomain = wurl('hostname', newUrl);
const sameDomainUrl = (currentDomain === newDomain || newUrl === currentUrl + '/');
const isUploadsUrl = newUrl.includes(currentUrl + '/user_uploads/');
const isInternalUrl = newUrl.includes('/#narrow') || isUploadsUrl;
return {
isInternalUrl: sameDomainUrl && isInternalUrl,
isUploadsUrl
};
}
isImage(url: string): boolean {
// test for images extension as well as urls like .png?s=100
const isImageUrl = /\.(bmp|gif|jpg|jpeg|png|webp)\?*.*$/i;
return isImageUrl.test(url);
}
isPDF(url: string): boolean {
// test for pdf extension
const isPDFUrl = /\.(pdf)\?*.*$/i;
return isPDFUrl.test(url);
export function openBrowser(url: URL): void {
if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
shell.openExternal(url.href);
} else {
// For security, indirect links to non-whitelisted protocols
// through a real web browser via a local HTML file.
const dir = fs.mkdtempSync(
path.join(os.tmpdir(), 'zulip-redirect-')
);
const file = path.join(dir, 'redirect.html');
fs.writeFileSync(file, `\
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Refresh" content="0; url=${escape(url.href)}" />
<title>Redirecting</title>
<style>
html {
font-family: menu, "Helvetica Neue", sans-serif;
}
</style>
</head>
<body>
<p>Opening <a href="${escape(url.href)}">${escape(url.href)}</a>…</p>
</body>
</html>
`);
shell.openItem(file);
setTimeout(() => {
fs.unlinkSync(file);
fs.rmdirSync(dir);
}, 15000);
}
}
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 path = require('path');
import electron = require('electron');
import Logger = require('./logger-util');
import fs from 'fs';
import path from 'path';
import electron from 'electron';
import Logger from './logger-util';
const remote =
process.type === 'renderer' ? electron.remote : electron;
@@ -16,61 +15,47 @@ const logger = new Logger({
/* To make the util runnable in both main and renderer process */
const { dialog, app } = remote;
let instance: null | LinuxUpdateUtil = null;
class LinuxUpdateUtil {
db: JsonDB;
let db: JsonDB;
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
reloadDB();
this.reloadDB();
return instance;
}
getUpdateItem(key: string, defaultValue: any = null): any {
this.reloadDB();
const value = this.db.getData('/')[key];
if (value === undefined) {
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 function getUpdateItem(key: string, defaultValue: any = null): any {
reloadDB();
const value = db.getData('/')[key];
if (value === undefined) {
setUpdateItem(key, defaultValue);
return defaultValue;
} else {
return value;
}
}
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 { sentryInit, captureException } from './sentry-util';
import fs = require('fs');
import os = require('os');
import isDev = require('electron-is-dev');
import electron = require('electron');
import fs from 'fs';
import os from 'os';
import isDev from 'electron-is-dev';
import electron from 'electron';
// this interface adds [key: string]: any so
// we can do console[type] later on in the code
interface PatchedConsole extends Console {
@@ -13,7 +13,7 @@ interface PatchedConsole extends Console {
}
interface LoggerOptions {
timestamp?: any;
timestamp?: true | (() => string);
file?: string;
level?: boolean;
logInDevMode?: boolean;
@@ -44,20 +44,20 @@ if (process.type === 'renderer') {
const browserConsole: PatchedConsole = console;
const logDir = `${app.getPath('userData')}/Logs`;
class Logger {
export default class Logger {
nodeConsole: PatchedConsole;
timestamp: any; // TODO: TypeScript - Figure out how to make this work with string | Function.
timestamp?: () => string;
level: boolean;
logInDevMode: boolean;
[key: string]: any;
constructor(opts: LoggerOptions = {}) {
constructor(options: LoggerOptions = {}) {
let {
timestamp = true,
file = 'console.log',
level = true,
logInDevMode = false
} = opts;
} = options;
file = `${logDir}/${file}`;
if (timestamp === true) {
@@ -92,7 +92,7 @@ class Logger {
case typeof timestamp === 'function':
args.unshift(timestamp() + ' |\t');
case (level !== false):
case (level):
args.unshift(type.toUpperCase() + ' |');
case isDev || logInDevMode:
@@ -107,7 +107,7 @@ class Logger {
}
setUpConsole(): void {
for (const type in browserConsole) {
for (const type of Object.keys(browserConsole)) {
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');
import ConfigUtil = require('./config-util');
let instance: null | ProxyUtil = null;
interface ProxyRule {
export interface ProxyRule {
hostname?: string;
port?: number;
}
class ProxyUtil {
constructor() {
if (instance) {
return instance;
} else {
instance = this;
}
return instance;
// Return proxy to be used for a particular uri, to be used for request
export function getProxy(_uri: string): ProxyRule | void {
let uri;
try {
uri = new URL(_uri);
} catch (err) {
return;
}
// Return proxy to be used for a particular uri, to be used for request
getProxy(_uri: string): ProxyRule | void {
const parsedUri = url.parse(_uri);
if (parsedUri === null) {
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;
}
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;
}
// TODO: Refactor to async function
resolveSystemProxy(mainWindow: Electron.BrowserWindow): void {
const page = mainWindow.webContents;
const ses = page.session;
const resolveProxyUrl = 'www.example.com';
// 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);
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;
}
}
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 Logger = require('./logger-util');
import { ipcRenderer } from 'electron';
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({
file: `domain-util.log`,
file: 'domain-util.log',
timestamp: true
});
class ReconnectUtil {
// TODO: TypeScript - Figure out how to annotate serverManagerView
// it should be ServerManagerView; maybe make it a generic so we can
// pass the class from main.js
serverManagerView: any;
export default class ReconnectUtil {
webview: WebView;
url: string;
alreadyReloaded: boolean;
fibonacciBackoff: backoff.Backoff;
constructor(serverManagerView: any) {
this.serverManagerView = serverManagerView;
constructor(webview: WebView) {
this.webview = webview;
this.url = webview.props.url;
this.alreadyReloaded = false;
this.clearState();
}
clearState(): void {
this.alreadyReloaded = false;
this.fibonacciBackoff = backoff.fibonacci({
initialDelay: 5000,
maxDelay: 300000
});
}
pollInternetAndReload(): void {
const pollInterval = setInterval(() => {
this._checkAndReload()
.then(status => {
if (status) {
this.alreadyReloaded = true;
clearInterval(pollInterval);
}
});
}, 1500);
}
// TODO: Make this a async function
_checkAndReload(): Promise<boolean> {
async isOnline(): Promise<boolean> {
return new Promise(resolve => {
if (!this.alreadyReloaded) { // eslint-disable-line no-negated-condition
isOnline()
.then((online: boolean) => {
if (online) {
if (!this.alreadyReloaded) {
this.serverManagerView.reloadCurrentView();
}
logger.log('You\'re back online.');
return resolve(true);
}
logger.log('There is no internet connection, try checking network cables, modem and router.');
const errMsgHolder = document.querySelector('#description');
if (errMsgHolder) {
errMsgHolder.innerHTML = `
<div>Your internet connection doesn't seem to work properly!</div>
<div>Verify that it works and then click try again.</div>`;
}
return resolve(false);
});
} else {
return resolve(true);
try {
const ignoreCerts = DomainUtil.shouldIgnoreCerts(this.url);
if (ignoreCerts === null) {
return;
}
request(
{
url: `${this.url}/static/favicon.ico`,
...RequestUtil.requestOptions(this.url, ignoreCerts)
},
(error: Error, response: any) => {
const isValidResponse =
!error && response.statusCode >= 200 && response.statusCode < 400;
resolve(isValidResponse);
}
);
} catch (err) {
logger.log(err);
}
});
}
}
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 ConfigUtil = require('./config-util');
import Logger = require('./logger-util');
import ProxyUtil = require('./proxy-util');
import CertificateUtil = require('./certificate-util');
import SystemUtil = require('./system-util');
import { remote } from 'electron';
import fs from 'fs';
import path from 'path';
import * as ConfigUtil from './config-util';
import Logger from './logger-util';
import * as ProxyUtil from './proxy-util';
import * as CertificateUtil from './certificate-util';
import * as SystemUtil from './system-util';
const { app } = remote;
const logger = new Logger({
file: `request-util.log`,
file: 'request-util.log',
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 {
ca: string;
proxy: string | void | object;
proxy: string | void | ProxyUtil.ProxyRule;
ecdhCurve: 'auto';
headers: { 'User-Agent': string };
rejectUnauthorized: boolean;
}
class RequestUtil {
constructor() {
if (!instance) {
instance = this;
}
// ignoreCerts parameter helps in fetching server icon and
// other server details when user chooses to ignore certificate warnings
export function requestOptions(domain: string, ignoreCerts: boolean): RequestUtilResponse {
domain = formatUrl(domain);
const certificate = CertificateUtil.getCertificate(
encodeURIComponent(domain)
);
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
// other server details when user chooses to ignore certificate warnings
requestOptions(domain: string, ignoreCerts: boolean): RequestUtilResponse {
domain = this.formatUrl(domain);
const certificate = CertificateUtil.getCertificate(
encodeURIComponent(domain)
);
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}`;
let certificateLocation = '';
if (certificate) {
// To handle case where certificate has been moved from the location in certificates.json
try {
certificateLocation = fs.readFileSync(certificateFile, 'utf8');
} catch (err) {
logger.warn(`Error while trying to get certificate: ${err}`);
}
}
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
// If certificate for the domain exists add it as a ca key in the request's parameter else consider only domain as the parameter for request
// Add proxy as a parameter if it is being used.
return {
ca: certificateLocation ? certificateLocation : '',
proxy: proxyEnabled ? ProxyUtil.getProxy(domain) : '',
ecdhCurve: 'auto',
headers: { 'User-Agent': SystemUtil.getUserAgent() },
rejectUnauthorized: !ignoreCerts
};
}
const requestUtil = new RequestUtil();
export = requestUtil;
function formatUrl(domain: string): string {
const hasPrefix = domain.startsWith('http', 0);
if (hasPrefix) {
return domain;
} else {
return domain.includes('localhost:') ? `http://${domain}` : `https://${domain}`;
}
}

View File

@@ -1,14 +1,11 @@
import { init } from '@sentry/electron';
import isDev = require('electron-is-dev');
import path = require('path');
import dotenv = require('dotenv');
dotenv.config({ path: path.resolve(__dirname, '/../../../../.env') });
import isDev from 'electron-is-dev';
export const sentryInit = (): void => {
if (!isDev) {
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
// This error mainly comes from the console logs.
// This is a temp solution until Sentry supports disabling the console logs

View File

@@ -1,60 +1,34 @@
'use strict';
import { remote } from 'electron';
import { ipcRenderer } from 'electron';
import os = require('os');
import os from 'os';
const { app } = remote;
let instance: null | SystemUtil = null;
export const connectivityERR: string[] = [
'ERR_INTERNET_DISCONNECTED',
'ERR_PROXY_CONNECTION_FAILED',
'ERR_CONNECTION_RESET',
'ERR_NOT_CONNECTED',
'ERR_NAME_NOT_RESOLVED',
'ERR_NETWORK_CHANGED'
];
class SystemUtil {
connectivityERR: string[];
const userAgent = ipcRenderer.sendSync('fetch-user-agent');
userAgent: string | null;
constructor() {
if (instance) {
return instance;
export function 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 {
instance = this;
return 'Windows 10';
}
this.connectivityERR = [
'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;
} else {
return '';
}
}
export = new SystemUtil();
export function getUserAgent(): string {
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>
<span id="reload-tooltip" style="display:none">Reload</span>
</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>
<span id="loading-tooltip" style="display:none">Loading</span>
</div>

View File

@@ -9,14 +9,18 @@
<body>
<div id="content">
<div id="picture"><img src="img/zulip_network.png"></div>
<div id="title">Zulip can't connect</div>
<div id="description">
<div>Your computer seems to be offline.</div>
<div>We will keep trying to reconnect, or you can try now.</div>
<div id="title">We can't connect to this organization</div>
<div id="subtitle">This could be because</div>
<ul id="description">
<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 id="reconnect">Try now</div>
</div>
</body>
<script src="js/pages/network.js"></script>
<script>require('./js/shared/preventdrag.js')</script>
</html>

Binary file not shown.

View File

@@ -1,27 +1,48 @@
class Messages {
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).`;
}
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.`;
}
interface DialogBoxError {
title: string;
content: string;
}
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": "За Зулип",
"Actual Size": "Реален размер",
"About Zulip": "За Zulip",
"Actual Size": "Действителен размер",
"Add Custom Certificates": "Добавяне на персонализирани сертификати",
"Add Organization": "Добавяне на организация",
"Add a Zulip organization": "Добавете организация Zulip",
"Add custom CSS": "Добавете персонализиран CSS",
"Advanced": "напреднал",
"All the connected organizations will appear here": "Всички свързани организации ще се появят тук",
"Always start minimized": "Винаги започвайте да минимизирате",
"App Updates": "Актуализации на приложения",
"Appearance": "Външен вид",
"Application Shortcuts": "Клавишни комбинации за приложения",
"Are you sure you want to disconnect this organization?": "Наистина ли искате да прекъснете връзката с тази организация?",
"Auto hide Menu bar": "Автоматично скриване на лентата с менюта",
"Auto hide menu bar (Press Alt key to display)": "Автоматично скриване на лентата с менюта (натиснете клавиша Alt за показване)",
"Back": "обратно",
"Bounce dock on new private message": "Прескочи док в новото лично съобщение",
"Certificate file": "Файл за сертификат",
"Change": "промяна",
"Check for Updates": "Провери за обновления",
"Close": "Близо",
"Connect": "Свържете",
"Connect to another organization": "Свържете се с друга организация",
"Connected organizations": "Свързани организации",
"Copy": "копие",
"Copy Zulip URL": "Копирайте URL адреса на Zulip",
"Create a new organization": "Създайте нова организация",
"Cut": "Разрез",
"Default download location": "Място на изтегляне по подразбиране",
"Delete": "Изтрий",
"Desktop App Settings": "Настройки за настолни приложения",
"Desktop App Settings": "Настройки на приложението за работния плот",
"Desktop Notifications": "Известия за работния плот",
"Desktop Settings": "Настройки на работния плот",
"Disconnect": "Прекъсване на връзката",
"Download App Logs": "Изтеглете регистрационни файлове на приложенията",
"Edit": "редактиране",
"Edit Shortcuts": "Редактиране на преки пътища",
"Enable auto updates": "Активиране на автоматичните актуализации",
"Enable error reporting (requires restart)": "Активиране на отчитането за грешки (изисква се рестартиране)",
"Enable spellchecker (requires restart)": "Активиране на проверката на правописа (изисква се рестартиране)",
"Factory Reset": "Фабрично нулиране",
"File": "досие",
"Find accounts": "Намерете профили",
"Find accounts by email": "Намерете профили по имейл",
"Flash taskbar on new message": "Flash лентата на задачите в новото съобщение",
"Forward": "напред",
"Hard Reload": "Трудно презареждане",
"Functionality": "Функционалност",
"General": "Общ",
"Get beta updates": "Изтеглете бета актуализации",
"Hard Reload": "Hard Reload",
"Help": "Помогне",
"Help Center": "Помощен център",
"History": "история",
"History Shortcuts": "Преки пътища в историята",
"Keyboard Shortcuts": "Комбинация от клавиши",
"Log Out": "Излез от профила си",
"Log Out of Organization": "Излезте от организацията",
"Manual proxy configuration": "Ръчна конфигурация на прокси",
"Minimize": "Минимизиране",
"Mute all sounds from Zulip": "Заглуши всички звуци от Zulip",
"NO": "НЕ",
"Network": "мрежа",
"OR": "ИЛИ",
"Organization URL": "URL адрес на организацията",
"Organizations": "организации",
"Paste": "паста",
"Paste and Match Style": "Поставяне и съвпадение на стила",
"Paste and Match Style": "Поставяне и стил на съвпадение",
"Proxy": "пълномощник",
"Proxy bypass rules": "Правила за заобикаляне на прокси",
"Proxy rules": "Прокси правила",
"Quit": "напускам",
"Quit Zulip": "Прекрати Zulip",
"Redo": "ремонтирам",
"Release Notes": "Бележки към изданието",
"Reload": "Презареди",
"Report an issue...": "Подаване на сигнал за проблем ...",
"Reset App Settings": "Нулирайте настройките на приложението",
"Report an Issue": "Подаване на сигнал за проблем",
"Reset App Data": "Нулиране на данни за приложения",
"Reset App Settings": "Нулиране на настройките на приложението",
"Reset Application Data": "Нулиране на данните на приложението",
"Save": "Запази",
"Select All": "Избери всички",
"Show App Logs": "Показване на регистрационните файлове на приложения",
"Toggle DevTools for Active Tab": "Превключете DevTools за активен раздел",
"Settings": "Настройки",
"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 Do Not Disturb": "Превключване Не безпокойте",
"Toggle Full Screen": "Превключване на цял екран",
"Toggle Sidebar": "Превключване на страничната лента",
"Toggle Tray Icon": "Превключване на иконата на тавата",
"Toggle Tray Icon": "Превключете иконата на тава",
"Tools": "Инструменти",
"Undo": "премахвам",
"Upload": "Качи",
"Use system proxy settings (requires restart)": "Използване на системните прокси настройки (изисква рестартиране)",
"View": "изглед",
"View Shortcuts": "Преглед на преки пътища",
"Window": "прозорец",
"Window Shortcuts": "Клавишни комбинации",
"YES": "ДА",
"Zoom In": "Увеличавам",
"Zoom Out": "Отдалечавам",
"Zulip Help": "Помощ за Zulip"
"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",
"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í",
"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",
"Connect": "Připojit",
"Connect to another organization": "Připojte se k jiné organizaci",
"Connected organizations": "Připojené organizace",
"Copy": "kopírovat",
"Copy Zulip URL": "Kopírovat adresu URL aplikace Zulip",
"Create a new organization": "Vytvořit novou organizaci",
"Cut": "Střih",
"Delete": "Vymazat",
"Default download location": "Výchozí umístění stahování",
"Delete": "Smazat",
"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 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",
"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",
"Functionality": "Funkčnost",
"General": "Všeobecné",
"Get beta updates": "Získejte aktualizace beta",
"Hard Reload": "Hard Reload",
"Help": "Pomoc",
"Help Center": "Centrum nápovědy",
"History": "Dějiny",
"History Shortcuts": "Zkratky historie",
"Keyboard Shortcuts": "Klávesové zkratky",
"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 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 Zulip": "Ukončete Zulip",
"Redo": "Předělat",
"Release Notes": "Poznámky k vydání",
"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 Application Data": "Resetovat data aplikace",
"Save": "Uložit",
"Select All": "Vybrat vše",
"Show App Logs": "Zobrazit protokoly aplikace",
"Toggle DevTools for Active Tab": "Přepínač DevTools pro aktivní kartu",
"Toggle DevTools for Zulip App": "Přepnout nástroj DevTools pro aplikaci Zulip App",
"Settings": "Nastavení",
"Shortcuts": "Zkratky",
"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 Sidebar": "Přepnout postranní panel",
"Toggle Tray Icon": "Přepnout ikonu zásobníku",
"Tools": "Nástroje",
"Undo": "vrátit",
"Upload": "nahrát",
"Use system proxy settings (requires restart)": "Použít nastavení proxy serveru (vyžaduje restart)",
"View": "Pohled",
"View Shortcuts": "Zobrazit zástupce",
"Window": "Okno",
"Window Shortcuts": "Klávesové zkratky",
"YES": "ANO",
"Zoom In": "Přiblížit",
"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",
"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",
"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",
"Connect": "Verbinden",
"Connect to another organization": "Stellen Sie eine Verbindung zu einer anderen Organisation her",
"Connected organizations": "Verbundene Organisationen",
"Copy": "Kopieren",
"Copy Zulip URL": "Zulip-URL kopieren",
"Create a new organization": "Erstellen Sie eine neue Organisation",
"Cut": "Schnitt",
"Default download location": "Standard-Download-Speicherort",
"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 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",
"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",
"Hard Reload": "Hard Reload",
"Functionality": "Funktionalität",
"General": "Allgemeines",
"Get beta updates": "Erhalten Sie Beta-Updates",
"Hard Reload": "Festes Nachladen",
"Help": "Hilfe",
"Help Center": "Hilfezentrum",
"History": "Geschichte",
"History Shortcuts": "Verlaufsverknüpfungen",
"Keyboard Shortcuts": "Tastatürkürzel",
"Log Out": "Ausloggen",
"Log Out of Organization": "Aus Organisation ausloggen",
"Manual proxy configuration": "Manuelle Proxy-Konfiguration",
"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 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",
"Redo": "Wiederholen",
"Quit Zulip": "Beenden Sie Zulip",
"Redo": "Redo",
"Release Notes": "Versionshinweise",
"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 Application Data": "Anwendungsdaten zurücksetzen",
"Save": "sparen",
"Select All": "Wählen Sie Alle",
"Show App Logs": "App-Logs anzeigen",
"Toggle DevTools for Active Tab": "Schalten Sie DevTools für Active Tab ein",
"Toggle DevTools for Zulip App": "Umschalt DevTools für Zulip App",
"Settings": "die Einstellungen",
"Shortcuts": "Verknüpfungen",
"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 Sidebar": "Toggle Seitenleiste",
"Toggle Tray Icon": "Toggle Tray-Symbol",
"Toggle Sidebar": "Seitenleiste umschalten",
"Toggle Tray Icon": "Taskleistensymbol umschalten",
"Tools": "Werkzeuge",
"Undo": "Rückgängig machen",
"Upload": "Hochladen",
"Use system proxy settings (requires restart)": "System-Proxy-Einstellungen verwenden (Neustart erforderlich)",
"View": "Aussicht",
"View Shortcuts": "Verknüpfungen anzeigen",
"Window": "Fenster",
"Window Shortcuts": "Fensterverknüpfungen",
"YES": "JA",
"Zoom In": "Hineinzoomen",
"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",
"Actual Size": "Actual Size",
"Add Custom Certificates": "Add Custom Certificates",
"Add Organization": "Add Organization",
"Add a Zulip organization": "Add a Zulip organization",
"Add custom CSS": "Add custom CSS",
"Advanced": "Advanced",
"All the connected organizations will appear here": "All the connected organizations will appear here",
"Always start minimized": "Always start minimized",
"App Updates": "App Updates",
"Appearance": "Appearance",
"Application Shortcuts": "Application Shortcuts",
"Are you sure you want to disconnect this organization?": "Are you sure you want to disconnect this organization?",
"Auto hide Menu bar": "Auto hide Menu bar",
"Auto hide menu bar (Press Alt key to display)": "Auto hide menu bar (Press Alt key to display)",
"Back": "Back",
"Bounce dock on new private message": "Bounce dock on new private message",
"Certificate file": "Certificate file",
"Change": "Change",
"Check for Updates": "Check for Updates",
"Close": "Close",
"Connect": "Connect",
"Connect to another organization": "Connect to another organization",
"Connected organizations": "Connected organizations",
"Copy": "Copy",
"Copy Zulip URL": "Copy Zulip URL",
"Create a new organization": "Create a new organization",
"Cut": "Cut",
"Default download location": "Default download location",
"Delete": "Delete",
"Desktop App Settings": "Desktop App Settings",
"Desktop Notifications": "Desktop Notifications",
"Desktop Settings": "Desktop Settings",
"Disconnect": "Disconnect",
"Download App Logs": "Download App Logs",
"Edit": "Edit",
"Edit Shortcuts": "Edit Shortcuts",
"Enable auto updates": "Enable auto updates",
"Enable error reporting (requires restart)": "Enable error reporting (requires restart)",
"Enable spellchecker (requires restart)": "Enable spellchecker (requires restart)",
"Factory Reset": "Factory Reset",
"File": "File",
"Find accounts": "Find accounts",
"Find accounts by email": "Find accounts by email",
"Flash taskbar on new message": "Flash taskbar on new message",
"Forward": "Forward",
"Functionality": "Functionality",
"General": "General",
"Get beta updates": "Get beta updates",
"Hard Reload": "Hard Reload",
"Help": "Help",
"Help Center": "Help Center",
"History": "History",
"History Shortcuts": "History Shortcuts",
"Keyboard Shortcuts": "Keyboard Shortcuts",
"Log Out": "Log Out",
"Log Out of Organization": "Log Out of Organization",
"Manual proxy configuration": "Manual proxy configuration",
"Minimize": "Minimize",
"Mute all sounds from Zulip": "Mute all sounds from Zulip",
"NO": "NO",
"Network": "Network",
"OR": "OR",
"Organization URL": "Organization URL",
"Organizations": "Organizations",
"Paste": "Paste",
"Paste and Match Style": "Paste and Match Style",
"Proxy": "Proxy",
"Proxy bypass rules": "Proxy bypass rules",
"Proxy rules": "Proxy rules",
"Quit": "Quit",
"Quit Zulip": "Quit Zulip",
"Redo": "Redo",
"Release Notes": "Release Notes",
"Reload": "Reload",
"Report an issue...": "Report an issue...",
"Report an Issue": "Report an Issue",
"Reset App Data": "Reset App Data",
"Reset App Settings": "Reset App Settings",
"Reset Application Data": "Reset Application Data",
"Save": "Save",
"Select All": "Select All",
"Settings": "Settings",
"Shortcuts": "Shortcuts",
"Show App Logs": "Show App Logs",
"Show app icon in system tray": "Show app icon in system tray",
"Show app unread badge": "Show app unread badge",
"Show desktop notifications": "Show desktop notifications",
"Show downloaded files in file manager": "Show downloaded files in file manager",
"Show sidebar": "Show sidebar",
"Start app at login": "Start app at login",
"Switch to Next Organization": "Switch to Next Organization",
"Switch to Previous Organization": "Switch to Previous Organization",
"These desktop app shortcuts extend the Zulip webapp's": "These desktop app shortcuts extend the Zulip webapp's",
"This will delete all application data including all added accounts and preferences": "This will delete all application data including all added accounts and preferences",
"Tip": "Tip",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Toggle Do Not Disturb": "Toggle Do Not Disturb",
"Toggle Full Screen": "Toggle Full Screen",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon",
"Tools": "Tools",
"Undo": "Undo",
"Upload": "Upload",
"Use system proxy settings (requires restart)": "Use system proxy settings (requires restart)",
"View": "View",
"View Shortcuts": "View Shortcuts",
"Window": "Window",
"Window Shortcuts": "Window Shortcuts",
"YES": "YES",
"Zoom In": "Zoom In",
"Zoom Out": "Zoom Out",
"Zulip Help": "Zulip Help"
"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",
"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",
"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 Zulip URL": "Copiar URL de Zulip",
"Create a new organization": "Crear una nueva organización",
"Cut": "Cortar",
"Default download location": "Ubicación de descarga predeterminada",
"Delete": "Borrar",
"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",
"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",
"Hard Reload": "Recargar duro",
"Functionality": "Funcionalidad",
"General": "General",
"Get beta updates": "Recibe actualizaciones beta",
"Hard Reload": "Recarga dura",
"Help": "Ayuda",
"Help Center": "Centro de ayuda",
"History": "Historia",
"History Shortcuts": "Atajos de historia",
"Keyboard Shortcuts": "Atajos de teclado",
"Log Out": "Cerrar sesión",
"Log Out of Organization": "Salir de la organización",
"Manual proxy configuration": "Configuración de proxy manual",
"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 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 Zulip": "Dejar Zulip",
"Redo": "Rehacer",
"Release Notes": "Notas de lanzamiento",
"Reload": "Recargar",
"Report an issue...": "Reportar un problema...",
"Reset App Settings": "Restablecer configuraciones",
"Report an Issue": "Reportar un problema",
"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",
"Settings": "Ajustes",
"Shortcuts": "Atajos",
"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 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 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",
"Upload": "Subir",
"Use system proxy settings (requires restart)": "Usar la configuración del proxy del sistema (requiere reinicio)",
"View": "Ver",
"View Shortcuts": "Ver accesos directos",
"Window": "Ventana",
"Window Shortcuts": "Atajos de ventana",
"YES": "SÍ",
"Zoom In": "Acercarse",
"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",
"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",
"Connect": "Relier",
"Connect to another organization": "Se connecter à une autre organisation",
"Connected organizations": "Organisations connectées",
"Copy": "Copie",
"Copy Zulip URL": "Copier l'URL Zulip",
"Create a new organization": "Créer une nouvelle organisation",
"Cut": "Couper",
"Default download location": "Emplacement de téléchargement par défaut",
"Delete": "Effacer",
"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 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",
"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",
"Functionality": "La fonctionnalité",
"General": "Général",
"Get beta updates": "Obtenir les mises à jour bêta",
"Hard Reload": "Rechargement dur",
"Help": "Aidez-moi",
"History": "Histoire",
"Help Center": "Centre d'aide",
"History": "L'histoire",
"History Shortcuts": "Raccourcis Histoire",
"Keyboard Shortcuts": "Raccourcis clavier",
"Log Out": "Connectez - Out",
"Log Out of Organization": "Déconnexion de l'organisation",
"Manual proxy configuration": "Configuration manuelle du proxy",
"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 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 Zulip": "Quittez Zulip",
"Redo": "Refaire",
"Release Notes": "Notes de version",
"Reload": "Recharger",
"Report an issue...": "Signaler un problème...",
"Reset App Settings": "Réinitialiser les paramètres",
"Report an Issue": "Signaler un problème",
"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",
"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 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 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",
"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",
"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 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": "वास्तविक आकार",
"Add Custom Certificates": "कस्टम प्रमाण पत्र जोड़ें",
"Add Organization": "संगठन जोड़ें",
"Add a Zulip organization": "एक जूलिप संगठन जोड़ें",
"Add custom CSS": "कस्टम CSS जोड़ें",
"Advanced": "उन्नत",
"All the connected organizations will appear here": "सभी जुड़े संगठन यहां दिखाई देंगे",
"Always start minimized": "हमेशा कम से कम शुरू करें",
"App Updates": "ऐप अपडेट",
"Appearance": "दिखावट",
"Application Shortcuts": "आवेदन शॉर्टकट",
"Are you sure you want to disconnect this organization?": "क्या आप वाकई इस संगठन को डिस्कनेक्ट करना चाहते हैं?",
"Auto hide Menu bar": "ऑटो मेनू मेनू छुपाएँ",
"Auto hide menu bar (Press Alt key to display)": "ऑटो छिपाने मेनू बार (प्रेस Alt कुंजी प्रदर्शित करने के लिए)",
"Back": "वापस",
"Bounce dock on new private message": "नए निजी संदेश पर बाउंस डॉक",
"Certificate file": "प्रमाणपत्र फ़ाइल",
"Change": "परिवर्तन",
"Check for Updates": "अद्यतन के लिए जाँच",
"Close": "बंद करे",
"Connect": "जुडिये",
"Connect to another organization": "किसी अन्य संगठन से कनेक्ट करें",
"Connected organizations": "जुड़े हुए संगठन",
"Copy": "प्रतिलिपि",
"Copy Zulip URL": "Zulip URL को कॉपी करें",
"Create a new organization": "एक नया संगठन बनाएं",
"Cut": "कट गया",
"Default download location": "डिफ़ॉल्ट डाउनलोड स्थान",
"Delete": "हटाना",
"Desktop App Settings": "डेस्कटॉप ऐप सेटिंग्स",
"Desktop Notifications": "डेस्कटॉप सूचनाएं",
"Desktop Settings": "डेस्कटॉप सेटिंग्स",
"Disconnect": "डिस्कनेक्ट",
"Download App Logs": "ऐप लॉग डाउनलोड करें",
"Edit": "संपादित करें",
"Edit Shortcuts": "शॉर्टकट संपादित करें",
"Enable auto updates": "ऑटो अपडेट सक्षम करें",
"Enable error reporting (requires restart)": "त्रुटि रिपोर्टिंग सक्षम करें (पुनरारंभ की आवश्यकता है)",
"Enable spellchecker (requires restart)": "वर्तनी जाँचक सक्षम करें (पुनः आरंभ करने की आवश्यकता है)",
"Factory Reset": "नए यंत्र जैसी सेटिंग",
"File": "फ़ाइल",
"Find accounts": "खाते ढूंढे",
"Find accounts by email": "ईमेल द्वारा खाते ढूंढें",
"Flash taskbar on new message": "नए संदेश पर फ्लैश टास्कबार",
"Forward": "आगे",
"Hard Reload": "कठिन पुन: लोड करें",
"Functionality": "कार्यक्षमता",
"General": "सामान्य",
"Get beta updates": "बीटा अपडेट प्राप्त करें",
"Hard Reload": "हार्ड रीलोड",
"Help": "मदद",
"Help Center": "सहायता केंद्र",
"History": "इतिहास",
"History Shortcuts": "इतिहास शॉर्टकट",
"Keyboard Shortcuts": "कुंजीपटल अल्प मार्ग",
"Log Out": "लोग आउट",
"Log Out of Organization": "संगठन से बाहर प्रवेश करें",
"Manual proxy configuration": "मैनुअल प्रॉक्सी कॉन्फ़िगरेशन",
"Minimize": "छोटा करना",
"Mute all sounds from Zulip": "ज़ूलिप से सभी ध्वनियों को म्यूट करें",
"NO": "नहीं",
"Network": "नेटवर्क",
"OR": "या",
"Organization URL": "संगठन का URL",
"Organizations": "संगठन",
"Paste": "चिपकाएं",
"Paste and Match Style": "चिपकाएं और शैली का मिलान करें",
"Paste and Match Style": "पेस्ट और मैच स्टाइल",
"Proxy": "प्रतिनिधि",
"Proxy bypass rules": "प्रॉक्सी बायपास नियम",
"Proxy rules": "प्रॉक्सी नियम",
"Quit": "छोड़ना",
"Quit Zulip": "ज़ुल्फ़ छोड़ो",
"Redo": "फिर से करना",
"Release Notes": "रिलीज नोट्स",
"Reload": "सीमा से अधिक लादना",
"Report an issue...": "मामले की रिपोर्ट करें...",
"Reset App Settings": "ऐप ेटिंग्स रीसेट करें",
"Report an Issue": "मामले की रिपोर्ट करें",
"Reset App Data": "ऐप ेट रीसेट करें",
"Reset App Settings": "ऐप सेटिंग रीसेट करें",
"Reset Application Data": "एप्लिकेशन डेटा रीसेट करें",
"Save": "बचाना",
"Select All": "सभी का चयन करे",
"Settings": "सेटिंग्स",
"Shortcuts": "शॉर्टकट",
"Show App Logs": "ऐप लॉग दिखाएं",
"Show app icon in system tray": "सिस्टम ट्रे में ऐप आइकन दिखाएं",
"Show app unread badge": "ऐप अपठित बैज दिखाएं",
"Show desktop notifications": "डेस्कटॉप सूचनाएं दिखाएं",
"Show downloaded files in file manager": "फ़ाइल प्रबंधक में डाउनलोड की गई फ़ाइलें दिखाएं",
"Show sidebar": "साइडबार दिखाओ",
"Start app at login": "लॉगिन पर ऐप शुरू करें",
"Switch to Next Organization": "अगला संगठन पर स्विच करें",
"Switch to Previous Organization": "पिछले संगठन पर स्विच करें",
"These desktop app shortcuts extend the Zulip webapp's": "ये डेस्कटॉप ऐप शॉर्टकट Zulip webapp's का विस्तार करते हैं",
"This will delete all application data including all added accounts and preferences": "यह सभी जोड़े गए खातों और वरीयताओं सहित सभी एप्लिकेशन डेटा को हटा देगा",
"Tip": "टिप",
"Toggle DevTools for Active Tab": "सक्रिय टैब के लिए DevTools टॉगल करें",
"Toggle DevTools for Zulip App": "Zulip ऐप के लिए DevTools टॉगल करें",
"Toggle DevTools for Zulip App": "Zulip App के लिए DevTools टॉगल करें",
"Toggle Do Not Disturb": "टॉगल डू नॉट डिस्टर्ब",
"Toggle Full Screen": "पूर्णस्क्रीन चालू करें",
"Toggle Sidebar": "साइडबार टॉगल करें",
"Toggle Tray Icon": "ट्रे आइकन टॉगल करें",
"Toggle Sidebar": "टॉगल साइडबार",
"Toggle Tray Icon": "टॉगल ट्रे आइकन",
"Tools": "उपकरण",
"Undo": "पूर्ववत करें",
"Upload": "अपलोड",
"Use system proxy settings (requires restart)": "सिस्टम प्रॉक्सी सेटिंग्स का उपयोग करें (पुनः आरंभ करने की आवश्यकता है)",
"View": "राय",
"View Shortcuts": "शॉर्टकट देखें",
"Window": "खिड़की",
"Window Shortcuts": "विंडो शॉर्टकट",
"YES": "हाँ",
"Zoom In": "ज़ूम इन",
"Zoom Out": "ज़ूम आउट",
"Zulip Help": "Zulip सहायता"
}
"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",
"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",
"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",
"Connect": "Csatlakozás",
"Connect to another organization": "Csatlakozás egy másik szervezethez",
"Connected organizations": "Kapcsolódó szervezetek",
"Copy": "Másolat",
"Copy Zulip URL": "Zulip URL másolása",
"Create a new organization": "Hozzon létre egy új szervezetet",
"Cut": "Vágott",
"Default download location": "Alapértelmezett letöltési hely",
"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 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",
"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",
"Functionality": "funkcionalitás",
"General": "Tábornok",
"Get beta updates": "Béta frissítéseket kap",
"Hard Reload": "Hard Reload",
"Help": "Segítség",
"Help Center": "Segítség Központ",
"History": "Történelem",
"History Shortcuts": "Előzmények parancsikonjai",
"Keyboard Shortcuts": "Gyorsbillentyűket",
"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",
"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 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 Zulip": "Lépjen ki Zulipból",
"Redo": "Újra",
"Release Notes": "Kiadási megjegyzések",
"Reload": "Reload",
"Report an issue...": "Hibajelentés ...",
"Reset App Settings": "Az alkalmazás beállításainak visszaállítása",
"Report an Issue": "Jelentés bejelentése",
"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",
"Settings": "Beállítások",
"Shortcuts": "parancsikonok",
"Show App Logs": "Alkalmazásnaplók megjelenítése",
"Toggle DevTools for Active Tab": "A DevTools aktiválása az Aktív laphoz",
"Toggle DevTools for Zulip App": "Kapcsolja a DevTools-ot a Zulip App-hoz",
"Show app icon in system tray": "Az alkalmazás ikonjának megjelenítése a tálcán",
"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 Sidebar": "Oldalsáv átkapcsolása",
"Toggle Tray Icon": "Tálcaikon átkapcsolása",
"Toggle Sidebar": "Az oldalsáv átkapcsolása",
"Toggle Tray Icon": "Átváltás a tálca ikonjára",
"Tools": "Eszközök",
"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 Shortcuts": "Gyorsbillentyűk megtekintése",
"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",
"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",
"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",
"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",
"Connect": "Menghubungkan",
"Connect to another organization": "Terhubung ke organisasi lain",
"Connected organizations": "Organisasi yang terhubung",
"Copy": "Salinan",
"Copy Zulip URL": "Salin URL Zulip",
"Create a new organization": "Buat organisasi baru",
"Cut": "Memotong",
"Default download location": "Lokasi unduhan default",
"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 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",
"Find accounts": "Temukan akun",
"Find accounts by email": "Temukan akun melalui email",
"Flash taskbar on new message": "Taskbar flash pada pesan baru",
"Forward": "Meneruskan",
"Functionality": "Fungsionalitas",
"General": "Umum",
"Get beta updates": "Dapatkan pembaruan beta",
"Hard Reload": "Hard Reload",
"Help": "Membantu",
"Help Center": "Pusat Bantuan",
"History": "Sejarah",
"Keyboard Shortcuts": "Cara pintas keyboard",
"History Shortcuts": "Pintasan Riwayat",
"Keyboard Shortcuts": "Pintasan keyboard",
"Log Out": "Keluar",
"Log Out of Organization": "Keluar dari Organisasi",
"Manual proxy configuration": "Konfigurasi proxy manual",
"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 and Match Style": "Tempel dan Cocokkan Gaya",
"Proxy": "Proksi",
"Proxy bypass rules": "Aturan memotong proxy",
"Proxy rules": "Aturan proxy",
"Quit": "Berhenti",
"Quit Zulip": "Berhenti Zulip",
"Redo": "Mengulangi",
"Reload": "Reload",
"Report an issue...": "Laporkan masalah ...",
"Reset App Settings": "Setel ulang Pengaturan Aplikasi",
"Release Notes": "Catatan Rilis",
"Reload": "Muat ulang",
"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",
"Show App Logs": "Tampilkan log aplikasi",
"Toggle DevTools for Active Tab": "Toggle DevTools untuk Tab Aktif",
"Toggle DevTools for Zulip App": "Toggle DevTools untuk Aplikasi Zulip",
"Toggle Full Screen": "Toggle Full Screen",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Toggle Tray Icon",
"Undo": "Membuka",
"Settings": "Pengaturan",
"Shortcuts": "Pintasan",
"Show App Logs": "Tampilkan Log Aplikasi",
"Show app icon in system tray": "Tampilkan ikon aplikasi di baki sistem",
"Show app unread badge": "Tampilkan lencana aplikasi yang belum dibaca",
"Show desktop notifications": "Tampilkan pemberitahuan desktop",
"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 Shortcuts": "Lihat Pintasan",
"Window": "Jendela",
"Window Shortcuts": "Pintasan Jendela",
"YES": "IYA NIH",
"Zoom In": "Perbesar",
"Zoom Out": "Zoom Out",
"Zulip Help": "Zulip Help"
"Zoom Out": "Perkecil",
"Zulip Help": "Bantuan Zulip",
"keyboard shortcuts": "pintasan keyboard",
"script": "naskah"
}

View File

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

View File

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

View File

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

View File

@@ -1,39 +1,117 @@
{
"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",
"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",
"Connect": "Aansluiten",
"Connect to another organization": "Maak verbinding met een andere organisatie",
"Connected organizations": "Verbonden organisaties",
"Copy": "Kopiëren",
"Copy Zulip URL": "Kopieer Zulip-URL",
"Create a new organization": "Maak een nieuwe organisatie",
"Cut": "Besnoeiing",
"Default download location": "Standaard downloadlocatie",
"Delete": "Verwijder",
"Desktop App Settings": "Desktop-app-instellingen",
"Desktop Notifications": "Bureaublad notificaties",
"Desktop Settings": "Desktop-instellingen",
"Disconnect": "Loskoppelen",
"Download App Logs": "Applogs downloaden",
"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",
"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",
"Hard Reload": "Moeilijke herladen",
"Functionality": "functionaliteit",
"General": "Algemeen",
"Get beta updates": "Ontvang bèta-updates",
"Hard Reload": "Harde herladen",
"Help": "Helpen",
"Help Center": "Helpcentrum",
"History": "Geschiedenis",
"History Shortcuts": "Geschiedenis Sneltoetsen",
"Keyboard Shortcuts": "Toetsenbord sneltoetsen",
"Log Out": "Uitloggen",
"Log Out of Organization": "Uitloggen van organisatie",
"Manual proxy configuration": "Handmatige proxyconfiguratie",
"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",
"Proxy": "volmacht",
"Proxy bypass rules": "Proxy-bypassregels",
"Proxy rules": "Proxy-regels",
"Quit": "ophouden",
"Quit Zulip": "Sluit Zulip",
"Redo": "Opnieuw doen",
"Release Notes": "Releaseopmerkingen",
"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 Application Data": "Herstel toepassingsgegevens",
"Save": "Opslaan",
"Select All": "Selecteer alles",
"Settings": "instellingen",
"Shortcuts": "shortcuts",
"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 Zulip App": "DevTools voor Zulip-app omschakelen",
"Toggle Do Not Disturb": "Schakel Niet storen in",
"Toggle Full Screen": "Volledig scherm activeren",
"Toggle Sidebar": "Zijbalk verschuiven",
"Toggle Tray Icon": "Pictogram Lade wisselen",
"Tools": "Hulpmiddelen",
"Undo": "ongedaan maken",
"Upload": "Uploaden",
"Use system proxy settings (requires restart)": "Systeem proxy-instellingen gebruiken (opnieuw opstarten vereist)",
"View": "Uitzicht",
"View Shortcuts": "Bekijk snelkoppelingen",
"Window": "Venster",
"Window Shortcuts": "Venster snelkoppelingen",
"YES": "JA",
"Zoom In": "In zoomen",
"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",
"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",
"Connect": "Połączyć",
"Connect to another organization": "Połącz się z inną organizacją",
"Connected organizations": "Związane organizacje",
"Copy": "Kopiuj",
"Copy Zulip URL": "Skopiuj adres URL Zulip",
"Create a new organization": "Utwórz nową organizację",
"Cut": "Ciąć",
"Default download location": "Domyślna lokalizacja pobierania",
"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 Shortcuts": "Edytuj skróty",
"Enable auto updates": "Włącz automatyczne aktualizacje",
"Enable error reporting (requires restart)": "Włącz raportowanie błędów (wymaga ponownego uruchomienia)",
"Enable spellchecker (requires restart)": "Włącz sprawdzanie pisowni (wymaga ponownego uruchomienia)",
"Factory Reset": "Przywrócenie ustawień fabrycznych",
"File": "Plik",
"Find accounts": "Znajdź konta",
"Find accounts by email": "Znajdź konta pocztą e-mail",
"Flash taskbar on new message": "Pasek zadań Flash w nowej wiadomości",
"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 Center": "Centrum pomocy",
"History": "Historia",
"History Shortcuts": "Skróty historii",
"Keyboard Shortcuts": "Skróty klawiszowe",
"Log Out": "Wyloguj",
"Log Out of Organization": "Wyloguj się z organizacji",
"Manual proxy configuration": "Ręczna konfiguracja proxy",
"Minimize": "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 and Match Style": "Wklej i dopasuj styl",
"Proxy": "Pełnomocnik",
"Proxy bypass rules": "Zasady omijania proxy",
"Proxy rules": "Zasady proxy",
"Quit": "Porzucić",
"Quit Zulip": "Zamknij Zulipa",
"Redo": "Przerobić",
"Release Notes": "Informacje o wydaniu",
"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 Application Data": "Zresetuj dane aplikacji",
"Save": "Zapisać",
"Select All": "Zaznacz wszystko",
"Settings": "Ustawienia",
"Shortcuts": "Skróty",
"Show App Logs": "Pokaż dzienniki aplikacji",
"Show app icon in system tray": "Pokaż ikonę aplikacji w zasobniku systemowym",
"Show app unread badge": "Pokaż 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 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 Sidebar": "Przełącz boczny pasek",
"Toggle Sidebar": "Przełącz pasek boczny",
"Toggle Tray Icon": "Przełącz ikonę tacy",
"Tools": "Przybory",
"Undo": "Cofnij",
"Upload": "Przekazać plik",
"Use system proxy settings (requires restart)": "Użyj ustawień serwera proxy (wymaga ponownego uruchomienia)",
"View": "Widok",
"View Shortcuts": "Wyświetl skróty",
"Window": "Okno",
"Window Shortcuts": "Skróty do okien",
"YES": "TAK",
"Zoom In": "Zbliżenie",
"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",
"Back": "Costas",
"Close": "Fechar",
"Add Custom Certificates": "Adicionar certificados personalizados",
"Add Organization": "Adicionar Organização",
"Add a Zulip organization": "Adicione uma organização Zulip",
"Add custom CSS": "Adicionar CSS personalizado",
"Advanced": "Avançado",
"All the connected organizations will appear here": "Todas as organizações conectadas aparecerão aqui",
"Always start minimized": "Começar sempre minimizado",
"App Updates": "Atualizações de aplicativos",
"Appearance": "Aparência",
"Application Shortcuts": "Atalhos de aplicativos",
"Are you sure you want to disconnect this organization?": "Tem certeza de que deseja desconectar essa organização?",
"Auto hide Menu bar": "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 Zulip URL": "Copiar URL do Zulip",
"Create a new organization": "Crie uma nova organização",
"Cut": "Cortar",
"Default download location": "Local de download padrão",
"Delete": "Excluir",
"Desktop App Settings": "Configurações 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 Shortcuts": "Editar atalhos",
"Enable auto updates": "Ativar atualizações automáticas",
"Enable error reporting (requires restart)": "Ativar relatório de erros (requer reinicialização)",
"Enable spellchecker (requires restart)": "Ativar verificação ortográfica (requer reinicialização)",
"Factory Reset": "Restauração de fábrica",
"File": "Arquivo",
"Find accounts": "Encontrar contas",
"Find accounts by email": "Encontre contas por email",
"Flash taskbar on new message": "Barra de tarefas em Flash na nova mensagem",
"Forward": "frente",
"Hard Reload": "Hard Recargar",
"Functionality": "Funcionalidade",
"General": "Geral",
"Get beta updates": "Receba atualizações beta",
"Hard Reload": "Hard Reload",
"Help": "Socorro",
"Help Center": "Centro de ajuda",
"History": "História",
"History Shortcuts": "Atalhos da História",
"Keyboard Shortcuts": "Atalhos do teclado",
"Log Out": "Sair",
"Log Out of Organization": "Sair da organização",
"Manual proxy configuration": "Configuração manual de proxy",
"Minimize": "Minimizar",
"Mute all sounds from Zulip": "Silencie todos os sons de Zulip",
"NO": "NÃO",
"Network": "Rede",
"OR": "OU",
"Organization URL": "URL da organização",
"Organizations": "Organizações",
"Paste": "Colar",
"Paste and Match Style": "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 Zulip": "Saia de Zulip",
"Redo": "Refazer",
"Release Notes": "Notas de Lançamento",
"Reload": "recarregar",
"Report an issue...": "Relatar um problema ...",
"Reset App Settings": "Redefinir as configurações da aplicação",
"Report an Issue": "Comunicar um problema",
"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",
"Show App Logs": "Mostrar logs de aplicativos",
"Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
"Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
"Settings": "Definições",
"Shortcuts": "Atalhos",
"Show App Logs": "Mostrar registros do aplicativo",
"Show app icon in system tray": "Mostrar ícone do aplicativo na bandeja do sistema",
"Show app unread badge": "Mostrar crachá não lido do aplicativo",
"Show desktop notifications": "Mostrar notificações da área de trabalho",
"Show downloaded files in file manager": "Mostrar arquivos baixados no gerenciador de arquivos",
"Show sidebar": "Mostrar barra lateral",
"Start app at login": "Inicie o aplicativo no login",
"Switch to Next Organization": "Mudar para a próxima organização",
"Switch to Previous Organization": "Mudar para a organização anterior",
"These desktop app shortcuts extend the Zulip webapp's": "Esses atalhos para aplicativos de desktop estendem o aplicativo da web do Zulip",
"This will delete all application data including all added accounts and preferences": "Isso excluirá todos os dados do aplicativo, incluindo todas as contas e preferências adicionadas",
"Tip": "Gorjeta",
"Toggle DevTools for Active Tab": "Alternar DevTools para a guia ativa",
"Toggle DevTools for Zulip App": "Alternar DevTools para Zulip App",
"Toggle Do Not Disturb": "Alternar não perturbe",
"Toggle Full Screen": "Alternar para o modo tela cheia",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Tray Icon": "Ícone Toggle Tray",
"Toggle Sidebar": "Alternar Barra Lateral",
"Toggle Tray Icon": "Alternar ícone da bandeja",
"Tools": "Ferramentas",
"Undo": "Desfazer",
"Upload": "Envio",
"Use system proxy settings (requires restart)": "Use as configurações de proxy do sistema (requer reinicialização)",
"View": "Visão",
"View Shortcuts": "Exibir atalhos",
"Window": "Janela",
"Window Shortcuts": "Atalhos de janela",
"YES": "SIM",
"Zoom In": "Mais Zoom",
"Zoom Out": "Reduzir o zoom",
"Zulip Help": "Zulip 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": "Фактический размер",
"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": "Закрыть",
"Copy": "копия",
"Cut": "Порез",
"Connect": "Подключиться",
"Connect to another organization": "Подключиться к другой организации",
"Connected organizations": "Связанные организации",
"Copy": "Копировать",
"Copy Zulip URL": "Скопируйте Zulip URL",
"Create a new organization": "Создать новую организацию",
"Cut": "Вырезать",
"Default download location": "Местоположение загрузок по умолчанию",
"Delete": "Удалить",
"Desktop App Settings": "Настройки настольных приложений",
"Edit": "редактировать",
"File": "файл",
"Desktop App Settings": "Настройки приложения",
"Desktop Notifications": "Уведомления в приложении",
"Desktop Settings": "Настройки приложения",
"Disconnect": "Отключить",
"Download App Logs": "Скачать журналы приложения",
"Edit": "Правка",
"Edit Shortcuts": "Изменить горячие клавиши",
"Enable auto updates": "Включить автообновления",
"Enable error reporting (requires restart)": "Включить отчет об ошибках (требуется перезагрузка)",
"Enable spellchecker (requires restart)": "Включить проверку орфографии (требуется перезагрузка)",
"Factory Reset": "Сброс к заводским настройкам",
"File": "Файл",
"Find accounts": "Найти аккаунты",
"Find accounts by email": "Найти аккаунты по электронной почте",
"Flash taskbar on new message": "Flash-панель задач на новое сообщение",
"Forward": "Вперед",
"Hard Reload": "Hard Reload",
"Help": "Помогите",
"History": "история",
"Functionality": "Функциональность",
"General": "Общий",
"Get beta updates": "Получить бета-обновления",
"Hard Reload": "Принудительная перезагрузка",
"Help": "Помощь",
"Help Center": "Центр помощи",
"History": "История",
"History Shortcuts": "Горячие клавиши истории",
"Keyboard Shortcuts": "Горячие клавиши",
"Log Out": "Выйти",
"Log Out of Organization": "Выйти из организации",
"Manual proxy configuration": "Ручная настройка прокси",
"Minimize": "Минимизировать",
"Mute all sounds from Zulip": "Отключить все звуки от Zulip",
"NO": "НЕТ",
"Network": "сеть",
"OR": "ИЛИ",
"Organization URL": "URL организации",
"Organizations": "Организации",
"Paste": "Вставить",
"Paste and Match Style": "Стиль вставки и совпадения",
"Quit": "Уволиться",
"Redo": "переделывать",
"Reload": "перезагружать",
"Report an issue...": "Сообщить о проблеме...",
"Paste and Match Style": "Вставить и соответствовать стилю",
"Proxy": "Прокси",
"Proxy bypass rules": "Правила обхода прокси",
"Proxy rules": "Правила проксирования",
"Quit": "Выйти",
"Quit Zulip": "Выйти из Zulip",
"Redo": "Повторить",
"Release Notes": "Примечания к выпуску",
"Reload": "Перезагрузить",
"Report an Issue": "Сообщить о проблеме",
"Reset App Data": "Сбросить данные приложения",
"Reset App Settings": "Сбросить настройки приложения",
"Reset Application Data": "Сбросить данные приложения",
"Save": "Сохранить",
"Select All": "Выбрать все",
"Show App Logs": "Показать журналы приложений",
"Toggle DevTools for Active Tab": "Toggle DevTools для активной вкладки",
"Toggle DevTools for Zulip App": "Toggle DevTools для приложения Zulip",
"Settings": "Настройки",
"Shortcuts": "Горячие клавиши",
"Show App Logs": "Показать журналы приложения",
"Show app icon in system tray": "Показать значок приложения в системном трее",
"Show app unread badge": "Показать непрочитанный значок приложения",
"Show desktop notifications": "Показывать уведомления на рабочем столе",
"Show downloaded files in file manager": "Показать загруженные файлы в файловом менеджере",
"Show sidebar": "Показать боковую панель",
"Start app at login": "Запустить приложение при входе",
"Switch to Next Organization": "Переключиться на следующую организацию",
"Switch to Previous Organization": "Переключиться на предыдущую организацию",
"These desktop app shortcuts extend the Zulip webapp's": "Эти горячие клавиши приложения расширяют возможности Zulip.",
"This will delete all application data including all added accounts and preferences": "Это удалит все данные приложения, включая все добавленные учетные записи и настройки",
"Tip": "Совет",
"Toggle DevTools for Active Tab": "Переключить DevTools для активной вкладки",
"Toggle DevTools for Zulip App": "Переключить DevTools для приложения Zulip",
"Toggle Do Not Disturb": "Переключить в режим не беспокоить",
"Toggle Full Screen": "Включить полноэкранный режим",
"Toggle Sidebar": "Переключить боковую панель",
"Toggle Tray Icon": "Иконка Toggle Tray",
"Undo": "расстегивать",
"View": "Посмотреть",
"Toggle Tray Icon": "Переключить Значок в трее",
"Tools": "Инструменты",
"Undo": "Повторить",
"Upload": "Загрузить",
"Use system proxy settings (requires restart)": "Использовать настройки системного прокси (требуется перезагрузка)",
"View": "Вид",
"View Shortcuts": "Посмотреть горячие клавиши окна",
"Window": "Окно",
"Zoom In": "Приблизить",
"Window Shortcuts": "Горячие клавиши окна",
"YES": "ДА",
"Zoom In": "Увеличить",
"Zoom Out": "Уменьшить",
"Zulip Help": "Zulip Help"
"Zulip Help": "Zulip Помощь",
"keyboard shortcuts": "горячие клавиши",
"script": "скрипт"
}

View File

@@ -1,39 +1,117 @@
{
"About Zulip": "О Зулипу",
"Actual Size": "Стварна величина",
"Add Custom Certificates": "Додајте прилагођене цертификате",
"Add Organization": "Додај организацију",
"Add a Zulip organization": "Додајте Зулип организацију",
"Add custom CSS": "Додајте прилагођени ЦСС",
"Advanced": "Напредно",
"All the connected organizations will appear here": "Овде ће се појавити све повезане организације",
"Always start minimized": "Увек започните минимизирано",
"App Updates": "Апп Упдатес",
"Appearance": "Изглед",
"Application Shortcuts": "Пречице за апликације",
"Are you sure you want to disconnect this organization?": "Јесте ли сигурни да желите прекинути везу с овом организацијом?",
"Auto hide Menu bar": "Ауто хиде Мену бар",
"Auto hide menu bar (Press Alt key to display)": "Аутоматско скривање траке менија (притисните тастер Алт да бисте приказали)",
"Back": "Назад",
"Bounce dock on new private message": "Одскочите у нову приватну поруку",
"Certificate file": "Датотека сертификата",
"Change": "Цханге",
"Check for Updates": "Провери ажурирања",
"Close": "Близу",
"Connect": "Повежи",
"Connect to another organization": "Повежите се са другом организацијом",
"Connected organizations": "Повезане организације",
"Copy": "Копирај",
"Copy Zulip URL": "Цопи Зулип УРЛ",
"Create a new organization": "Направите нову организацију",
"Cut": "Цут",
"Delete": "Избриши",
"Desktop App Settings": "Подешавања за десктоп апликације",
"Default download location": "Дефаулт довнлоад лоцатион",
"Delete": "Обриши",
"Desktop App Settings": "Подешавања апликације за десктоп рачунаре",
"Desktop Notifications": "Обавештења о радној површини",
"Desktop Settings": "Десктоп Сеттингс",
"Disconnect": "Дисцоннецт",
"Download App Logs": "Довнлоад Апп Логс",
"Edit": "Уредити",
"Edit Shortcuts": "Уреди пречице",
"Enable auto updates": "Омогући аутоматско ажурирање",
"Enable error reporting (requires restart)": "Омогући извештавање о грешкама (захтева поновно покретање)",
"Enable spellchecker (requires restart)": "Омогући провјеру правописа (захтијева поновно покретање)",
"Factory Reset": "Фацтори Ресет",
"File": "Филе",
"Find accounts": "Нађи рачуне",
"Find accounts by email": "Пронађите рачуне путем е-поште",
"Flash taskbar on new message": "Фласх трака задатака у новој поруци",
"Forward": "Напријед",
"Functionality": "Функционалност",
"General": "Генерал",
"Get beta updates": "Набавите бета ажурирања",
"Hard Reload": "Хард Релоад",
"Help": "Помоћ",
"History": "Историја",
"Help Center": "Центар за помоћ",
"History": "Хистори",
"History Shortcuts": "Историјске пречице",
"Keyboard Shortcuts": "Пречице на тастатури",
"Log Out": "Одјавити се",
"Minimize": "Минимизирај",
"Log Out of Organization": "Одјавите се из организације",
"Manual proxy configuration": "Мануал проки цонфигуратион",
"Minimize": "Минимизе",
"Mute all sounds from Zulip": "Искључите све звукове из Зулипа",
"NO": "НЕ",
"Network": "Мрежа",
"OR": "ОР",
"Organization URL": "УРЛ организације",
"Organizations": "Организације",
"Paste": "Пасте",
"Paste and Match Style": "Пасте и Матцх Стиле",
"Paste and Match Style": "Залепите и подесите стил",
"Proxy": "Заступник",
"Proxy bypass rules": "Проки бипасс правила",
"Proxy rules": "Проки рулес",
"Quit": "Одустати",
"Redo": "Поново",
"Quit Zulip": "Куит Зулип",
"Redo": "Редо",
"Release Notes": "Релеасе Нотес",
"Reload": "Освежи",
"Report an issue...": "Пријавите проблем...",
"Reset App Settings": "Поново поставите подешавања апликације",
"Report an Issue": "Пријавите проблем",
"Reset App Data": "Ресетирај податке о апликацији",
"Reset App Settings": "Ресетуј поставке апликације",
"Reset Application Data": "Ресетуј податке апликације",
"Save": "сачувати",
"Select All": "Изабери све",
"Show App Logs": рикажи евиденције апликација",
"Toggle DevTools for Active Tab": "Пребаците ДевТоолс за Ацтиве Таб",
"Toggle DevTools for Zulip App": "Пребаците ДевТоолс за Зулип Апп",
"Toggle Full Screen": "Пребаците цео екран",
"Toggle Sidebar": "Пребаците бочну траку",
"Settings": одешавања",
"Shortcuts": "Пречице",
"Show App Logs": "Прикажи дневнике апликација",
"Show app icon in system tray": "Покажи икону апликације у системској палети",
"Show app unread badge": "Покажи непрочитану значку апликације",
"Show desktop notifications": "Прикажи обавештења радне површине",
"Show downloaded files in file manager": "Прикажи преузете датотеке у управитељу датотека",
"Show sidebar": "Схов сидебар",
"Start app at login": "Покрените апликацију приликом пријављивања",
"Switch to Next Organization": "Пребаци се на следећу организацију",
"Switch to Previous Organization": "Пребаци се на претходну организацију",
"These desktop app shortcuts extend the Zulip webapp's": "Пречице за десктоп апликације проширују Зулип вебаппове",
"This will delete all application data including all added accounts and preferences": "Ово ће избрисати све податке о апликацији, укључујући све додатне налоге и поставке",
"Tip": "Савет",
"Toggle DevTools for Active Tab": "Пребаци ДевТоолс за Ацтиве Таб",
"Toggle DevTools for Zulip App": "Пребаци ДевТоолс за Зулип Апп",
"Toggle Do Not Disturb": "Тоггле До Нот Дистурб",
"Toggle Full Screen": "Тоггле Фулл Сцреен",
"Toggle Sidebar": "Тоггле Сидебар",
"Toggle Tray Icon": "Тоггле Траи Ицон",
"Tools": "Алати",
"Undo": "Ундо",
"Upload": "Отпремити",
"Use system proxy settings (requires restart)": "Користи поставке системског прокија (потребно је поново покренути)",
"View": "Поглед",
"View Shortcuts": "Прикажи пречице",
"Window": "Прозор",
"Window Shortcuts": "Пречице за прозор",
"YES": "ДА",
"Zoom In": "Увеличати",
"Zoom Out": "Зумирај",
"Zulip Help": "Зулип Хелп"
"Zoom Out": "Зоом Оут",
"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": "உண்மையான அளவு",
"Add Custom Certificates": "தனிப்பயன் சான்றிதழ்களைச் சேர்க்கவும்",
"Add Organization": "அமைப்பைச் சேர்",
"Add a Zulip organization": "ஒரு ஜூலிப் அமைப்பைச் சேர்க்கவும்",
"Add custom CSS": "தனிப்பயன் CSS ஐச் சேர்க்கவும்",
"Advanced": "மேம்பட்ட",
"All the connected organizations will appear here": "இணைக்கப்பட்ட அனைத்து அமைப்புகளும் இங்கே தோன்றும்",
"Always start minimized": "எப்போதும் குறைக்கத் தொடங்குங்கள்",
"App Updates": "பயன்பாட்டு புதுப்பிப்புகள்",
"Appearance": "தோற்றம்",
"Application Shortcuts": "பயன்பாட்டு குறுக்குவழிகள்",
"Are you sure you want to disconnect this organization?": "இந்த அமைப்பைத் துண்டிக்க விரும்புகிறீர்களா?",
"Auto hide Menu bar": "தானாக மறை பட்டி பட்டி",
"Auto hide menu bar (Press Alt key to display)": "தானாக மறை மெனு பட்டியை (காண்பிக்க Alt விசையை அழுத்தவும்)",
"Back": "மீண்டும்",
"Bounce dock on new private message": "புதிய தனிப்பட்ட செய்தியில் கப்பல்துறை பவுன்ஸ்",
"Certificate file": "சான்றிதழ் கோப்பு",
"Change": "மாற்றம்",
"Check for Updates": "புதுப்பிப்புகளைச் சரிபார்க்கவும்",
"Close": "நெருக்கமான",
"Connect": "இணைக்கவும்",
"Connect to another organization": "வேறொரு நிறுவனத்துடன் இணைக்கவும்",
"Connected organizations": "இணைக்கப்பட்ட நிறுவனங்கள்",
"Copy": "நகல்",
"Copy Zulip URL": "ஜூலிப் URL ஐ நகலெடுக்கவும்",
"Create a new organization": "புதிய அமைப்பை உருவாக்கவும்",
"Cut": "வெட்டு",
"Default download location": "இயல்புநிலை பதிவிறக்க இடம்",
"Delete": "அழி",
"Desktop App Settings": "டெஸ்க்டாப் பயன்பாட்டு அமைப்புகள்",
"Desktop Notifications": "டெஸ்க்டாப் அறிவிப்புகள்",
"Desktop Settings": "டெஸ்க்டாப் அமைப்புகள்",
"Disconnect": "துண்டி",
"Download App Logs": "பயன்பாட்டு பதிவுகள் பதிவிறக்கவும்",
"Edit": "தொகு",
"Edit Shortcuts": "குறுக்குவழிகளைத் திருத்து",
"Enable auto updates": "தானியங்கு புதுப்பிப்புகளை இயக்கு",
"Enable error reporting (requires restart)": "பிழை அறிக்கையை இயக்கு (மறுதொடக்கம் தேவை)",
"Enable spellchecker (requires restart)": "எழுத்துப்பிழை சரிபார்ப்பை இயக்கு (மறுதொடக்கம் தேவை)",
"Factory Reset": "தொழிற்சாலை மீட்டமைப்பு",
"File": "கோப்பு",
"Find accounts": "கணக்குகளைக் கண்டறியவும்",
"Find accounts by email": "மின்னஞ்சல் மூலம் கணக்குகளைக் கண்டறியவும்",
"Flash taskbar on new message": "புதிய செய்தியில் ஃபிளாஷ் பணிப்பட்டி",
"Forward": "முன்னோக்கி",
"Hard Reload": "கடினமாக மீண்டும் ஏற்றவும்",
"Functionality": "செயல்பாடு",
"General": "பொது",
"Get beta updates": "பீட்டா புதுப்பிப்புகளைப் பெறுங்கள்",
"Hard Reload": "கடின மறுஏற்றம்",
"Help": "உதவி",
"Help Center": "உதவி மையம்",
"History": "வரலாறு",
"History Shortcuts": "வரலாறு குறுக்குவழிகள்",
"Keyboard Shortcuts": "விசைப்பலகை குறுக்குவழிகள்",
"Log Out": "வெளியேறு",
"Log Out of Organization": "நிறுவனத்திலிருந்து வெளியேறு",
"Manual proxy configuration": "கையேடு ப்ராக்ஸி உள்ளமைவு",
"Minimize": "குறைத்தல்",
"Mute all sounds from Zulip": "ஜூலிப்பிலிருந்து எல்லா ஒலிகளையும் முடக்கு",
"NO": "இல்லை",
"Network": "வலைப்பின்னல்",
"OR": "அல்லது",
"Organization URL": "அமைப்பு URL",
"Organizations": "அமைப்புக்கள்",
"Paste": "ஒட்டு",
"Paste and Match Style": "உடை ஒட்டு மற்றும் பொருந்தும்",
"Paste and Match Style": "ஒட்டு மற்றும் போட்டி நடை",
"Proxy": "பதிலாள்",
"Proxy bypass rules": "ப்ராக்ஸி பைபாஸ் விதிகள்",
"Proxy rules": "ப்ராக்ஸி விதிகள்",
"Quit": "விட்டுவிட",
"Quit Zulip": "ஜூலிப்பை விட்டு வெளியேறு",
"Redo": "மீண்டும் செய்",
"Release Notes": "வெளியீட்டு குறிப்புகள்",
"Reload": "ஏற்றவும்",
"Report an issue...": "சிக்கலைப் புகார ...",
"Reset App Settings": "பயன்பாட்டு அமைப்புகளை மீட்டமைக்கவும்",
"Report an Issue": "ஒரு சிக்கலைப் புகாரளிக்கவும்",
"Reset App Data": "பயன்பாட்டு தரவை மீட்டமைக்கவும்",
"Reset App Settings": "பயன்பாட்டு அமைப்புகளை மீட்டமை",
"Reset Application Data": "பயன்பாட்டு தரவை மீட்டமைக்கவும்",
"Save": "சேமி",
"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 Zulip App": "Zulip பயன்பாட்டிற்கான DevTools ஐ மாற்று",
"Toggle DevTools for Zulip App": "ஜூலிப் பயன்பாட்டிற்கான DevTools ஐ மாற்று",
"Toggle Do Not Disturb": "தொந்தரவு செய்ய வேண்டாம் என்பதை நிலைமாற்று",
"Toggle Full Screen": "மாற்று முழுத்திரை",
"Toggle Sidebar": "பக்கப்பட்டி மாறு",
"Toggle Tray Icon": "ட்ரே ஐகானை மாற்று",
"Toggle Sidebar": "பக்கப்பட்டியை நிலைமாற்று",
"Toggle Tray Icon": "ட்டு ஐகானை மாற்று",
"Tools": "கருவிகள்",
"Undo": "செயல்தவிர்",
"Upload": "பதிவேற்றம்",
"Use system proxy settings (requires restart)": "கணினி ப்ராக்ஸி அமைப்புகளைப் பயன்படுத்தவும் (மறுதொடக்கம் தேவை)",
"View": "காண்க",
"View Shortcuts": "குறுக்குவழிகளைக் காண்க",
"Window": "ஜன்னல்",
"Window Shortcuts": "சாளர குறுக்குவழிகள்",
"YES": "ஆம்",
"Zoom In": "பெரிதாக்க",
"Zoom Out": "பெரிதாக்கு",
"Zulip Help": "Zulip உதவி"
"Zoom Out": "பெரிதாக்கு",
"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",
"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",
"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",
"Connect": "bağlamak",
"Connect to another organization": "Başka bir kuruma bağlan",
"Connected organizations": "Bağlı kuruluşlar",
"Copy": "kopya",
"Copy Zulip URL": "Zulip URL'sini kopyala",
"Create a new organization": "Yeni bir organizasyon oluştur",
"Cut": "Kesmek",
"Default download location": "Varsayılan indirme yeri",
"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 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",
"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",
"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 Center": "Yardım Merkezi",
"History": "Tarihçe",
"History Shortcuts": "Tarihçe Kısayolları",
"Keyboard Shortcuts": "Klavye kısayolları",
"Log Out": ıkış Yap",
"Log Out of Organization": "Kuruluştan Çıkma",
"Manual proxy configuration": "Manuel proxy yapılandırması",
"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 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 Zulip": "Zulip'i bırak",
"Redo": "yeniden yapmak",
"Release Notes": "Sürüm notları",
"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 Application Data": "Uygulama Verilerini Sıfırla",
"Save": "Kayıt etmek",
"Select All": "Hepsini seç",
"Show App Logs": "Uygulama Günlüğlerini Göster",
"Toggle DevTools for Active Tab": "Aktif Sekme İçin DevTools'a Geçiş Yap",
"Toggle DevTools for Zulip App": "Zulip App için DevTools'a Geçiş Yap",
"Settings": "Ayarlar",
"Shortcuts": "Kısayollar",
"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 Sidebar": "Kenar Çubuğunu Aç / Kapat",
"Toggle Tray Icon": "Tepsi Simgesini Aç / Kapat",
"Toggle Sidebar": "Kenar Çubuğunu Değiştir",
"Toggle Tray Icon": "Tepsi İkonunu Aç / Kapat",
"Tools": "Araçlar",
"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 Shortcuts": "Kısayolları Göster",
"Window": "pencere",
"Window Shortcuts": "Pencere Kısayolları",
"YES": "EVET",
"Zoom In": "Yakınlaştır",
"Zoom Out": "Uzaklaştır",
"Zulip Help": "Zulip Yardımı"
"Zoom Out": "Uzaklaştırmak",
"Zulip Help": "Zulip Yardım",
"keyboard shortcuts": "Klavye kısayolları",
"script": "senaryo"
}

View File

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

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