├── .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 | 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 |
20 |
21 |
Organization URL
22 | 23 |
24 |
25 |
Certificate file
26 | 27 |
28 |
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 |
23 |
24 | 25 | 26 |
27 |
28 | `; 29 | } else { 30 | return ` 31 |
32 |
33 | 34 | 35 |
36 |
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 += ``; 20 | } 21 | 22 | return ` 23 |
24 |
Settings
25 | 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 |
28 |
29 | PAC script 30 | 31 |
32 |
33 | Proxy rules 34 | 35 |
36 |
37 | Proxy bypass rules 38 | 39 |
40 |
41 |
42 | Save 43 |
44 |
45 |
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 |
16 |
Organization URL
17 |
18 | 19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 |
OR
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
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 | 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 |
13 | 16 | 45 |
46 |
47 |
48 |
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 |
11 | 12 |
13 |
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 --------------------------------------------------------------------------------