├── .editorconfig
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .htmlhintrc
├── .node-version
├── .stylelintrc
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app
├── main
│ ├── autoupdater.js
│ ├── index.js
│ ├── linuxupdater.js
│ ├── menu.js
│ └── startup.js
├── package-lock.json
├── package.json
├── renderer
│ ├── about.html
│ ├── css
│ │ ├── about.css
│ │ ├── main.css
│ │ ├── network.css
│ │ ├── preference.css
│ │ └── preload.css
│ ├── fonts
│ │ ├── MaterialIcons-Regular.ttf
│ │ └── Montserrat-Regular.ttf
│ ├── img
│ │ ├── ic_loading.gif
│ │ ├── ic_server_tab_default.png
│ │ ├── icon.png
│ │ └── zulip_network.png
│ ├── js
│ │ ├── components
│ │ │ ├── base.js
│ │ │ ├── functional-tab.js
│ │ │ ├── handle-external-link.js
│ │ │ ├── server-tab.js
│ │ │ ├── tab.js
│ │ │ └── webview.js
│ │ ├── electron-bridge.js
│ │ ├── feedback.js
│ │ ├── main.js
│ │ ├── notification
│ │ │ ├── darwin-notifications.js
│ │ │ ├── default-notification.js
│ │ │ ├── helpers.js
│ │ │ └── index.js
│ │ ├── pages
│ │ │ ├── network.js
│ │ │ └── preference
│ │ │ │ ├── add-certificate.js
│ │ │ │ ├── badge-settings.js
│ │ │ │ ├── base-section.js
│ │ │ │ ├── connected-org-section.js
│ │ │ │ ├── general-section.js
│ │ │ │ ├── nav.js
│ │ │ │ ├── network-section.js
│ │ │ │ ├── new-server-form.js
│ │ │ │ ├── preference.js
│ │ │ │ ├── server-info-form.js
│ │ │ │ ├── servers-section.js
│ │ │ │ └── shortcuts-section.js
│ │ ├── preload.js
│ │ ├── shared
│ │ │ └── preventdrag.js
│ │ ├── spellchecker.js
│ │ ├── tray.js
│ │ └── utils
│ │ │ ├── certificate-util.js
│ │ │ ├── common-util.js
│ │ │ ├── config-util.js
│ │ │ ├── default-util.js
│ │ │ ├── dnd-util.js
│ │ │ ├── domain-util.js
│ │ │ ├── link-util.js
│ │ │ ├── linux-update-util.js
│ │ │ ├── logger-util.js
│ │ │ ├── params-util.js
│ │ │ ├── proxy-util.js
│ │ │ ├── reconnect-util.js
│ │ │ ├── request-util.js
│ │ │ ├── sentry-util.js
│ │ │ └── system-util.js
│ ├── main.html
│ ├── network.html
│ └── preference.html
├── resources
│ ├── Icon.icns
│ ├── Icon.ico
│ ├── Icon.png
│ ├── sounds
│ │ └── ding.ogg
│ ├── tray
│ │ ├── traylinux.png
│ │ ├── trayosx.png
│ │ ├── trayosx@2x.png
│ │ ├── trayosx@3x.png
│ │ ├── trayosx@4x.png
│ │ └── traywin.ico
│ └── zulip.png
└── translations
│ ├── bg.json
│ ├── cs.json
│ ├── de.json
│ ├── en.json
│ ├── es.json
│ ├── fr.json
│ ├── hi.json
│ ├── hu.json
│ ├── id.json
│ ├── ja.json
│ ├── ko.json
│ ├── ml.json
│ ├── nl.json
│ ├── pl.json
│ ├── pt.json
│ ├── ru.json
│ ├── sr.json
│ ├── ta.json
│ └── tr.json
├── appveyor.yml
├── build
├── appdmg.png
├── entitlements.mas.plist
├── icon.icns
├── icon.ico
├── icons
│ ├── 1024x1024.png
│ ├── 128x128.png
│ ├── 16x16.png
│ ├── 24x24.png
│ ├── 256x256.png
│ ├── 32x32.png
│ ├── 48x48.png
│ ├── 512x512.png
│ ├── 64x64.png
│ └── 96x96.png
└── zulip.png
├── changelog.md
├── development.md
├── docs
├── Home.md
├── Windows.md
├── _Footer.md
└── desktop-release.md
├── gulpfile.js
├── how-to-install.md
├── package-lock.json
├── package.json
├── scripts
├── debian-add-repo.sh
├── debian-uninstaller.sh
├── travis-build-test.sh
└── travis-xvfb.sh
├── snap
├── gui
│ └── icon.png
└── snapcraft.yaml
├── tests
├── config.js
├── e2e
│ └── package.json
├── index.js
├── setup.js
├── test-add-organization.js
└── test-new-organization.js
├── tools
├── fetch-pull-request
├── fetch-pull-request.cmd
├── fetch-rebase-pull-request
├── fetch-rebase-pull-request.cmd
├── locale-helper
│ ├── index.js
│ ├── locale-template.json
│ └── supported-locales.js
├── push-to-pull-request
├── reinstall-node-modules
├── reinstall-node-modules.cmd
├── reinstall-node-modules.js
└── reset-to-pull-request
├── troubleshooting.md
└── zulip-electron-launcher.sh
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [{package.json,*.yml}]
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
3 | package-lock.json binary
4 | app/package-lock.json binary
5 | *.gif binary
6 | *.jpg binary
7 | *.jpeg binary
8 | *.eot binary
9 | *.woff binary
10 | *.woff2 binary
11 | *.svg binary
12 | *.ttf binary
13 | *.png binary
14 | *.otf binary
15 | *.tif binary
16 | *.ogg binary
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | - **Operating System**:
4 | - [ ] Windows
5 | - [ ] Linux/Ubuntu
6 | - [ ] macOS
7 | - **Clear steps to reproduce the issue**:
8 | - **Relevant error messages and/or screenshots**:
9 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ---
2 |
6 |
7 | **What's this PR do?**
8 |
9 | **Any background context you want to provide?**
10 |
11 | **Screenshots?**
12 |
13 | **You have tested this PR on:**
14 | - [ ] Windows
15 | - [ ] Linux/Ubuntu
16 | - [ ] macOS
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependency directories
2 | node_modules/
3 |
4 | # npm cache directory
5 | .npm
6 |
7 | # Compiled binary build directory
8 | dist/
9 |
10 | #snap generated files
11 | snap/parts
12 | snap/prime
13 | snap/snap
14 | snap/stage
15 | snap/*.snap
16 |
17 | # Logs
18 | logs
19 | *.log
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
24 | // osx garbage
25 | *.DS_Store
26 | .DS_Store
27 |
28 | # dotenv environment variables file
29 | .env
30 |
31 | # miscellaneous
32 | .idea
33 | config.gypi
34 |
35 | # Test generated files
36 | # tests/package.json
37 |
38 | .python-version
39 |
--------------------------------------------------------------------------------
/.htmlhintrc:
--------------------------------------------------------------------------------
1 | {
2 | "tagname-lowercase": true,
3 | "attr-lowercase": true,
4 | "attr-value-double-quotes": true,
5 | "attr-value-not-empty": false,
6 | "attr-no-duplication": true,
7 | "doctype-first": true,
8 | "tag-pair": true,
9 | "empty-tag-not-self-closed": true,
10 | "spec-char-escape": true,
11 | "id-unique": true,
12 | "src-not-empty": true,
13 | "title-require": true,
14 | "alt-require": false,
15 | "doctype-html5": true,
16 | "id-class-value": "dash",
17 | "style-disabled": false,
18 | "inline-style-disabled": false,
19 | "inline-script-disabled": false,
20 | "space-tab-mixed-disabled": "space4",
21 | "id-class-ad-disabled": false,
22 | "href-abs-or-rel": false,
23 | "attr-unsafe-chars": true,
24 | "head-script-disabled": true
25 | }
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 6.9.4
2 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | # Stylistic rules for CSS.
4 | "function-comma-space-after": "always",
5 | "function-comma-space-before": "never",
6 | "function-max-empty-lines": 0,
7 | "function-whitespace-after": "always",
8 |
9 | "value-keyword-case": "lower",
10 | "value-list-comma-newline-after": "always-multi-line",
11 | "value-list-comma-space-after": "always-single-line",
12 | "value-list-comma-space-before": "never",
13 | "value-list-max-empty-lines": 0,
14 |
15 | "unit-case": "lower",
16 | "property-case": "lower",
17 | "color-hex-case": "lower",
18 |
19 | "declaration-bang-space-before": "always",
20 | "declaration-colon-newline-after": "always-multi-line",
21 | "declaration-colon-space-after": "always-single-line",
22 | "declaration-colon-space-before": "never",
23 | "declaration-block-semicolon-newline-after": "always",
24 | "declaration-block-semicolon-space-before": "never",
25 | "declaration-block-trailing-semicolon": "always",
26 |
27 | "block-closing-brace-empty-line-before": "never",
28 | "block-closing-brace-newline-after": "always",
29 | "block-closing-brace-newline-before": "always",
30 | "block-opening-brace-newline-after": "always",
31 | "block-opening-brace-space-before": "always",
32 |
33 | "selector-attribute-brackets-space-inside": "never",
34 | "selector-attribute-operator-space-after": "never",
35 | "selector-attribute-operator-space-before": "never",
36 | "selector-combinator-space-after": "always",
37 | "selector-combinator-space-before": "always",
38 | "selector-descendant-combinator-no-non-space": true,
39 | "selector-pseudo-class-parentheses-space-inside": "never",
40 | "selector-pseudo-element-case": "lower",
41 | "selector-pseudo-element-colon-notation": "double",
42 | "selector-type-case": "lower",
43 | "selector-list-comma-newline-after": "always",
44 | "selector-list-comma-space-before": "never",
45 |
46 | "media-feature-colon-space-after": "always",
47 | "media-feature-colon-space-before": "never",
48 | "media-feature-name-case": "lower",
49 | "media-feature-parentheses-space-inside": "never",
50 | "media-feature-range-operator-space-after": "always",
51 | "media-feature-range-operator-space-before": "always",
52 | "media-query-list-comma-newline-after": "always",
53 | "media-query-list-comma-space-before": "never",
54 |
55 | "at-rule-name-case": "lower",
56 | "at-rule-name-space-after": "always",
57 | "at-rule-semicolon-newline-after": "always",
58 | "at-rule-semicolon-space-before": "never",
59 |
60 | "comment-whitespace-inside": "always",
61 | "indentation": 4,
62 |
63 | # Limit language features
64 | "color-no-hex": true,
65 | "color-named": "never",
66 | }
67 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 |
4 | os:
5 | - osx
6 | - linux
7 |
8 | addons:
9 | apt:
10 | packages:
11 | - build-essential
12 | - libxext-dev
13 | - libxtst-dev
14 | - libxkbfile-dev
15 |
16 | language: node_js
17 | node_js:
18 | - '8'
19 |
20 | before_install:
21 | - ./scripts/travis-xvfb.sh
22 | - npm install -g gulp
23 | - npm install
24 |
25 | cache:
26 | directories:
27 | - node_modules
28 | - app/node_modules
29 |
30 | script:
31 | - npm run travis
32 | notifications:
33 | webhooks:
34 | urls:
35 | - https://zulip.org/zulipbot/travis
36 | on_success: always
37 | on_failure: always
38 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thanks for taking the time to contribute!
4 |
5 | The following is a set of guidelines for contributing to Zulip Electron Desktop Client. These are just guidelines, not rules, so use your best judgement and feel free to propose changes to this document in a pull request.
6 |
7 | ## Getting Started
8 |
9 | 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.
10 |
11 | ## Community
12 |
13 | * The whole Zulip documentation, such as setting up a development environment, setting up with the Zulip webapp project, and testing, can be read [here](https://zulip.readthedocs.io).
14 |
15 | * If you have any questions regarding zulip-electron, open an [issue](https://github.com/zulip/zulip-electron/issues/new/) or ask it on [chat.zulip.org](https://chat.zulip.org/#narrow/stream/16-desktop).
16 |
17 | ## Issue
18 | Ensure the bug was not already reported by searching on GitHub under [issues](https://github.com/zulip/zulip-electron/issues). If you're unable to find an open issue addressing the bug, open a [new issue](https://github.com/zulip/zulip-electron/issues/new).
19 |
20 | The [zulipbot](https://github.com/zulip/zulipbot) helps to claim an issue by commenting the following in the comment section: "**@zulipbot** claim". **@zulipbot** will assign you to the issue and label the issue as **in progress**. For more details, check out [**@zulipbot**](https://github.com/zulip/zulipbot).
21 |
22 | Please pay attention to the following points while opening an issue.
23 |
24 | ### Does it happen on web browsers? (especially Chrome)
25 | Zulip's desktop client is based on Electron, which integrates the Chrome engine within a standalone application.
26 | If the problem you encounter can be reproduced on web browsers, it may be an issue with [Zulip web app](https://github.com/zulip/zulip).
27 |
28 | ### Write detailed information
29 | Detailed information is very helpful to understand an issue.
30 |
31 | For example:
32 | * How to reproduce the issue, step-by-step.
33 | * The expected behavior (or what is wrong).
34 | * Screenshots for GUI issues.
35 | * The application version.
36 | * The operating system.
37 | * The Zulip-Desktop version.
38 |
39 |
40 | ## Pull Requests
41 | Pull Requests are always welcome.
42 |
43 | 1. When you edit the code, please run `npm run test` to check the formatting of your code before you `git commit`.
44 | 2. Ensure the PR description clearly describes the problem and solution. It should include:
45 | * The operating system on which you tested.
46 | * The Zulip-Desktop version on which you tested.
47 | * The relevant issue number, if applicable.
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Deprecated Zulip Desktop repository
2 |
3 | This is a dead repository.
4 |
5 | See [zulip-desktop](https://github.com/zulip/zulip-desktop) for the current Zulip desktop app.
6 |
--------------------------------------------------------------------------------
/app/main/autoupdater.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { app, dialog, shell } = require('electron');
3 | const { autoUpdater } = require('electron-updater');
4 | const isDev = require('electron-is-dev');
5 |
6 | const ConfigUtil = require('./../renderer/js/utils/config-util.js');
7 |
8 | function appUpdater(updateFromMenu = false) {
9 | // Don't initiate auto-updates in development
10 | if (isDev) {
11 | return;
12 | }
13 |
14 | if (process.platform === 'linux' && !process.env.APPIMAGE) {
15 | const { linuxUpdateNotification } = require('./linuxupdater');
16 | linuxUpdateNotification();
17 | return;
18 | }
19 |
20 | let updateAvailable = false;
21 |
22 | // Create Logs directory
23 | const LogsDir = `${app.getPath('userData')}/Logs`;
24 |
25 | // Log whats happening
26 | const log = require('electron-log');
27 |
28 | log.transports.file.file = `${LogsDir}/updates.log`;
29 | log.transports.file.level = 'info';
30 | autoUpdater.logger = log;
31 |
32 | // Handle auto updates for beta/pre releases
33 | const isBetaUpdate = ConfigUtil.getConfigItem('betaUpdate');
34 |
35 | autoUpdater.allowPrerelease = isBetaUpdate || false;
36 |
37 | const eventsListenerRemove = ['update-available', 'update-not-available'];
38 | autoUpdater.on('update-available', info => {
39 | if (updateFromMenu) {
40 | dialog.showMessageBox({
41 | message: `A new version ${info.version}, of Zulip Desktop is available`,
42 | detail: 'The update will be downloaded in the background. You will be notified when it is ready to be installed.'
43 | });
44 |
45 | updateAvailable = true;
46 |
47 | // This is to prevent removal of 'update-downloaded' and 'error' event listener.
48 | eventsListenerRemove.forEach(event => {
49 | autoUpdater.removeAllListeners(event);
50 | });
51 | }
52 | });
53 |
54 | autoUpdater.on('update-not-available', () => {
55 | if (updateFromMenu) {
56 | dialog.showMessageBox({
57 | message: 'No updates available',
58 | detail: `You are running the latest version of Zulip Desktop.\nVersion: ${app.getVersion()}`
59 | });
60 | // Remove all autoUpdator listeners so that next time autoUpdator is manually called these
61 | // listeners don't trigger multiple times.
62 | autoUpdater.removeAllListeners();
63 | }
64 | });
65 |
66 | autoUpdater.on('error', error => {
67 | if (updateFromMenu) {
68 | const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates');
69 | dialog.showMessageBox({
70 | type: 'error',
71 | buttons: ['Manual Download', 'Cancel'],
72 | message: messageText,
73 | detail: (error).toString() + `\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulipchat.com/apps/.\n
74 | Current Version: ${app.getVersion()}`
75 | }, response => {
76 | if (response === 0) {
77 | shell.openExternal('https://zulipchat.com/apps/');
78 | }
79 | });
80 | // Remove all autoUpdator listeners so that next time autoUpdator is manually called these
81 | // listeners don't trigger multiple times.
82 | autoUpdater.removeAllListeners();
83 | }
84 | });
85 |
86 | // Ask the user if update is available
87 | // eslint-disable-next-line no-unused-vars
88 | autoUpdater.on('update-downloaded', event => {
89 | // Ask user to update the app
90 | dialog.showMessageBox({
91 | type: 'question',
92 | buttons: ['Install and Relaunch', 'Install Later'],
93 | defaultId: 0,
94 | message: `A new update ${event.version} has been downloaded`,
95 | detail: 'It will be installed the next time you restart the application'
96 | }, response => {
97 | if (response === 0) {
98 | setTimeout(() => {
99 | autoUpdater.quitAndInstall();
100 | // force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
101 | app.quit();
102 | }, 1000);
103 | }
104 | });
105 | });
106 | // Init for updates
107 | autoUpdater.checkForUpdates();
108 | }
109 |
110 | module.exports = {
111 | appUpdater
112 | };
113 |
--------------------------------------------------------------------------------
/app/main/linuxupdater.js:
--------------------------------------------------------------------------------
1 | const { app } = require('electron');
2 | const { Notification } = require('electron');
3 |
4 | const request = require('request');
5 | const semver = require('semver');
6 | const ConfigUtil = require('../renderer/js/utils/config-util');
7 | const ProxyUtil = require('../renderer/js/utils/proxy-util');
8 | const LinuxUpdateUtil = require('../renderer/js/utils/linux-update-util');
9 | const Logger = require('../renderer/js/utils/logger-util');
10 |
11 | const logger = new Logger({
12 | file: 'linux-update-util.log',
13 | timestamp: true
14 | });
15 |
16 | function linuxUpdateNotification() {
17 | let url = 'https://api.github.com/repos/zulip/zulip-electron/releases';
18 | url = ConfigUtil.getConfigItem('betaUpdate') ? url : url + '/latest';
19 | const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
20 |
21 | const options = {
22 | url,
23 | headers: {'User-Agent': 'request'},
24 | proxy: proxyEnabled ? ProxyUtil.getProxy(url) : '',
25 | ecdhCurve: 'auto'
26 | };
27 |
28 | request(options, (error, response, body) => {
29 | if (error) {
30 | logger.error('Linux update error.');
31 | logger.error(error);
32 | return;
33 | }
34 | if (response.statusCode < 400) {
35 | const data = JSON.parse(body);
36 | const latestVersion = ConfigUtil.getConfigItem('betaUpdate') ? data[0].tag_name : data.tag_name;
37 |
38 | if (semver.gt(latestVersion, app.getVersion())) {
39 | const notified = LinuxUpdateUtil.getUpdateItem(latestVersion);
40 | if (notified === null) {
41 | new Notification({title: 'Zulip Update', body: 'A new version ' + latestVersion + ' is available. Please update using your package manager.'}).show();
42 | LinuxUpdateUtil.setUpdateItem(latestVersion, true);
43 | }
44 | }
45 | } else {
46 | logger.log('Linux update response status: ', response.statusCode);
47 | }
48 | });
49 | }
50 |
51 | module.exports = {
52 | linuxUpdateNotification
53 | };
54 |
--------------------------------------------------------------------------------
/app/main/startup.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { app } = require('electron');
3 | const AutoLaunch = require('auto-launch');
4 | const isDev = require('electron-is-dev');
5 | const ConfigUtil = require('./../renderer/js/utils/config-util.js');
6 |
7 | const setAutoLaunch = AutoLaunchValue => {
8 | // Don't run this in development
9 | if (isDev) {
10 | return;
11 | }
12 |
13 | // On Mac, work around a bug in auto-launch where it opens a Terminal window
14 | // See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
15 |
16 | const appPath = process.platform === 'darwin' ? app.getPath('exe').replace(/\.app\/Content.*/, '.app') : undefined; // Use the default
17 |
18 | const ZulipAutoLauncher = new AutoLaunch({
19 | name: 'Zulip',
20 | path: appPath,
21 | isHidden: false
22 | });
23 | const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue);
24 |
25 | if (autoLaunchOption) {
26 | ZulipAutoLauncher.enable();
27 | } else {
28 | ZulipAutoLauncher.disable();
29 | }
30 | };
31 |
32 | module.exports = {
33 | setAutoLaunch
34 | };
35 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zulip",
3 | "productName": "Zulip",
4 | "version": "2.5.0-beta",
5 | "description": "Zulip Desktop App",
6 | "license": "Apache-2.0",
7 | "copyright": "Kandra Labs, Inc.",
8 | "author": {
9 | "name": "Kandra Labs, Inc.",
10 | "email": "support@zulipchat.com"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/zulip/zulip-electron.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/zulip/zulip-electron/issues"
18 | },
19 | "main": "main/index.js",
20 | "keywords": [
21 | "Zulip",
22 | "Group Chat app",
23 | "electron-app",
24 | "electron",
25 | "Desktop app",
26 | "InstantMessaging"
27 | ],
28 | "dependencies": {
29 | "@electron-elements/send-feedback": "1.0.8",
30 | "@sentry/electron": "0.14.0",
31 | "adm-zip": "0.4.11",
32 | "auto-launch": "5.0.5",
33 | "electron-is-dev": "0.3.0",
34 | "electron-log": "2.2.14",
35 | "electron-spellchecker": "1.1.2",
36 | "electron-updater": "4.0.6",
37 | "electron-window-state": "5.0.3",
38 | "escape-html": "1.0.3",
39 | "is-online": "7.0.0",
40 | "node-json-db": "0.9.1",
41 | "request": "2.85.0",
42 | "semver": "5.4.1",
43 | "wurl": "2.5.0"
44 | },
45 | "optionalDependencies": {
46 | "node-mac-notifier": "1.1.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/renderer/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Zulip - About
8 |
9 |
10 |
11 |
12 |

13 |
v?.?.?
14 |
15 |
16 | Maintained by
17 | Zulip
18 |
19 |
20 | Available under the
21 | Apache 2.0 License
22 |
23 |
24 |
25 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/app/renderer/css/about.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: rgba(250, 250, 250, 1.000);
3 | font-family: menu, "Helvetica Neue", sans-serif;
4 | -webkit-font-smoothing: subpixel-antialiased;
5 | }
6 |
7 | .logo {
8 | display: block;
9 | margin: -40px auto;
10 | }
11 |
12 | #version {
13 | color: rgba(68, 67, 67, 1.000);
14 | font-size: 1.3em;
15 | padding-top: 40px;
16 | }
17 |
18 | .about {
19 | margin: 25vh auto;
20 | height: 25vh;
21 | text-align: center;
22 | }
23 |
24 | .about p {
25 | font-size: 20px;
26 | color: rgba(0, 0, 0, 0.62);
27 | }
28 |
29 | .about img {
30 | width: 150px;
31 | }
32 |
33 | .detail {
34 | text-align: center;
35 | }
36 |
37 | .detail.maintainer {
38 | font-size: 1.2em;
39 | font-weight: 500;
40 | }
41 |
42 | .detail.license {
43 | font-size: 0.8em;
44 | }
45 |
46 | .maintenance-info {
47 | cursor: pointer;
48 | position: absolute;
49 | width: 100%;
50 | left: 0px;
51 | color: rgba(68, 68, 68, 1.000);
52 | }
53 |
54 | .maintenance-info p {
55 | margin: 0;
56 | font-size: 1em;
57 | width: 100%;
58 | }
59 |
60 | p.detail a {
61 | color: rgba(53, 95, 76, 1.000);
62 | }
63 |
64 | p.detail a:hover {
65 | text-decoration: underline;
66 | }
67 |
--------------------------------------------------------------------------------
/app/renderer/css/network.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | cursor: default;
5 | font-size: 14px;
6 | color: rgba(51, 51, 51, 1.000);
7 | background: rgba(255, 255, 255, 1.000);
8 | user-select: none;
9 | }
10 |
11 | #content {
12 | display: flex;
13 | flex-direction: column;
14 | font-family: "Trebuchet MS", Helvetica, sans-serif;
15 | margin: 100px 200px;
16 | text-align: center;
17 | }
18 |
19 | #title {
20 | font-size: 24px;
21 | font-weight: bold;
22 | margin: 20px 0;
23 | }
24 |
25 | #description {
26 | font-size: 16px;
27 | }
28 |
29 | #reconnect {
30 | font-size: 16px;
31 | background: rgba(0, 150, 136, 1.000);
32 | color: rgba(255, 255, 255, 1.000);
33 | width: 84px;
34 | height: 32px;
35 | border-radius: 5px;
36 | line-height: 32px;
37 | margin: 20px auto 0;
38 | cursor: pointer;
39 | }
40 |
41 | #reconnect:hover {
42 | opacity: 0.8;
43 | }
--------------------------------------------------------------------------------
/app/renderer/css/preload.css:
--------------------------------------------------------------------------------
1 | /* Override css rules */
2 |
3 | .portico-wrap > .header {
4 | display: none;
5 | }
6 |
7 | .portico-container > .footer {
8 | display: none;
9 | }
--------------------------------------------------------------------------------
/app/renderer/fonts/MaterialIcons-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/renderer/fonts/MaterialIcons-Regular.ttf
--------------------------------------------------------------------------------
/app/renderer/fonts/Montserrat-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/renderer/fonts/Montserrat-Regular.ttf
--------------------------------------------------------------------------------
/app/renderer/img/ic_loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/renderer/img/ic_loading.gif
--------------------------------------------------------------------------------
/app/renderer/img/ic_server_tab_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/renderer/img/ic_server_tab_default.png
--------------------------------------------------------------------------------
/app/renderer/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/renderer/img/icon.png
--------------------------------------------------------------------------------
/app/renderer/img/zulip_network.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/renderer/img/zulip_network.png
--------------------------------------------------------------------------------
/app/renderer/js/components/base.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class BaseComponent {
4 | generateNodeFromTemplate(template) {
5 | const wrapper = document.createElement('div');
6 | wrapper.innerHTML = template;
7 | return wrapper.firstElementChild;
8 | }
9 | }
10 |
11 | module.exports = BaseComponent;
12 |
--------------------------------------------------------------------------------
/app/renderer/js/components/functional-tab.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Tab = require(__dirname + '/../components/tab.js');
4 |
5 | class FunctionalTab extends Tab {
6 | template() {
7 | return `
8 |
9 | close
10 |
11 |
12 | ${this.props.materialIcon}
13 |
14 |
`;
15 | }
16 |
17 | init() {
18 | this.$el = this.generateNodeFromTemplate(this.template());
19 | if (this.props.name !== 'Settings') {
20 | this.props.$root.appendChild(this.$el);
21 | this.$closeButton = this.$el.getElementsByClassName('server-tab-badge')[0];
22 | this.registerListeners();
23 | }
24 | }
25 |
26 | registerListeners() {
27 | super.registerListeners();
28 |
29 | this.$el.addEventListener('mouseover', () => {
30 | this.$closeButton.classList.add('active');
31 | });
32 |
33 | this.$el.addEventListener('mouseout', () => {
34 | this.$closeButton.classList.remove('active');
35 | });
36 |
37 | this.$closeButton.addEventListener('click', e => {
38 | this.props.onDestroy();
39 | e.stopPropagation();
40 | });
41 | }
42 | }
43 |
44 | module.exports = FunctionalTab;
45 |
--------------------------------------------------------------------------------
/app/renderer/js/components/handle-external-link.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer } = require('electron');
2 | const { shell, app } = require('electron').remote;
3 | const LinkUtil = require('../utils/link-util');
4 | const DomainUtil = require('../utils/domain-util');
5 | const ConfigUtil = require('../utils/config-util');
6 |
7 | const dingSound = new Audio('../resources/sounds/ding.ogg');
8 |
9 | function handleExternalLink(event) {
10 | const { url } = event;
11 | const domainPrefix = DomainUtil.getDomain(this.props.index).url;
12 | const downloadPath = ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`);
13 | const shouldShowInFolder = ConfigUtil.getConfigItem('showDownloadFolder', false);
14 |
15 | // Whitelist URLs which are allowed to be opened in the app
16 | const {
17 | isInternalUrl: isWhiteListURL,
18 | isUploadsUrl: isUploadsURL
19 | } = LinkUtil.isInternal(domainPrefix, url);
20 |
21 | if (isWhiteListURL) {
22 | event.preventDefault();
23 |
24 | // Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
25 | // Show pdf attachments in a new window
26 | // if (LinkUtil.isPDF(url) && isUploadsURL) {
27 | // ipcRenderer.send('pdf-view', url);
28 | // return;
29 | // }
30 |
31 | // download txt, mp3, mp4 etc.. by using downloadURL in the
32 | // main process which allows the user to save the files to their desktop
33 | // and not trigger webview reload while image in webview will
34 | // do nothing and will not save it
35 |
36 | // Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
37 | // if (!LinkUtil.isImage(url) && !LinkUtil.isPDF(url) && isUploadsURL) {
38 | if (!LinkUtil.isImage(url) && isUploadsURL) {
39 | ipcRenderer.send('downloadFile', url, downloadPath);
40 | ipcRenderer.once('downloadFileCompleted', (event, filePath, fileName) => {
41 | const downloadNotification = new Notification('Download Complete', {
42 | body: shouldShowInFolder ? `Click to show ${fileName} in folder` : `Click to open ${fileName}`,
43 | silent: true // We'll play our own sound - ding.ogg
44 | });
45 |
46 | // Play sound to indicate download complete
47 | if (!ConfigUtil.getConfigItem('silent')) {
48 | dingSound.play();
49 | }
50 |
51 | downloadNotification.onclick = () => {
52 | if (shouldShowInFolder) {
53 | // Reveal file in download folder
54 | shell.showItemInFolder(filePath);
55 | } else {
56 | // Open file in the default native app
57 | shell.openItem(filePath);
58 | }
59 | };
60 | ipcRenderer.removeAllListeners('downloadFileFailed');
61 | });
62 |
63 | ipcRenderer.once('downloadFileFailed', () => {
64 | // Automatic download failed, so show save dialog prompt and download
65 | // through webview
66 | this.$el.downloadURL(url);
67 | ipcRenderer.removeAllListeners('downloadFileCompleted');
68 | });
69 | return;
70 | }
71 |
72 | // open internal urls inside the current webview.
73 | this.$el.loadURL(url);
74 | } else {
75 | event.preventDefault();
76 | shell.openExternal(url);
77 | }
78 | }
79 |
80 | module.exports = handleExternalLink;
81 |
--------------------------------------------------------------------------------
/app/renderer/js/components/server-tab.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Tab = require(__dirname + '/../components/tab.js');
4 | const SystemUtil = require(__dirname + '/../utils/system-util.js');
5 |
6 | const {ipcRenderer} = require('electron');
7 |
8 | class ServerTab extends Tab {
9 | template() {
10 | return `
11 |
${this.props.name}
12 |
13 |
14 |

15 |
16 |
${this.generateShortcutText()}
17 |
`;
18 | }
19 |
20 | init() {
21 | super.init();
22 |
23 | this.$badge = this.$el.getElementsByClassName('server-tab-badge')[0];
24 | }
25 |
26 | updateBadge(count) {
27 | if (count > 0) {
28 | const formattedCount = count > 999 ? '1K+' : count;
29 |
30 | this.$badge.innerHTML = formattedCount;
31 | this.$badge.classList.add('active');
32 | } else {
33 | this.$badge.classList.remove('active');
34 | }
35 | }
36 |
37 | generateShortcutText() {
38 | // Only provide shortcuts for server [0..10]
39 | if (this.props.index >= 10) {
40 | return '';
41 | }
42 |
43 | const shownIndex = this.props.index + 1;
44 |
45 | let shortcutText = '';
46 |
47 | if (SystemUtil.getOS() === 'Mac') {
48 | shortcutText = `⌘ ${shownIndex}`;
49 | } else {
50 | shortcutText = `Ctrl+${shownIndex}`;
51 | }
52 |
53 | // Array index == Shown index - 1
54 | ipcRenderer.send('switch-server-tab', shownIndex - 1);
55 |
56 | return shortcutText;
57 | }
58 | }
59 |
60 | module.exports = ServerTab;
61 |
--------------------------------------------------------------------------------
/app/renderer/js/components/tab.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const BaseComponent = require(__dirname + '/../components/base.js');
4 |
5 | class Tab extends BaseComponent {
6 | constructor(props) {
7 | super();
8 |
9 | this.props = props;
10 | this.webview = this.props.webview;
11 |
12 | this.init();
13 | }
14 |
15 | init() {
16 | this.$el = this.generateNodeFromTemplate(this.template());
17 | this.props.$root.appendChild(this.$el);
18 |
19 | this.registerListeners();
20 | }
21 |
22 | registerListeners() {
23 | this.$el.addEventListener('click', this.props.onClick);
24 | this.$el.addEventListener('mouseover', this.props.onHover);
25 | this.$el.addEventListener('mouseout', this.props.onHoverOut);
26 | }
27 |
28 | isLoading() {
29 | return this.webview.isLoading;
30 | }
31 |
32 | activate() {
33 | this.$el.classList.add('active');
34 | this.webview.load();
35 | }
36 |
37 | deactivate() {
38 | this.$el.classList.remove('active');
39 | this.webview.hide();
40 | }
41 |
42 | destroy() {
43 | this.$el.parentNode.removeChild(this.$el);
44 | this.webview.$el.parentNode.removeChild(this.webview.$el);
45 | }
46 | }
47 |
48 | module.exports = Tab;
49 |
--------------------------------------------------------------------------------
/app/renderer/js/components/webview.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 |
6 | const ConfigUtil = require(__dirname + '/../utils/config-util.js');
7 | const SystemUtil = require(__dirname + '/../utils/system-util.js');
8 | const { app, dialog } = require('electron').remote;
9 |
10 | const BaseComponent = require(__dirname + '/../components/base.js');
11 | const handleExternalLink = require(__dirname + '/../components/handle-external-link.js');
12 |
13 | const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
14 | class WebView extends BaseComponent {
15 | constructor(props) {
16 | super();
17 |
18 | this.props = props;
19 |
20 | this.zoomFactor = 1.0;
21 | this.loading = true;
22 | this.badgeCount = 0;
23 | this.customCSS = ConfigUtil.getConfigItem('customCSS');
24 | this.$webviewsContainer = document.querySelector('#webviews-container').classList;
25 | }
26 |
27 | template() {
28 | return `
38 | `;
39 | }
40 |
41 | init() {
42 | this.$el = this.generateNodeFromTemplate(this.template());
43 | this.props.$root.appendChild(this.$el);
44 |
45 | this.registerListeners();
46 | }
47 |
48 | registerListeners() {
49 | this.$el.addEventListener('new-window', event => {
50 | handleExternalLink.call(this, event);
51 | });
52 |
53 | if (shouldSilentWebview) {
54 | this.$el.addEventListener('dom-ready', () => {
55 | this.$el.setAudioMuted(true);
56 | });
57 | }
58 |
59 | this.$el.addEventListener('page-title-updated', event => {
60 | const { title } = event;
61 | this.badgeCount = this.getBadgeCount(title);
62 | this.props.onTitleChange();
63 | });
64 |
65 | this.$el.addEventListener('did-navigate-in-page', event => {
66 | const isSettingPage = event.url.includes('renderer/preference.html');
67 | if (isSettingPage) {
68 | return;
69 | }
70 | this.canGoBackButton();
71 | });
72 |
73 | this.$el.addEventListener('did-navigate', () => {
74 | this.canGoBackButton();
75 | });
76 |
77 | this.$el.addEventListener('page-favicon-updated', event => {
78 | const { favicons } = event;
79 |
80 | // This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
81 | // https://chat.zulip.org/static/images/favicon/favicon-pms.png
82 | if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') {
83 | // This api is only supported on macOS
84 | app.dock.setBadge('●');
85 | // bounce the dock
86 | if (ConfigUtil.getConfigItem('dockBouncing')) {
87 | app.dock.bounce();
88 | }
89 | }
90 | });
91 |
92 | this.$el.addEventListener('dom-ready', () => {
93 | if (this.props.role === 'server') {
94 | this.$el.classList.add('onload');
95 | }
96 | this.loading = false;
97 | this.show();
98 |
99 | // Refocus text boxes after reload
100 | // Remove when upstream issue https://github.com/electron/electron/issues/14474 is fixed
101 | this.$el.blur();
102 | this.$el.focus();
103 | });
104 |
105 | this.$el.addEventListener('did-fail-load', event => {
106 | const { errorDescription } = event;
107 | const hasConnectivityErr = (SystemUtil.connectivityERR.indexOf(errorDescription) >= 0);
108 | if (hasConnectivityErr) {
109 | console.error('error', errorDescription);
110 | this.props.onNetworkError();
111 | }
112 | });
113 |
114 | this.$el.addEventListener('did-start-loading', () => {
115 | let userAgent = SystemUtil.getUserAgent();
116 | if (!userAgent) {
117 | SystemUtil.setUserAgent(this.$el.getUserAgent());
118 | userAgent = SystemUtil.getUserAgent();
119 | }
120 | this.$el.setUserAgent(userAgent);
121 | });
122 | }
123 |
124 | getBadgeCount(title) {
125 | const messageCountInTitle = (/\(([0-9]+)\)/).exec(title);
126 | return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
127 | }
128 |
129 | show() {
130 | // Do not show WebView if another tab was selected and this tab should be in background.
131 | if (!this.props.isActive()) {
132 | return;
133 | }
134 |
135 | // To show or hide the loading indicator in the the active tab
136 | if (this.loading) {
137 | this.$webviewsContainer.remove('loaded');
138 | } else {
139 | this.$webviewsContainer.add('loaded');
140 | }
141 |
142 | this.$el.classList.remove('disabled');
143 | this.$el.classList.add('active');
144 | setTimeout(() => {
145 | if (this.props.role === 'server') {
146 | this.$el.classList.remove('onload');
147 | }
148 | }, 1000);
149 | this.focus();
150 | this.props.onTitleChange();
151 | // Injecting preload css in webview to override some css rules
152 | this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8'));
153 |
154 | // get customCSS again from config util to avoid warning user again
155 | this.customCSS = ConfigUtil.getConfigItem('customCSS');
156 | if (this.customCSS) {
157 | if (!fs.existsSync(this.customCSS)) {
158 | this.customCSS = null;
159 | ConfigUtil.setConfigItem('customCSS', null);
160 |
161 | const errMsg = 'The custom css previously set is deleted!';
162 | dialog.showErrorBox('custom css file deleted!', errMsg);
163 | return;
164 | }
165 |
166 | this.$el.insertCSS(fs.readFileSync(path.resolve(__dirname, this.customCSS), 'utf8'));
167 | }
168 | }
169 |
170 | focus() {
171 | // focus Webview and it's contents when Window regain focus.
172 | const webContents = this.$el.getWebContents();
173 | // HACK: webContents.isFocused() seems to be true even without the element
174 | // being in focus. So, we check against `document.activeElement`.
175 | if (webContents && this.$el !== document.activeElement) {
176 | // HACK: Looks like blur needs to be called on the previously focused
177 | // element to transfer focus correctly, in Electron v3.0.10
178 | // See https://github.com/electron/electron/issues/15718
179 | document.activeElement.blur();
180 | this.$el.focus();
181 | webContents.focus();
182 | }
183 | }
184 |
185 | hide() {
186 | this.$el.classList.add('disabled');
187 | this.$el.classList.remove('active');
188 | }
189 |
190 | load() {
191 | if (this.$el) {
192 | this.show();
193 | } else {
194 | this.init();
195 | }
196 | }
197 |
198 | zoomIn() {
199 | this.zoomFactor += 0.1;
200 | this.$el.setZoomFactor(this.zoomFactor);
201 | }
202 |
203 | zoomOut() {
204 | this.zoomFactor -= 0.1;
205 | this.$el.setZoomFactor(this.zoomFactor);
206 | }
207 |
208 | zoomActualSize() {
209 | this.zoomFactor = 1.0;
210 | this.$el.setZoomFactor(this.zoomFactor);
211 | }
212 |
213 | logOut() {
214 | this.$el.executeJavaScript('logout()');
215 | }
216 |
217 | showShortcut() {
218 | this.$el.executeJavaScript('shortcut()');
219 | }
220 |
221 | openDevTools() {
222 | this.$el.openDevTools();
223 | }
224 |
225 | back() {
226 | if (this.$el.canGoBack()) {
227 | this.$el.goBack();
228 | this.focus();
229 | }
230 | }
231 |
232 | canGoBackButton() {
233 | const $backButton = document.querySelector('#actions-container #back-action');
234 | if (this.$el.canGoBack()) {
235 | $backButton.classList.remove('disable');
236 | } else {
237 | $backButton.classList.add('disable');
238 | }
239 | }
240 |
241 | forward() {
242 | if (this.$el.canGoForward()) {
243 | this.$el.goForward();
244 | }
245 | }
246 |
247 | reload() {
248 | this.hide();
249 | // Shows the loading indicator till the webview is reloaded
250 | this.$webviewsContainer.remove('loaded');
251 | this.loading = true;
252 | this.$el.reload();
253 | }
254 |
255 | send(...param) {
256 | this.$el.send(...param);
257 | }
258 | }
259 |
260 | module.exports = WebView;
261 |
--------------------------------------------------------------------------------
/app/renderer/js/electron-bridge.js:
--------------------------------------------------------------------------------
1 | const events = require('events');
2 | const { ipcRenderer } = require('electron');
3 |
4 | // we have and will have some non camelcase stuff
5 | // while working with zulip so just turning the rule off
6 | // for the whole file.
7 | /* eslint-disable camelcase */
8 | class ElectronBridge extends events {
9 | send_event(...args) {
10 | this.emit(...args);
11 | }
12 |
13 | on_event(...args) {
14 | this.on(...args);
15 | }
16 | }
17 |
18 | const electron_bridge = new ElectronBridge();
19 |
20 | electron_bridge.on('total_unread_count', (...args) => {
21 | ipcRenderer.send('unread-count', ...args);
22 | });
23 |
24 | electron_bridge.on('realm_name', realmName => {
25 | const serverURL = location.origin;
26 | ipcRenderer.send('realm-name-changed', serverURL, realmName);
27 | });
28 |
29 | electron_bridge.on('realm_icon_url', iconURL => {
30 | const serverURL = location.origin;
31 | iconURL = iconURL.includes('http') ? iconURL : `${serverURL}${iconURL}`;
32 | ipcRenderer.send('realm-icon-changed', serverURL, iconURL);
33 | });
34 |
35 | // this follows node's idiomatic implementation of event
36 | // emitters to make event handling more simpler instead of using
37 | // functions zulip side will emit event using ElectronBrigde.send_event
38 | // which is alias of .emit and on this side we can handle the data by adding
39 | // a listener for the event.
40 | module.exports = electron_bridge;
41 |
--------------------------------------------------------------------------------
/app/renderer/js/feedback.js:
--------------------------------------------------------------------------------
1 | const { app } = require('electron').remote;
2 | const path = require('path');
3 | const fs = require('fs');
4 | const SendFeedback = require('@electron-elements/send-feedback');
5 |
6 | // make the button color match zulip app's theme
7 | SendFeedback.customStyles = `
8 | button:hover, button:focus {
9 | border-color: #4EBFAC;
10 | color: #4EBFAC;
11 | }
12 |
13 | button:active {
14 | background-color: #f1f1f1;
15 | color: #4EBFAC;
16 | }
17 |
18 | button {
19 | background-color: #4EBFAC;
20 | border-color: #4EBFAC;
21 | }
22 | `;
23 |
24 | customElements.define('send-feedback', SendFeedback);
25 | const sendFeedback = document.querySelector('send-feedback');
26 | const feedbackHolder = sendFeedback.parentElement;
27 |
28 | // customize the fields of custom elements
29 | sendFeedback.title = 'Report Issue';
30 | sendFeedback.titleLabel = 'Issue title:';
31 | sendFeedback.titlePlaceholder = 'Enter issue title';
32 | sendFeedback.textareaLabel = 'Describe the issue:';
33 | sendFeedback.textareaPlaceholder = 'Succinctly describe your issue and steps to reproduce it...';
34 | sendFeedback.buttonLabel = 'Report Issue';
35 | sendFeedback.loaderSuccessText = '';
36 |
37 | sendFeedback.useReporter('emailReporter', {
38 | email: 'akash@zulipchat.com'
39 | });
40 |
41 | feedbackHolder.addEventListener('click', e => {
42 | // only remove the class if the grey out faded
43 | // part is clicked and not the feedback element itself
44 | if (e.target === e.currentTarget) {
45 | feedbackHolder.classList.remove('show');
46 | }
47 | });
48 |
49 | sendFeedback.addEventListener('feedback-submitted', () => {
50 | setTimeout(() => {
51 | feedbackHolder.classList.remove('show');
52 | }, 1000);
53 | });
54 |
55 | const dataDir = app.getPath('userData');
56 | const logsDir = path.join(dataDir, '/Logs');
57 | sendFeedback.logs.push(...fs.readdirSync(logsDir).map(file => path.join(logsDir, file)));
58 |
59 | module.exports = {
60 | feedbackHolder,
61 | sendFeedback
62 | };
63 |
--------------------------------------------------------------------------------
/app/renderer/js/notification/darwin-notifications.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { ipcRenderer } = require('electron');
4 | const url = require('url');
5 | const MacNotifier = require('node-mac-notifier');
6 | const ConfigUtil = require('../utils/config-util');
7 | const {
8 | appId, customReply, focusCurrentServer, parseReply, setupReply
9 | } = require('./helpers');
10 |
11 | let replyHandler;
12 | let clickHandler;
13 | class DarwinNotification {
14 | constructor(title, opts) {
15 | const silent = ConfigUtil.getConfigItem('silent') || false;
16 | const { host, protocol } = location;
17 | const { icon } = opts;
18 | const profilePic = url.resolve(`${protocol}//${host}`, icon);
19 |
20 | this.tag = opts.tag;
21 | const notification = new MacNotifier(title, Object.assign(opts, {
22 | bundleId: appId,
23 | canReply: true,
24 | silent,
25 | icon: profilePic
26 | }));
27 |
28 | notification.addEventListener('click', () => {
29 | // focus to the server who sent the
30 | // notification if not focused already
31 | if (clickHandler) {
32 | clickHandler();
33 | }
34 |
35 | focusCurrentServer();
36 | ipcRenderer.send('focus-app');
37 | });
38 |
39 | notification.addEventListener('reply', this.notificationHandler);
40 | }
41 |
42 | static requestPermission() {
43 | return; // eslint-disable-line no-useless-return
44 | }
45 |
46 | // Override default Notification permission
47 | static get permission() {
48 | return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
49 | }
50 |
51 | set onreply(handler) {
52 | replyHandler = handler;
53 | }
54 |
55 | get onreply() {
56 | return replyHandler;
57 | }
58 |
59 | set onclick(handler) {
60 | clickHandler = handler;
61 | }
62 |
63 | get onclick() {
64 | return clickHandler;
65 | }
66 |
67 | // not something that is common or
68 | // used by zulip server but added to be
69 | // future proff.
70 | addEventListener(event, handler) {
71 | if (event === 'click') {
72 | clickHandler = handler;
73 | }
74 |
75 | if (event === 'reply') {
76 | replyHandler = handler;
77 | }
78 | }
79 |
80 | notificationHandler({ response }) {
81 | response = parseReply(response);
82 | focusCurrentServer();
83 | setupReply(this.tag);
84 |
85 | if (replyHandler) {
86 | replyHandler(response);
87 | return;
88 | }
89 |
90 | customReply(response);
91 | }
92 |
93 | // method specific to notification api
94 | // used by zulip
95 | close() {
96 | return; // eslint-disable-line no-useless-return
97 | }
98 | }
99 |
100 | module.exports = DarwinNotification;
101 |
--------------------------------------------------------------------------------
/app/renderer/js/notification/default-notification.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { ipcRenderer } = require('electron');
4 | const ConfigUtil = require('../utils/config-util');
5 | const { focusCurrentServer } = require('./helpers');
6 |
7 | const NativeNotification = window.Notification;
8 | class BaseNotification extends NativeNotification {
9 | constructor(title, opts) {
10 | opts.silent = true;
11 | super(title, opts);
12 |
13 | this.addEventListener('click', () => {
14 | // focus to the server who sent the
15 | // notification if not focused already
16 | focusCurrentServer();
17 | ipcRenderer.send('focus-app');
18 | });
19 | }
20 |
21 | static requestPermission() {
22 | return; // eslint-disable-line no-useless-return
23 | }
24 |
25 | // Override default Notification permission
26 | static get permission() {
27 | return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
28 | }
29 | }
30 |
31 | module.exports = BaseNotification;
32 |
--------------------------------------------------------------------------------
/app/renderer/js/notification/helpers.js:
--------------------------------------------------------------------------------
1 | const { remote } = require('electron');
2 | const Logger = require('../utils/logger-util.js');
3 |
4 | const logger = new Logger({
5 | file: 'errors.log',
6 | timestamp: true
7 | });
8 |
9 | // Do not change this
10 | const appId = 'org.zulip.zulip-electron';
11 |
12 | const botsList = [];
13 | let botsListLoaded = false;
14 |
15 | // this function load list of bots from the server
16 | // sync=True for a synchronous getJSON request
17 | // in case botsList isn't already completely loaded when required in parseRely
18 | function loadBots(sync = false) {
19 | const { $ } = window;
20 | botsList.length = 0;
21 | if (sync) {
22 | $.ajaxSetup({async: false});
23 | }
24 | $.getJSON('/json/users')
25 | .done(data => {
26 | const members = data.members;
27 | members.forEach(membersRow => {
28 | if (membersRow.is_bot) {
29 | const bot = `@${membersRow.full_name}`;
30 | const mention = `@**${bot.replace(/^@/, '')}**`;
31 | botsList.push([bot, mention]);
32 | }
33 | });
34 | botsListLoaded = true;
35 | })
36 | .fail(error => {
37 | logger.log('Load bots request failed: ', error.responseText);
38 | logger.log('Load bots request status: ', error.statusText);
39 | });
40 | if (sync) {
41 | $.ajaxSetup({async: true});
42 | }
43 | }
44 |
45 | function checkElements(...elements) {
46 | let status = true;
47 | elements.forEach(element => {
48 | if (element === null || element === undefined) {
49 | status = false;
50 | }
51 | });
52 | return status;
53 | }
54 |
55 | function customReply(reply) {
56 | // server does not support notification reply yet.
57 | const buttonSelector = '.messagebox #send_controls button[type=submit]';
58 | const messageboxSelector = '.selected_message .messagebox .messagebox-border .messagebox-content';
59 | const textarea = document.querySelector('#compose-textarea');
60 | const messagebox = document.querySelector(messageboxSelector);
61 | const sendButton = document.querySelector(buttonSelector);
62 |
63 | // sanity check for old server versions
64 | const elementsExists = checkElements(textarea, messagebox, sendButton);
65 | if (!elementsExists) {
66 | return;
67 | }
68 |
69 | textarea.value = reply;
70 | messagebox.click();
71 | sendButton.click();
72 | }
73 |
74 | const currentWindow = remote.getCurrentWindow();
75 | const webContents = remote.getCurrentWebContents();
76 | const webContentsId = webContents.id;
77 |
78 | // this function will focus the server that sent
79 | // the notification. Main function implemented in main.js
80 | function focusCurrentServer() {
81 | currentWindow.send('focus-webview-with-id', webContentsId);
82 | }
83 | // this function parses the reply from to notification
84 | // making it easier to reply from notification eg
85 | // @username in reply will be converted to @**username**
86 | // #stream in reply will be converted to #**stream**
87 | // bot mentions are not yet supported
88 | function parseReply(reply) {
89 | const usersDiv = document.querySelectorAll('#user_presences li');
90 | const streamHolder = document.querySelectorAll('#stream_filters li');
91 | const users = [];
92 | const streams = [];
93 |
94 | usersDiv.forEach(userRow => {
95 | const anchor = userRow.querySelector('span a');
96 | if (anchor !== null) {
97 | const user = `@${anchor.textContent.trim()}`;
98 | const mention = `@**${user.replace(/^@/, '')}**`;
99 | users.push([user, mention]);
100 | }
101 | });
102 |
103 | streamHolder.forEach(stream => {
104 | const streamAnchor = stream.querySelector('div a');
105 | if (streamAnchor !== null) {
106 | const streamName = `#${streamAnchor.textContent.trim()}`;
107 | const streamMention = `#**${streamName.replace(/^#/, '')}**`;
108 | streams.push([streamName, streamMention]);
109 | }
110 | });
111 |
112 | users.forEach(([user, mention]) => {
113 | if (reply.includes(user)) {
114 | const regex = new RegExp(user, 'g');
115 | reply = reply.replace(regex, mention);
116 | }
117 | });
118 |
119 | streams.forEach(([stream, streamMention]) => {
120 | const regex = new RegExp(stream, 'g');
121 | reply = reply.replace(regex, streamMention);
122 | });
123 |
124 | // If botsList isn't completely loaded yet, make a synchronous getJSON request for list
125 | if (botsListLoaded === false) {
126 | loadBots(true);
127 | }
128 |
129 | // Iterate for every bot name and replace in reply
130 | // @botname with @**botname**
131 | botsList.forEach(([bot, mention]) => {
132 | if (reply.includes(bot)) {
133 | const regex = new RegExp(bot, 'g');
134 | reply = reply.replace(regex, mention);
135 | }
136 | });
137 |
138 | reply = reply.replace(/\\n/, '\n');
139 | return reply;
140 | }
141 |
142 | function setupReply(id) {
143 | const { narrow } = window;
144 | const narrowByTopic = narrow.by_topic || narrow.by_subject;
145 | narrowByTopic(id, { trigger: 'notification' });
146 | }
147 |
148 | module.exports = {
149 | appId,
150 | checkElements,
151 | customReply,
152 | parseReply,
153 | setupReply,
154 | focusCurrentServer,
155 | loadBots
156 | };
157 |
--------------------------------------------------------------------------------
/app/renderer/js/notification/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {
4 | remote: { app }
5 | } = require('electron');
6 |
7 | const params = require('../utils/params-util.js');
8 | const DefaultNotification = require('./default-notification');
9 | const { appId, loadBots } = require('./helpers');
10 |
11 | // From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
12 | // On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
13 | app.setAppUserModelId(appId);
14 |
15 | window.Notification = DefaultNotification;
16 |
17 | if (process.platform === 'darwin') {
18 | const DarwinNotification = require('./darwin-notifications');
19 | window.Notification = DarwinNotification;
20 | }
21 |
22 | window.addEventListener('load', () => {
23 | // eslint-disable-next-line no-undef, camelcase
24 | if (params.isPageParams() && page_params.realm_uri) {
25 | loadBots();
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/app/renderer/js/pages/network.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {ipcRenderer} = require('electron');
4 |
5 | class NetworkTroubleshootingView {
6 | constructor() {
7 | this.$reconnectButton = document.getElementById('reconnect');
8 | }
9 |
10 | init() {
11 | this.$reconnectButton.addEventListener('click', () => {
12 | ipcRenderer.send('forward-message', 'reload-viewer');
13 | });
14 | }
15 | }
16 |
17 | window.onload = () => {
18 | const networkTroubleshootingView = new NetworkTroubleshootingView();
19 | networkTroubleshootingView.init();
20 | };
21 |
--------------------------------------------------------------------------------
/app/renderer/js/pages/preference/add-certificate.js:
--------------------------------------------------------------------------------
1 | 'use-strict';
2 |
3 | const { dialog } = require('electron').remote;
4 | const path = require('path');
5 |
6 | const BaseComponent = require(__dirname + '/../../components/base.js');
7 | const CertificateUtil = require(__dirname + '/../../utils/certificate-util.js');
8 | const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
9 |
10 | class AddCertificate extends BaseComponent {
11 | constructor(props) {
12 | super();
13 | this.props = props;
14 | this._certFile = '';
15 | }
16 |
17 | template() {
18 | return `
19 |
29 | `;
30 | }
31 |
32 | init() {
33 | this.$addCertificate = this.generateNodeFromTemplate(this.template());
34 | this.props.$root.appendChild(this.$addCertificate);
35 | this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button');
36 | this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0];
37 | this.initListeners();
38 | }
39 |
40 | validateAndAdd() {
41 | const certificate = this._certFile;
42 | const serverUrl = this.serverUrl.value;
43 | if (certificate !== '' && serverUrl !== '') {
44 | const server = encodeURIComponent(DomainUtil.formatUrl(serverUrl));
45 | const fileName = path.basename(certificate);
46 | const copy = CertificateUtil.copyCertificate(server, certificate, fileName);
47 | if (!copy) {
48 | return;
49 | }
50 | CertificateUtil.setCertificate(server, fileName);
51 | dialog.showMessageBox({
52 | title: 'Success',
53 | message: `Certificate saved!`
54 | });
55 | this.serverUrl.value = '';
56 | } else {
57 | dialog.showErrorBox('Error', `Please, ${serverUrl === '' ?
58 | 'Enter an Organization URL' : 'Choose certificate file'}`);
59 | }
60 | }
61 |
62 | addHandler() {
63 | const showDialogOptions = {
64 | title: 'Select file',
65 | defaultId: 1,
66 | properties: ['openFile'],
67 | filters: [{ name: 'crt, pem', extensions: ['crt', 'pem'] }]
68 | };
69 | dialog.showOpenDialog(showDialogOptions, selectedFile => {
70 | if (selectedFile) {
71 | this._certFile = selectedFile[0] || '';
72 | this.validateAndAdd();
73 | }
74 | });
75 | }
76 |
77 | initListeners() {
78 | this.addCertificateButton.addEventListener('click', () => {
79 | this.addHandler();
80 | });
81 |
82 | this.serverUrl.addEventListener('keypress', event => {
83 | const EnterkeyCode = event.keyCode;
84 |
85 | if (EnterkeyCode === 13) {
86 | this.addHandler();
87 | }
88 | });
89 | }
90 | }
91 |
92 | module.exports = AddCertificate;
93 |
--------------------------------------------------------------------------------
/app/renderer/js/pages/preference/badge-settings.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const electron = require('electron');
3 | const { app } = require('electron');
4 |
5 | const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
6 |
7 | let instance = null;
8 |
9 | class BadgeSettings {
10 | constructor() {
11 | if (instance) {
12 | return instance;
13 | } else {
14 | instance = this;
15 | }
16 |
17 | return instance;
18 | }
19 |
20 | showBadgeCount(messageCount, mainWindow) {
21 | if (process.platform === 'darwin') {
22 | app.setBadgeCount(messageCount);
23 | }
24 | if (process.platform === 'win32') {
25 | this.updateOverlayIcon(messageCount, mainWindow);
26 | }
27 | }
28 |
29 | hideBadgeCount(mainWindow) {
30 | if (process.platform === 'darwin') {
31 | app.setBadgeCount(0);
32 | }
33 | if (process.platform === 'win32') {
34 | mainWindow.setOverlayIcon(null, '');
35 | }
36 | }
37 |
38 | updateBadge(badgeCount, mainWindow) {
39 | if (ConfigUtil.getConfigItem('badgeOption', true)) {
40 | this.showBadgeCount(badgeCount, mainWindow);
41 | } else {
42 | this.hideBadgeCount(mainWindow);
43 | }
44 | }
45 |
46 | updateOverlayIcon(messageCount, mainWindow) {
47 | if (!mainWindow.isFocused()) {
48 | mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
49 | }
50 | if (messageCount === 0) {
51 | mainWindow.setOverlayIcon(null, '');
52 | } else {
53 | mainWindow.webContents.send('render-taskbar-icon', messageCount);
54 | }
55 | }
56 |
57 | updateTaskbarIcon(data, text, mainWindow) {
58 | const img = electron.nativeImage.createFromDataURL(data);
59 | mainWindow.setOverlayIcon(img, text);
60 | }
61 | }
62 |
63 | module.exports = new BadgeSettings();
64 |
--------------------------------------------------------------------------------
/app/renderer/js/pages/preference/base-section.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {ipcRenderer} = require('electron');
4 |
5 | const BaseComponent = require(__dirname + '/../../components/base.js');
6 |
7 | class BaseSection extends BaseComponent {
8 | generateSettingOption(props) {
9 | const {$element, value, clickHandler} = props;
10 |
11 | $element.innerHTML = '';
12 |
13 | const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value));
14 | $element.appendChild($optionControl);
15 |
16 | $optionControl.addEventListener('click', clickHandler);
17 | }
18 |
19 | generateOptionTemplate(settingOption) {
20 | if (settingOption) {
21 | return `
22 |
28 | `;
29 | } else {
30 | return `
31 |
37 | `;
38 | }
39 | }
40 |
41 | reloadApp() {
42 | ipcRenderer.send('forward-message', 'reload-viewer');
43 | }
44 | }
45 |
46 | module.exports = BaseSection;
47 |
--------------------------------------------------------------------------------
/app/renderer/js/pages/preference/connected-org-section.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const BaseSection = require(__dirname + '/base-section.js');
4 | const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
5 | const ServerInfoForm = require(__dirname + '/server-info-form.js');
6 | const AddCertificate = require(__dirname + '/add-certificate.js');
7 |
8 | class ConnectedOrgSection extends BaseSection {
9 | constructor(props) {
10 | super();
11 | this.props = props;
12 | }
13 |
14 | template() {
15 | return `
16 |
17 |
Connected organizations
18 |
All the connected orgnizations will appear here.
19 |
20 |
21 |
Add Custom Certificates
22 |
23 |
24 | `;
25 | }
26 |
27 | init() {
28 | this.initServers();
29 | }
30 |
31 | initServers() {
32 | this.props.$root.innerHTML = '';
33 |
34 | const servers = DomainUtil.getDomains();
35 | this.props.$root.innerHTML = this.template();
36 |
37 | this.$serverInfoContainer = document.getElementById('server-info-container');
38 | this.$existingServers = document.getElementById('existing-servers');
39 | this.$newOrgButton = document.getElementById('new-org-button');
40 | this.$addCertificateContainer = document.getElementById('add-certificate-container');
41 |
42 | const noServerText = 'All the connected orgnizations will appear here';
43 | // Show noServerText if no servers are there otherwise hide it
44 | this.$existingServers.innerHTML = servers.length === 0 ? noServerText : '';
45 |
46 | for (let i = 0; i < servers.length; i++) {
47 | new ServerInfoForm({
48 | $root: this.$serverInfoContainer,
49 | server: servers[i],
50 | index: i,
51 | onChange: this.reloadApp
52 | }).init();
53 | }
54 |
55 | this.$newOrgButton.addEventListener('click', () => {
56 | // We don't need to import this since it's already imported in other files
57 | // eslint-disable-next-line no-undef
58 | ipcRenderer.send('forward-message', 'open-org-tab');
59 | });
60 |
61 | this.initAddCertificate();
62 | }
63 |
64 | initAddCertificate() {
65 | new AddCertificate({
66 | $root: this.$addCertificateContainer
67 | }).init();
68 | }
69 |
70 | }
71 |
72 | module.exports = ConnectedOrgSection;
73 |
--------------------------------------------------------------------------------
/app/renderer/js/pages/preference/nav.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const BaseComponent = require(__dirname + '/../../components/base.js');
4 |
5 | class PreferenceNav extends BaseComponent {
6 | constructor(props) {
7 | super();
8 |
9 | this.props = props;
10 |
11 | this.navItems = ['General', 'Network', 'AddServer', 'Organizations', 'Shortcuts'];
12 |
13 | this.init();
14 | }
15 |
16 | template() {
17 | let navItemsTemplate = '';
18 | for (const navItem of this.navItems) {
19 | navItemsTemplate += `${navItem}
`;
20 | }
21 |
22 | return `
23 |
24 |
25 |
${navItemsTemplate}
26 |
27 | `;
28 | }
29 |
30 | init() {
31 | this.$el = this.generateNodeFromTemplate(this.template());
32 | this.props.$root.appendChild(this.$el);
33 |
34 | this.registerListeners();
35 | }
36 |
37 | registerListeners() {
38 | for (const navItem of this.navItems) {
39 | const $item = document.getElementById(`nav-${navItem}`);
40 | $item.addEventListener('click', () => {
41 | this.props.onItemSelected(navItem);
42 | });
43 | }
44 | }
45 |
46 | select(navItemToSelect) {
47 | for (const navItem of this.navItems) {
48 | if (navItem === navItemToSelect) {
49 | this.activate(navItem);
50 | } else {
51 | this.deactivate(navItem);
52 | }
53 | }
54 | }
55 |
56 | activate(navItem) {
57 | const $item = document.getElementById(`nav-${navItem}`);
58 | $item.classList.add('active');
59 | }
60 |
61 | deactivate(navItem) {
62 | const $item = document.getElementById(`nav-${navItem}`);
63 | $item.classList.remove('active');
64 | }
65 | }
66 |
67 | module.exports = PreferenceNav;
68 |
--------------------------------------------------------------------------------
/app/renderer/js/pages/preference/network-section.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {ipcRenderer} = require('electron');
4 |
5 | const BaseSection = require(__dirname + '/base-section.js');
6 | const ConfigUtil = require(__dirname + '/../../utils/config-util.js');
7 |
8 | class NetworkSection extends BaseSection {
9 | constructor(props) {
10 | super();
11 | this.props = props;
12 | }
13 |
14 | template() {
15 | return `
16 |
17 |
Proxy
18 |
19 |
20 |
Use system proxy settings (requires restart)
21 |
22 |
23 |
24 |
Manual proxy configuration
25 |
26 |
27 |
46 |
47 |
48 | `;
49 | }
50 |
51 | init() {
52 | this.props.$root.innerHTML = this.template();
53 | this.$proxyPAC = document.querySelector('#proxy-pac-option .setting-input-value');
54 | this.$proxyRules = document.querySelector('#proxy-rules-option .setting-input-value');
55 | this.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value');
56 | this.$proxySaveAction = document.getElementById('proxy-save-action');
57 | this.$manualProxyBlock = this.props.$root.querySelector('.manual-proxy-block');
58 | this.initProxyOption();
59 |
60 | this.$proxyPAC.value = ConfigUtil.getConfigItem('proxyPAC', '');
61 | this.$proxyRules.value = ConfigUtil.getConfigItem('proxyRules', '');
62 | this.$proxyBypass.value = ConfigUtil.getConfigItem('proxyBypass', '');
63 |
64 | this.$proxySaveAction.addEventListener('click', () => {
65 | ConfigUtil.setConfigItem('proxyPAC', this.$proxyPAC.value);
66 | ConfigUtil.setConfigItem('proxyRules', this.$proxyRules.value);
67 | ConfigUtil.setConfigItem('proxyBypass', this.$proxyBypass.value);
68 |
69 | ipcRenderer.send('forward-message', 'reload-proxy', true);
70 | });
71 | }
72 |
73 | initProxyOption() {
74 | const manualProxyEnabled = ConfigUtil.getConfigItem('useManualProxy', false);
75 | this.toggleManualProxySettings(manualProxyEnabled);
76 |
77 | this.updateProxyOption();
78 | }
79 |
80 | toggleManualProxySettings(option) {
81 | if (option) {
82 | this.$manualProxyBlock.classList.remove('hidden');
83 | } else {
84 | this.$manualProxyBlock.classList.add('hidden');
85 | }
86 | }
87 |
88 | updateProxyOption() {
89 | this.generateSettingOption({
90 | $element: document.querySelector('#use-system-settings .setting-control'),
91 | value: ConfigUtil.getConfigItem('useSystemProxy', false),
92 | clickHandler: () => {
93 | const newValue = !ConfigUtil.getConfigItem('useSystemProxy');
94 | const manualProxyValue = ConfigUtil.getConfigItem('useManualProxy');
95 | if (manualProxyValue && newValue) {
96 | ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue);
97 | this.toggleManualProxySettings(!manualProxyValue);
98 | }
99 | if (newValue === false) {
100 | // Remove proxy system proxy settings
101 | ConfigUtil.setConfigItem('proxyRules', '');
102 | ipcRenderer.send('forward-message', 'reload-proxy', false);
103 | }
104 | ConfigUtil.setConfigItem('useSystemProxy', newValue);
105 | this.updateProxyOption();
106 | }
107 | });
108 | this.generateSettingOption({
109 | $element: document.querySelector('#use-manual-settings .setting-control'),
110 | value: ConfigUtil.getConfigItem('useManualProxy', false),
111 | clickHandler: () => {
112 | const newValue = !ConfigUtil.getConfigItem('useManualProxy');
113 | const systemProxyValue = ConfigUtil.getConfigItem('useSystemProxy');
114 | this.toggleManualProxySettings(newValue);
115 | if (systemProxyValue && newValue) {
116 | ConfigUtil.setConfigItem('useSystemProxy', !systemProxyValue);
117 | }
118 | ConfigUtil.setConfigItem('proxyRules', '');
119 | ConfigUtil.setConfigItem('useManualProxy', newValue);
120 | // Reload app only when turning manual proxy off, hence !newValue
121 | ipcRenderer.send('forward-message', 'reload-proxy', !newValue);
122 | this.updateProxyOption();
123 | }
124 | });
125 | }
126 | }
127 |
128 | module.exports = NetworkSection;
129 |
--------------------------------------------------------------------------------
/app/renderer/js/pages/preference/new-server-form.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const BaseComponent = require(__dirname + '/../../components/base.js');
4 | const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
5 | const shell = require('electron').shell;
6 |
7 | class NewServerForm extends BaseComponent {
8 | constructor(props) {
9 | super();
10 | this.props = props;
11 | }
12 |
13 | template() {
14 | return `
15 |
36 | `;
37 | }
38 |
39 | init() {
40 | this.initForm();
41 | this.initActions();
42 | }
43 |
44 | initForm() {
45 | this.$newServerForm = this.generateNodeFromTemplate(this.template());
46 | this.$saveServerButton = this.$newServerForm.getElementsByClassName('server-save-action')[0];
47 | this.props.$root.innerHTML = '';
48 | this.props.$root.appendChild(this.$newServerForm);
49 |
50 | this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0];
51 | }
52 |
53 | submitFormHandler() {
54 | this.$saveServerButton.children[0].innerHTML = 'Connecting...';
55 | DomainUtil.checkDomain(this.$newServerUrl.value).then(serverConf => {
56 | DomainUtil.addDomain(serverConf).then(() => {
57 | this.props.onChange(this.props.index);
58 | });
59 | }, errorMessage => {
60 | this.$saveServerButton.children[0].innerHTML = 'Connect';
61 | alert(errorMessage);
62 | });
63 | }
64 |
65 | openCreateNewOrgExternalLink() {
66 | const link = 'https://zulipchat.com/new/';
67 | const externalCreateNewOrgEl = document.getElementById('open-create-org-link');
68 | externalCreateNewOrgEl.addEventListener('click', () => {
69 | shell.openExternal(link);
70 | });
71 | }
72 |
73 | initActions() {
74 | this.$saveServerButton.addEventListener('click', () => {
75 | this.submitFormHandler();
76 | });
77 | this.$newServerUrl.addEventListener('keypress', event => {
78 | const EnterkeyCode = event.keyCode;
79 | // Submit form when Enter key is pressed
80 | if (EnterkeyCode === 13) {
81 | this.submitFormHandler();
82 | }
83 | });
84 | // open create new org link in default browser
85 | this.openCreateNewOrgExternalLink();
86 | }
87 | }
88 |
89 | module.exports = NewServerForm;
90 |
--------------------------------------------------------------------------------
/app/renderer/js/pages/preference/preference.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const BaseComponent = require(__dirname + '/js/components/base.js');
4 | const { ipcRenderer } = require('electron');
5 |
6 | const Nav = require(__dirname + '/js/pages/preference/nav.js');
7 | const ServersSection = require(__dirname + '/js/pages/preference/servers-section.js');
8 | const GeneralSection = require(__dirname + '/js/pages/preference/general-section.js');
9 | const NetworkSection = require(__dirname + '/js/pages/preference/network-section.js');
10 | const ConnectedOrgSection = require(__dirname + '/js/pages/preference/connected-org-section.js');
11 | const ShortcutsSection = require(__dirname + '/js/pages/preference/shortcuts-section.js');
12 |
13 | class PreferenceView extends BaseComponent {
14 | constructor() {
15 | super();
16 |
17 | this.$sidebarContainer = document.getElementById('sidebar');
18 | this.$settingsContainer = document.getElementById('settings-container');
19 | }
20 |
21 | init() {
22 | this.nav = new Nav({
23 | $root: this.$sidebarContainer,
24 | onItemSelected: this.handleNavigation.bind(this)
25 | });
26 |
27 | this.setDefaultView();
28 | this.registerIpcs();
29 | }
30 |
31 | setDefaultView() {
32 | let nav = 'General';
33 | const hasTag = window.location.hash;
34 | if (hasTag) {
35 | nav = hasTag.substring(1);
36 | }
37 | this.handleNavigation(nav);
38 | }
39 |
40 | handleNavigation(navItem) {
41 | this.nav.select(navItem);
42 | switch (navItem) {
43 | case 'AddServer': {
44 | this.section = new ServersSection({
45 | $root: this.$settingsContainer
46 | });
47 | break;
48 | }
49 | case 'General': {
50 | this.section = new GeneralSection({
51 | $root: this.$settingsContainer
52 | });
53 | break;
54 | }
55 | case 'Organizations': {
56 | this.section = new ConnectedOrgSection({
57 | $root: this.$settingsContainer
58 | });
59 | break;
60 | }
61 | case 'Network': {
62 | this.section = new NetworkSection({
63 | $root: this.$settingsContainer
64 | });
65 | break;
66 | }
67 | case 'Shortcuts': {
68 | this.section = new ShortcutsSection({
69 | $root: this.$settingsContainer
70 | });
71 | break;
72 | }
73 | default: break;
74 | }
75 | this.section.init();
76 | window.location.hash = `#${navItem}`;
77 | }
78 |
79 | // Handle toggling and reflect changes in preference page
80 | handleToggle(elementName, state) {
81 | const inputSelector = `#${elementName} .action .switch input`;
82 | const input = document.querySelector(inputSelector);
83 | if (input) {
84 | input.checked = state;
85 | }
86 | }
87 |
88 | registerIpcs() {
89 | ipcRenderer.on('switch-settings-nav', (event, navItem) => {
90 | this.handleNavigation(navItem);
91 | });
92 |
93 | ipcRenderer.on('toggle-sidebar-setting', (event, state) => {
94 | this.handleToggle('sidebar-option', state);
95 | });
96 |
97 | ipcRenderer.on('toggletray', (event, state) => {
98 | this.handleToggle('tray-option', state);
99 | });
100 |
101 | ipcRenderer.on('toggle-dnd', (event, state, newSettings) => {
102 | this.handleToggle('show-notification-option', newSettings.showNotification);
103 | this.handleToggle('silent-option', newSettings.silent);
104 |
105 | if (process.platform === 'win32') {
106 | this.handleToggle('flash-taskbar-option', newSettings.flashTaskbarOnMessage);
107 | }
108 | });
109 | }
110 | }
111 |
112 | window.onload = () => {
113 | const preferenceView = new PreferenceView();
114 | preferenceView.init();
115 | };
116 |
--------------------------------------------------------------------------------
/app/renderer/js/pages/preference/server-info-form.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { dialog } = require('electron').remote;
3 | const { ipcRenderer } = require('electron');
4 |
5 | const BaseComponent = require(__dirname + '/../../components/base.js');
6 | const DomainUtil = require(__dirname + '/../../utils/domain-util.js');
7 |
8 | class ServerInfoForm extends BaseComponent {
9 | constructor(props) {
10 | super();
11 | this.props = props;
12 | }
13 |
14 | template() {
15 | return `
16 |
17 |
18 |

19 |
20 | ${this.props.server.alias}
21 | open_in_new
22 |
23 |
24 |
25 |
26 | ${this.props.server.url}
27 |
28 |
29 |
30 | Disconnect
31 |
32 |
33 |
34 |
35 | `;
36 | }
37 |
38 | init() {
39 | this.initForm();
40 | this.initActions();
41 | }
42 |
43 | initForm() {
44 | this.$serverInfoForm = this.generateNodeFromTemplate(this.template());
45 | this.$serverInfoAlias = this.$serverInfoForm.getElementsByClassName('server-info-alias')[0];
46 | this.$serverIcon = this.$serverInfoForm.getElementsByClassName('server-info-icon')[0];
47 | this.$deleteServerButton = this.$serverInfoForm.getElementsByClassName('server-delete-action')[0];
48 | this.$openServerButton = this.$serverInfoForm.getElementsByClassName('open-tab-button')[0];
49 | this.props.$root.appendChild(this.$serverInfoForm);
50 | }
51 |
52 | initActions() {
53 | this.$deleteServerButton.addEventListener('click', () => {
54 | dialog.showMessageBox({
55 | type: 'warning',
56 | buttons: ['YES', 'NO'],
57 | defaultId: 0,
58 | message: 'Are you sure you want to disconnect this organization?'
59 | }, response => {
60 | if (response === 0) {
61 | DomainUtil.removeDomain(this.props.index);
62 | this.props.onChange(this.props.index);
63 | }
64 | });
65 | });
66 |
67 | this.$openServerButton.addEventListener('click', () => {
68 | ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
69 | });
70 |
71 | this.$serverInfoAlias.addEventListener('click', () => {
72 | ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
73 | });
74 |
75 | this.$serverIcon.addEventListener('click', () => {
76 | ipcRenderer.send('forward-message', 'switch-server-tab', this.props.index);
77 | });
78 | }
79 |
80 | }
81 |
82 | module.exports = ServerInfoForm;
83 |
--------------------------------------------------------------------------------
/app/renderer/js/pages/preference/servers-section.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const BaseSection = require(__dirname + '/base-section.js');
4 | const NewServerForm = require(__dirname + '/new-server-form.js');
5 |
6 | class ServersSection extends BaseSection {
7 | constructor(props) {
8 | super();
9 | this.props = props;
10 | }
11 |
12 | template() {
13 | return `
14 |
15 |
16 |
17 |
Add a Zulip organization
18 |
19 |
20 |
21 |
22 | `;
23 | }
24 |
25 | init() {
26 | this.initServers();
27 | }
28 |
29 | initServers() {
30 | this.props.$root.innerHTML = '';
31 |
32 | this.props.$root.innerHTML = this.template();
33 | this.$newServerContainer = document.getElementById('new-server-container');
34 |
35 | this.initNewServerForm();
36 | }
37 |
38 | initNewServerForm() {
39 | new NewServerForm({
40 | $root: this.$newServerContainer,
41 | onChange: this.reloadApp
42 | }).init();
43 | }
44 | }
45 |
46 | module.exports = ServersSection;
47 |
--------------------------------------------------------------------------------
/app/renderer/js/preload.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { ipcRenderer, shell } = require('electron');
4 | const SetupSpellChecker = require('./spellchecker');
5 |
6 | const ConfigUtil = require(__dirname + '/utils/config-util.js');
7 | const LinkUtil = require(__dirname + '/utils/link-util.js');
8 | const params = require(__dirname + '/utils/params-util.js');
9 |
10 | // eslint-disable-next-line import/no-unassigned-import
11 | require('./notification');
12 |
13 | // Prevent drag and drop event in main process which prevents remote code executaion
14 | require(__dirname + '/shared/preventdrag.js');
15 |
16 | // eslint-disable-next-line camelcase
17 | window.electron_bridge = require('./electron-bridge');
18 |
19 | const logout = () => {
20 | // Create the menu for the below
21 | document.querySelector('.dropdown-toggle').click();
22 |
23 | const nodes = document.querySelectorAll('.dropdown-menu li:last-child a');
24 | nodes[nodes.length - 1].click();
25 | };
26 |
27 | const shortcut = () => {
28 | // Create the menu for the below
29 | const node = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
30 | // Additional check
31 | if (node.text.trim().toLowerCase() === 'keyboard shortcuts (?)') {
32 | node.click();
33 | } else {
34 | // Atleast click the dropdown
35 | document.querySelector('.dropdown-toggle').click();
36 | }
37 | };
38 |
39 | process.once('loaded', () => {
40 | global.logout = logout;
41 | global.shortcut = shortcut;
42 | });
43 |
44 | // To prevent failing this script on linux we need to load it after the document loaded
45 | document.addEventListener('DOMContentLoaded', () => {
46 | if (params.isPageParams()) {
47 | // Get the default language of the server
48 | const serverLanguage = page_params.default_language; // eslint-disable-line no-undef, camelcase
49 | if (serverLanguage) {
50 | // Set spellcheker language
51 | ConfigUtil.setConfigItem('spellcheckerLanguage', serverLanguage);
52 | // Init spellchecker
53 | SetupSpellChecker.init();
54 | }
55 | // redirect users to network troubleshooting page
56 | const getRestartButton = document.querySelector('.restart_get_events_button');
57 | if (getRestartButton) {
58 | getRestartButton.addEventListener('click', () => {
59 | ipcRenderer.send('forward-message', 'reload-viewer');
60 | });
61 | }
62 | // Open image attachment link in the lightbox instead of opening in the default browser
63 | const { $, lightbox } = window;
64 | $('#main_div').on('click', '.message_content p a', function (e) {
65 | const url = $(this).attr('href');
66 |
67 | if (LinkUtil.isImage(url)) {
68 | const $img = $(this).parent().siblings('.message_inline_image').find('img');
69 |
70 | // prevent the image link from opening in a new page.
71 | e.preventDefault();
72 | // prevent the message compose dialog from happening.
73 | e.stopPropagation();
74 |
75 | // Open image in the default browser if image preview is unavailable
76 | if (!$img[0]) {
77 | shell.openExternal(window.location.origin + url);
78 | }
79 | // Open image in lightbox
80 | lightbox.open($img);
81 | }
82 | });
83 | }
84 | });
85 |
86 | // Clean up spellchecker events after you navigate away from this page;
87 | // otherwise, you may experience errors
88 | window.addEventListener('beforeunload', () => {
89 | SetupSpellChecker.unsubscribeSpellChecker();
90 | });
91 |
92 | // electron's globalShortcut can cause unexpected results
93 | // so adding the reload shortcut in the old-school way
94 | // Zoom from numpad keys is not supported by electron, so adding it through listeners.
95 | document.addEventListener('keydown', event => {
96 | if (event.code === 'F5') {
97 | ipcRenderer.send('forward-message', 'hard-reload');
98 | } else if (event.ctrlKey && event.code === 'NumpadAdd') {
99 | ipcRenderer.send('forward-message', 'zoomIn');
100 | } else if (event.ctrlKey && event.code === 'NumpadSubtract') {
101 | ipcRenderer.send('forward-message', 'zoomOut');
102 | } else if (event.ctrlKey && event.code === 'Numpad0') {
103 | ipcRenderer.send('forward-message', 'zoomActualSize');
104 | }
105 | });
106 |
--------------------------------------------------------------------------------
/app/renderer/js/shared/preventdrag.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a security fix. Following function prevents drag and drop event in the app
4 | // so that attackers can't execute any remote code within the app
5 | // It doesn't affect the compose box so that users can still
6 | // use drag and drop event to share files etc
7 |
8 | const preventDragAndDrop = () => {
9 | const preventEvents = ['dragover', 'drop'];
10 | preventEvents.forEach(dragEvents => {
11 | document.addEventListener(dragEvents, event => {
12 | event.preventDefault();
13 | });
14 | });
15 | };
16 |
17 | preventDragAndDrop();
18 |
--------------------------------------------------------------------------------
/app/renderer/js/spellchecker.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } = require('electron-spellchecker');
4 |
5 | const ConfigUtil = require(__dirname + '/utils/config-util.js');
6 | const Logger = require(__dirname + '/utils/logger-util.js');
7 |
8 | const logger = new Logger({
9 | file: 'errors.log',
10 | timestamp: true
11 | });
12 |
13 | class SetupSpellChecker {
14 | init() {
15 | if (ConfigUtil.getConfigItem('enableSpellchecker')) {
16 | this.enableSpellChecker();
17 | }
18 | this.enableContextMenu();
19 | }
20 |
21 | enableSpellChecker() {
22 | try {
23 | this.SpellCheckHandler = new SpellCheckHandler();
24 | } catch (err) {
25 | logger.error(err);
26 | }
27 | }
28 |
29 | enableContextMenu() {
30 | if (this.SpellCheckHandler) {
31 | this.SpellCheckHandler.attachToInput();
32 |
33 | const userLanguage = ConfigUtil.getConfigItem('spellcheckerLanguage');
34 |
35 | this.SpellCheckHandler.switchLanguage(userLanguage);
36 | }
37 |
38 | const contextMenuBuilder = new ContextMenuBuilder(this.SpellCheckHandler);
39 | this.contextMenuListener = new ContextMenuListener(info => {
40 | contextMenuBuilder.showPopupMenu(info);
41 | });
42 | }
43 |
44 | unsubscribeSpellChecker() {
45 | // eslint-disable-next-line no-undef
46 | if (this.SpellCheckHandler) {
47 | this.SpellCheckHandler.unsubscribe();
48 | }
49 | if (this.contextMenuListener) {
50 | this.contextMenuListener.unsubscribe();
51 | }
52 | }
53 | }
54 |
55 | module.exports = new SetupSpellChecker();
56 |
--------------------------------------------------------------------------------
/app/renderer/js/tray.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const path = require('path');
3 |
4 | const electron = require('electron');
5 |
6 | const { ipcRenderer, remote } = electron;
7 |
8 | const { Tray, Menu, nativeImage, BrowserWindow } = remote;
9 |
10 | const APP_ICON = path.join(__dirname, '../../resources/tray', 'tray');
11 |
12 | const ConfigUtil = require(__dirname + '/utils/config-util.js');
13 |
14 | const iconPath = () => {
15 | if (process.platform === 'linux') {
16 | return APP_ICON + 'linux.png';
17 | }
18 | return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'osx.png');
19 | };
20 |
21 | let unread = 0;
22 |
23 | const trayIconSize = () => {
24 | switch (process.platform) {
25 | case 'darwin':
26 | return 20;
27 | case 'win32':
28 | return 100;
29 | case 'linux':
30 | return 100;
31 | default: return 80;
32 | }
33 | };
34 |
35 | // Default config for Icon we might make it OS specific if needed like the size
36 | const config = {
37 | pixelRatio: window.devicePixelRatio,
38 | unreadCount: 0,
39 | showUnreadCount: true,
40 | unreadColor: '#000000',
41 | readColor: '#000000',
42 | unreadBackgroundColor: '#B9FEEA',
43 | readBackgroundColor: '#B9FEEA',
44 | size: trayIconSize(),
45 | thick: process.platform === 'win32'
46 | };
47 |
48 | const renderCanvas = function (arg) {
49 | config.unreadCount = arg;
50 |
51 | return new Promise(resolve => {
52 | const SIZE = config.size * config.pixelRatio;
53 | const PADDING = SIZE * 0.05;
54 | const CENTER = SIZE / 2;
55 | const HAS_COUNT = config.showUnreadCount && config.unreadCount;
56 | const color = config.unreadCount ? config.unreadColor : config.readColor;
57 | const backgroundColor = config.unreadCount ? config.unreadBackgroundColor : config.readBackgroundColor;
58 |
59 | const canvas = document.createElement('canvas');
60 | canvas.width = SIZE;
61 | canvas.height = SIZE;
62 | const ctx = canvas.getContext('2d');
63 |
64 | // Circle
65 | // If (!config.thick || config.thick && HAS_COUNT) {
66 | ctx.beginPath();
67 | ctx.arc(CENTER, CENTER, (SIZE / 2) - PADDING, 0, 2 * Math.PI, false);
68 | ctx.fillStyle = backgroundColor;
69 | ctx.fill();
70 | ctx.lineWidth = SIZE / (config.thick ? 10 : 20);
71 | ctx.strokeStyle = backgroundColor;
72 | ctx.stroke();
73 | // Count or Icon
74 | if (HAS_COUNT) {
75 | ctx.fillStyle = color;
76 | ctx.textAlign = 'center';
77 | if (config.unreadCount > 99) {
78 | ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.4}px Helvetica`;
79 | ctx.fillText('99+', CENTER, CENTER + (SIZE * 0.15));
80 | } else if (config.unreadCount < 10) {
81 | ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
82 | ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.20));
83 | } else {
84 | ctx.font = `${config.thick ? 'bold ' : ''}${SIZE * 0.5}px Helvetica`;
85 | ctx.fillText(config.unreadCount, CENTER, CENTER + (SIZE * 0.15));
86 | }
87 |
88 | resolve(canvas);
89 | }
90 | });
91 | };
92 | /**
93 | * Renders the tray icon as a native image
94 | * @param arg: Unread count
95 | * @return the native image
96 | */
97 | const renderNativeImage = function (arg) {
98 | return Promise.resolve()
99 | .then(() => renderCanvas(arg))
100 | .then(canvas => {
101 | const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
102 | return Promise.resolve(nativeImage.createFromBuffer(pngData, {
103 | scaleFactor: config.pixelRatio
104 | }));
105 | });
106 | };
107 |
108 | function sendAction(action) {
109 | const win = BrowserWindow.getAllWindows()[0];
110 |
111 | if (process.platform === 'darwin') {
112 | win.restore();
113 | }
114 |
115 | win.webContents.send(action);
116 | }
117 |
118 | const createTray = function () {
119 | window.tray = new Tray(iconPath());
120 | const contextMenu = Menu.buildFromTemplate([
121 | {
122 | label: 'Zulip',
123 | click() {
124 | ipcRenderer.send('focus-app');
125 | }
126 | },
127 | {
128 | label: 'Settings',
129 | click() {
130 | ipcRenderer.send('focus-app');
131 | sendAction('open-settings');
132 | }
133 | },
134 | {
135 | type: 'separator'
136 | },
137 | {
138 | label: 'Quit',
139 | click() {
140 | ipcRenderer.send('quit-app');
141 | }
142 | }
143 | ]);
144 | window.tray.setContextMenu(contextMenu);
145 | if (process.platform === 'linux' || process.platform === 'win32') {
146 | window.tray.on('click', () => {
147 | ipcRenderer.send('toggle-app');
148 | });
149 | }
150 | };
151 |
152 | ipcRenderer.on('destroytray', event => {
153 | if (!window.tray) {
154 | return;
155 | }
156 |
157 | window.tray.destroy();
158 | if (window.tray.isDestroyed()) {
159 | window.tray = null;
160 | } else {
161 | throw new Error('Tray icon not properly destroyed.');
162 | }
163 |
164 | return event;
165 | });
166 |
167 | ipcRenderer.on('tray', (event, arg) => {
168 | if (!window.tray) {
169 | return;
170 | }
171 | // We don't want to create tray from unread messages on macOS since it already has dock badges.
172 | if (process.platform === 'linux' || process.platform === 'win32') {
173 | if (arg === 0) {
174 | unread = arg;
175 | window.tray.setImage(iconPath());
176 | window.tray.setToolTip('No unread messages');
177 | } else {
178 | unread = arg;
179 | renderNativeImage(arg).then(image => {
180 | window.tray.setImage(image);
181 | window.tray.setToolTip(arg + ' unread messages');
182 | });
183 | }
184 | }
185 | });
186 |
187 | function toggleTray() {
188 | let state;
189 | if (window.tray) {
190 | state = false;
191 | window.tray.destroy();
192 | if (window.tray.isDestroyed()) {
193 | window.tray = null;
194 | }
195 | ConfigUtil.setConfigItem('trayIcon', false);
196 | } else {
197 | state = true;
198 | createTray();
199 | if (process.platform === 'linux' || process.platform === 'win32') {
200 | renderNativeImage(unread).then(image => {
201 | window.tray.setImage(image);
202 | window.tray.setToolTip(unread + ' unread messages');
203 | });
204 | }
205 | ConfigUtil.setConfigItem('trayIcon', true);
206 | }
207 | const selector = 'webview:not([class*=disabled])';
208 | const webview = document.querySelector(selector);
209 | const webContents = webview.getWebContents();
210 | webContents.send('toggletray', state);
211 | }
212 |
213 | ipcRenderer.on('toggletray', toggleTray);
214 |
215 | if (ConfigUtil.getConfigItem('trayIcon', true)) {
216 | createTray();
217 | }
218 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/certificate-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { app, dialog } = require('electron').remote;
4 | const fs = require('fs');
5 | const path = require('path');
6 | const JsonDB = require('node-json-db');
7 | const Logger = require('./logger-util');
8 | const { initSetUp } = require('./default-util');
9 |
10 | initSetUp();
11 |
12 | const logger = new Logger({
13 | file: `certificate-util.log`,
14 | timestamp: true
15 | });
16 |
17 | let instance = null;
18 | const certificatesDir = `${app.getPath('userData')}/certificates`;
19 |
20 | class CertificateUtil {
21 | constructor() {
22 | if (instance) {
23 | return instance;
24 | } else {
25 | instance = this;
26 | }
27 |
28 | this.reloadDB();
29 | return instance;
30 | }
31 | getCertificate(server, defaultValue = null) {
32 | this.reloadDB();
33 | const value = this.db.getData('/')[server];
34 | if (value === undefined) {
35 | return defaultValue;
36 | } else {
37 | return value;
38 | }
39 | }
40 | // Function to copy the certificate to userData folder
41 | copyCertificate(server, location, fileName) {
42 | let copied = false;
43 | const filePath = `${certificatesDir}/${fileName}`;
44 | try {
45 | fs.copyFileSync(location, filePath);
46 | copied = true;
47 | } catch (err) {
48 | dialog.showErrorBox(
49 | 'Error saving certificate',
50 | 'We encountered error while saving the certificate.'
51 | );
52 | logger.error('Error while copying the certificate to certificates folder.');
53 | logger.error(err);
54 | }
55 | return copied;
56 | }
57 | setCertificate(server, fileName) {
58 | const filePath = `${certificatesDir}/${fileName}`;
59 | this.db.push(`/${server}`, filePath, true);
60 | this.reloadDB();
61 | }
62 | removeCertificate(server) {
63 | this.db.delete(`/${server}`);
64 | this.reloadDB();
65 | }
66 | reloadDB() {
67 | const settingsJsonPath = path.join(app.getPath('userData'), '/config/certificates.json');
68 | try {
69 | const file = fs.readFileSync(settingsJsonPath, 'utf8');
70 | JSON.parse(file);
71 | } catch (err) {
72 | if (fs.existsSync(settingsJsonPath)) {
73 | fs.unlinkSync(settingsJsonPath);
74 | dialog.showErrorBox(
75 | 'Error saving settings',
76 | 'We encountered error while saving the certificate.'
77 | );
78 | logger.error('Error while JSON parsing certificates.json: ');
79 | logger.error(err);
80 | }
81 | }
82 | this.db = new JsonDB(settingsJsonPath, true, true);
83 | }
84 | }
85 |
86 | module.exports = new CertificateUtil();
87 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/common-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | let instance = null;
4 |
5 | class CommonUtil {
6 | constructor() {
7 | if (instance) {
8 | return instance;
9 | } else {
10 | instance = this;
11 | }
12 | return instance;
13 | }
14 |
15 | // unescape already encoded/escaped strings
16 | decodeString(string) {
17 | const parser = new DOMParser();
18 | const dom = parser.parseFromString(
19 | '' + string,
20 | 'text/html');
21 | return dom.body.textContent;
22 | }
23 | }
24 |
25 | module.exports = new CommonUtil();
26 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/config-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const process = require('process');
6 | const JsonDB = require('node-json-db');
7 | const Logger = require('./logger-util');
8 |
9 | const logger = new Logger({
10 | file: 'config-util.log',
11 | timestamp: true
12 | });
13 |
14 | let instance = null;
15 | let dialog = null;
16 | let app = null;
17 |
18 | /* To make the util runnable in both main and renderer process */
19 | if (process.type === 'renderer') {
20 | const remote = require('electron').remote;
21 | dialog = remote.dialog;
22 | app = remote.app;
23 | } else {
24 | const electron = require('electron');
25 | dialog = electron.dialog;
26 | app = electron.app;
27 | }
28 |
29 | class ConfigUtil {
30 | constructor() {
31 | if (instance) {
32 | return instance;
33 | } else {
34 | instance = this;
35 | }
36 |
37 | this.reloadDB();
38 | return instance;
39 | }
40 |
41 | getConfigItem(key, defaultValue = null) {
42 | try {
43 | this.db.reload();
44 | } catch (err) {
45 | logger.error('Error while reloading settings.json: ');
46 | logger.error(err);
47 | }
48 | const value = this.db.getData('/')[key];
49 | if (value === undefined) {
50 | this.setConfigItem(key, defaultValue);
51 | return defaultValue;
52 | } else {
53 | return value;
54 | }
55 | }
56 | // This function returns whether a key exists in the configuration file (settings.json)
57 | isConfigItemExists(key) {
58 | try {
59 | this.db.reload();
60 | } catch (err) {
61 | logger.error('Error while reloading settings.json: ');
62 | logger.error(err);
63 | }
64 | const value = this.db.getData('/')[key];
65 | return (value !== undefined);
66 | }
67 |
68 | setConfigItem(key, value) {
69 | this.db.push(`/${key}`, value, true);
70 | this.db.save();
71 | }
72 |
73 | removeConfigItem(key) {
74 | this.db.delete(`/${key}`);
75 | this.db.save();
76 | }
77 |
78 | reloadDB() {
79 | const settingsJsonPath = path.join(app.getPath('userData'), '/config/settings.json');
80 | try {
81 | const file = fs.readFileSync(settingsJsonPath, 'utf8');
82 | JSON.parse(file);
83 | } catch (err) {
84 | if (fs.existsSync(settingsJsonPath)) {
85 | fs.unlinkSync(settingsJsonPath);
86 | dialog.showErrorBox(
87 | 'Error saving settings',
88 | 'We encountered an error while saving the settings.'
89 | );
90 | logger.error('Error while JSON parsing settings.json: ');
91 | logger.error(err);
92 | logger.reportSentry(err);
93 | }
94 | }
95 | this.db = new JsonDB(settingsJsonPath, true, true);
96 | }
97 | }
98 |
99 | module.exports = new ConfigUtil();
100 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/default-util.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | let app = null;
4 | let setupCompleted = false;
5 | if (process.type === 'renderer') {
6 | app = require('electron').remote.app;
7 | } else {
8 | app = require('electron').app;
9 | }
10 |
11 | const zulipDir = app.getPath('userData');
12 | const logDir = `${zulipDir}/Logs/`;
13 | const certificatesDir = `${zulipDir}/certificates/`;
14 | const configDir = `${zulipDir}/config/`;
15 | const initSetUp = () => {
16 | // if it is the first time the app is running
17 | // create zulip dir in userData folder to
18 | // avoid errors
19 | if (!setupCompleted) {
20 | if (!fs.existsSync(zulipDir)) {
21 | fs.mkdirSync(zulipDir);
22 | }
23 |
24 | if (!fs.existsSync(logDir)) {
25 | fs.mkdirSync(logDir);
26 | }
27 |
28 | if (!fs.existsSync(certificatesDir)) {
29 | fs.mkdirSync(certificatesDir);
30 | }
31 |
32 | // Migrate config files from app data folder to config folder inside app
33 | // data folder. This will be done once when a user updates to the new version.
34 | if (!fs.existsSync(configDir)) {
35 | fs.mkdirSync(configDir);
36 | const domainJson = `${zulipDir}/domain.json`;
37 | const certificatesJson = `${zulipDir}/certificates.json`;
38 | const settingsJson = `${zulipDir}/settings.json`;
39 | const updatesJson = `${zulipDir}/updates.json`;
40 | const windowStateJson = `${zulipDir}/window-state.json`;
41 | const configData = [
42 | {
43 | path: domainJson,
44 | fileName: `domain.json`
45 | },
46 | {
47 | path: certificatesJson,
48 | fileName: `certificates.json`
49 | },
50 | {
51 | path: settingsJson,
52 | fileName: `settings.json`
53 | },
54 | {
55 | path: updatesJson,
56 | fileName: `updates.json`
57 | }
58 | ];
59 | configData.forEach(data => {
60 | if (fs.existsSync(data.path)) {
61 | fs.copyFileSync(data.path, configDir + data.fileName);
62 | fs.unlinkSync(data.path);
63 | }
64 | });
65 | // window-state.json is only deleted not moved, as the electron-window-state
66 | // package will recreate the file in the config folder.
67 | if (fs.existsSync(windowStateJson)) {
68 | fs.unlinkSync(windowStateJson);
69 | }
70 | }
71 |
72 | setupCompleted = true;
73 | }
74 | };
75 |
76 | module.exports = {
77 | initSetUp
78 | };
79 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/dnd-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ConfigUtil = require(__dirname + '/config-util.js');
4 |
5 | function toggle() {
6 | const dnd = !ConfigUtil.getConfigItem('dnd', false);
7 | const dndSettingList = ['showNotification', 'silent'];
8 | if (process.platform === 'win32') {
9 | dndSettingList.push('flashTaskbarOnMessage');
10 | }
11 |
12 | let newSettings;
13 | if (dnd) {
14 | const oldSettings = {};
15 | newSettings = {};
16 |
17 | // Iterate through the dndSettingList.
18 | for (const settingName of dndSettingList) {
19 | // Store the current value of setting.
20 | oldSettings[settingName] = ConfigUtil.getConfigItem(settingName);
21 | // New value of setting.
22 | newSettings[settingName] = (settingName === 'silent');
23 | }
24 |
25 | // Store old value in oldSettings.
26 | ConfigUtil.setConfigItem('dndPreviousSettings', oldSettings);
27 | } else {
28 | newSettings = ConfigUtil.getConfigItem('dndPreviousSettings');
29 | }
30 |
31 | for (const settingName of dndSettingList) {
32 | ConfigUtil.setConfigItem(settingName, newSettings[settingName]);
33 | }
34 |
35 | ConfigUtil.setConfigItem('dnd', dnd);
36 | return {dnd, newSettings};
37 | }
38 |
39 | module.exports = {
40 | toggle
41 | };
42 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/domain-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { app, dialog } = require('electron').remote;
4 | const fs = require('fs');
5 | const path = require('path');
6 | const JsonDB = require('node-json-db');
7 | const request = require('request');
8 | const escape = require('escape-html');
9 |
10 | const Logger = require('./logger-util');
11 |
12 | const RequestUtil = require(__dirname + '/../utils/request-util.js');
13 |
14 | const logger = new Logger({
15 | file: `domain-util.log`,
16 | timestamp: true
17 | });
18 |
19 | let instance = null;
20 |
21 | const defaultIconUrl = '../renderer/img/icon.png';
22 |
23 | class DomainUtil {
24 | constructor() {
25 | if (instance) {
26 | return instance;
27 | } else {
28 | instance = this;
29 | }
30 |
31 | this.reloadDB();
32 | // Migrate from old schema
33 | if (this.db.getData('/').domain) {
34 | this.addDomain({
35 | alias: 'Zulip',
36 | url: this.db.getData('/domain')
37 | });
38 | this.db.delete('/domain');
39 | }
40 |
41 | return instance;
42 | }
43 |
44 | getDomains() {
45 | this.reloadDB();
46 | if (this.db.getData('/').domains === undefined) {
47 | return [];
48 | } else {
49 | return this.db.getData('/domains');
50 | }
51 | }
52 |
53 | getDomain(index) {
54 | this.reloadDB();
55 | return this.db.getData(`/domains[${index}]`);
56 | }
57 |
58 | updateDomain(index, server) {
59 | this.reloadDB();
60 | this.db.push(`/domains[${index}]`, server, true);
61 | }
62 |
63 | addDomain(server) {
64 | const ignoreCerts = server.ignoreCerts;
65 | return new Promise(resolve => {
66 | if (server.icon) {
67 | this.saveServerIcon(server, ignoreCerts).then(localIconUrl => {
68 | server.icon = localIconUrl;
69 | this.db.push('/domains[]', server, true);
70 | this.reloadDB();
71 | resolve();
72 | });
73 | } else {
74 | server.icon = defaultIconUrl;
75 | this.db.push('/domains[]', server, true);
76 | this.reloadDB();
77 | resolve();
78 | }
79 | });
80 | }
81 |
82 | removeDomains() {
83 | this.db.delete('/domains');
84 | this.reloadDB();
85 | }
86 |
87 | removeDomain(index) {
88 | this.db.delete(`/domains[${index}]`);
89 | this.reloadDB();
90 | }
91 |
92 | // Check if domain is already added
93 | duplicateDomain(domain) {
94 | domain = this.formatUrl(domain);
95 | const servers = this.getDomains();
96 | for (const i in servers) {
97 | if (servers[i].url === domain) {
98 | return true;
99 | }
100 | }
101 | return false;
102 | }
103 |
104 | // ignoreCerts parameter helps in fetching server icon and
105 | // other server details when user chooses to ignore certificate warnings
106 | checkDomain(domain, ignoreCerts = false, silent = false) {
107 | if (!silent && this.duplicateDomain(domain)) {
108 | // Do not check duplicate in silent mode
109 | return Promise.reject('This server has been added.');
110 | }
111 |
112 | domain = this.formatUrl(domain);
113 | const checkDomain = {
114 | url: domain + '/static/audio/zulip.ogg',
115 | ...RequestUtil.requestOptions(domain, ignoreCerts)
116 | };
117 |
118 | const serverConf = {
119 | icon: defaultIconUrl,
120 | url: domain,
121 | alias: domain,
122 | ignoreCerts
123 | };
124 |
125 | return new Promise((resolve, reject) => {
126 | request(checkDomain, (error, response) => {
127 | // If the domain contains following strings we just bypass the server
128 | const whitelistDomains = [
129 | 'zulipdev.org'
130 | ];
131 |
132 | // make sure that error is an error or string not undefined
133 | // so validation does not throw error.
134 | error = error || '';
135 |
136 | const certsError = error.toString().includes('certificate');
137 | if (!error && response.statusCode < 400) {
138 | // Correct
139 | this.getServerSettings(domain, serverConf.ignoreCerts).then(serverSettings => {
140 | resolve(serverSettings);
141 | }, () => {
142 | resolve(serverConf);
143 | });
144 | } else if (domain.indexOf(whitelistDomains) >= 0 || certsError) {
145 | if (silent) {
146 | this.getServerSettings(domain, serverConf.ignoreCerts).then(serverSettings => {
147 | resolve(serverSettings);
148 | }, () => {
149 | resolve(serverConf);
150 | });
151 | } else {
152 | // Report error to sentry to get idea of possible certificate errors
153 | // users get when adding the servers
154 | logger.reportSentry(new Error(error));
155 | const certErrorMessage = `Do you trust certificate from ${domain}? \n ${error}`;
156 | const certErrorDetail = `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.
157 | \nIf you have a valid certificate please add it from Settings>Organizations and try to add the organization again.
158 | \nUnless you have a good reason to believe otherwise, you should not proceed.
159 | \nYou can click here if you'd like to proceed with the connection.`;
160 |
161 | dialog.showMessageBox({
162 | type: 'warning',
163 | buttons: ['Yes', 'No'],
164 | defaultId: 1,
165 | message: certErrorMessage,
166 | detail: certErrorDetail
167 | }, response => {
168 | if (response === 0) {
169 | // set ignoreCerts parameter to true in case user responds with yes
170 | serverConf.ignoreCerts = true;
171 | this.getServerSettings(domain, serverConf.ignoreCerts).then(serverSettings => {
172 | resolve(serverSettings);
173 | }, () => {
174 | resolve(serverConf);
175 | });
176 | } else {
177 | reject('Untrusted Certificate.');
178 | }
179 | });
180 | }
181 | } else {
182 | const invalidZulipServerError = `${domain} does not appear to be a valid Zulip server. Make sure that \
183 | \n (1) you can connect to that URL in a web browser and \n (2) if you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings \n (3) its a zulip server \
184 | \n (4) the server has a valid certificate, you can add custom certificates in Settings>Organizations`;
185 | reject(invalidZulipServerError);
186 | }
187 | });
188 | });
189 | }
190 |
191 | getServerSettings(domain, ignoreCerts = false) {
192 | const serverSettingsOptions = {
193 | url: domain + '/api/v1/server_settings',
194 | ...RequestUtil.requestOptions(domain, ignoreCerts)
195 | };
196 |
197 | return new Promise((resolve, reject) => {
198 | request(serverSettingsOptions, (error, response) => {
199 | if (!error && response.statusCode === 200) {
200 | const data = JSON.parse(response.body);
201 | if (data.hasOwnProperty('realm_icon') && data.realm_icon) {
202 | resolve({
203 | // Some Zulip Servers use absolute URL for server icon whereas others use relative URL
204 | // Following check handles both the cases
205 | icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
206 | url: data.realm_uri,
207 | alias: escape(data.realm_name),
208 | ignoreCerts
209 | });
210 | }
211 | } else {
212 | reject('Zulip server version < 1.6.');
213 | }
214 | });
215 | });
216 | }
217 |
218 | saveServerIcon(server, ignoreCerts = false) {
219 | const url = server.icon;
220 | const domain = server.url;
221 |
222 | const serverIconOptions = {
223 | url,
224 | ...RequestUtil.requestOptions(domain, ignoreCerts)
225 | };
226 |
227 | // The save will always succeed. If url is invalid, downgrade to default icon.
228 | return new Promise(resolve => {
229 | const filePath = this.generateFilePath(url);
230 | const file = fs.createWriteStream(filePath);
231 | try {
232 | request(serverIconOptions).on('response', response => {
233 | response.on('error', err => {
234 | logger.log('Could not get server icon.');
235 | logger.log(err);
236 | logger.reportSentry(err);
237 | resolve(defaultIconUrl);
238 | });
239 | response.pipe(file).on('finish', () => {
240 | resolve(filePath);
241 | });
242 | }).on('error', err => {
243 | logger.log('Could not get server icon.');
244 | logger.log(err);
245 | logger.reportSentry(err);
246 | resolve(defaultIconUrl);
247 | });
248 | } catch (err) {
249 | logger.log('Could not get server icon.');
250 | logger.log(err);
251 | logger.reportSentry(err);
252 | resolve(defaultIconUrl);
253 | }
254 | });
255 | }
256 |
257 | updateSavedServer(url, index) {
258 | // Does not promise successful update
259 | const ignoreCerts = this.getDomain(index).ignoreCerts;
260 | this.checkDomain(url, ignoreCerts, true).then(newServerConf => {
261 | this.saveServerIcon(newServerConf, ignoreCerts).then(localIconUrl => {
262 | newServerConf.icon = localIconUrl;
263 | this.updateDomain(index, newServerConf);
264 | this.reloadDB();
265 | });
266 | });
267 | }
268 |
269 | reloadDB() {
270 | const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
271 | try {
272 | const file = fs.readFileSync(domainJsonPath, 'utf8');
273 | JSON.parse(file);
274 | } catch (err) {
275 | if (fs.existsSync(domainJsonPath)) {
276 | fs.unlinkSync(domainJsonPath);
277 | dialog.showErrorBox(
278 | 'Error saving new organization',
279 | 'There seems to be error while saving new organization, ' +
280 | 'you may have to re-add your previous organizations back.'
281 | );
282 | logger.error('Error while JSON parsing domain.json: ');
283 | logger.error(err);
284 | logger.reportSentry(err);
285 | }
286 | }
287 | this.db = new JsonDB(domainJsonPath, true, true);
288 | }
289 |
290 | generateFilePath(url) {
291 | const dir = `${app.getPath('userData')}/server-icons`;
292 | const extension = path.extname(url).split('?')[0];
293 |
294 | let hash = 5381;
295 | let len = url.length;
296 |
297 | while (len) {
298 | hash = (hash * 33) ^ url.charCodeAt(--len);
299 | }
300 |
301 | // Create 'server-icons' directory if not existed
302 | if (!fs.existsSync(dir)) {
303 | fs.mkdirSync(dir);
304 | }
305 |
306 | return `${dir}/${hash >>> 0}${extension}`;
307 | }
308 |
309 | formatUrl(domain) {
310 | const hasPrefix = (domain.indexOf('http') === 0);
311 | if (hasPrefix) {
312 | return domain;
313 | } else {
314 | return (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`;
315 | }
316 | }
317 | }
318 |
319 | module.exports = new DomainUtil();
320 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/link-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const wurl = require('wurl');
4 |
5 | let instance = null;
6 |
7 | class LinkUtil {
8 | constructor() {
9 | if (instance) {
10 | return instance;
11 | } else {
12 | instance = this;
13 | }
14 |
15 | return instance;
16 | }
17 |
18 | isInternal(currentUrl, newUrl) {
19 | const currentDomain = wurl('hostname', currentUrl);
20 | const newDomain = wurl('hostname', newUrl);
21 |
22 | const sameDomainUrl = (currentDomain === newDomain || newUrl === currentUrl + '/');
23 | const isUploadsUrl = newUrl.includes(currentUrl + '/user_uploads/');
24 | const isInternalUrl = newUrl.includes('/#narrow') || isUploadsUrl;
25 |
26 | return {
27 | isInternalUrl: sameDomainUrl && isInternalUrl,
28 | isUploadsUrl
29 | };
30 | }
31 |
32 | isImage(url) {
33 | // test for images extension as well as urls like .png?s=100
34 | const isImageUrl = /\.(bmp|gif|jpg|jpeg|png|webp)\?*.*$/i;
35 | return isImageUrl.test(url);
36 | }
37 |
38 | isPDF(url) {
39 | // test for pdf extension
40 | const isPDFUrl = /\.(pdf)\?*.*$/i;
41 | return isPDFUrl.test(url);
42 | }
43 | }
44 |
45 | module.exports = new LinkUtil();
46 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/linux-update-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const process = require('process');
6 | const remote =
7 | process.type === 'renderer' ? require('electron').remote : require('electron');
8 | const JsonDB = require('node-json-db');
9 | const Logger = require('./logger-util');
10 |
11 | const logger = new Logger({
12 | file: 'linux-update-util.log',
13 | timestamp: true
14 | });
15 |
16 | /* To make the util runnable in both main and renderer process */
17 | const { dialog, app } = remote;
18 |
19 | let instance = null;
20 |
21 | class LinuxUpdateUtil {
22 | constructor() {
23 | if (instance) {
24 | return instance;
25 | } else {
26 | instance = this;
27 | }
28 |
29 | this.reloadDB();
30 | return instance;
31 | }
32 |
33 | getUpdateItem(key, defaultValue = null) {
34 | this.reloadDB();
35 | const value = this.db.getData('/')[key];
36 | if (value === undefined) {
37 | this.setUpdateItem(key, defaultValue);
38 | return defaultValue;
39 | } else {
40 | return value;
41 | }
42 | }
43 |
44 | setUpdateItem(key, value) {
45 | this.db.push(`/${key}`, value, true);
46 | this.reloadDB();
47 | }
48 |
49 | removeUpdateItem(key) {
50 | this.db.delete(`/${key}`);
51 | this.reloadDB();
52 | }
53 |
54 | reloadDB() {
55 | const linuxUpdateJsonPath = path.join(app.getPath('userData'), '/config/updates.json');
56 | try {
57 | const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
58 | JSON.parse(file);
59 | } catch (err) {
60 | if (fs.existsSync(linuxUpdateJsonPath)) {
61 | fs.unlinkSync(linuxUpdateJsonPath);
62 | dialog.showErrorBox(
63 | 'Error saving update notifications.',
64 | 'We encountered an error while saving the update notifications.'
65 | );
66 | logger.error('Error while JSON parsing updates.json: ');
67 | logger.error(err);
68 | }
69 | }
70 | this.db = new JsonDB(linuxUpdateJsonPath, true, true);
71 | }
72 | }
73 |
74 | module.exports = new LinuxUpdateUtil();
75 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/logger-util.js:
--------------------------------------------------------------------------------
1 | const NodeConsole = require('console').Console;
2 | const fs = require('fs');
3 | const os = require('os');
4 | const isDev = require('electron-is-dev');
5 | const { initSetUp } = require('./default-util');
6 | const { sentryInit, captureException } = require('./sentry-util');
7 |
8 | initSetUp();
9 |
10 | let app = null;
11 | let reportErrors = true;
12 | if (process.type === 'renderer') {
13 | app = require('electron').remote.app;
14 |
15 | // Report Errors to Sentry only if it is enabled in settings
16 | // Gets the value of reportErrors from config-util for renderer process
17 | // For main process, sentryInit() is handled in index.js
18 | const { ipcRenderer } = require('electron');
19 | ipcRenderer.send('error-reporting');
20 | ipcRenderer.on('error-reporting-val', (event, errorReporting) => {
21 | reportErrors = errorReporting;
22 | if (reportErrors) {
23 | sentryInit();
24 | }
25 | });
26 | } else {
27 | app = require('electron').app;
28 | }
29 |
30 | const browserConsole = console;
31 | const logDir = `${app.getPath('userData')}/Logs`;
32 |
33 | class Logger {
34 | constructor(opts = {}) {
35 | let {
36 | timestamp = true,
37 | file = 'console.log',
38 | level = true,
39 | logInDevMode = false
40 | } = opts;
41 |
42 | file = `${logDir}/${file}`;
43 | if (timestamp === true) {
44 | timestamp = this.getTimestamp;
45 | }
46 |
47 | // Trim log according to type of process
48 | if (process.type === 'renderer') {
49 | requestIdleCallback(() => this.trimLog(file));
50 | } else {
51 | process.nextTick(() => this.trimLog(file));
52 | }
53 |
54 | const fileStream = fs.createWriteStream(file, { flags: 'a' });
55 | const nodeConsole = new NodeConsole(fileStream);
56 |
57 | this.nodeConsole = nodeConsole;
58 | this.timestamp = timestamp;
59 | this.level = level;
60 | this.logInDevMode = logInDevMode;
61 | this.setUpConsole();
62 | }
63 |
64 | _log(type, ...args) {
65 | const {
66 | nodeConsole, timestamp, level, logInDevMode
67 | } = this;
68 | let nodeConsoleLog;
69 |
70 | /* eslint-disable no-fallthrough */
71 | switch (true) {
72 | case typeof timestamp === 'function':
73 | args.unshift(timestamp() + ' |\t');
74 |
75 | case (level !== false):
76 | args.unshift(type.toUpperCase() + ' |');
77 |
78 | case isDev || logInDevMode:
79 | nodeConsoleLog = nodeConsole[type] || nodeConsole.log;
80 | nodeConsoleLog.apply(null, args);
81 |
82 | default: break;
83 | }
84 | /* eslint-enable no-fallthrough */
85 |
86 | browserConsole[type].apply(null, args);
87 | }
88 |
89 | setUpConsole() {
90 | for (const type in browserConsole) {
91 | this.setupConsoleMethod(type);
92 | }
93 | }
94 |
95 | setupConsoleMethod(type) {
96 | this[type] = (...args) => {
97 | this._log(type, ...args);
98 | };
99 | }
100 |
101 | getTimestamp() {
102 | const date = new Date();
103 | const timestamp =
104 | `${date.getMonth()}/${date.getDate()} ` +
105 | `${date.getMinutes()}:${date.getSeconds()}`;
106 | return timestamp;
107 | }
108 |
109 | reportSentry(err) {
110 | if (reportErrors) {
111 | captureException(err);
112 | }
113 | }
114 |
115 | trimLog(file) {
116 | fs.readFile(file, 'utf8', (err, data) => {
117 | if (err) {
118 | throw err;
119 | }
120 | const MAX_LOG_FILE_LINES = 500;
121 | const logs = data.split(os.EOL);
122 | const logLength = logs.length - 1;
123 |
124 | // Keep bottom MAX_LOG_FILE_LINES of each log instance
125 | if (logLength > MAX_LOG_FILE_LINES) {
126 | const trimmedLogs = logs.slice(logLength - MAX_LOG_FILE_LINES);
127 | const toWrite = trimmedLogs.join(os.EOL);
128 | fs.writeFileSync(file, toWrite);
129 | }
130 | });
131 | }
132 | }
133 |
134 | module.exports = Logger;
135 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/params-util.js:
--------------------------------------------------------------------------------
1 | // This util function returns the page params if they're present else returns null
2 | function isPageParams() {
3 | let webpageParams = null;
4 | try {
5 | // eslint-disable-next-line no-undef, camelcase
6 | webpageParams = page_params;
7 | } catch (err) {
8 | webpageParams = null;
9 | }
10 | return webpageParams;
11 | }
12 |
13 | module.exports = {
14 | isPageParams
15 | };
16 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/proxy-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const url = require('url');
4 | const ConfigUtil = require('./config-util.js');
5 |
6 | let instance = null;
7 |
8 | class ProxyUtil {
9 | constructor() {
10 | if (instance) {
11 | return instance;
12 | } else {
13 | instance = this;
14 | }
15 |
16 | return instance;
17 | }
18 |
19 | // Return proxy to be used for a particular uri, to be used for request
20 | getProxy(uri) {
21 | uri = url.parse(uri);
22 | const proxyRules = ConfigUtil.getConfigItem('proxyRules', '').split(';');
23 | // If SPS is on and system uses no proxy then request should not try to use proxy from
24 | // environment. NO_PROXY = '*' makes request ignore all environment proxy variables.
25 | if (proxyRules[0] === '') {
26 | process.env.NO_PROXY = '*';
27 | return;
28 | }
29 |
30 | const proxyRule = {};
31 | if (uri.protocol === 'http:') {
32 | proxyRules.forEach(proxy => {
33 | if (proxy.includes('http=')) {
34 | proxyRule.hostname = proxy.split('http=')[1].trim().split(':')[0];
35 | proxyRule.port = proxy.split('http=')[1].trim().split(':')[1];
36 | }
37 | });
38 | return proxyRule;
39 | }
40 |
41 | if (uri.protocol === 'https:') {
42 | proxyRules.forEach(proxy => {
43 | if (proxy.includes('https=')) {
44 | proxyRule.hostname = proxy.split('https=')[1].trim().split(':')[0];
45 | proxyRule.port = proxy.split('https=')[1].trim().split(':')[1];
46 | }
47 | });
48 | return proxyRule;
49 | }
50 | }
51 |
52 | resolveSystemProxy(mainWindow) {
53 | const page = mainWindow.webContents;
54 | const ses = page.session;
55 | const resolveProxyUrl = 'www.example.com';
56 |
57 | // Check HTTP Proxy
58 | const httpProxy = new Promise(resolve => {
59 | ses.resolveProxy('http://' + resolveProxyUrl, proxy => {
60 | let httpString = '';
61 | if (proxy !== 'DIRECT') {
62 | // in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
63 | // for all other HTTP or direct url:port both uses PROXY
64 | if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
65 | httpString = 'http=' + proxy.split('PROXY')[1] + ';';
66 | }
67 | }
68 | resolve(httpString);
69 | });
70 | });
71 | // Check HTTPS Proxy
72 | const httpsProxy = new Promise(resolve => {
73 | ses.resolveProxy('https://' + resolveProxyUrl, proxy => {
74 | let httpsString = '';
75 | if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) {
76 | // in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
77 | // for all other HTTP or direct url:port both uses PROXY
78 | if (proxy.includes('PROXY' || proxy.includes('HTTPS'))) {
79 | httpsString += 'https=' + proxy.split('PROXY')[1] + ';';
80 | }
81 | }
82 | resolve(httpsString);
83 | });
84 | });
85 |
86 | // Check FTP Proxy
87 | const ftpProxy = new Promise(resolve => {
88 | ses.resolveProxy('ftp://' + resolveProxyUrl, proxy => {
89 | let ftpString = '';
90 | if (proxy !== 'DIRECT') {
91 | if (proxy.includes('PROXY')) {
92 | ftpString += 'ftp=' + proxy.split('PROXY')[1] + ';';
93 | }
94 | }
95 | resolve(ftpString);
96 | });
97 | });
98 |
99 | // Check SOCKS Proxy
100 | const socksProxy = new Promise(resolve => {
101 | ses.resolveProxy('socks4://' + resolveProxyUrl, proxy => {
102 | let socksString = '';
103 | if (proxy !== 'DIRECT') {
104 | if (proxy.includes('SOCKS5')) {
105 | socksString += 'socks=' + proxy.split('SOCKS5')[1] + ';';
106 | } else if (proxy.includes('SOCKS4')) {
107 | socksString += 'socks=' + proxy.split('SOCKS4')[1] + ';';
108 | } else if (proxy.includes('PROXY')) {
109 | socksString += 'socks=' + proxy.split('PROXY')[1] + ';';
110 | }
111 | }
112 | resolve(socksString);
113 | });
114 | });
115 |
116 | Promise.all([httpProxy, httpsProxy, ftpProxy, socksProxy]).then(values => {
117 | let proxyString = '';
118 | values.forEach(proxy => {
119 | proxyString += proxy;
120 | });
121 | ConfigUtil.setConfigItem('systemProxyRules', proxyString);
122 | const useSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
123 | if (useSystemProxy) {
124 | ConfigUtil.setConfigItem('proxyRules', proxyString);
125 | }
126 | });
127 | }
128 | }
129 |
130 | module.exports = new ProxyUtil();
131 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/reconnect-util.js:
--------------------------------------------------------------------------------
1 | const isOnline = require('is-online');
2 | const Logger = require('./logger-util');
3 |
4 | const logger = new Logger({
5 | file: `domain-util.log`,
6 | timestamp: true
7 | });
8 |
9 | class ReconnectUtil {
10 | constructor(serverManagerView) {
11 | this.serverManagerView = serverManagerView;
12 | this.alreadyReloaded = false;
13 | }
14 |
15 | clearState() {
16 | this.alreadyReloaded = false;
17 | }
18 |
19 | pollInternetAndReload() {
20 | const pollInterval = setInterval(() => {
21 | this._checkAndReload()
22 | .then(status => {
23 | if (status) {
24 | this.alreadyReloaded = true;
25 | clearInterval(pollInterval);
26 | }
27 | });
28 | }, 1500);
29 | }
30 |
31 | _checkAndReload() {
32 | return new Promise(resolve => {
33 | if (!this.alreadyReloaded) { // eslint-disable-line no-negated-condition
34 | isOnline()
35 | .then(online => {
36 | if (online) {
37 | if (!this.alreadyReloaded) {
38 | this.serverManagerView.reloadView();
39 | }
40 | logger.log('You\'re back online.');
41 | return resolve(true);
42 | }
43 |
44 | logger.log('There is no internet connection, try checking network cables, modem and router.');
45 | const errMsgHolder = document.querySelector('#description');
46 | if (errMsgHolder) {
47 | errMsgHolder.innerHTML = `
48 | Your internet connection doesn't seem to work properly!
49 | Verify that it works and then click try again.
`;
50 | }
51 | return resolve(false);
52 | });
53 | } else {
54 | return resolve(true);
55 | }
56 | });
57 | }
58 | }
59 |
60 | module.exports = ReconnectUtil;
61 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/request-util.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const Logger = require('./logger-util');
3 |
4 | const CertificateUtil = require(__dirname + '/certificate-util.js');
5 | const ProxyUtil = require(__dirname + '/proxy-util.js');
6 | const ConfigUtil = require(__dirname + '/config-util.js');
7 | const SystemUtil = require(__dirname + '/../utils/system-util.js');
8 |
9 | const logger = new Logger({
10 | file: `request-util.log`,
11 | timestamp: true
12 | });
13 |
14 | let instance = null;
15 |
16 | class RequestUtil {
17 | constructor() {
18 | if (!instance) {
19 | instance = this;
20 | }
21 | return instance;
22 | }
23 |
24 | // ignoreCerts parameter helps in fetching server icon and
25 | // other server details when user chooses to ignore certificate warnings
26 | requestOptions(domain, ignoreCerts) {
27 | domain = this.formatUrl(domain);
28 | const certificate = CertificateUtil.getCertificate(
29 | encodeURIComponent(domain)
30 | );
31 | let certificateLocation = '';
32 | if (certificate) {
33 | // To handle case where certificate has been moved from the location in certificates.json
34 | try {
35 | certificateLocation = fs.readFileSync(certificate);
36 | } catch (err) {
37 | logger.warn('Error while trying to get certificate: ' + err);
38 | }
39 | }
40 | const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
41 | // 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
42 | // Add proxy as a parameter if it is being used.
43 | return {
44 | ca: certificateLocation ? certificateLocation : '',
45 | proxy: proxyEnabled ? ProxyUtil.getProxy(domain) : '',
46 | ecdhCurve: 'auto',
47 | headers: { 'User-Agent': SystemUtil.getUserAgent() },
48 | rejectUnauthorized: !ignoreCerts
49 | };
50 | }
51 |
52 | formatUrl(domain) {
53 | const hasPrefix = (domain.indexOf('http') === 0);
54 | if (hasPrefix) {
55 | return domain;
56 | } else {
57 | return (domain.indexOf('localhost:') >= 0) ? `http://${domain}` : `https://${domain}`;
58 | }
59 | }
60 | }
61 |
62 | module.exports = new RequestUtil();
63 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/sentry-util.js:
--------------------------------------------------------------------------------
1 | const { init, captureException } = require('@sentry/electron');
2 | const isDev = require('electron-is-dev');
3 |
4 | const sentryInit = () => {
5 | if (!isDev) {
6 | init({
7 | dsn: 'SENTRY_DSN',
8 | // We should ignore this error since it's harmless and we know the reason behind this
9 | // This error mainly comes from the console logs.
10 | // This is a temp solution until Sentry supports disabling the console logs
11 | ignoreErrors: ['does not appear to be a valid Zulip server'],
12 | sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second
13 | });
14 | }
15 | };
16 |
17 | module.exports = {
18 | sentryInit,
19 | captureException
20 | };
21 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/system-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {app} = require('electron').remote;
4 |
5 | const os = require('os');
6 |
7 | let instance = null;
8 |
9 | class SystemUtil {
10 | constructor() {
11 | if (instance) {
12 | return instance;
13 | } else {
14 | instance = this;
15 | }
16 |
17 | this.connectivityERR = [
18 | 'ERR_INTERNET_DISCONNECTED',
19 | 'ERR_PROXY_CONNECTION_FAILED',
20 | 'ERR_CONNECTION_RESET',
21 | 'ERR_NOT_CONNECTED',
22 | 'ERR_NAME_NOT_RESOLVED',
23 | 'ERR_NETWORK_CHANGED'
24 | ];
25 | this.userAgent = null;
26 |
27 | return instance;
28 | }
29 |
30 | getOS() {
31 | if (os.platform() === 'darwin') {
32 | return 'Mac';
33 | }
34 | if (os.platform() === 'linux') {
35 | return 'Linux';
36 | }
37 | if (os.platform() === 'win32' || os.platform() === 'win64') {
38 | if (parseFloat(os.release()) < 6.2) {
39 | return 'Windows 7';
40 | } else {
41 | return 'Windows 10';
42 | }
43 | }
44 | }
45 |
46 | setUserAgent(webViewUserAgent) {
47 | this.userAgent = 'ZulipElectron/' + app.getVersion() + ' ' + webViewUserAgent;
48 | }
49 |
50 | getUserAgent() {
51 | return this.userAgent;
52 | }
53 | }
54 |
55 | module.exports = new SystemUtil();
56 |
--------------------------------------------------------------------------------
/app/renderer/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Zulip
8 |
9 |
10 |
11 |
12 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/renderer/network.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Zulip - Network Troubleshooting
7 |
8 |
9 |
10 |
11 |
12 |
Zulip can't connect
13 |
14 |
Your computer seems to be offline.
15 |
We will keep trying to reconnect, or you can try now.
16 |
17 |
Try now
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/renderer/preference.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Zulip - Settings
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/resources/Icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/resources/Icon.icns
--------------------------------------------------------------------------------
/app/resources/Icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/resources/Icon.ico
--------------------------------------------------------------------------------
/app/resources/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/resources/Icon.png
--------------------------------------------------------------------------------
/app/resources/sounds/ding.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/resources/sounds/ding.ogg
--------------------------------------------------------------------------------
/app/resources/tray/traylinux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/resources/tray/traylinux.png
--------------------------------------------------------------------------------
/app/resources/tray/trayosx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/resources/tray/trayosx.png
--------------------------------------------------------------------------------
/app/resources/tray/trayosx@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/resources/tray/trayosx@2x.png
--------------------------------------------------------------------------------
/app/resources/tray/trayosx@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/resources/tray/trayosx@3x.png
--------------------------------------------------------------------------------
/app/resources/tray/trayosx@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/resources/tray/trayosx@4x.png
--------------------------------------------------------------------------------
/app/resources/tray/traywin.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/resources/tray/traywin.ico
--------------------------------------------------------------------------------
/app/resources/zulip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/app/resources/zulip.png
--------------------------------------------------------------------------------
/app/translations/bg.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "За Зулип",
3 | "Actual Size": "Реален размер",
4 | "Back": "обратно",
5 | "Close": "Близо",
6 | "Copy": "копие",
7 | "Cut": "Разрез",
8 | "Delete": "Изтрий",
9 | "Desktop App Settings": "Настройки за настолни приложения",
10 | "Edit": "редактиране",
11 | "File": "досие",
12 | "Forward": "напред",
13 | "Hard Reload": "Трудно презареждане",
14 | "Help": "Помогне",
15 | "History": "история",
16 | "Keyboard Shortcuts": "Комбинация от клавиши",
17 | "Log Out": "Излез от профила си",
18 | "Minimize": "Минимизиране",
19 | "Paste": "паста",
20 | "Paste and Match Style": "Поставяне и съвпадение на стила",
21 | "Quit": "напускам",
22 | "Redo": "ремонтирам",
23 | "Reload": "Презареди",
24 | "Report an issue...": "Подаване на сигнал за проблем ...",
25 | "Reset App Settings": "Нулирайте настройките на приложението",
26 | "Select All": "Избери всички",
27 | "Show App Logs": "Показване на регистрационните файлове на приложения",
28 | "Toggle DevTools for Active Tab": "Превключете DevTools за активен раздел",
29 | "Toggle DevTools for Zulip App": "Превключете DevTools за Zulip App",
30 | "Toggle Full Screen": "Превключване на цял екран",
31 | "Toggle Sidebar": "Превключване на страничната лента",
32 | "Toggle Tray Icon": "Превключване на иконата на тавата",
33 | "Undo": "премахвам",
34 | "View": "изглед",
35 | "Window": "прозорец",
36 | "Zoom In": "Увеличавам",
37 | "Zoom Out": "Отдалечавам",
38 | "Zulip Help": "Помощ за Zulip"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/cs.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "O společnosti Zulip",
3 | "Actual Size": "Aktuální velikost",
4 | "Back": "Zadní",
5 | "Close": "Zavřít",
6 | "Copy": "kopírovat",
7 | "Cut": "Střih",
8 | "Delete": "Vymazat",
9 | "Desktop App Settings": "Nastavení aplikace Desktop",
10 | "Edit": "Upravit",
11 | "File": "Soubor",
12 | "Forward": "Vpřed",
13 | "Hard Reload": "Hard Reload",
14 | "Help": "Pomoc",
15 | "History": "Dějiny",
16 | "Keyboard Shortcuts": "Klávesové zkratky",
17 | "Log Out": "Odhlásit se",
18 | "Minimize": "Minimalizujte",
19 | "Paste": "Vložit",
20 | "Paste and Match Style": "Vložit a přizpůsobit styl",
21 | "Quit": "Přestat",
22 | "Redo": "Předělat",
23 | "Reload": "Znovu načíst",
24 | "Report an issue...": "Nahlásit problém...",
25 | "Reset App Settings": "Obnovit nastavení aplikace",
26 | "Select All": "Vybrat vše",
27 | "Show App Logs": "Zobrazit protokoly aplikace",
28 | "Toggle DevTools for Active Tab": "Přepínač DevTools pro aktivní kartu",
29 | "Toggle DevTools for Zulip App": "Přepnout nástroj DevTools pro aplikaci Zulip App",
30 | "Toggle Full Screen": "Přepnout na celou obrazovku",
31 | "Toggle Sidebar": "Přepnout postranní panel",
32 | "Toggle Tray Icon": "Přepnout ikonu zásobníku",
33 | "Undo": "vrátit",
34 | "View": "Pohled",
35 | "Window": "Okno",
36 | "Zoom In": "Přiblížit",
37 | "Zoom Out": "Oddálit",
38 | "Zulip Help": "Zulip Nápověda"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "Über Zulip",
3 | "Actual Size": "Tatsächliche Größe",
4 | "Back": "Zurück",
5 | "Close": "Schließen",
6 | "Copy": "Kopieren",
7 | "Cut": "Schnitt",
8 | "Delete": "Löschen",
9 | "Desktop App Settings": "Desktop App Einstellungen",
10 | "Edit": "Bearbeiten",
11 | "File": "Datei",
12 | "Forward": "Nach vorne",
13 | "Hard Reload": "Hard Reload",
14 | "Help": "Hilfe",
15 | "History": "Geschichte",
16 | "Keyboard Shortcuts": "Tastatürkürzel",
17 | "Log Out": "Ausloggen",
18 | "Minimize": "Minimieren",
19 | "Paste": "Einfügen",
20 | "Paste and Match Style": "Einfügen und Anpassen von Stilen",
21 | "Quit": "Verlassen",
22 | "Redo": "Wiederholen",
23 | "Reload": "Neu laden",
24 | "Report an issue...": "Ein Problem melden...",
25 | "Reset App Settings": "App-Einstellungen zurücksetzen",
26 | "Select All": "Wählen Sie Alle",
27 | "Show App Logs": "App-Logs anzeigen",
28 | "Toggle DevTools for Active Tab": "Schalten Sie DevTools für Active Tab ein",
29 | "Toggle DevTools for Zulip App": "Umschalt DevTools für Zulip App",
30 | "Toggle Full Screen": "Vollbild umschalten",
31 | "Toggle Sidebar": "Toggle Seitenleiste",
32 | "Toggle Tray Icon": "Toggle Tray-Symbol",
33 | "Undo": "Rückgängig machen",
34 | "View": "Aussicht",
35 | "Window": "Fenster",
36 | "Zoom In": "Hineinzoomen",
37 | "Zoom Out": "Rauszoomen",
38 | "Zulip Help": "Zulip Hilfe"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "About Zulip",
3 | "Actual Size": "Actual Size",
4 | "Back": "Back",
5 | "Close": "Close",
6 | "Copy": "Copy",
7 | "Cut": "Cut",
8 | "Delete": "Delete",
9 | "Desktop App Settings": "Desktop App Settings",
10 | "Edit": "Edit",
11 | "File": "File",
12 | "Forward": "Forward",
13 | "Hard Reload": "Hard Reload",
14 | "Help": "Help",
15 | "History": "History",
16 | "Keyboard Shortcuts": "Keyboard Shortcuts",
17 | "Log Out": "Log Out",
18 | "Minimize": "Minimize",
19 | "Paste": "Paste",
20 | "Paste and Match Style": "Paste and Match Style",
21 | "Quit": "Quit",
22 | "Redo": "Redo",
23 | "Reload": "Reload",
24 | "Report an issue...": "Report an issue...",
25 | "Reset App Settings": "Reset App Settings",
26 | "Select All": "Select All",
27 | "Show App Logs": "Show App Logs",
28 | "Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
29 | "Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
30 | "Toggle Full Screen": "Toggle Full Screen",
31 | "Toggle Sidebar": "Toggle Sidebar",
32 | "Toggle Tray Icon": "Toggle Tray Icon",
33 | "Undo": "Undo",
34 | "View": "View",
35 | "Window": "Window",
36 | "Zoom In": "Zoom In",
37 | "Zoom Out": "Zoom Out",
38 | "Zulip Help": "Zulip Help"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "Acerca de Zulip",
3 | "Actual Size": "Tamaño real",
4 | "Back": "Espalda",
5 | "Close": "Cerca",
6 | "Copy": "Dupdo",
7 | "Cut": "Cortar",
8 | "Delete": "Borrar",
9 | "Desktop App Settings": "Configuración de la aplicación de escritorio",
10 | "Edit": "Editar",
11 | "File": "Archivo",
12 | "Forward": "Adelante",
13 | "Hard Reload": "Recargar duro",
14 | "Help": "Ayuda",
15 | "History": "Historia",
16 | "Keyboard Shortcuts": "Atajos de teclado",
17 | "Log Out": "Cerrar sesión",
18 | "Minimize": "Minimizar",
19 | "Paste": "Pegar",
20 | "Paste and Match Style": "Pegar y combinar estilo",
21 | "Quit": "Dejar",
22 | "Redo": "Rehacer",
23 | "Reload": "Recargar",
24 | "Report an issue...": "Reportar un problema...",
25 | "Reset App Settings": "Restablecer configuraciones",
26 | "Select All": "Seleccionar todo",
27 | "Show App Logs": "Mostrar registros de aplicaciones",
28 | "Toggle DevTools for Active Tab": "Alternar DevTools para Active Tab",
29 | "Toggle DevTools for Zulip App": "Alternar DevTools para la aplicación Zulip",
30 | "Toggle Full Screen": "Alternar pantalla completa",
31 | "Toggle Sidebar": "Alternar barra lateral",
32 | "Toggle Tray Icon": "Alternar el icono de la bandeja",
33 | "Undo": "Deshacer",
34 | "View": "Ver",
35 | "Window": "Ventana",
36 | "Zoom In": "Acercarse",
37 | "Zoom Out": "Disminuir el zoom",
38 | "Zulip Help": "Ayuda de Zulip"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "À propos de Zulip",
3 | "Actual Size": "Taille actuelle",
4 | "Back": "Arrière",
5 | "Close": "Fermer",
6 | "Copy": "Copie",
7 | "Cut": "Couper",
8 | "Delete": "Effacer",
9 | "Desktop App Settings": "Paramètres de l'application de bureau",
10 | "Edit": "modifier",
11 | "File": "Fichier",
12 | "Forward": "Vers l'avant",
13 | "Hard Reload": "Rechargement dur",
14 | "Help": "Aidez-moi",
15 | "History": "Histoire",
16 | "Keyboard Shortcuts": "Raccourcis clavier",
17 | "Log Out": "Connectez - Out",
18 | "Minimize": "Minimiser",
19 | "Paste": "Coller",
20 | "Paste and Match Style": "Coller et assortir le style",
21 | "Quit": "Quitter",
22 | "Redo": "Refaire",
23 | "Reload": "Recharger",
24 | "Report an issue...": "Signaler un problème...",
25 | "Reset App Settings": "Réinitialiser les paramètres",
26 | "Select All": "Tout sélectionner",
27 | "Show App Logs": "Afficher les journaux d'applications",
28 | "Toggle DevTools for Active Tab": "Basculer DevTools pour l'onglet actif",
29 | "Toggle DevTools for Zulip App": "Basculer DevTools pour l'application Zulip",
30 | "Toggle Full Screen": "Basculer en plein écran",
31 | "Toggle Sidebar": "Basculer la barre latérale",
32 | "Toggle Tray Icon": "Toggle Tray Icône",
33 | "Undo": "annuler",
34 | "View": "Vue",
35 | "Window": "Fenêtre",
36 | "Zoom In": "Agrandir",
37 | "Zoom Out": "Dézoomer",
38 | "Zulip Help": "Aide Zulip"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/hi.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "Zulip के बारे में",
3 | "Actual Size": "वास्तविक आकार",
4 | "Back": "वापस",
5 | "Close": "बंद करे",
6 | "Copy": "प्रतिलिपि",
7 | "Cut": "कट गया",
8 | "Delete": "हटाना",
9 | "Desktop App Settings": "डेस्कटॉप ऐप सेटिंग्स",
10 | "Edit": "संपादित करें",
11 | "File": "फ़ाइल",
12 | "Forward": "आगे",
13 | "Hard Reload": "कठिन पुन: लोड करें",
14 | "Help": "मदद",
15 | "History": "इतिहास",
16 | "Keyboard Shortcuts": "कुंजीपटल अल्प मार्ग",
17 | "Log Out": "लोग आउट",
18 | "Minimize": "छोटा करना",
19 | "Paste": "चिपकाएं",
20 | "Paste and Match Style": "चिपकाएं और शैली का मिलान करें",
21 | "Quit": "छोड़ना",
22 | "Redo": "फिर से करना",
23 | "Reload": "सीमा से अधिक लादना",
24 | "Report an issue...": "मामले की रिपोर्ट करें...",
25 | "Reset App Settings": "ऐप सेटिंग्स रीसेट करें",
26 | "Select All": "सभी का चयन करे",
27 | "Show App Logs": "ऐप लॉग दिखाएं",
28 | "Toggle DevTools for Active Tab": "सक्रिय टैब के लिए DevTools टॉगल करें",
29 | "Toggle DevTools for Zulip App": "Zulip ऐप के लिए DevTools टॉगल करें",
30 | "Toggle Full Screen": "पूर्णस्क्रीन चालू करें",
31 | "Toggle Sidebar": "साइडबार टॉगल करें",
32 | "Toggle Tray Icon": "ट्रे आइकन टॉगल करें",
33 | "Undo": "पूर्ववत करें",
34 | "View": "राय",
35 | "Window": "खिड़की",
36 | "Zoom In": "ज़ूम इन",
37 | "Zoom Out": "ज़ूम आउट",
38 | "Zulip Help": "Zulip सहायता"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/hu.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "A Zulipról",
3 | "Actual Size": "Valódi méret",
4 | "Back": "Hát",
5 | "Close": "Bezárás",
6 | "Copy": "Másolat",
7 | "Cut": "Vágott",
8 | "Delete": "Töröl",
9 | "Desktop App Settings": "Asztali alkalmazások beállításai",
10 | "Edit": "szerkesztése",
11 | "File": "fájl",
12 | "Forward": "Előre",
13 | "Hard Reload": "Hard Reload",
14 | "Help": "Segítség",
15 | "History": "Történelem",
16 | "Keyboard Shortcuts": "Gyorsbillentyűket",
17 | "Log Out": "Kijelentkezés",
18 | "Minimize": "Kis méret",
19 | "Paste": "Paszta",
20 | "Paste and Match Style": "Beillesztés és stílusok illesztése",
21 | "Quit": "quit",
22 | "Redo": "Újra",
23 | "Reload": "Reload",
24 | "Report an issue...": "Hibajelentés ...",
25 | "Reset App Settings": "Az alkalmazás beállításainak visszaállítása",
26 | "Select All": "Mindet kiválaszt",
27 | "Show App Logs": "Alkalmazásnaplók megjelenítése",
28 | "Toggle DevTools for Active Tab": "A DevTools aktiválása az Aktív laphoz",
29 | "Toggle DevTools for Zulip App": "Kapcsolja a DevTools-ot a Zulip App-hoz",
30 | "Toggle Full Screen": "Teljes képernyőre váltás",
31 | "Toggle Sidebar": "Oldalsáv átkapcsolása",
32 | "Toggle Tray Icon": "Tálcaikon átkapcsolása",
33 | "Undo": "kibont",
34 | "View": "Kilátás",
35 | "Window": "Ablak",
36 | "Zoom In": "Nagyítás",
37 | "Zoom Out": "Kicsinyítés",
38 | "Zulip Help": "Zulip Súgó"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/id.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "Tentang Zulip",
3 | "Actual Size": "Ukuran sebenarnya",
4 | "Back": "Kembali",
5 | "Close": "Dekat",
6 | "Copy": "Salinan",
7 | "Cut": "Memotong",
8 | "Delete": "Menghapus",
9 | "Desktop App Settings": "Setelan Aplikasi Desktop",
10 | "Edit": "Edit",
11 | "File": "Mengajukan",
12 | "Forward": "Meneruskan",
13 | "Hard Reload": "Hard Reload",
14 | "Help": "Membantu",
15 | "History": "Sejarah",
16 | "Keyboard Shortcuts": "Cara pintas keyboard",
17 | "Log Out": "Keluar",
18 | "Minimize": "Memperkecil",
19 | "Paste": "Pasta",
20 | "Paste and Match Style": "Tempel dan Cocokkan Gaya",
21 | "Quit": "Berhenti",
22 | "Redo": "Mengulangi",
23 | "Reload": "Reload",
24 | "Report an issue...": "Laporkan masalah ...",
25 | "Reset App Settings": "Setel ulang Pengaturan Aplikasi",
26 | "Select All": "Pilih Semua",
27 | "Show App Logs": "Tampilkan log aplikasi",
28 | "Toggle DevTools for Active Tab": "Toggle DevTools untuk Tab Aktif",
29 | "Toggle DevTools for Zulip App": "Toggle DevTools untuk Aplikasi Zulip",
30 | "Toggle Full Screen": "Toggle Full Screen",
31 | "Toggle Sidebar": "Toggle Sidebar",
32 | "Toggle Tray Icon": "Toggle Tray Icon",
33 | "Undo": "Membuka",
34 | "View": "Melihat",
35 | "Window": "Jendela",
36 | "Zoom In": "Perbesar",
37 | "Zoom Out": "Zoom Out",
38 | "Zulip Help": "Zulip Help"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/ja.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "チューリップについて",
3 | "Actual Size": "実際のサイズ",
4 | "Back": "バック",
5 | "Close": "閉じる",
6 | "Copy": "コピー",
7 | "Cut": "カット",
8 | "Delete": "削除",
9 | "Desktop App Settings": "デスクトップアプリケーションの設定",
10 | "Edit": "編集",
11 | "File": "ファイル",
12 | "Forward": "フォワード",
13 | "Hard Reload": "ハードリロード",
14 | "Help": "助けて",
15 | "History": "歴史",
16 | "Keyboard Shortcuts": "キーボードショートカット",
17 | "Log Out": "ログアウト",
18 | "Minimize": "最小化する",
19 | "Paste": "ペースト",
20 | "Paste and Match Style": "スタイルの貼り付けと一致",
21 | "Quit": "終了する",
22 | "Redo": "やり直し",
23 | "Reload": "リロード",
24 | "Report an issue...": "問題を報告する...",
25 | "Reset App Settings": "アプリの設定をリセットする",
26 | "Select All": "すべて選択",
27 | "Show App Logs": "アプリケーションログを表示する",
28 | "Toggle DevTools for Active Tab": "DevTools for Activeタブを切り替える",
29 | "Toggle DevTools for Zulip App": "Zulip AppのDevToolsの切り替え",
30 | "Toggle Full Screen": "フルスクリーン切り替え",
31 | "Toggle Sidebar": "サイドバーの切り替え",
32 | "Toggle Tray Icon": "トレイアイコンを切り替える",
33 | "Undo": "元に戻す",
34 | "View": "ビュー",
35 | "Window": "窓",
36 | "Zoom In": "ズームイン",
37 | "Zoom Out": "ズームアウトする",
38 | "Zulip Help": "チューリップヘルプ"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/ko.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "튤립 소개",
3 | "Actual Size": "실제 크기",
4 | "Back": "뒤로",
5 | "Close": "닫기",
6 | "Copy": "부",
7 | "Cut": "절단",
8 | "Delete": "지우다",
9 | "Desktop App Settings": "데스크톱 앱 설정",
10 | "Edit": "편집하다",
11 | "File": "파일",
12 | "Forward": "앞으로",
13 | "Hard Reload": "하드 다시로드",
14 | "Help": "도움",
15 | "History": "역사",
16 | "Keyboard Shortcuts": "키보드 단축키",
17 | "Log Out": "로그 아웃",
18 | "Minimize": "최소화",
19 | "Paste": "풀",
20 | "Paste and Match Style": "붙여 넣기 및 스타일 일치",
21 | "Quit": "떠나다",
22 | "Redo": "다시 하다",
23 | "Reload": "다시로드",
24 | "Report an issue...": "문제 신고 ...",
25 | "Reset App Settings": "앱 설정 재설정",
26 | "Select All": "모두 선택",
27 | "Show App Logs": "앱 로그 표시",
28 | "Toggle DevTools for Active Tab": "DevTools for Active Tab 토글",
29 | "Toggle DevTools for Zulip App": "Zulip App 용 DevTools 토글",
30 | "Toggle Full Screen": "전체 화면 토글",
31 | "Toggle Sidebar": "사이드 바 전환",
32 | "Toggle Tray Icon": "트레이 아이콘 토글",
33 | "Undo": "끄르다",
34 | "View": "전망",
35 | "Window": "창문",
36 | "Zoom In": "확대",
37 | "Zoom Out": "축소",
38 | "Zulip Help": "튤립 도움말"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/ml.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "സുലിപ്",
3 | "Actual Size": "യഥാർത്ഥ വലുപ്പം",
4 | "Back": "തിരികെ",
5 | "Close": "അടയ്ക്കുക",
6 | "Copy": "പകർത്തുക",
7 | "Cut": "മുറിക്കുക",
8 | "Delete": "ഇല്ലാതാക്കുക",
9 | "Desktop App Settings": "ഡെസ്ക്ടോപ്പ് അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങൾ",
10 | "Edit": "എഡിറ്റുചെയ്യുക",
11 | "File": "ഫയൽ",
12 | "Forward": "മുന്നോട്ട്",
13 | "Hard Reload": "ഹാർഡ് റീലോഡ്",
14 | "Help": "സഹായിക്കൂ",
15 | "History": "ചരിത്രം",
16 | "Keyboard Shortcuts": "കീബോർഡ് കുറുക്കുവഴികൾ",
17 | "Log Out": "ലോഗ് ഔട്ട്",
18 | "Minimize": "ചെറുതാക്കുക",
19 | "Paste": "പേസ്റ്റ്",
20 | "Paste and Match Style": "ശൈലി ഒട്ടിക്കുകയും പൊരുത്തപ്പെടുത്തുകയും ചെയ്യുക",
21 | "Quit": "പുറത്തുകടക്കുക",
22 | "Redo": "വീണ്ടും ചെയ്യുക",
23 | "Reload": "വീണ്ടും ലോഡുചെയ്യുക",
24 | "Report an issue...": "ഒരു പ്രശ്നം റിപ്പോർട്ടുചെയ്യുക ...",
25 | "Reset App Settings": "അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങൾ പുനഃസജ്ജമാക്കുക",
26 | "Select All": "എല്ലാം തിരഞ്ഞെടുക്കുക",
27 | "Show App Logs": "അപ്ലിക്കേഷൻ ലോഗുകൾ കാണിക്കുക",
28 | "Toggle DevTools for Active Tab": "സജീവ ടാബിനായി DevTools ടോഗിൾ ചെയ്യുക",
29 | "Toggle DevTools for Zulip App": "സുലിപ്പ് ആപ്ലിക്കേഷനായി DevTools ടോഗിൾ ചെയ്യുക",
30 | "Toggle Full Screen": "പൂർണ്ണ സ്ക്രീൻ ടോഗിൾ ചെയ്യുക",
31 | "Toggle Sidebar": "സൈഡ്ബാർ ടോഗിൾ ചെയ്യുക",
32 | "Toggle Tray Icon": "ട്രേ ഐക്കൺ ടോഗിൾ ചെയ്യുക",
33 | "Undo": "പഴയപടിയാക്കുക",
34 | "View": "കാണുക",
35 | "Window": "ജാലകം",
36 | "Zoom In": "വലുതാക്കുക",
37 | "Zoom Out": "സൂം ഔട്ട്",
38 | "Zulip Help": "സുലിപ്പ് സഹായം"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/nl.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "Over Zulip",
3 | "Actual Size": "Daadwerkelijke grootte",
4 | "Back": "Terug",
5 | "Close": "Dichtbij",
6 | "Copy": "Kopiëren",
7 | "Cut": "Besnoeiing",
8 | "Delete": "Verwijder",
9 | "Desktop App Settings": "Desktop-app-instellingen",
10 | "Edit": "Bewerk",
11 | "File": "het dossier",
12 | "Forward": "Vooruit",
13 | "Hard Reload": "Moeilijke herladen",
14 | "Help": "Helpen",
15 | "History": "Geschiedenis",
16 | "Keyboard Shortcuts": "Toetsenbord sneltoetsen",
17 | "Log Out": "Uitloggen",
18 | "Minimize": "verkleinen",
19 | "Paste": "Plakken",
20 | "Paste and Match Style": "Plak en match stijl",
21 | "Quit": "ophouden",
22 | "Redo": "Opnieuw doen",
23 | "Reload": "vernieuwen",
24 | "Report an issue...": "Een probleem melden...",
25 | "Reset App Settings": "App-instellingen opnieuw instellen",
26 | "Select All": "Selecteer alles",
27 | "Show App Logs": "App-logs weergeven",
28 | "Toggle DevTools for Active Tab": "DevTools voor actieve tabblad omschakelen",
29 | "Toggle DevTools for Zulip App": "DevTools voor Zulip-app omschakelen",
30 | "Toggle Full Screen": "Volledig scherm activeren",
31 | "Toggle Sidebar": "Zijbalk verschuiven",
32 | "Toggle Tray Icon": "Pictogram Lade wisselen",
33 | "Undo": "ongedaan maken",
34 | "View": "Uitzicht",
35 | "Window": "Venster",
36 | "Zoom In": "In zoomen",
37 | "Zoom Out": "Uitzoomen",
38 | "Zulip Help": "Zulip Help"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/pl.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "O Zulip",
3 | "Actual Size": "Rzeczywisty rozmiar",
4 | "Back": "Plecy",
5 | "Close": "Blisko",
6 | "Copy": "Kopiuj",
7 | "Cut": "Ciąć",
8 | "Delete": "Kasować",
9 | "Desktop App Settings": "Ustawienia aplikacji na komputer",
10 | "Edit": "Edytować",
11 | "File": "Plik",
12 | "Forward": "Naprzód",
13 | "Hard Reload": "Trudne przeładowanie",
14 | "Help": "Wsparcie",
15 | "History": "Historia",
16 | "Keyboard Shortcuts": "Skróty klawiszowe",
17 | "Log Out": "Wyloguj",
18 | "Minimize": "Zminimalizować",
19 | "Paste": "Pasta",
20 | "Paste and Match Style": "Wklej i dopasuj styl",
21 | "Quit": "Porzucić",
22 | "Redo": "Przerobić",
23 | "Reload": "Przeładować",
24 | "Report an issue...": "Zgłoś problem...",
25 | "Reset App Settings": "Zresetuj ustawienia aplikacji",
26 | "Select All": "Zaznacz wszystko",
27 | "Show App Logs": "Pokaż dzienniki aplikacji",
28 | "Toggle DevTools for Active Tab": "Przełącz DevTools dla Active Tab",
29 | "Toggle DevTools for Zulip App": "Przełącz DevTools dla aplikacji Zulip",
30 | "Toggle Full Screen": "Przełącz tryb pełnoekranowy",
31 | "Toggle Sidebar": "Przełącz boczny pasek",
32 | "Toggle Tray Icon": "Przełącz ikonę tacy",
33 | "Undo": "Cofnij",
34 | "View": "Widok",
35 | "Window": "Okno",
36 | "Zoom In": "Zbliżenie",
37 | "Zoom Out": "Pomniejsz",
38 | "Zulip Help": "Zulip Help"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/pt.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "Sobre Zulip",
3 | "Actual Size": "Tamanho atual",
4 | "Back": "Costas",
5 | "Close": "Fechar",
6 | "Copy": "cópia de",
7 | "Cut": "Cortar",
8 | "Delete": "Excluir",
9 | "Desktop App Settings": "Configurações da aplicação Desktop",
10 | "Edit": "Editar",
11 | "File": "Arquivo",
12 | "Forward": "frente",
13 | "Hard Reload": "Hard Recargar",
14 | "Help": "Socorro",
15 | "History": "História",
16 | "Keyboard Shortcuts": "Atalhos do teclado",
17 | "Log Out": "Sair",
18 | "Minimize": "Minimizar",
19 | "Paste": "Colar",
20 | "Paste and Match Style": "Estilo de colar e combinar",
21 | "Quit": "Sair",
22 | "Redo": "Refazer",
23 | "Reload": "recarregar",
24 | "Report an issue...": "Relatar um problema ...",
25 | "Reset App Settings": "Redefinir as configurações da aplicação",
26 | "Select All": "Selecionar tudo",
27 | "Show App Logs": "Mostrar logs de aplicativos",
28 | "Toggle DevTools for Active Tab": "Toggle DevTools for Active Tab",
29 | "Toggle DevTools for Zulip App": "Toggle DevTools for Zulip App",
30 | "Toggle Full Screen": "Alternar para o modo tela cheia",
31 | "Toggle Sidebar": "Toggle Sidebar",
32 | "Toggle Tray Icon": "Ícone Toggle Tray",
33 | "Undo": "Desfazer",
34 | "View": "Visão",
35 | "Window": "Janela",
36 | "Zoom In": "Mais Zoom",
37 | "Zoom Out": "Reduzir o zoom",
38 | "Zulip Help": "Zulip Help"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "О пользователе Zulip",
3 | "Actual Size": "Фактический размер",
4 | "Back": "назад",
5 | "Close": "Закрыть",
6 | "Copy": "копия",
7 | "Cut": "Порез",
8 | "Delete": "Удалить",
9 | "Desktop App Settings": "Настройки настольных приложений",
10 | "Edit": "редактировать",
11 | "File": "файл",
12 | "Forward": "Вперед",
13 | "Hard Reload": "Hard Reload",
14 | "Help": "Помогите",
15 | "History": "история",
16 | "Keyboard Shortcuts": "Горячие клавиши",
17 | "Log Out": "Выйти",
18 | "Minimize": "Минимизировать",
19 | "Paste": "Вставить",
20 | "Paste and Match Style": "Стиль вставки и совпадения",
21 | "Quit": "Уволиться",
22 | "Redo": "переделывать",
23 | "Reload": "перезагружать",
24 | "Report an issue...": "Сообщить о проблеме...",
25 | "Reset App Settings": "Сбросить настройки приложения",
26 | "Select All": "Выбрать все",
27 | "Show App Logs": "Показать журналы приложений",
28 | "Toggle DevTools for Active Tab": "Toggle DevTools для активной вкладки",
29 | "Toggle DevTools for Zulip App": "Toggle DevTools для приложения Zulip",
30 | "Toggle Full Screen": "Включить полноэкранный режим",
31 | "Toggle Sidebar": "Переключить боковую панель",
32 | "Toggle Tray Icon": "Иконка Toggle Tray",
33 | "Undo": "расстегивать",
34 | "View": "Посмотреть",
35 | "Window": "Окно",
36 | "Zoom In": "Приблизить",
37 | "Zoom Out": "Уменьшить",
38 | "Zulip Help": "Zulip Help"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/sr.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "О Зулипу",
3 | "Actual Size": "Стварна величина",
4 | "Back": "Назад",
5 | "Close": "Близу",
6 | "Copy": "Копирај",
7 | "Cut": "Цут",
8 | "Delete": "Избриши",
9 | "Desktop App Settings": "Подешавања за десктоп апликације",
10 | "Edit": "Уредити",
11 | "File": "Филе",
12 | "Forward": "Напријед",
13 | "Hard Reload": "Хард Релоад",
14 | "Help": "Помоћ",
15 | "History": "Историја",
16 | "Keyboard Shortcuts": "Пречице на тастатури",
17 | "Log Out": "Одјавити се",
18 | "Minimize": "Минимизирај",
19 | "Paste": "Пасте",
20 | "Paste and Match Style": "Пасте и Матцх Стиле",
21 | "Quit": "Одустати",
22 | "Redo": "Поново",
23 | "Reload": "Освежи",
24 | "Report an issue...": "Пријавите проблем...",
25 | "Reset App Settings": "Поново поставите подешавања апликације",
26 | "Select All": "Изабери све",
27 | "Show App Logs": "Прикажи евиденције апликација",
28 | "Toggle DevTools for Active Tab": "Пребаците ДевТоолс за Ацтиве Таб",
29 | "Toggle DevTools for Zulip App": "Пребаците ДевТоолс за Зулип Апп",
30 | "Toggle Full Screen": "Пребаците цео екран",
31 | "Toggle Sidebar": "Пребаците бочну траку",
32 | "Toggle Tray Icon": "Тоггле Траи Ицон",
33 | "Undo": "Ундо",
34 | "View": "Поглед",
35 | "Window": "Прозор",
36 | "Zoom In": "Увеличати",
37 | "Zoom Out": "Зумирај",
38 | "Zulip Help": "Зулип Хелп"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/ta.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "ஜுலிப் பற்றி",
3 | "Actual Size": "உண்மையான அளவு",
4 | "Back": "மீண்டும்",
5 | "Close": "நெருக்கமான",
6 | "Copy": "நகல்",
7 | "Cut": "வெட்டு",
8 | "Delete": "அழி",
9 | "Desktop App Settings": "டெஸ்க்டாப் பயன்பாட்டு அமைப்புகள்",
10 | "Edit": "தொகு",
11 | "File": "கோப்பு",
12 | "Forward": "முன்னோக்கி",
13 | "Hard Reload": "கடினமாக மீண்டும் ஏற்றவும்",
14 | "Help": "உதவி",
15 | "History": "வரலாறு",
16 | "Keyboard Shortcuts": "விசைப்பலகை குறுக்குவழிகள்",
17 | "Log Out": "வெளியேறு",
18 | "Minimize": "குறைத்தல்",
19 | "Paste": "ஒட்டு",
20 | "Paste and Match Style": "உடை ஒட்டு மற்றும் பொருந்தும்",
21 | "Quit": "விட்டுவிட",
22 | "Redo": "மீண்டும் செய்",
23 | "Reload": "ஏற்றவும்",
24 | "Report an issue...": "சிக்கலைப் புகார ...",
25 | "Reset App Settings": "பயன்பாட்டு அமைப்புகளை மீட்டமைக்கவும்",
26 | "Select All": "அனைத்தையும் தெரிவுசெய்",
27 | "Show App Logs": "பயன்பாட்டு பதிவுகள் காட்டு",
28 | "Toggle DevTools for Active Tab": "செயலில் தாவலுக்கு DevTools ஐ மாற்று",
29 | "Toggle DevTools for Zulip App": "Zulip பயன்பாட்டிற்கான DevTools ஐ மாற்று",
30 | "Toggle Full Screen": "மாற்று முழுத்திரை",
31 | "Toggle Sidebar": "பக்கப்பட்டி மாறு",
32 | "Toggle Tray Icon": "ட்ரே ஐகானை மாற்று",
33 | "Undo": "செயல்தவிர்",
34 | "View": "காண்க",
35 | "Window": "ஜன்னல்",
36 | "Zoom In": "பெரிதாக்க",
37 | "Zoom Out": "பெரிதாக்குக",
38 | "Zulip Help": "Zulip உதவி"
39 | }
40 |
--------------------------------------------------------------------------------
/app/translations/tr.json:
--------------------------------------------------------------------------------
1 | {
2 | "About Zulip": "Zulip Hakkinda",
3 | "Actual Size": "Gerçek boyutu",
4 | "Back": "Geri",
5 | "Close": "Kapat",
6 | "Copy": "kopya",
7 | "Cut": "Kesmek",
8 | "Delete": "silmek",
9 | "Desktop App Settings": "Masaüstü Uygulama Ayarları",
10 | "Edit": "Düzenle",
11 | "File": "Dosya",
12 | "Forward": "ileri",
13 | "Hard Reload": "Sert Yeniden Yükle",
14 | "Help": "yardım et",
15 | "History": "Tarihçe",
16 | "Keyboard Shortcuts": "Klavye kısayolları",
17 | "Log Out": "Çıkış Yap",
18 | "Minimize": "küçültmek",
19 | "Paste": "Yapıştırmak",
20 | "Paste and Match Style": "Stili Yapıştır ve Eşleştir",
21 | "Quit": "çıkmak",
22 | "Redo": "yeniden yapmak",
23 | "Reload": "Tekrar yükle",
24 | "Report an issue...": "Bir sorun bildir ...",
25 | "Reset App Settings": "Uygulama Ayarlarını Sıfırla",
26 | "Select All": "Hepsini seç",
27 | "Show App Logs": "Uygulama Günlüğlerini Göster",
28 | "Toggle DevTools for Active Tab": "Aktif Sekme İçin DevTools'a Geçiş Yap",
29 | "Toggle DevTools for Zulip App": "Zulip App için DevTools'a Geçiş Yap",
30 | "Toggle Full Screen": "Tam ekrana geç",
31 | "Toggle Sidebar": "Kenar Çubuğunu Aç / Kapat",
32 | "Toggle Tray Icon": "Tepsi Simgesini Aç / Kapat",
33 | "Undo": "Geri alma",
34 | "View": "Görünüm",
35 | "Window": "pencere",
36 | "Zoom In": "Yakınlaştır",
37 | "Zoom Out": "Uzaklaştır",
38 | "Zulip Help": "Zulip Yardımı"
39 | }
40 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: build{build}
2 |
3 | platform:
4 | - x64
5 | os: Previous Visual Studio 2015
6 |
7 | cache:
8 | - node_modules
9 |
10 | install:
11 | - ps: Install-Product node 8 x64
12 | - git reset --hard HEAD
13 | - npm install npm -g
14 | - node --version
15 | - npm --version
16 | - python --version
17 | - npm install
18 | - npm install -g gulp
19 |
20 | build: off
21 |
22 | test_script:
23 | - npm run test
24 | # - npm run test-e2e
25 |
--------------------------------------------------------------------------------
/build/appdmg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/appdmg.png
--------------------------------------------------------------------------------
/build/entitlements.mas.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.network.client
8 |
9 | com.apple.security.files.user-selected.read-only
10 |
11 | com.apple.security.files.user-selected.read-write
12 |
13 | com.apple.security.files.downloads.read-write
14 |
15 |
16 |
--------------------------------------------------------------------------------
/build/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/icon.icns
--------------------------------------------------------------------------------
/build/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/icon.ico
--------------------------------------------------------------------------------
/build/icons/1024x1024.png:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:1b92ae5c6e5fb0e5b7fcdb46fe102fa0ca7f4d150016cf1a461c0e86921bc731
3 | size 163336
4 |
--------------------------------------------------------------------------------
/build/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/icons/128x128.png
--------------------------------------------------------------------------------
/build/icons/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/icons/16x16.png
--------------------------------------------------------------------------------
/build/icons/24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/icons/24x24.png
--------------------------------------------------------------------------------
/build/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/icons/256x256.png
--------------------------------------------------------------------------------
/build/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/icons/32x32.png
--------------------------------------------------------------------------------
/build/icons/48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/icons/48x48.png
--------------------------------------------------------------------------------
/build/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/icons/512x512.png
--------------------------------------------------------------------------------
/build/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/icons/64x64.png
--------------------------------------------------------------------------------
/build/icons/96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/icons/96x96.png
--------------------------------------------------------------------------------
/build/zulip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/build/zulip.png
--------------------------------------------------------------------------------
/development.md:
--------------------------------------------------------------------------------
1 | # Improve development guide
2 |
3 | # Development setup
4 |
5 | This is a guide to running the Zulip desktop app from a source tree, in order to contribute to developing it. The Zulip electron development environment can be installed on **macOS, Windows, and Linux** (Debian or Ubuntu recommended). You’ll need at least **2GB of available RAM**. Installing the Zulip electron development environment requires downloading several hundred megabytes of dependencies, so you will need an **active, reasonably fast, internet connection throughout the entire installation processes.**
6 |
7 | # Set up Git & GitHub
8 |
9 | You can skip this step if you already have Git, GitHub.
10 |
11 | Follow our [Git Guide](https://zulip.readthedocs.io/en/latest/git/setup.html) in order to install Git, set up a GitHub account
12 |
13 |
14 | # Install Prerequisites
15 |
16 | Jump to:
17 |
18 | - [MacOS](https://github.com/zulip/zulip-electron/blob/master/development.md#macos)
19 | - [Ubuntu/Debian](https://github.com/zulip/zulip-electron/blob/master/development.md#ubuntudebian)
20 | - [Windows](https://github.com/zulip/zulip-electron/blob/master/development.md#windows)
21 |
22 |
23 | ## MacOS
24 |
25 |
26 | **Node JS**
27 | Go to the [Node.js Downloads page](https://nodejs.org/en/download/). Download Node.js for MacOS (`v6.9.0` or above recommended). Run the downloaded Node.js `.pkg` Installer. You're finished! To ensure Node.js has been installed, run `node -v` in your terminal - you should get something like `v6.9.0` or above
28 |
29 |
30 | **if** [**NPM**](https://www.npmjs.com/get-npm) **and** [**node-gyp**](https://github.com/nodejs/node-gyp#installation) **don't come bundled with your Node.js installation, Download manually**
31 |
32 |
33 | Now you are ready for next step [: Get Zulip Electron Code.](https://github.com/zulip/zulip-electron/blob/master/development.md#get-zulip-electron-code)
34 |
35 |
36 | ## Ubuntu/Debian
37 |
38 |
39 | If you’re in a hurry, you can copy and paste the following into your terminal
40 |
41 | sudo apt install git nodejs node-gyp python build-essential snapcraft libxext-dev libxtst-dev lib xkbfile-dev libgconf-2-4
42 |
43 | after pasting you can jump to next step [: Get Zulip Electron Code](https://github.com/zulip/zulip-electron/blob/master/development.md#get-zulip-electron-code).
44 |
45 |
46 | **For a step-by-step explanation, read on.**
47 |
48 | 1. **Node JS**
49 |
50 | `$ sudo apt-get install nodejs`
51 |
52 | 2. **Install** [**Node-gyp**](https://github.com/nodejs/node-gyp#installation)
53 |
54 | 3. **Python (v2.7.x recommended)**
55 |
56 | `$ sudo apt install python2.7`
57 |
58 | 4. **C++ compiler compatible with C++11**
59 |
60 | `$ sudo apt install build-essential`
61 |
62 | 5. **Snapcraft**
63 |
64 | `$ sudo apt install snapcraft`
65 |
66 | 6. **Development** **headers**
67 |
68 | `$ sudo apt install libxext-dev libxtst-dev libxkbfile-dev libgconf-2-4`
69 |
70 |
71 | **if** [**NPM**](https://www.npmjs.com/get-npm) **don't come bundled with your Node.js installation, Download manually**
72 |
73 |
74 | Now you are ready for next step [: Get Zulip Electron Code.](https://github.com/zulip/zulip-electron/blob/master/development.md#get-zulip-electron-code)
75 |
76 |
77 | ## Windows
78 |
79 | **Node JS**
80 | Go to the [Node.js Downloads page](https://nodejs.org/en/download/). Download Node.js for windows (`v6.9.0` or above recommended). Run the downloaded Node.js `.msi` Installer. You're finished! To ensure Node.js has been installed, run `node -v` in your terminal - you should get something like `v6.9.0` or above
81 |
82 |
83 | **Followings are optional yet recommended prerequisites -**
84 |
85 | **Cmder**
86 | 1. Download the [latest release](https://github.com/cmderdev/cmder/releases/)
87 | 2. Extract the archive. *Note: This path should not be* `C:\Program Files` *or anywhere else that would require Administrator access for modifying configuration files*
88 | 3. (optional) Place your own executable files into the `%cmder_root%\bin` folder to be injected into your PATH.
89 | 4. Run `Cmder.exe`
90 |
91 | **Chocolatey**
92 | You can download chocolatey from here https://chocolatey.org/ and for Installing Chocolatey on your machine follow this steps
93 | 1. First, ensure that you are using an administrative shell.
94 | 2. Copy the text specific to your command shell - [cmd.exe](https://chocolatey.org/install#install-with-cmdexe) or [powershell.exe](https://chocolatey.org/install#install-with-powershellexe).
95 | 3. Paste the copied text into your shell and press Enter.
96 | 4. Wait a few seconds for the command to complete.
97 | 5. If you don't see any errors, you are ready to use Chocolatey! Type `choco` or `choco -?`
98 |
99 |
100 | **System specific dependencies**
101 |
102 | - use only 32bit or 64bit for all of the installers, do not mix architectures
103 | - install using default settings
104 | - open Windows Powershell as Admin and paste this
105 | C:\Windows\system32> npm install --global --production windows-build-tools
106 |
107 |
108 | **if** [**NPM**](https://www.npmjs.com/get-npm) **and** [**node-gyp**](https://github.com/nodejs/node-gyp#installation) **don't come bundled with your Node.js installation, Download manually**
109 |
110 | Now you are ready for next step [: Get Zulip Electron Code.](https://github.com/zulip/zulip-electron/blob/master/development.md#get-zulip-electron-code)
111 |
112 |
113 | # Get Zulip Electron Code
114 |
115 | 1. In your browser, visit https://github.com/zulip/zulip-electron and click the `fork` button. You will need to be logged in to GitHub to do this.
116 | 2. Open Terminal (macOS/Ubuntu) or Git BASH (Windows; must **run as an Administrator**).
117 | 3. In Terminal/Git BASH, [clone your fork of the zulip-electron repository](https://github.com/zulip/zulip-electron/blob/master/development.md#clone-to-your-machine) and [connect the zulip-electron upstream repository](https://github.com/zulip/zulip-electron/blob/master/development.md#connect-your-fork-to-zulip-electron-upstream)
118 |
119 |
120 | ## Clone to your machine
121 | 1. On GitHub, navigate to the main page of your fork repository.
122 | 2. Under the repository name, click **Clone or download**.
123 | 3. In the Clone with HTTPs section, click to copy the clone URL for the repository.
124 | 4. Open Terminal, Change the current working directory to the location where you want the cloned directory to be made.
125 |
126 | git clone https://github.com/YOURUSERNAME/zulip-electron.git
127 |
128 | Don’t forget to replace YOURUSERNAME with your git username
129 |
130 |
131 | ## Connect your fork to zulip-electron upstream
132 |
133 | cd zulip-electron
134 | git remote add -f upstream https://github.com/zulip/zulip-electron.git
135 |
136 |
137 | # build and run
138 |
139 |
140 | ## Install project dependencies:
141 | $ npm install
142 |
143 |
144 | ## There two ways to start the app:
145 |
146 | **vanilla method**
147 |
148 | $ npm start
149 |
150 | **start and watch changes recommended for dev’s**
151 |
152 | $ npm run dev
153 |
--------------------------------------------------------------------------------
/docs/Home.md:
--------------------------------------------------------------------------------
1 | # Installation instructions
2 | * [[Windows]]
3 |
--------------------------------------------------------------------------------
/docs/Windows.md:
--------------------------------------------------------------------------------
1 | ** Windows Set up instructions **
2 |
3 | ## Prerequisites
4 |
5 | * [Git](http://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
6 | * [Node.js](https://nodejs.org) >= v6.9.0
7 | * [python](https://www.python.org/downloads/release/python-2713/) (v2.7.x recommended)
8 | * [node-gyp](https://github.com/nodejs/node-gyp#installation) (installed via powershell)
9 |
10 | ## System specific dependencies
11 |
12 | * use only 32bit or 64bit for all of the installers, do not mix architectures
13 | * install using default settings
14 | * open Windows Powershell as Admin
15 | ```powershell
16 | C:\Windows\system32> npm install --global --production windows-build-tools
17 | ```
18 |
19 | ## Installation
20 |
21 | Clone the source locally:
22 |
23 | ```sh
24 | $ git clone https://github.com/zulip/zulip-electron
25 | $ cd zulip-electron
26 | ```
27 |
28 | Install project dependencies:
29 |
30 | ```sh
31 | $ npm install
32 | ```
33 |
34 | Start the app:
35 |
36 | ```sh
37 | $ npm start
38 | ```
39 |
40 | Start and watch changes
41 |
42 | ```sh
43 | $ npm run dev
44 | ```
45 | ### Making a release
46 |
47 | To package app into an installer use command:
48 | ```
49 | npm run pack
50 | npm run dist
51 | ```
52 | It will start the packaging process. The ready for distribution file (e.g. dmg, windows installer, deb package) will be outputted to the `dist` directory.
53 |
54 | # Troubleshooting
55 | If you have any problems running the app please see the [most common issues](./troubleshooting.md).
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/docs/_Footer.md:
--------------------------------------------------------------------------------
1 | ### Want to contribute to this Wiki?
2 |
3 | [Edit `/docs` files and send a pull request.](https://github.com/zulip/zulip-electron/tree/master/docs)
4 |
--------------------------------------------------------------------------------
/docs/desktop-release.md:
--------------------------------------------------------------------------------
1 | # New release checklist -
2 |
3 | ## We need to cross check following things before pushing a new release + after updating electron version. This is just to make sure that nothing gets broken.
4 | ## - Desktop notifications
5 | ## - Spellchecker
6 | ## - Auto updates
7 | **Check for the logs in -**
8 | - **on Linux:** `~/.config/Zulip/log.log`
9 | - **on OS X:** `~/Library/Logs/Zulip/log.log`
10 | - **on Windows:** `%USERPROFILE%\AppData\Roaming\Zulip\log.log`
11 | ## - All the installer i.e.
12 | - Linux (.deb, AppImage)
13 | - Mac - (.dmg)
14 | - Windows - (web installer for 32/64bit)
15 | ## - Check for errors in console (if any)
16 | ## - Code signing verification on Mac and Windows
17 | ## - Tray and menu options
18 | # We need to cross check all these things on -
19 | - Windows 7
20 | - Windows 8
21 | - Windows 10
22 | - Ubuntu 14.04/16.04
23 | - macOSX
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const gulp = require('gulp');
3 | const electron = require('electron-connect').server.create({
4 | verbose: true
5 | });
6 | const tape = require('gulp-tape');
7 | const tapColorize = require('tap-colorize');
8 |
9 | gulp.task('dev', () => {
10 | // Start browser process
11 | electron.start();
12 | // Restart browser process
13 | gulp.watch('app/main/*.js', gulp.series('restart:browser'));
14 | // Reload renderer process
15 | gulp.watch('app/renderer/css/*.css', gulp.series('reload:renderer'));
16 | gulp.watch('app/renderer/*.html', gulp.series('reload:renderer'));
17 | gulp.watch('app/renderer/js/**/*.js', gulp.series('reload:renderer'));
18 | });
19 |
20 | gulp.task('restart:browser', done => {
21 | electron.stop();
22 | electron.restart();
23 | done();
24 | });
25 |
26 | gulp.task('reload:renderer', done => {
27 | // Reload renderer process
28 | electron.reload();
29 | done();
30 | });
31 |
32 | gulp.task('test-e2e', () => {
33 | return gulp.src('tests/*.js')
34 | .pipe(tape({
35 | reporter: tapColorize()
36 | }));
37 | });
38 |
39 | gulp.task('default', gulp.parallel('dev', 'test-e2e'));
40 |
--------------------------------------------------------------------------------
/how-to-install.md:
--------------------------------------------------------------------------------
1 | # How to install
2 |
3 | **Note:** If you download from the [releases page](https://github.com/zulip/zulip-electron/releases), be careful what version you pick. Releases that end with `-beta` are beta releases and the rest are stable.
4 | - **beta:** these releases are the right balance between getting new features early while staying away from nasty bugs.
5 | - **stable:** these releases are more thoroughly tested; they receive new features later, but there's a lower chance that things will go wrong.
6 |
7 | [LR]: https://github.com/zulip/zulip-electron/releases
8 |
9 | ## OS X
10 |
11 | **DMG or zip**:
12 |
13 | 1. Download [Zulip-x.x.x.dmg][LR] or [Zulip-x.x.x-mac.zip][LR]
14 | 2. Open or unzip the file and drag the app into the `Applications` folder
15 | 3. Done! The app will update automatically
16 |
17 | **Using brew**:
18 |
19 | 1. Run `brew cask install zulip` in your terminal
20 | 2. The app will be installed in your `Applications`
21 | 3. Done! The app will update automatically (you can also use `brew update && brew upgrade zulip`)
22 |
23 | ## Windows
24 |
25 | **Installer (recommended)**:
26 |
27 | 1. Download [Zulip-Web-Setup-x.x.x.exe][LR]
28 | 2. Run the installer, wait until it finishes
29 | 3. Done! The app will update automatically
30 |
31 | **Portable**:
32 |
33 | 1. Download [zulip-x.x.x-arch.nsis.7z][LR] [*here arch = ia32 (32-bit), x64 (64-bit)*]
34 | 2. Extract the zip wherever you want (e.g. a flash drive) and run the app from there
35 |
36 | ## Linux
37 |
38 | **Ubuntu, Debian 8+ (deb package)**:
39 |
40 | 1. Download [Zulip-x.x.x-amd64.deb][LR]
41 | 2. Double click and install, or run `dpkg -i Zulip-x.x.x-amd64.deb` in the terminal
42 | 3. Start the app with your app launcher or by running `zulip` in a terminal
43 | 4. Done! The app will NOT update automatically, but you can still check for updates
44 |
45 | **Other distros (Fedora, CentOS, Arch Linux etc)** :
46 | 1. Download Zulip-x.x.x-x86_64.AppImage[LR]
47 | 2. Make it executable using chmod a+x Zulip-x.x.x-x86_64.AppImage
48 | 3. Start the app with your app launcher
49 |
50 | **You can also use `apt-get` (recommended)**:
51 |
52 | * First download our signing key to make sure the deb you download is correct:
53 |
54 | ```
55 | sudo apt-key adv --keyserver pool.sks-keyservers.net --recv 69AD12704E71A4803DCA3A682424BE5AE9BD10D9
56 | ```
57 |
58 | * Add the repo to your apt source list :
59 | ```
60 | echo "deb https://dl.bintray.com/zulip/debian/ beta main" |
61 | sudo tee -a /etc/apt/sources.list.d/zulip.list
62 | ```
63 |
64 | * Now install the client :
65 | ```
66 | sudo apt-get update
67 | sudo apt-get install zulip
68 | ```
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zulip",
3 | "productName": "Zulip",
4 | "version": "2.5.0-beta",
5 | "main": "./app/main",
6 | "description": "Zulip Desktop App",
7 | "license": "Apache-2.0",
8 | "copyright": "Kandra Labs, Inc.",
9 | "author": {
10 | "name": "Kandra Labs, Inc.",
11 | "email": "support@zulipchat.com"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/zulip/zulip-electron.git"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/zulip/zulip-electron/issues"
19 | },
20 | "engines": {
21 | "node": ">=6.0.0"
22 | },
23 | "scripts": {
24 | "start": "electron app --disable-http-cache --no-electron-connect",
25 | "reinstall": "node ./tools/reinstall-node-modules.js",
26 | "postinstall": "electron-builder install-app-deps",
27 | "lint-css": "stylelint app/renderer/css/*.css",
28 | "lint-html": "./node_modules/.bin/htmlhint \"app/renderer/*.html\" ",
29 | "lint-js": "xo",
30 | "test": "npm run lint-html && npm run lint-css && npm run lint-js",
31 | "test-e2e": "gulp test-e2e",
32 | "dev": "gulp dev & nodemon --watch app/main --watch app/renderer --exec 'npm test' -e html,css,js",
33 | "pack": "electron-builder --dir",
34 | "dist": "electron-builder",
35 | "mas": "electron-builder --mac mas",
36 | "travis": "cd ./scripts && ./travis-build-test.sh",
37 | "build-locales": "node tools/locale-helper"
38 | },
39 | "pre-commit": [
40 | "test"
41 | ],
42 | "build": {
43 | "appId": "org.zulip.zulip-electron",
44 | "asar": true,
45 | "files": [
46 | "**/*",
47 | "!docs${/*}",
48 | "!node_modules/@paulcbetts/cld/deps/cld${/*}"
49 | ],
50 | "copyright": "©2019 Kandra Labs, Inc.",
51 | "mac": {
52 | "category": "public.app-category.productivity",
53 | "artifactName": "${productName}-${version}-${arch}.${ext}"
54 | },
55 | "linux": {
56 | "category": "Chat;GNOME;GTK;Network;InstantMessaging",
57 | "packageCategory": "GNOME;GTK;Network;InstantMessaging",
58 | "description": "Zulip Desktop Client for Linux",
59 | "target": [
60 | "deb",
61 | "zip",
62 | "AppImage",
63 | "snap"
64 | ],
65 | "maintainer": "Akash Nimare ",
66 | "artifactName": "${productName}-${version}-${arch}.${ext}"
67 | },
68 | "deb": {
69 | "synopsis": "Zulip Desktop App",
70 | "afterInstall": "./scripts/debian-add-repo.sh",
71 | "afterRemove": "./scripts/debian-uninstaller.sh"
72 | },
73 | "snap": {
74 | "synopsis": "Zulip Desktop App"
75 | },
76 | "dmg": {
77 | "background": "build/appdmg.png",
78 | "icon": "build/icon.icns",
79 | "iconSize": 100,
80 | "contents": [
81 | {
82 | "x": 380,
83 | "y": 280,
84 | "type": "link",
85 | "path": "/Applications"
86 | },
87 | {
88 | "x": 110,
89 | "y": 280,
90 | "type": "file"
91 | }
92 | ],
93 | "window": {
94 | "width": 500,
95 | "height": 500
96 | }
97 | },
98 | "win": {
99 | "target": [
100 | {
101 | "target": "nsis-web",
102 | "arch": [
103 | "x64",
104 | "ia32"
105 | ]
106 | }
107 | ],
108 | "icon": "build/icon.ico",
109 | "artifactName": "${productName}-Web-Setup-${version}.${ext}",
110 | "publisherName": "Kandra Labs, Inc."
111 | },
112 | "nsis": {
113 | "allowToChangeInstallationDirectory": true,
114 | "oneClick": false,
115 | "perMachine": false
116 | }
117 | },
118 | "keywords": [
119 | "Zulip",
120 | "Group Chat app",
121 | "electron-app",
122 | "electron",
123 | "Desktop app",
124 | "InstantMessaging"
125 | ],
126 | "devDependencies": {
127 | "assert": "1.4.1",
128 | "cp-file": "5.0.0",
129 | "devtron": "1.4.0",
130 | "electron": "3.0.10",
131 | "electron-builder": "20.40.2",
132 | "electron-connect": "0.6.2",
133 | "electron-debug": "1.4.0",
134 | "google-translate-api": "2.3.0",
135 | "gulp": "4.0.0",
136 | "gulp-tape": "0.0.9",
137 | "htmlhint": "0.11.0",
138 | "is-ci": "1.0.10",
139 | "nodemon": "1.14.11",
140 | "pre-commit": "1.2.2",
141 | "spectron": "5.0.0",
142 | "stylelint": "9.10.1",
143 | "tap-colorize": "1.2.0",
144 | "tape": "4.8.0",
145 | "xo": "0.18.2"
146 | },
147 | "xo": {
148 | "parserOptions": {
149 | "sourceType": "script",
150 | "ecmaFeatures": {
151 | "globalReturn": true
152 | }
153 | },
154 | "esnext": true,
155 | "overrides": [
156 | {
157 | "files": "app*/**/*.js",
158 | "rules": {
159 | "max-lines": [
160 | "warn",
161 | {
162 | "max": 700,
163 | "skipBlankLines": true,
164 | "skipComments": true
165 | }
166 | ],
167 | "no-warning-comments": 0,
168 | "object-curly-spacing": 0,
169 | "capitalized-comments": 0,
170 | "no-else-return": 0,
171 | "no-path-concat": 0,
172 | "no-alert": 0,
173 | "guard-for-in": 0,
174 | "prefer-promise-reject-errors": 0,
175 | "import/no-unresolved": 0,
176 | "import/no-extraneous-dependencies": 0,
177 | "no-prototype-builtins": 0
178 | }
179 | }
180 | ],
181 | "ignore": [
182 | "tests/*.js",
183 | "tools/locale-helper/*.js"
184 | ],
185 | "envs": [
186 | "node",
187 | "browser",
188 | "mocha"
189 | ]
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/scripts/debian-add-repo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script runs when user install the debian package
4 |
5 | # Link to the binary
6 | ln -sf '/opt/${productFilename}/${executable}' '/usr/local/bin/${executable}';
7 | echo 'Successfully added /opt/${productFilename}/${executable} to /usr/local/bin/${executable}'
8 |
9 | # Install apt repository source list if it does not exist
10 | if ! grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep zulip.list; then
11 | sudo apt-key adv --keyserver pool.sks-keyservers.net --recv 69AD12704E71A4803DCA3A682424BE5AE9BD10D9
12 | echo "deb https://dl.bintray.com/zulip/debian/ stable main" | \
13 | sudo tee -a /etc/apt/sources.list.d/zulip.list;
14 | fi
15 |
--------------------------------------------------------------------------------
/scripts/debian-uninstaller.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script runs when user uninstall the debian package.
4 | # It will remove all the config files and anything which was added by the app.
5 |
6 | # Remove apt repository source list when user uninstalls Zulip app
7 | if grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep zulip.list; then
8 | sudo apt-key del 69AD12704E71A4803DCA3A682424BE5AE9BD10D9;
9 | sudo rm /etc/apt/sources.list.d/zulip.list;
10 | fi
11 |
12 | # Get the root user
13 | if [ $SUDO_USER ];
14 | then getSudoUser=$SUDO_USER;
15 | else getSudoUser=`whoami`;
16 | fi
17 |
18 | # Get the path for Zulip's desktop entry which is created by auto-launch script
19 | getDesktopEntry=/home/$getSudoUser/.config/autostart/zulip.desktop;
20 |
21 | # Remove desktop entry if exists
22 | if [ -f $getDesktopEntry ]; then
23 | sudo rm $getDesktopEntry;
24 | fi
25 |
26 | # App directory which contains all the config, setting files
27 | appDirectory=/home/$getSudoUser/.config/Zulip/;
28 |
29 | if [ -d $appDirectory ]; then
30 | sudo rm -rf $appDirectory;
31 | fi
32 |
33 | # Delete the link to the binary
34 | echo 'Removing binary link'
35 | sudo rm -f '/usr/local/bin/${executable}';
--------------------------------------------------------------------------------
/scripts/travis-build-test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # exit script if fails
4 | set -e;
5 |
6 | if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
7 | export {no_proxy,NO_PROXY}="127.0.0.1,localhost"
8 | export DISPLAY=:99.0
9 | sh -e /etc/init.d/xvfb start
10 | sleep 3
11 |
12 | echo 'Travis Screen Resolution:'
13 | xdpyinfo | grep dimensions
14 | fi
15 |
16 | npm run test
17 |
18 | # if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
19 | # npm run test-e2e
20 | # fi
21 |
--------------------------------------------------------------------------------
/scripts/travis-xvfb.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
4 | /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16
5 | fi
6 |
--------------------------------------------------------------------------------
/snap/gui/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zulip/zulip-electron/d65e753ba4bf6a9d84501231ff24de4fe39da01c/snap/gui/icon.png
--------------------------------------------------------------------------------
/snap/snapcraft.yaml:
--------------------------------------------------------------------------------
1 | name: zulip
2 | version: 2.3.82
3 | summary: Zulip Desktop Client for Linux
4 | description: Zulip combines the immediacy of Slack with an email threading model. With Zulip, you can catch up on important conversations while ignoring irrelevant ones.
5 | confinement: strict
6 | grade: stable
7 | icon: snap/gui/icon.png
8 | apps:
9 | zulip:
10 | command: env TMPDIR=$XDG_RUNTIME_DIR desktop-launch $SNAP/zulip
11 | plugs:
12 | - desktop
13 | - desktop-legacy
14 | - home
15 | - x11
16 | - unity7
17 | - browser-support
18 | - network
19 | - gsettings
20 | - pulseaudio
21 | - opengl
22 | parts:
23 | app:
24 | plugin: dump
25 | stage-packages:
26 | - libasound2
27 | - libgconf2-4
28 | - libnotify4
29 | - libnspr4
30 | - libnss3
31 | - libpcre3
32 | - libpulse0
33 | - libxss1
34 | - libxtst6
35 | source: dist/linux-unpacked
36 | after:
37 | - desktop-gtk3
38 |
--------------------------------------------------------------------------------
/tests/config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const TEST_APP_PRODUCT_NAME = 'ZulipTest'
4 |
5 | module.exports = {
6 | TEST_APP_PRODUCT_NAME
7 | }
8 |
--------------------------------------------------------------------------------
/tests/e2e/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zulip",
3 | "productName": "Zulip",
4 | "version": "2.4.0",
5 | "main": "../../app/main",
6 | "description": "Zulip Desktop App",
7 | "license": "Apache-2.0",
8 | "copyright": "Kandra Labs, Inc.",
9 | "author": {
10 | "name": "Kandra Labs, Inc.",
11 | "email": "support@zulipchat.com"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/zulip/zulip-electron.git"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/zulip/zulip-electron/issues"
19 | },
20 | "engines": {
21 | "node": ">=6.0.0"
22 | },
23 | "scripts": {
24 | "start": "electron app --disable-http-cache --no-electron-connect",
25 | "reinstall": "node ./tools/reinstall-node-modules.js",
26 | "postinstall": "electron-builder install-app-deps",
27 | "test": "xo",
28 | "test-e2e": "gulp test-e2e",
29 | "dev": "gulp dev & nodemon --watch app/main --watch app/renderer --exec 'npm test' -e html,css,js",
30 | "pack": "electron-builder --dir",
31 | "dist": "electron-builder",
32 | "mas": "electron-builder --mac mas",
33 | "travis": "cd ./scripts && ./travis-build-test.sh",
34 | "build-locales": "node tools/locale-helper"
35 | },
36 | "pre-commit": [
37 | "test"
38 | ],
39 | "build": {
40 | "appId": "org.zulip.zulip-electron",
41 | "asar": true,
42 | "files": [
43 | "**/*",
44 | "!docs${/*}",
45 | "!node_modules/@paulcbetts/cld/deps/cld${/*}"
46 | ],
47 | "copyright": "©2017 Kandra Labs, Inc.",
48 | "mac": {
49 | "category": "public.app-category.productivity",
50 | "artifactName": "${productName}-${version}-${arch}.${ext}"
51 | },
52 | "linux": {
53 | "category": "Chat;GNOME;GTK;Network;InstantMessaging",
54 | "packageCategory": "GNOME;GTK;Network;InstantMessaging",
55 | "description": "Zulip Desktop Client for Linux",
56 | "target": [
57 | "deb",
58 | "zip",
59 | "AppImage",
60 | "snap"
61 | ],
62 | "maintainer": "Akash Nimare ",
63 | "artifactName": "${productName}-${version}-${arch}.${ext}"
64 | },
65 | "deb": {
66 | "synopsis": "Zulip Desktop App",
67 | "afterInstall": "./scripts/debian-add-repo.sh",
68 | "afterRemove": "./scripts/debian-uninstaller.sh"
69 | },
70 | "snap": {
71 | "synopsis": "Zulip Desktop App"
72 | },
73 | "dmg": {
74 | "background": "build/appdmg.png",
75 | "icon": "build/icon.icns",
76 | "iconSize": 100,
77 | "contents": [
78 | {
79 | "x": 380,
80 | "y": 280,
81 | "type": "link",
82 | "path": "/Applications"
83 | },
84 | {
85 | "x": 110,
86 | "y": 280,
87 | "type": "file"
88 | }
89 | ],
90 | "window": {
91 | "width": 500,
92 | "height": 500
93 | }
94 | },
95 | "win": {
96 | "target": [
97 | {
98 | "target": "nsis-web",
99 | "arch": [
100 | "x64",
101 | "ia32"
102 | ]
103 | }
104 | ],
105 | "icon": "build/icon.ico",
106 | "publisherName": "Kandra Labs, Inc."
107 | },
108 | "nsis": {
109 | "allowToChangeInstallationDirectory": true
110 | }
111 | },
112 | "keywords": [
113 | "Zulip",
114 | "Group Chat app",
115 | "electron-app",
116 | "electron",
117 | "Desktop app",
118 | "InstantMessaging"
119 | ],
120 | "devDependencies": {
121 | "assert": "1.4.1",
122 | "cp-file": "^5.0.0",
123 | "devtron": "1.4.0",
124 | "electron": "3.0.10",
125 | "electron-builder": "20.38.4",
126 | "electron-connect": "0.6.2",
127 | "electron-debug": "1.4.0",
128 | "google-translate-api": "2.3.0",
129 | "gulp": "^4.0.0",
130 | "gulp-tape": "0.0.9",
131 | "is-ci": "^1.0.10",
132 | "nodemon": "^1.14.11",
133 | "pre-commit": "1.2.2",
134 | "spectron": "3.8.0",
135 | "tap-colorize": "^1.2.0",
136 | "tape": "^4.8.0",
137 | "xo": "0.18.2"
138 | },
139 | "xo": {
140 | "parserOptions": {
141 | "sourceType": "script",
142 | "ecmaFeatures": {
143 | "globalReturn": true
144 | }
145 | },
146 | "esnext": true,
147 | "overrides": [
148 | {
149 | "files": "app*/**/*.js",
150 | "rules": {
151 | "max-lines": [
152 | "warn",
153 | {
154 | "max": 600,
155 | "skipBlankLines": true,
156 | "skipComments": true
157 | }
158 | ],
159 | "no-warning-comments": 0,
160 | "object-curly-spacing": 0,
161 | "capitalized-comments": 0,
162 | "no-else-return": 0,
163 | "no-path-concat": 0,
164 | "no-alert": 0,
165 | "guard-for-in": 0,
166 | "prefer-promise-reject-errors": 0,
167 | "import/no-unresolved": 0,
168 | "import/no-extraneous-dependencies": 0,
169 | "no-prototype-builtins": 0
170 | }
171 | }
172 | ],
173 | "ignore": [
174 | "tests/*.js",
175 | "tools/locale-helper/*.js"
176 | ],
177 | "envs": [
178 | "node",
179 | "browser",
180 | "mocha"
181 | ]
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/tests/index.js:
--------------------------------------------------------------------------------
1 | const test = require('tape')
2 | const setup = require('./setup')
3 |
4 | test('app runs', function (t) {
5 | t.timeoutAfter(10e3)
6 | setup.resetTestDataDir()
7 | const app = setup.createApp()
8 | setup.waitForLoad(app, t)
9 | .then(() => app.client.windowByIndex(1)) // focus on webview
10 | .then(() => app.client.waitForExist('//*[@id="connect"]')) // id of the connect button
11 | .then(() => setup.endTest(app, t),
12 | (err) => setup.endTest(app, t, err || 'error'))
13 | })
--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
1 | const Application = require('spectron').Application
2 | const cpFile = require('cp-file')
3 | const fs = require('fs')
4 | const isCI = require('is-ci')
5 | const mkdirp = require('mkdirp')
6 | const path = require('path')
7 | const rimraf = require('rimraf')
8 |
9 | const config = require('./config')
10 |
11 | module.exports = {
12 | createApp,
13 | endTest,
14 | waitForLoad,
15 | wait,
16 | resetTestDataDir
17 | }
18 |
19 | // Runs Zulip Desktop.
20 | // Returns a promise that resolves to a Spectron Application once the app has loaded.
21 | // Takes a Tape test. Makes some basic assertions to verify that the app loaded correctly.
22 | function createApp (t) {
23 | generateTestAppPackageJson()
24 | return new Application({
25 | path: path.join(__dirname, '..', 'node_modules', '.bin',
26 | 'electron' + (process.platform === 'win32' ? '.cmd' : '')),
27 | args: [path.join(__dirname)], // Ensure this dir has a package.json file with a 'main' entry piont
28 | env: {NODE_ENV: 'test'},
29 | waitTimeout: 10e3
30 | })
31 | }
32 |
33 | // Generates package.json for test app
34 | // Reads app package.json and updates the productName to config.TEST_APP_PRODUCT_NAME
35 | // We do this so that the app integration doesn't doesn't share the same appDataDir as the dev application
36 | function generateTestAppPackageJson () {
37 | let packageJson = require(path.join(__dirname, '../package.json'))
38 | packageJson.productName = config.TEST_APP_PRODUCT_NAME
39 | packageJson.main = '../app/main'
40 |
41 | const testPackageJsonPath = path.join(__dirname, 'package.json')
42 | fs.writeFileSync(testPackageJsonPath, JSON.stringify(packageJson, null, ' '), 'utf-8')
43 | }
44 |
45 | // Starts the app, waits for it to load, returns a promise
46 | function waitForLoad (app, t, opts) {
47 | if (!opts) opts = {}
48 | return app.start().then(function () {
49 | return app.client.waitUntilWindowLoaded()
50 | })
51 | .then(function() {
52 | return app.client.pause(2000);
53 | })
54 | .then(function () {
55 | return app.webContents.getTitle()
56 | }).then(function (title) {
57 | t.equal(title, 'Zulip', 'html title')
58 | })
59 | }
60 |
61 | // Returns a promise that resolves after 'ms' milliseconds. Default: 1 second
62 | function wait (ms) {
63 | if (ms === undefined) ms = 1000 // Default: wait long enough for the UI to update
64 | return new Promise(function (resolve, reject) {
65 | setTimeout(resolve, ms)
66 | })
67 | }
68 |
69 | // Quit the app, end the test, either in success (!err) or failure (err)
70 | function endTest (app, t, err) {
71 | return app.stop().then(function () {
72 | t.end(err)
73 | })
74 | }
75 |
76 | function getAppDataDir () {
77 | let base
78 |
79 | if (process.platform === 'darwin') {
80 | base = path.join(process.env.HOME, 'Library', 'Application Support')
81 | } else if (process.platform === 'linux') {
82 | base = process.env.XDG_CONFIG_HOME ?
83 | process.env.XDG_CONFIG_HOME : path.join(process.env.HOME, '.config')
84 | } else if (process.platform === 'win32') {
85 | base = process.env.APPDATA
86 | } else {
87 | console.log('Could not detect app data dir base. Exiting...')
88 | process.exit(1)
89 | }
90 | console.log('Detected App Data Dir base:', base)
91 | return path.join(base, config.TEST_APP_PRODUCT_NAME)
92 | }
93 |
94 | // Resets the test directory, containing domain.json, window-state.json, etc
95 | function resetTestDataDir () {
96 | appDataDir = getAppDataDir()
97 | rimraf.sync(appDataDir)
98 | rimraf.sync(path.join(__dirname, 'package.json'))
99 | }
100 |
--------------------------------------------------------------------------------
/tests/test-add-organization.js:
--------------------------------------------------------------------------------
1 | const test = require('tape')
2 | const setup = require('./setup')
3 |
4 | test('add-organization', function (t) {
5 | t.timeoutAfter(50e3)
6 | setup.resetTestDataDir()
7 | const app = setup.createApp()
8 | setup.waitForLoad(app, t)
9 | .then(() => app.client.windowByIndex(1)) // focus on webview
10 | .then(() => app.client.setValue('.setting-input-value', 'chat.zulip.org'))
11 | .then(() => app.client.click('.server-save-action'))
12 | .then(() => setup.wait(5000))
13 | .then(() => app.client.windowByIndex(0)) // Switch focus back to main win
14 | .then(() => app.client.windowByIndex(1)) // Switch focus back to org webview
15 | .then(() => app.client.waitForExist('//*[@id="id_username"]'))
16 | .then(() => setup.endTest(app, t),
17 | (err) => setup.endTest(app, t, err || 'error'))
18 | })
19 |
20 |
--------------------------------------------------------------------------------
/tests/test-new-organization.js:
--------------------------------------------------------------------------------
1 | const test = require('tape')
2 | const setup = require('./setup')
3 |
4 | // Create new org link should open in the default browser [WIP]
5 |
6 | test('new-org-link', function (t) {
7 | t.timeoutAfter(50e3)
8 | setup.resetTestDataDir()
9 | const app = setup.createApp()
10 | setup.waitForLoad(app, t)
11 | .then(() => app.client.windowByIndex(1)) // focus on webview
12 | .then(() => app.client.click('#open-create-org-link')) // Click on new org link button
13 | .then(() => setup.wait(5000))
14 | .then(() => setup.endTest(app, t),
15 | (err) => setup.endTest(app, t, err || 'error'))
16 | })
17 |
18 |
--------------------------------------------------------------------------------
/tools/fetch-pull-request:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | set -x
4 |
5 | if ! git diff-index --quiet HEAD; then
6 | set +x
7 | echo "There are uncommitted changes:"
8 | git status --short
9 | echo "Doing nothing to avoid losing your work."
10 | exit 1
11 | fi
12 | request_id="$1"
13 | remote=${2:-"upstream"}
14 | git fetch "$remote" "pull/$request_id/head"
15 | git checkout -B "review-original-${request_id}"
16 | git reset --hard FETCH_HEAD
17 |
--------------------------------------------------------------------------------
/tools/fetch-pull-request.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 | git diff-index --quiet HEAD
3 | if %ERRORLEVEL% NEQ 0 (
4 | echo "There are uncommitted changes:"
5 | git status --short
6 | echo "Doing nothing to avoid losing your work."
7 | exit /B 1
8 | )
9 |
10 | if "%~1"=="" (
11 | echo "Error you must specify the PR number"
12 | )
13 |
14 | if "%~2"=="" (
15 | set remote="upstream"
16 | ) else (
17 | set remote=%2
18 | )
19 |
20 | set request_id="%1"
21 | git fetch "%remote%" "pull/%request_id%/head"
22 | git checkout -B "review-%request_id%"
23 | git reset --hard FETCH_HEAD
24 |
--------------------------------------------------------------------------------
/tools/fetch-rebase-pull-request:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | set -x
4 |
5 | if ! git diff-index --quiet HEAD; then
6 | set +x
7 | echo "There are uncommitted changes:"
8 | git status --short
9 | echo "Doing nothing to avoid losing your work."
10 | exit 1
11 | fi
12 | request_id="$1"
13 | remote=${2:-"upstream"}
14 | git fetch "$remote" "pull/$request_id/head"
15 | git checkout -B "review-${request_id}" $remote/master
16 | git reset --hard FETCH_HEAD
17 | git pull --rebase
18 |
--------------------------------------------------------------------------------
/tools/fetch-rebase-pull-request.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 | git diff-index --quiet HEAD
3 | if %errorlevel% neq 0 (
4 | echo "There are uncommitted changes:"
5 | git status --short
6 | echo "Doing nothing to avoid losing your work."
7 | exit \B 1
8 | )
9 |
10 | if "%~1"=="" (
11 | echo "Error you must specify the PR number"
12 | )
13 |
14 | if "%~2"=="" (
15 | set remote="upstream"
16 | ) else (
17 | set remote=%2
18 | )
19 |
20 | set request_id="%1"
21 | git fetch "%remote%" "pull/%request_id%/head"
22 | git checkout -B "review-%request_id%" %remote%/master
23 | git reset --hard FETCH_HEAD
24 | git pull --rebase
25 |
--------------------------------------------------------------------------------
/tools/locale-helper/index.js:
--------------------------------------------------------------------------------
1 | const translate = require('google-translate-api');
2 | const path = require('path');
3 | const fs = require('fs');
4 |
5 | const translationDir = path.resolve(__dirname, '../../app/translations');
6 |
7 | function writeJSON(file, data) {
8 | const filePath = path.resolve(translationDir, file);
9 | fs.writeFileSync(filePath, `${JSON.stringify(data, null, '\t')}\n`, 'utf8');
10 | }
11 |
12 | const { phrases } = require('./locale-template');
13 | const supportedLocales = require('./supported-locales');
14 |
15 | phrases.sort();
16 | for (let locale in supportedLocales) {
17 | console.log(`fetching translation for: ${supportedLocales[locale]} - ${locale}..`);
18 | translate(phrases.join('\n'), { to: locale })
19 | .then(res => {
20 | const localeFile = `${locale}.json`;
21 | const translatedText = res.text.split('\n');
22 | const translationJSON = {};
23 | phrases.forEach((phrase, index) => {
24 | translationJSON[phrase] = translatedText[index];
25 | });
26 |
27 | writeJSON(localeFile, translationJSON);
28 | console.log(`create: ${localeFile}`);
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/tools/locale-helper/locale-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "phrases": [
3 | "About Zulip",
4 | "Actual Size",
5 | "Back",
6 | "Close",
7 | "Copy",
8 | "Cut",
9 | "Delete",
10 | "Desktop App Settings",
11 | "Edit",
12 | "File",
13 | "Forward",
14 | "Hard Reload",
15 | "Help",
16 | "History",
17 | "Keyboard Shortcuts",
18 | "Log Out",
19 | "Minimize",
20 | "Paste",
21 | "Paste and Match Style",
22 | "Quit",
23 | "Redo",
24 | "Reload",
25 | "Report an issue...",
26 | "Reset App Settings",
27 | "Select All",
28 | "Show App Logs",
29 | "Toggle DevTools for Active Tab",
30 | "Toggle DevTools for Zulip App",
31 | "Toggle Full Screen",
32 | "Toggle Sidebar",
33 | "Toggle Tray Icon",
34 | "Undo",
35 | "View",
36 | "Window",
37 | "Zoom In",
38 | "Zoom Out",
39 | "Zulip Help"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/tools/locale-helper/supported-locales.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "de": "Deutsch",
3 | "pl": "polski",
4 | "en": "English",
5 | "cs": "česky",
6 | "hi": "Hindi",
7 | "ru": "Русский",
8 | "id": "Indonesia",
9 | "bg": "български",
10 | "hu": "Magyar",
11 | "sr": "српски",
12 | "ml": "Malayalam",
13 | "ta": "தமிழ்",
14 | "nl": "Nederlands",
15 | "ja": "日本語",
16 | "pt": "Português",
17 | "tr": "Türkçe",
18 | "es": "español",
19 | "ko": "한국어",
20 | "fr": "français"
21 | };
22 |
--------------------------------------------------------------------------------
/tools/push-to-pull-request:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | usage () {
5 | cat >&2 <&2
45 | exit 1
46 | fi
47 |
48 | # See https://developer.github.com/v3/pulls/#get-a-single-pull-request .
49 | # This is the old REST API; the new GraphQL API does look neat, but it
50 | # seems to require authentication even for simple lookups of public data,
51 | # and that'd be a pain for a simple script like this.
52 | pr_url=https://api.github.com/repos/"${repo_fq}"/pulls/"${pr_id}"
53 | pr_details="$(curl -s "$pr_url")"
54 |
55 | pr_jq () {
56 | echo "$pr_details" | jq "$@"
57 | }
58 |
59 | if [ "$(pr_jq -r .message)" = "Not Found" ]; then
60 | echo "Invalid PR URL: $pr_url"
61 | exit 1
62 | fi
63 |
64 | if [ "$(pr_jq .maintainer_can_modify)" != "true" ]; then
65 | # This happens when the PR has already been merged or closed, or
66 | # if the contributor has turned off the (default) setting to allow
67 | # maintainers of the target repo to push to their PR branch.
68 | #
69 | # The latter seems to be rare (in Greg's experience doing the
70 | # manual equivalent of this script for many different
71 | # contributors, none have ever chosen this setting), but give a
72 | # decent error message if it does happen.
73 | echo "error: PR already closed, or contributor has disallowed pushing to branch" >&2
74 | exit 1
75 | fi
76 |
77 | pr_head_repo_fq="$(pr_jq -r .head.repo.full_name)"
78 | pr_head_refname="$(pr_jq -r .head.ref)"
79 |
80 | set -x
81 | exec git push git@github.com:"$pr_head_repo_fq" +@:"$pr_head_refname"
82 |
--------------------------------------------------------------------------------
/tools/reinstall-node-modules:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | set -x
4 |
5 | echo "Removing node_modules and app/node_modules"
6 | rm -rf node_modules
7 | rm -rf app/node_modules
8 |
9 | echo "node_modules removed reinstalling npm packages"
10 | npm i
11 |
--------------------------------------------------------------------------------
/tools/reinstall-node-modules.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | echo "Removing node_modules and app/node_modules"
4 | rmdir /s /q node_modules
5 | rmdir /s /q app/node_modules
6 |
7 | echo "node_modules removed reinstalling npm packages"
8 | npm i
9 |
--------------------------------------------------------------------------------
/tools/reinstall-node-modules.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const {exec} = require('child_process');
3 | const path = require('path');
4 |
5 | const isWindows = process.platform === 'win32';
6 | const command = path.join(__dirname, `reinstall-node-modules${isWindows ? '.cmd' : ''}`);
7 |
8 | const proc = exec(command, error => {
9 | if (error) {
10 | console.error(error);
11 | }
12 | });
13 |
14 | proc.stdout.on('data', data => console.log(data.toString()));
15 | proc.stderr.on('data', data => console.error(data.toString()));
16 | proc.on('exit', code => {
17 | process.exit(code);
18 | });
19 |
--------------------------------------------------------------------------------
/tools/reset-to-pull-request:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | if ! git diff-index --quiet HEAD; then
5 | set +x
6 | echo "There are uncommitted changes:"
7 | git status --short
8 | echo "Doing nothing to avoid losing your work."
9 | exit 1
10 | fi
11 |
12 | remote_default="$(git config zulip.zulipRemote || echo upstream)"
13 |
14 | request_id="$1"
15 | remote=${2:-"$remote_default"}
16 |
17 | set -x
18 | git fetch "$remote" "pull/$request_id/head"
19 | git reset --hard FETCH_HEAD
20 |
--------------------------------------------------------------------------------
/troubleshooting.md:
--------------------------------------------------------------------------------
1 | # Troubleshooting
2 |
3 | * App icon will only show in the release version. The dev version will use the Electron icon
4 | * If you see issue, try deleting `node_modules` and `npm install`
5 | * Electron is more or less Chrome, you can get developer tools using `CMD+ALT+I`
6 |
7 | ### Error : ChecksumMismatchError
8 | - Try deleteing `node_modules` && `app/node_modules` directories. Re-install dependencies using `npm install`
9 |
10 | ### Error : Module version mismatch. Expected 50, got 51
11 | - Make sure you have installed [node-gyp](https://github.com/nodejs/node-gyp#installation) dependencies properly
12 |
13 | ### Error: Desktop Notifications not working
14 | - Make sure the **Show Desktop Notifications** setting option is set to be true
15 | - Check your OS notifications center settings
16 |
--------------------------------------------------------------------------------
/zulip-electron-launcher.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Zulip Beta Client Launcher
4 |
5 | # This script ensures that you have the latest version of the specified branch
6 | # (defaults to master if none specified) and then updates or installs all your
7 | # required npm modules.
8 |
9 | # I recommend symlinking this script into your PATH.
10 |
11 | # {{{ showUsage()
12 |
13 | showUsage()
14 | {
15 | echo "Usage: $0 "
16 | echo "Example: $0 dev"
17 | exit 1
18 | }
19 |
20 | # }}}
21 | # {{{ envSetup()
22 |
23 | envSetup()
24 | {
25 | defaultBranch="master"
26 | startingDir=`pwd`
27 | requirePop=0
28 |
29 | # Check command line arguments
30 | if [ "$#" -gt "1" ]
31 | then
32 | showUsage
33 | elif [ "$#" -eq "1" ]
34 | then
35 | myBranch=$1
36 | else
37 | myBranch=$defaultBranch
38 | fi
39 |
40 | # Set workingDir
41 | if [ -L $0 ]
42 | then
43 | realPath=`ls -l $0 | cut -d '>' -f 2`
44 | workingDir=`dirname $realPath`
45 | else
46 | workingDir="."
47 | fi
48 |
49 | # Set name of upstreamRemote
50 | cd $workingDir
51 | git remote -v | grep "github\.com.zulip.zulip-electron.git (fetch)" > /dev/null 2>&1
52 | if [ $? -eq 0 ]
53 | then
54 | upstreamRemote=`git remote -v | grep "github\.com.zulip.zulip-electron.git (fetch)" | awk '{ print $1 }'`
55 | else
56 | upstreamRemote="origin"
57 | fi
58 | }
59 |
60 | # }}}
61 | # {{{ gitCheckout()
62 |
63 | gitCheckout()
64 | {
65 | git fetch $upstreamRemote
66 | git checkout $myBranch
67 | git rebase $upstreamRemote/master
68 | if [ $? -gt 0 ]
69 | then
70 | echo "Stashing uncommitted changes and doing a new git pull"
71 | git stash && requirePop=1
72 | git rebase $upstreamRemote/master
73 | fi
74 | }
75 |
76 | # }}}
77 | # {{{ npmInstallStart()
78 |
79 | npmInstallStart()
80 | {
81 | npm install
82 | npm start &
83 | }
84 |
85 | # }}}
86 | # {{{ cleanUp()
87 |
88 | cleanUp()
89 | {
90 | # Switch back to branch we started on
91 | git checkout -
92 |
93 | # Pop if we stashed
94 | if [ $requirePop -eq 1 ]
95 | then
96 | echo "Popping out uncommitted changes"
97 | git stash pop
98 | fi
99 |
100 | # Return the whatever dir we started in
101 | cd $startingDir
102 | }
103 |
104 | # }}}
105 |
106 | # this function is called when user hits Ctrl-C
107 | catchControl_c () {
108 | echo -en "\n## Ctrl-C caught; Quitting \n"
109 | # exit shell script
110 | exit $?;
111 | }
112 |
113 |
114 |
115 | envSetup $*
116 | gitCheckout
117 | npmInstallStart
118 | cleanUp
119 |
120 | # initialise trap to call catchControl_c function and trap keyboard interrupt (control-c)
121 | trap catchControl_c SIGINT
122 | sleep 1000
--------------------------------------------------------------------------------