Compare commits

...

23 Commits

Author SHA1 Message Date
Akash Nimare
35ad6fbad0 release: New release v5.4.3. 2020-09-09 12:24:43 +05:30
Anders Kaseorg
97f8fe71af Escape all strings inserted into CSS selectors.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-04 22:52:42 -07:00
Anders Kaseorg
a9d59b3dcd CVE-2020-24582: Escape all strings interpolated into HTML.
Also fix various variable names to consistently indicate which strings
contain HTML.

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

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

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

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

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

View File

@@ -6,7 +6,8 @@
Desktop client for Zulip. Available for Mac, Linux, and Windows. Desktop client for Zulip. Available for Mac, Linux, and Windows.
<img src="http://i.imgur.com/ChzTq4F.png"/> <img src="https://i.imgur.com/s1o6TRA.png"/>
<img src="https://i.imgur.com/vekKnW4.png"/>
# Download # Download
Please see the [installation guide](https://zulip.com/help/desktop-app-install-guide). Please see the [installation guide](https://zulip.com/help/desktop-app-install-guide).

View File

@@ -1,13 +1,15 @@
import {app, dialog, session} from 'electron'; import {app, dialog, session} from 'electron';
import {UpdateDownloadedEvent, UpdateInfo, autoUpdater} from 'electron-updater';
import util from 'util'; import util from 'util';
import {linuxUpdateNotification} from './linuxupdater'; // Required only in case of linux
import log from 'electron-log';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import log from 'electron-log';
import {UpdateDownloadedEvent, UpdateInfo, autoUpdater} from 'electron-updater';
import * as ConfigUtil from '../renderer/js/utils/config-util'; import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as LinkUtil from '../renderer/js/utils/link-util'; import * as LinkUtil from '../renderer/js/utils/link-util';
import {linuxUpdateNotification} from './linuxupdater'; // Required only in case of linux
const sleep = util.promisify(setTimeout); const sleep = util.promisify(setTimeout);
export async function appUpdater(updateFromMenu = false): Promise<void> { export async function appUpdater(updateFromMenu = false): Promise<void> {

View File

@@ -1,18 +1,20 @@
import {sentryInit} from '../renderer/js/utils/sentry-util';
import {appUpdater} from './autoupdater'; import electron, {app, dialog, ipcMain, session} from 'electron';
import {setAutoLaunch} from './startup'; import fs from 'fs';
import path from 'path';
import windowStateKeeper from 'electron-window-state'; import windowStateKeeper from 'electron-window-state';
import path from 'path';
import fs from 'fs';
import electron, {app, dialog, ipcMain, session} from 'electron';
import * as AppMenu from './menu';
import * as BadgeSettings from '../renderer/js/pages/preference/badge-settings'; import * as BadgeSettings from '../renderer/js/pages/preference/badge-settings';
import * as CertificateUtil from '../renderer/js/utils/certificate-util'; import * as CertificateUtil from '../renderer/js/utils/certificate-util';
import * as ConfigUtil from '../renderer/js/utils/config-util'; import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as ProxyUtil from '../renderer/js/utils/proxy-util'; import * as ProxyUtil from '../renderer/js/utils/proxy-util';
import {sentryInit} from '../renderer/js/utils/sentry-util';
import {appUpdater} from './autoupdater';
import * as AppMenu from './menu';
import {_getServerSettings, _saveServerIcon, _isOnline} from './request'; import {_getServerSettings, _saveServerIcon, _isOnline} from './request';
import {setAutoLaunch} from './startup';
let mainWindowState: windowStateKeeper.State; let mainWindowState: windowStateKeeper.State;
@@ -353,7 +355,7 @@ ${error}`
AppMenu.setMenu(props); AppMenu.setMenu(props);
const activeTab = props.tabs[props.activeTabIndex]; const activeTab = props.tabs[props.activeTabIndex];
if (activeTab) { if (activeTab) {
mainWindow.setTitle(`Zulip - ${activeTab.webview.props.name}`); mainWindow.setTitle(`Zulip - ${activeTab.webviewName}`);
} }
}); });

View File

@@ -2,9 +2,11 @@ import {app, Notification, net} from 'electron';
import getStream from 'get-stream'; import getStream from 'get-stream';
import semver from 'semver'; import semver from 'semver';
import * as ConfigUtil from '../renderer/js/utils/config-util'; import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as LinuxUpdateUtil from '../renderer/js/utils/linux-update-util'; import * as LinuxUpdateUtil from '../renderer/js/utils/linux-update-util';
import Logger from '../renderer/js/utils/logger-util'; import Logger from '../renderer/js/utils/logger-util';
import {fetchResponse} from './request'; import {fetchResponse} from './request';
const logger = new Logger({ const logger = new Logger({

View File

@@ -1,15 +1,17 @@
import {app, shell, BrowserWindow, Menu} from 'electron'; import {app, shell, BrowserWindow, Menu} from 'electron';
import {appUpdater} from './autoupdater';
import AdmZip from 'adm-zip'; import AdmZip from 'adm-zip';
import * as DNDUtil from '../renderer/js/utils/dnd-util';
import type {TabData} from '../renderer/js/main';
import * as ConfigUtil from '../renderer/js/utils/config-util'; import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as DNDUtil from '../renderer/js/utils/dnd-util';
import * as LinkUtil from '../renderer/js/utils/link-util'; import * as LinkUtil from '../renderer/js/utils/link-util';
import * as t from '../renderer/js/utils/translation-util'; import * as t from '../renderer/js/utils/translation-util';
import type {ServerOrFunctionalTab} from '../renderer/js/main';
import {appUpdater} from './autoupdater';
export interface MenuProps { export interface MenuProps {
tabs: ServerOrFunctionalTab[]; tabs: TabData[];
activeTabIndex?: number; activeTabIndex?: number;
enableMenu?: boolean; enableMenu?: boolean;
} }
@@ -213,7 +215,7 @@ function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
]; ];
} }
function getWindowSubmenu(tabs: ServerOrFunctionalTab[], activeTabIndex: number): Electron.MenuItemConstructorOptions[] { function getWindowSubmenu(tabs: TabData[], activeTabIndex: number): Electron.MenuItemConstructorOptions[] {
const initialSubmenu: Electron.MenuItemConstructorOptions[] = [{ const initialSubmenu: Electron.MenuItemConstructorOptions[] = [{
label: t.__('Minimize'), label: t.__('Minimize'),
role: 'minimize' role: 'minimize'
@@ -229,17 +231,17 @@ function getWindowSubmenu(tabs: ServerOrFunctionalTab[], activeTabIndex: number)
}); });
tabs.forEach(tab => { tabs.forEach(tab => {
// Do not add functional tab settings to list of windows in menu bar // Do not add functional tab settings to list of windows in menu bar
if (tab.props.role === 'function' && tab.props.name === 'Settings') { if (tab.role === 'function' && tab.name === 'Settings') {
return; return;
} }
initialSubmenu.push({ initialSubmenu.push({
label: tab.props.name, label: tab.name,
accelerator: tab.props.role === 'function' ? '' : `${ShortcutKey} + ${tab.props.index + 1}`, accelerator: tab.role === 'function' ? '' : `${ShortcutKey} + ${tab.index + 1}`,
checked: tab.props.index === activeTabIndex, checked: tab.index === activeTabIndex,
click(_item, focusedWindow) { click(_item, focusedWindow) {
if (focusedWindow) { if (focusedWindow) {
sendAction('switch-server-tab', tab.props.index); sendAction('switch-server-tab', tab.index);
} }
}, },
type: 'checkbox' type: 'checkbox'
@@ -545,20 +547,20 @@ async function checkForUpdate(): Promise<void> {
await appUpdater(true); await appUpdater(true);
} }
function getNextServer(tabs: ServerOrFunctionalTab[], activeTabIndex: number): number { function getNextServer(tabs: TabData[], activeTabIndex: number): number {
do { do {
activeTabIndex = (activeTabIndex + 1) % tabs.length; activeTabIndex = (activeTabIndex + 1) % tabs.length;
} }
while (tabs[activeTabIndex].props.role !== 'server'); while (tabs[activeTabIndex].role !== 'server');
return activeTabIndex; return activeTabIndex;
} }
function getPreviousServer(tabs: ServerOrFunctionalTab[], activeTabIndex: number): number { function getPreviousServer(tabs: TabData[], activeTabIndex: number): number {
do { do {
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length; activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
} }
while (tabs[activeTabIndex].props.role !== 'server'); while (tabs[activeTabIndex].role !== 'server');
return activeTabIndex; return activeTabIndex;
} }

View File

@@ -3,12 +3,13 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import stream from 'stream'; import stream from 'stream';
import util from 'util'; import util from 'util';
import * as Messages from '../resources/messages';
import Logger from '../renderer/js/utils/logger-util';
import {ServerConf} from '../renderer/js/utils/domain-util';
import escape from 'escape-html';
import getStream from 'get-stream'; import getStream from 'get-stream';
import {ServerConf} from '../renderer/js/utils/domain-util';
import Logger from '../renderer/js/utils/logger-util';
import * as Messages from '../resources/messages';
export async function fetchResponse(request: ClientRequest): Promise<IncomingMessage> { export async function fetchResponse(request: ClientRequest): Promise<IncomingMessage> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.on('response', resolve); request.on('response', resolve);
@@ -71,7 +72,7 @@ export const _getServerSettings = async (domain: string, session: Electron.sessi
// Following check handles both the cases // Following check handles both the cases
icon: realm_icon.startsWith('/') ? realm_uri + realm_icon : realm_icon, icon: realm_icon.startsWith('/') ? realm_uri + realm_icon : realm_icon,
url: realm_uri, url: realm_uri,
alias: escape(realm_name) alias: realm_name
}; };
}; };

View File

@@ -2,6 +2,7 @@ import {app} from 'electron';
import AutoLaunch from 'auto-launch'; import AutoLaunch from 'auto-launch';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import * as ConfigUtil from '../renderer/js/utils/config-util'; import * as ConfigUtil from '../renderer/js/utils/config-util';
export const setAutoLaunch = async (AutoLaunchValue: boolean): Promise<void> => { export const setAutoLaunch = async (AutoLaunchValue: boolean): Promise<void> => {

View File

@@ -26,7 +26,7 @@
const { app } = require('electron').remote; const { app } = require('electron').remote;
const version_tag = document.querySelector('#version'); const version_tag = document.querySelector('#version');
version_tag.innerHTML = 'v' + app.getVersion(); version_tag.textContent = 'v' + app.getVersion();
</script> </script>
<script>require('./js/shared/preventdrag.js')</script> <script>require('./js/shared/preventdrag.js')</script>
</body> </body>

View File

@@ -1,5 +1,5 @@
import {clipboard} from 'electron';
import crypto from 'crypto'; import crypto from 'crypto';
import {clipboard} from 'electron';
// This helper is exposed via electron_bridge for use in the social // This helper is exposed via electron_bridge for use in the social
// login flow. // login flow.

View File

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

View File

@@ -1,5 +1,7 @@
import {remote, ContextMenuParams} from 'electron'; import {remote, ContextMenuParams} from 'electron';
import * as t from '../utils/translation-util'; import * as t from '../utils/translation-util';
const {clipboard, Menu} = remote; const {clipboard, Menu} = remote;
export const contextMenu = (webContents: Electron.WebContents, event: Event, props: ContextMenuParams) => { export const contextMenu = (webContents: Electron.WebContents, event: Event, props: ContextMenuParams) => {

View File

@@ -1,3 +1,5 @@
import {htmlEscape} from 'escape-goat';
import Tab, {TabProps} from './tab'; import Tab, {TabProps} from './tab';
export default class FunctionalTab extends Tab { export default class FunctionalTab extends Tab {
@@ -8,19 +10,21 @@ export default class FunctionalTab extends Tab {
this.init(); this.init();
} }
template(): string { templateHTML(): string {
return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}"> return htmlEscape`
<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tab-badge close-button"> <div class="server-tab-badge close-button">
<i class="material-icons">close</i> <i class="material-icons">close</i>
</div> </div>
<div class="server-tab"> <div class="server-tab">
<i class="material-icons">${this.props.materialIcon}</i> <i class="material-icons">${this.props.materialIcon}</i>
</div> </div>
</div>`; </div>
`;
} }
init(): void { init(): void {
this.$el = this.generateNodeFromTemplate(this.template()); this.$el = this.generateNodeFromHTML(this.templateHTML());
if (this.props.name !== 'Settings') { if (this.props.name !== 'Settings') {
this.props.$root.append(this.$el); this.props.$root.append(this.$el);
this.$closeButton = this.$el.querySelectorAll('.server-tab-badge')[0]; this.$closeButton = this.$el.querySelectorAll('.server-tab-badge')[0];

View File

@@ -1,7 +1,8 @@
import {ipcRenderer, remote} from 'electron'; import {ipcRenderer, remote} from 'electron';
import * as LinkUtil from '../utils/link-util';
import * as ConfigUtil from '../utils/config-util'; import * as ConfigUtil from '../utils/config-util';
import * as LinkUtil from '../utils/link-util';
import type WebView from './webview'; import type WebView from './webview';
const {shell, app} = remote; const {shell, app} = remote;

View File

@@ -1,8 +1,11 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import Tab, {TabProps} from './tab'; import {htmlEscape} from 'escape-goat';
import * as SystemUtil from '../utils/system-util'; import * as SystemUtil from '../utils/system-util';
import Tab, {TabProps} from './tab';
export default class ServerTab extends Tab { export default class ServerTab extends Tab {
$badge: Element; $badge: Element;
@@ -11,19 +14,21 @@ export default class ServerTab extends Tab {
this.init(); this.init();
} }
template(): string { templateHTML(): string {
return `<div class="tab" data-tab-id="${this.props.tabIndex}"> return htmlEscape`
<div class="tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tooltip" style="display:none">${this.props.name}</div> <div class="server-tooltip" style="display:none">${this.props.name}</div>
<div class="server-tab-badge"></div> <div class="server-tab-badge"></div>
<div class="server-tab"> <div class="server-tab">
<img class="server-icons" src='${this.props.icon}'/> <img class="server-icons" src="${this.props.icon}"/>
</div> </div>
<div class="server-tab-shortcut">${this.generateShortcutText()}</div> <div class="server-tab-shortcut">${this.generateShortcutText()}</div>
</div>`; </div>
`;
} }
init(): void { init(): void {
this.$el = this.generateNodeFromTemplate(this.template()); this.$el = this.generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$el); this.props.$root.append(this.$el);
this.registerListeners(); this.registerListeners();
this.$badge = this.$el.querySelectorAll('.server-tab-badge')[0]; this.$badge = this.$el.querySelectorAll('.server-tab-badge')[0];
@@ -32,7 +37,7 @@ export default class ServerTab extends Tab {
updateBadge(count: number): void { updateBadge(count: number): void {
if (count > 0) { if (count > 0) {
const formattedCount = count > 999 ? '1K+' : count.toString(); const formattedCount = count > 999 ? '1K+' : count.toString();
this.$badge.innerHTML = formattedCount; this.$badge.textContent = formattedCount;
this.$badge.classList.add('active'); this.$badge.classList.add('active');
} else { } else {
this.$badge.classList.remove('active'); this.$badge.classList.remove('active');

View File

@@ -1,5 +1,5 @@
import WebView from './webview';
import BaseComponent from './base'; import BaseComponent from './base';
import WebView from './webview';
export interface TabProps { export interface TabProps {
role: string; role: string;

View File

@@ -1,12 +1,15 @@
import {ipcRenderer, remote} from 'electron'; import {ipcRenderer, remote} from 'electron';
import path from 'path';
import fs from 'fs'; import fs from 'fs';
import path from 'path';
import {htmlEscape} from 'escape-goat';
import * as ConfigUtil from '../utils/config-util'; import * as ConfigUtil from '../utils/config-util';
import * as SystemUtil from '../utils/system-util'; import * as SystemUtil from '../utils/system-util';
import BaseComponent from './base'; import BaseComponent from './base';
import handleExternalLink from './handle-external-link';
import {contextMenu} from './context-menu'; import {contextMenu} from './context-menu';
import handleExternalLink from './handle-external-link';
const {app, dialog} = remote; const {app, dialog} = remote;
@@ -49,13 +52,14 @@ export default class WebView extends BaseComponent {
this.$webviewsContainer = document.querySelector('#webviews-container').classList; this.$webviewsContainer = document.querySelector('#webviews-container').classList;
} }
template(): string { templateHTML(): string {
return `<webview return htmlEscape`
<webview
class="disabled" class="disabled"
data-tab-id="${this.props.tabIndex}" data-tab-id="${this.props.tabIndex}"
src="${this.props.url}" src="${this.props.url}"
${this.props.nodeIntegration ? 'nodeIntegration' : ''} ` + (this.props.nodeIntegration ? 'nodeIntegration' : '') + htmlEscape`
${this.props.preload ? 'preload="js/preload.js"' : ''} ` + (this.props.preload ? 'preload="js/preload.js"' : '') + htmlEscape`
partition="persist:webviewsession" partition="persist:webviewsession"
name="${this.props.name}" name="${this.props.name}"
webpreferences=" webpreferences="
@@ -63,11 +67,12 @@ export default class WebView extends BaseComponent {
${ConfigUtil.getConfigItem('enableSpellchecker') ? 'spellcheck,' : ''} ${ConfigUtil.getConfigItem('enableSpellchecker') ? 'spellcheck,' : ''}
javascript javascript
"> ">
</webview>`; </webview>
`;
} }
init(): void { init(): void {
this.$el = this.generateNodeFromTemplate(this.template()) as Electron.WebviewTag; this.$el = this.generateNodeFromHTML(this.templateHTML()) as Electron.WebviewTag;
this.domReady = new Promise(resolve => { this.domReady = new Promise(resolve => {
this.$el.addEventListener('dom-ready', () => resolve(), true); this.$el.addEventListener('dom-ready', () => resolve(), true);
}); });

View File

@@ -1,5 +1,4 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import {ClipboardDecrypterImpl} from './clipboard-decrypter'; import {ClipboardDecrypterImpl} from './clipboard-decrypter';

View File

@@ -1,8 +1,8 @@
import {remote} from 'electron'; import {remote} from 'electron';
import SendFeedback from '@electron-elements/send-feedback';
import path from 'path';
import fs from 'fs'; import fs from 'fs';
import path from 'path';
import SendFeedback from '@electron-elements/send-feedback';
const {app} = remote; const {app} = remote;

View File

@@ -1,27 +1,27 @@
import {ipcRenderer, remote, clipboard} from 'electron'; import {ipcRenderer, remote, clipboard} from 'electron';
import {feedbackHolder} from './feedback';
import path from 'path'; import path from 'path';
import escape from 'escape-html';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
const {session, app, Menu, dialog} = remote;
import * as Messages from '../../resources/messages';
import FunctionalTab from './components/functional-tab';
import ServerTab from './components/server-tab';
import WebView from './components/webview';
import {feedbackHolder} from './feedback';
import * as ConfigUtil from './utils/config-util';
import * as DNDUtil from './utils/dnd-util';
import type {DNDSettings} from './utils/dnd-util';
import * as DomainUtil from './utils/domain-util';
import * as EnterpriseUtil from './utils/enterprise-util';
import * as LinkUtil from './utils/link-util';
import Logger from './utils/logger-util';
import ReconnectUtil from './utils/reconnect-util';
// eslint-disable-next-line import/no-unassigned-import // eslint-disable-next-line import/no-unassigned-import
import './tray'; import './tray';
import * as DomainUtil from './utils/domain-util'; const {session, app, Menu, dialog} = remote;
import WebView from './components/webview';
import ServerTab from './components/server-tab';
import FunctionalTab from './components/functional-tab';
import * as ConfigUtil from './utils/config-util';
import * as DNDUtil from './utils/dnd-util';
import ReconnectUtil from './utils/reconnect-util';
import Logger from './utils/logger-util';
import * as CommonUtil from './utils/common-util';
import * as EnterpriseUtil from './utils/enterprise-util';
import * as LinkUtil from './utils/link-util';
import * as Messages from '../../resources/messages';
import type {DNDSettings} from './utils/dnd-util';
interface FunctionalTabProps { interface FunctionalTabProps {
name: string; name: string;
@@ -59,7 +59,14 @@ const logger = new Logger({
}); });
const rendererDirectory = path.resolve(__dirname, '..'); const rendererDirectory = path.resolve(__dirname, '..');
export type ServerOrFunctionalTab = ServerTab | FunctionalTab; type ServerOrFunctionalTab = ServerTab | FunctionalTab;
export interface TabData {
role: string;
name: string;
index: number;
webviewName: string;
}
class ServerManagerView { class ServerManagerView {
$addServerButton: HTMLButtonElement; $addServerButton: HTMLButtonElement;
@@ -116,7 +123,7 @@ class ServerManagerView {
this.$fullscreenPopup = document.querySelector('#fullscreen-popup'); this.$fullscreenPopup = document.querySelector('#fullscreen-popup');
this.$fullscreenEscapeKey = process.platform === 'darwin' ? '^⌘F' : 'F11'; this.$fullscreenEscapeKey = process.platform === 'darwin' ? '^⌘F' : 'F11';
this.$fullscreenPopup.innerHTML = `Press ${this.$fullscreenEscapeKey} to exit full screen`; this.$fullscreenPopup.textContent = `Press ${this.$fullscreenEscapeKey} to exit full screen`;
this.loading = new Set(); this.loading = new Set();
this.activeTabIndex = -1; this.activeTabIndex = -1;
@@ -349,7 +356,7 @@ class ServerManagerView {
this.tabs.push(new ServerTab({ this.tabs.push(new ServerTab({
role: 'server', role: 'server',
icon: server.icon, icon: server.icon,
name: CommonUtil.decodeString(server.alias), name: server.alias,
$root: this.$tabsContainer, $root: this.$tabsContainer,
onClick: this.activateLastTab.bind(this, index), onClick: this.activateLastTab.bind(this, index),
index, index,
@@ -362,7 +369,7 @@ class ServerManagerView {
tabIndex, tabIndex,
url: server.url, url: server.url,
role: 'server', role: 'server',
name: CommonUtil.decodeString(server.alias), name: server.alias,
hasPermission: (origin: string, permission: string) => hasPermission: (origin: string, permission: string) =>
origin === server.url && permission === 'notifications', origin === server.url && permission === 'notifications',
isActive: () => index === this.activeTabIndex, isActive: () => index === this.activeTabIndex,
@@ -454,7 +461,7 @@ class ServerManagerView {
const $parent = $img.parentElement; const $parent = $img.parentElement;
const $container = $parent.parentElement; const $container = $parent.parentElement;
const webviewId = $container.dataset.tabId; const webviewId = $container.dataset.tabId;
const $webview = document.querySelector(`webview[data-tab-id="${webviewId}"]`); const $webview = document.querySelector(`webview[data-tab-id="${CSS.escape(webviewId)}"]`);
const realmName = $webview.getAttribute('name'); const realmName = $webview.getAttribute('name');
if (realmName === null) { if (realmName === null) {
@@ -490,7 +497,7 @@ class ServerManagerView {
} }
onHover(index: number): void { onHover(index: number): void {
// `this.$serverIconTooltip[index].innerHTML` already has realm name, so we are just // `this.$serverIconTooltip[index].textContent` already has realm name, so we are just
// removing the style. // removing the style.
this.$serverIconTooltip[index].removeAttribute('style'); this.$serverIconTooltip[index].removeAttribute('style');
// To handle position of servers' tooltip due to scrolling of list of organizations // To handle position of servers' tooltip due to scrolling of list of organizations
@@ -590,19 +597,13 @@ class ServerManagerView {
// not crash app when this.tabs is passed into // not crash app when this.tabs is passed into
// ipcRenderer. Something about webview, and props.webview // ipcRenderer. Something about webview, and props.webview
// properties in ServerTab causes the app to crash. // properties in ServerTab causes the app to crash.
get tabsForIpc(): ServerOrFunctionalTab[] { get tabsForIpc(): TabData[] {
const tabs: ServerOrFunctionalTab[] = []; return this.tabs.map(tab => ({
this.tabs.forEach((tab: ServerOrFunctionalTab) => { role: tab.props.role,
const proto = Object.create(Object.getPrototypeOf(tab)); name: tab.props.name,
const tabClone = Object.assign(proto, tab); index: tab.props.index,
webviewName: tab.webview.props.name
tabClone.webview = {props: {}}; }));
tabClone.webview.props.name = tab.webview.props.name;
delete tabClone.props.webview;
tabs.push(tabClone);
});
return tabs;
} }
activateTab(index: number, hideOldTab = true): void { activateTab(index: number, hideOldTab = true): void {
@@ -681,8 +682,8 @@ class ServerManagerView {
this.functionalTabs.clear(); this.functionalTabs.clear();
// Clear DOM elements // Clear DOM elements
this.$tabsContainer.innerHTML = ''; this.$tabsContainer.textContent = '';
this.$webviewsContainer.innerHTML = ''; this.$webviewsContainer.textContent = '';
} }
async reloadView(): Promise<void> { async reloadView(): Promise<void> {
@@ -926,11 +927,11 @@ class ServerManagerView {
if (domain.url.includes(serverURL)) { if (domain.url.includes(serverURL)) {
const serverTooltipSelector = '.tab .server-tooltip'; const serverTooltipSelector = '.tab .server-tooltip';
const serverTooltips = document.querySelectorAll(serverTooltipSelector); const serverTooltips = document.querySelectorAll(serverTooltipSelector);
serverTooltips[index].innerHTML = escape(realmName); serverTooltips[index].textContent = realmName;
this.tabs[index].props.name = escape(realmName); this.tabs[index].props.name = realmName;
this.tabs[index].webview.props.name = realmName; this.tabs[index].webview.props.name = realmName;
domain.alias = escape(realmName); domain.alias = realmName;
DomainUtil.updateDomain(index, domain); DomainUtil.updateDomain(index, domain);
// Update the realm name also on the Window menu // Update the realm name also on the Window menu
ipcRenderer.send('update-menu', { ipcRenderer.send('update-menu', {
@@ -971,7 +972,7 @@ class ServerManagerView {
webviews.forEach(webview => { webviews.forEach(webview => {
const currentId = webview.getWebContentsId(); const currentId = webview.getWebContentsId();
const tabId = webview.getAttribute('data-tab-id'); const tabId = webview.getAttribute('data-tab-id');
const concurrentTab: HTMLButtonElement = document.querySelector(`div[data-tab-id="${tabId}"]`); const concurrentTab: HTMLButtonElement = document.querySelector(`div[data-tab-id="${CSS.escape(tabId)}"]`);
if (currentId === webviewId) { if (currentId === webviewId) {
concurrentTab.click(); concurrentTab.click();
} }

View File

@@ -1,12 +1,14 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import MacNotifier from 'node-mac-notifier';
import electron_bridge from '../electron-bridge';
import * as ConfigUtil from '../utils/config-util';
import { import {
appId, customReply, focusCurrentServer, parseReply appId, customReply, focusCurrentServer, parseReply
} from './helpers'; } from './helpers';
import MacNotifier from 'node-mac-notifier';
import * as ConfigUtil from '../utils/config-util';
import electron_bridge from '../electron-bridge';
type ReplyHandler = (response: string) => void; type ReplyHandler = (response: string) => void;
type ClickHandler = () => void; type ClickHandler = () => void;
let replyHandler: ReplyHandler; let replyHandler: ReplyHandler;

View File

@@ -1,8 +1,9 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import {focusCurrentServer} from './helpers';
import * as ConfigUtil from '../utils/config-util'; import * as ConfigUtil from '../utils/config-util';
import {focusCurrentServer} from './helpers';
const NativeNotification = window.Notification; const NativeNotification = window.Notification;
export default class BaseNotification extends NativeNotification { export default class BaseNotification extends NativeNotification {
constructor(title: string, options: NotificationOptions) { constructor(title: string, options: NotificationOptions) {

View File

@@ -1,8 +1,8 @@
import {remote} from 'electron'; import {remote} from 'electron';
import electron_bridge from '../electron-bridge';
import {appId, loadBots} from './helpers';
import DefaultNotification from './default-notification'; import DefaultNotification from './default-notification';
import {appId} from './helpers';
const {app} = remote; const {app} = remote;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid // From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
@@ -67,7 +67,3 @@ export function newNotification(
actions: notification.actions actions: notification.actions
}; };
} }
electron_bridge.once('zulip-loaded', async () => {
await loadBots();
});

View File

@@ -1,8 +1,8 @@
'use-strict';
import {remote, OpenDialogOptions} from 'electron'; import {remote, OpenDialogOptions} from 'electron';
import path from 'path'; import path from 'path';
import {htmlEscape} from 'escape-goat';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import * as CertificateUtil from '../../utils/certificate-util'; import * as CertificateUtil from '../../utils/certificate-util';
import * as DomainUtil from '../../utils/domain-util'; import * as DomainUtil from '../../utils/domain-util';
@@ -26,8 +26,8 @@ export default class AddCertificate extends BaseComponent {
this._certFile = ''; this._certFile = '';
} }
template(): string { templateHTML(): string {
return ` return htmlEscape`
<div class="settings-card certificates-card"> <div class="settings-card certificates-card">
<div class="certificate-input"> <div class="certificate-input">
<div>${t.__('Organization URL')}</div> <div>${t.__('Organization URL')}</div>
@@ -42,7 +42,7 @@ export default class AddCertificate extends BaseComponent {
} }
init(): void { init(): void {
this.$addCertificate = this.generateNodeFromTemplate(this.template()); this.$addCertificate = this.generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$addCertificate); this.props.$root.append(this.$addCertificate);
this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button'); this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button');
this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement; this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;

View File

@@ -1,5 +1,6 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import escape from 'escape-html';
import {htmlEscape} from 'escape-goat';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
@@ -14,9 +15,9 @@ export default class BaseSection extends BaseComponent {
generateSettingOption(props: BaseSectionProps): void { generateSettingOption(props: BaseSectionProps): void {
const {$element, disabled, value, clickHandler} = props; const {$element, disabled, value, clickHandler} = props;
$element.innerHTML = ''; $element.textContent = '';
const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value, disabled)); const $optionControl = this.generateNodeFromHTML(this.generateOptionHTML(value, disabled));
$element.append($optionControl); $element.append($optionControl);
if (!disabled) { if (!disabled) {
@@ -24,24 +25,24 @@ export default class BaseSection extends BaseComponent {
} }
} }
generateOptionTemplate(settingOption: boolean, disabled?: boolean): string { generateOptionHTML(settingOption: boolean, disabled?: boolean): string {
const label = disabled ? '<label class="disallowed" title="Setting locked by system administrator."/>' : '<label/>'; const labelHTML = disabled ? '<label class="disallowed" title="Setting locked by system administrator."></label>' : '<label></label>';
if (settingOption) { if (settingOption) {
return ` return htmlEscape`
<div class="action"> <div class="action">
<div class="switch"> <div class="switch">
<input class="toggle toggle-round" type="checkbox" checked disabled> <input class="toggle toggle-round" type="checkbox" checked disabled>
${label} ` + labelHTML + htmlEscape`
</div> </div>
</div> </div>
`; `;
} }
return ` return htmlEscape`
<div class="action"> <div class="action">
<div class="switch"> <div class="switch">
<input class="toggle toggle-round" type="checkbox"> <input class="toggle toggle-round" type="checkbox">
${label} ` + labelHTML + htmlEscape`
</div> </div>
</div> </div>
`; `;
@@ -50,13 +51,13 @@ export default class BaseSection extends BaseComponent {
/* A method that in future can be used to create dropdown menus using <select> <option> tags. /* A method that in future can be used to create dropdown menus using <select> <option> tags.
it needs an object which has ``key: value`` pairs and will return a string that can be appended to HTML it needs an object which has ``key: value`` pairs and will return a string that can be appended to HTML
*/ */
generateSelectTemplate(options: {[key: string]: string}, className?: string, idName?: string): string { generateSelectHTML(options: {[key: string]: string}, className?: string, idName?: string): string {
let select = `<select class="${escape(className)}" id="${escape(idName)}">\n`; let html = htmlEscape`<select class="${className}" id="${idName}">\n`;
Object.keys(options).forEach(key => { Object.keys(options).forEach(key => {
select += `<option name="${escape(key)}" value="${escape(key)}">${escape(options[key])}</option>\n`; html += htmlEscape`<option name="${key}" value="${key}">${options[key]}</option>\n`;
}); });
select += '</select>'; html += '</select>';
return select; return html;
} }
reloadApp(): void { reloadApp(): void {

View File

@@ -1,12 +1,15 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import BaseSection from './base-section'; import {htmlEscape} from 'escape-goat';
import * as DomainUtil from '../../utils/domain-util'; import * as DomainUtil from '../../utils/domain-util';
import ServerInfoForm from './server-info-form';
import AddCertificate from './add-certificate';
import FindAccounts from './find-accounts';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
import AddCertificate from './add-certificate';
import BaseSection from './base-section';
import FindAccounts from './find-accounts';
import ServerInfoForm from './server-info-form';
interface ConnectedOrgSectionProps { interface ConnectedOrgSectionProps {
$root: Element; $root: Element;
} }
@@ -23,8 +26,8 @@ export default class ConnectedOrgSection extends BaseSection {
this.props = props; this.props = props;
} }
template(): string { templateHTML(): string {
return ` return htmlEscape`
<div class="settings-pane" id="server-settings-pane"> <div class="settings-pane" id="server-settings-pane">
<div class="page-title">${t.__('Connected organizations')}</div> <div class="page-title">${t.__('Connected organizations')}</div>
<div class="title" id="existing-servers">${t.__('All the connected orgnizations will appear here.')}</div> <div class="title" id="existing-servers">${t.__('All the connected orgnizations will appear here.')}</div>
@@ -43,10 +46,10 @@ export default class ConnectedOrgSection extends BaseSection {
} }
initServers(): void { initServers(): void {
this.props.$root.innerHTML = ''; this.props.$root.textContent = '';
const servers = DomainUtil.getDomains(); const servers = DomainUtil.getDomains();
this.props.$root.innerHTML = this.template(); this.props.$root.innerHTML = this.templateHTML();
this.$serverInfoContainer = document.querySelector('#server-info-container'); this.$serverInfoContainer = document.querySelector('#server-info-container');
this.$existingServers = document.querySelector('#existing-servers'); this.$existingServers = document.querySelector('#existing-servers');
@@ -56,7 +59,7 @@ export default class ConnectedOrgSection extends BaseSection {
const noServerText = t.__('All the connected orgnizations will appear here'); const noServerText = t.__('All the connected orgnizations will appear here');
// Show noServerText if no servers are there otherwise hide it // Show noServerText if no servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? noServerText : ''; this.$existingServers.textContent = servers.length === 0 ? noServerText : '';
for (const [i, server] of servers.entries()) { for (const [i, server] of servers.entries()) {
new ServerInfoForm({ new ServerInfoForm({

View File

@@ -1,4 +1,4 @@
'use-strict'; import {htmlEscape} from 'escape-goat';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import * as LinkUtil from '../../utils/link-util'; import * as LinkUtil from '../../utils/link-util';
@@ -18,8 +18,8 @@ export default class FindAccounts extends BaseComponent {
this.props = props; this.props = props;
} }
template(): string { templateHTML(): string {
return ` return htmlEscape`
<div class="settings-card certificate-card"> <div class="settings-card certificate-card">
<div class="certificate-input"> <div class="certificate-input">
<div>${t.__('Organization URL')}</div> <div>${t.__('Organization URL')}</div>
@@ -33,7 +33,7 @@ export default class FindAccounts extends BaseComponent {
} }
init(): void { init(): void {
this.$findAccounts = this.generateNodeFromTemplate(this.template()); this.$findAccounts = this.generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$findAccounts); this.props.$root.append(this.$findAccounts);
this.$findAccountsButton = this.$findAccounts.querySelector('#find-accounts-button'); this.$findAccountsButton = this.$findAccounts.querySelector('#find-accounts-button');
this.$serverUrlField = this.$findAccounts.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement; this.$serverUrlField = this.$findAccounts.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;

View File

@@ -1,18 +1,20 @@
import {ipcRenderer, remote, OpenDialogOptions} from 'electron'; import {ipcRenderer, remote, OpenDialogOptions} from 'electron';
import path from 'path'; import path from 'path';
import Tagify from '@yaireo/tagify';
import {htmlEscape} from 'escape-goat';
import fs from 'fs-extra'; import fs from 'fs-extra';
import ISO6391 from 'iso-639-1';
const {app, dialog, session} = remote; import supportedLocales from '../../../../translations/supported-locales.json';
const currentBrowserWindow = remote.getCurrentWindow();
import BaseSection from './base-section';
import * as ConfigUtil from '../../utils/config-util'; import * as ConfigUtil from '../../utils/config-util';
import * as EnterpriseUtil from '../../utils/enterprise-util'; import * as EnterpriseUtil from '../../utils/enterprise-util';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
import supportedLocales from '../../../../translations/supported-locales.json';
import Tagify from '@yaireo/tagify'; import BaseSection from './base-section';
import ISO6391 from 'iso-639-1';
const {app, dialog, session} = remote;
const currentBrowserWindow = remote.getCurrentWindow();
interface GeneralSectionProps { interface GeneralSectionProps {
$root: Element; $root: Element;
@@ -25,8 +27,8 @@ export default class GeneralSection extends BaseSection {
this.props = props; this.props = props;
} }
template(): string { templateHTML(): string {
return ` return htmlEscape`
<div class="settings-pane"> <div class="settings-pane">
<div class="title">${t.__('Appearance')}</div> <div class="title">${t.__('Appearance')}</div>
<div id="appearance-option-settings" class="settings-card"> <div id="appearance-option-settings" class="settings-card">
@@ -156,7 +158,7 @@ export default class GeneralSection extends BaseSection {
} }
init(): void { init(): void {
this.props.$root.innerHTML = this.template(); this.props.$root.innerHTML = this.templateHTML();
this.updateTrayOption(); this.updateTrayOption();
this.updateBadgeOption(); this.updateBadgeOption();
this.updateSilentOption(); this.updateSilentOption();
@@ -398,8 +400,8 @@ export default class GeneralSection extends BaseSection {
setLocale(): void { setLocale(): void {
const langDiv: HTMLSelectElement = document.querySelector('.lang-div'); const langDiv: HTMLSelectElement = document.querySelector('.lang-div');
const langList = this.generateSelectTemplate(supportedLocales, 'lang-menu'); const langListHTML = this.generateSelectHTML(supportedLocales, 'lang-menu');
langDiv.innerHTML += langList; langDiv.innerHTML += langListHTML;
// `langMenu` is the select-option dropdown menu formed after executing the previous command // `langMenu` is the select-option dropdown menu formed after executing the previous command
const langMenu: HTMLSelectElement = document.querySelector('.lang-menu'); const langMenu: HTMLSelectElement = document.querySelector('.lang-menu');
@@ -515,7 +517,7 @@ export default class GeneralSection extends BaseSection {
const note: HTMLElement = document.querySelector('#note'); const note: HTMLElement = document.querySelector('#note');
note.append(t.__('You can select a maximum of 3 languages for spellchecking.')); note.append(t.__('You can select a maximum of 3 languages for spellchecking.'));
const spellDiv: HTMLElement = document.querySelector('#spellcheck-langs'); const spellDiv: HTMLElement = document.querySelector('#spellcheck-langs');
spellDiv.innerHTML += ` spellDiv.innerHTML += htmlEscape`
<div class="setting-description">${t.__('Spellchecker Languages')}</div> <div class="setting-description">${t.__('Spellchecker Languages')}</div>
<input name='spellcheck' placeholder='Enter Languages'>`; <input name='spellcheck' placeholder='Enter Languages'>`;
@@ -555,7 +557,7 @@ export default class GeneralSection extends BaseSection {
} }
}); });
const configuredLanguages: string[] = ConfigUtil.getConfigItem('spellcheckerLanguages').map((code: string) => [...languagePairs].filter(pair => (pair[1] === code))[0][0]); const configuredLanguages: string[] = ConfigUtil.getConfigItem('spellcheckerLanguages').map((code: string) => [...languagePairs].find(pair => (pair[1] === code))[0]);
tagify.addTags(configuredLanguages); tagify.addTags(configuredLanguages);
tagField.addEventListener('change', event => { tagField.addEventListener('change', event => {

View File

@@ -1,3 +1,5 @@
import {htmlEscape} from 'escape-goat';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
@@ -17,29 +19,29 @@ export default class PreferenceNav extends BaseComponent {
this.init(); this.init();
} }
template(): string { templateHTML(): string {
let navItemsTemplate = ''; let navItemsHTML = '';
for (const navItem of this.navItems) { for (const navItem of this.navItems) {
navItemsTemplate += `<div class="nav" id="nav-${navItem}">${t.__(navItem)}</div>`; navItemsHTML += htmlEscape`<div class="nav" id="nav-${navItem}">${t.__(navItem)}</div>`;
} }
return ` return htmlEscape`
<div> <div>
<div id="settings-header">${t.__('Settings')}</div> <div id="settings-header">${t.__('Settings')}</div>
<div id="nav-container">${navItemsTemplate}</div> <div id="nav-container">` + navItemsHTML + htmlEscape`</div>
</div> </div>
`; `;
} }
init(): void { init(): void {
this.$el = this.generateNodeFromTemplate(this.template()); this.$el = this.generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$el); this.props.$root.append(this.$el);
this.registerListeners(); this.registerListeners();
} }
registerListeners(): void { registerListeners(): void {
for (const navItem of this.navItems) { for (const navItem of this.navItems) {
const $item = document.querySelector(`#nav-${navItem}`); const $item = document.querySelector(`#nav-${CSS.escape(navItem)}`);
$item.addEventListener('click', () => { $item.addEventListener('click', () => {
this.props.onItemSelected(navItem); this.props.onItemSelected(navItem);
}); });
@@ -57,12 +59,12 @@ export default class PreferenceNav extends BaseComponent {
} }
activate(navItem: string): void { activate(navItem: string): void {
const $item = document.querySelector(`#nav-${navItem}`); const $item = document.querySelector(`#nav-${CSS.escape(navItem)}`);
$item.classList.add('active'); $item.classList.add('active');
} }
deactivate(navItem: string): void { deactivate(navItem: string): void {
const $item = document.querySelector(`#nav-${navItem}`); const $item = document.querySelector(`#nav-${CSS.escape(navItem)}`);
$item.classList.remove('active'); $item.classList.remove('active');
} }
} }

View File

@@ -1,9 +1,12 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import BaseSection from './base-section'; import {htmlEscape} from 'escape-goat';
import * as ConfigUtil from '../../utils/config-util'; import * as ConfigUtil from '../../utils/config-util';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
import BaseSection from './base-section';
interface NetworkSectionProps { interface NetworkSectionProps {
$root: Element; $root: Element;
} }
@@ -20,8 +23,8 @@ export default class NetworkSection extends BaseSection {
this.props = props; this.props = props;
} }
template(): string { templateHTML(): string {
return ` return htmlEscape`
<div class="settings-pane"> <div class="settings-pane">
<div class="title">${t.__('Proxy')}</div> <div class="title">${t.__('Proxy')}</div>
<div id="appearance-option-settings" class="settings-card"> <div id="appearance-option-settings" class="settings-card">
@@ -58,7 +61,7 @@ export default class NetworkSection extends BaseSection {
} }
init(): void { init(): void {
this.props.$root.innerHTML = this.template(); this.props.$root.innerHTML = this.templateHTML();
this.$proxyPAC = document.querySelector('#proxy-pac-option .setting-input-value'); this.$proxyPAC = document.querySelector('#proxy-pac-option .setting-input-value');
this.$proxyRules = document.querySelector('#proxy-rules-option .setting-input-value'); this.$proxyRules = document.querySelector('#proxy-rules-option .setting-input-value');
this.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value'); this.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value');

View File

@@ -1,5 +1,7 @@
import {ipcRenderer, remote} from 'electron'; import {ipcRenderer, remote} from 'electron';
import {htmlEscape} from 'escape-goat';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util'; import * as DomainUtil from '../../utils/domain-util';
import * as LinkUtil from '../../utils/link-util'; import * as LinkUtil from '../../utils/link-util';
@@ -22,8 +24,8 @@ export default class NewServerForm extends BaseComponent {
this.props = props; this.props = props;
} }
template(): string { templateHTML(): string {
return ` return htmlEscape`
<div class="server-input-container"> <div class="server-input-container">
<div class="title">${t.__('Organization URL')}</div> <div class="title">${t.__('Organization URL')}</div>
<div class="add-server-info-row"> <div class="add-server-info-row">
@@ -56,20 +58,20 @@ export default class NewServerForm extends BaseComponent {
} }
initForm(): void { initForm(): void {
this.$newServerForm = this.generateNodeFromTemplate(this.template()); this.$newServerForm = this.generateNodeFromHTML(this.templateHTML());
this.$saveServerButton = this.$newServerForm.querySelector('#connect'); this.$saveServerButton = this.$newServerForm.querySelector('#connect');
this.props.$root.innerHTML = ''; this.props.$root.textContent = '';
this.props.$root.append(this.$newServerForm); this.props.$root.append(this.$newServerForm);
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement; this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
} }
async submitFormHandler(): Promise<void> { async submitFormHandler(): Promise<void> {
this.$saveServerButton.innerHTML = 'Connecting...'; this.$saveServerButton.textContent = 'Connecting...';
let serverConf; let serverConf;
try { try {
serverConf = await DomainUtil.checkDomain(this.$newServerUrl.value); serverConf = await DomainUtil.checkDomain(this.$newServerUrl.value);
} catch (error) { } catch (error) {
this.$saveServerButton.innerHTML = 'Connect'; this.$saveServerButton.textContent = 'Connect';
await dialog.showMessageBox({ await dialog.showMessageBox({
type: 'error', type: 'error',
message: error.toString(), message: error.toString(),

View File

@@ -1,14 +1,15 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import Nav from './nav';
import ServersSection from './servers-section';
import GeneralSection from './general-section';
import NetworkSection from './network-section';
import ConnectedOrgSection from './connected-org-section';
import ShortcutsSection from './shortcuts-section';
import type {DNDSettings} from '../../utils/dnd-util'; import type {DNDSettings} from '../../utils/dnd-util';
import ConnectedOrgSection from './connected-org-section';
import GeneralSection from './general-section';
import Nav from './nav';
import NetworkSection from './network-section';
import ServersSection from './servers-section';
import ShortcutsSection from './shortcuts-section';
type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection; type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection;
export default class PreferenceView extends BaseComponent { export default class PreferenceView extends BaseComponent {

View File

@@ -1,8 +1,10 @@
import {remote, ipcRenderer} from 'electron'; import {remote, ipcRenderer} from 'electron';
import {htmlEscape} from 'escape-goat';
import * as Messages from '../../../../resources/messages';
import BaseComponent from '../../components/base'; import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util'; import * as DomainUtil from '../../utils/domain-util';
import * as Messages from '../../../../resources/messages';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
const {dialog} = remote; const {dialog} = remote;
@@ -26,8 +28,8 @@ export default class ServerInfoForm extends BaseComponent {
this.props = props; this.props = props;
} }
template(): string { templateHTML(): string {
return ` return htmlEscape`
<div class="settings-card"> <div class="settings-card">
<div class="server-info-left"> <div class="server-info-left">
<img class="server-info-icon" src="${this.props.server.icon}"/> <img class="server-info-icon" src="${this.props.server.icon}"/>
@@ -56,7 +58,7 @@ export default class ServerInfoForm extends BaseComponent {
} }
initForm(): void { initForm(): void {
this.$serverInfoForm = this.generateNodeFromTemplate(this.template()); this.$serverInfoForm = this.generateNodeFromHTML(this.templateHTML());
this.$serverInfoAlias = this.$serverInfoForm.querySelectorAll('.server-info-alias')[0]; this.$serverInfoAlias = this.$serverInfoForm.querySelectorAll('.server-info-alias')[0];
this.$serverIcon = this.$serverInfoForm.querySelectorAll('.server-info-icon')[0]; this.$serverIcon = this.$serverInfoForm.querySelectorAll('.server-info-icon')[0];
this.$deleteServerButton = this.$serverInfoForm.querySelectorAll('.server-delete-action')[0]; this.$deleteServerButton = this.$serverInfoForm.querySelectorAll('.server-delete-action')[0];

View File

@@ -1,6 +1,9 @@
import {htmlEscape} from 'escape-goat';
import * as t from '../../utils/translation-util';
import BaseSection from './base-section'; import BaseSection from './base-section';
import NewServerForm from './new-server-form'; import NewServerForm from './new-server-form';
import * as t from '../../utils/translation-util';
interface ServersSectionProps { interface ServersSectionProps {
$root: Element; $root: Element;
@@ -14,8 +17,8 @@ export default class ServersSection extends BaseSection {
this.props = props; this.props = props;
} }
template(): string { templateHTML(): string {
return ` return htmlEscape`
<div class="add-server-modal"> <div class="add-server-modal">
<div class="modal-container"> <div class="modal-container">
<div class="settings-pane" id="server-settings-pane"> <div class="settings-pane" id="server-settings-pane">
@@ -32,9 +35,9 @@ export default class ServersSection extends BaseSection {
} }
initServers(): void { initServers(): void {
this.props.$root.innerHTML = ''; this.props.$root.textContent = '';
this.props.$root.innerHTML = this.template(); this.props.$root.innerHTML = this.templateHTML();
this.$newServerContainer = document.querySelector('#new-server-container'); this.$newServerContainer = document.querySelector('#new-server-container');
this.initNewServerForm(); this.initNewServerForm();

View File

@@ -1,7 +1,10 @@
import BaseSection from './base-section'; import {htmlEscape} from 'escape-goat';
import * as LinkUtil from '../../utils/link-util'; import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util'; import * as t from '../../utils/translation-util';
import BaseSection from './base-section';
interface ShortcutsSectionProps { interface ShortcutsSectionProps {
$root: Element; $root: Element;
} }
@@ -13,14 +16,14 @@ export default class ShortcutsSection extends BaseSection {
this.props = props; this.props = props;
} }
// TODO - Deduplicate templateMac and templateWinLin functions. In theory // TODO - Deduplicate templateMacHTML and templateWinLinHTML functions. In theory
// they both should be the same the only thing different should be the userOSKey // they both should be the same the only thing different should be the userOSKey
// variable but there seems to be inconsistences between both function, one has more // variable but there seems to be inconsistences between both function, one has more
// lines though one may just be using more new lines and other thing is the use of +. // lines though one may just be using more new lines and other thing is the use of +.
templateMac(): string { templateMacHTML(): string {
const userOSKey = '⌘'; const userOSKey = '⌘';
return ` return htmlEscape`
<div class="settings-pane"> <div class="settings-pane">
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>${t.__('Tip')}: </b>${t.__('These desktop app shortcuts extend the Zulip webapp\'s')} <span id="open-hotkeys-link"> ${t.__('keyboard shortcuts')}</span>.</p></div> <div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>${t.__('Tip')}: </b>${t.__('These desktop app shortcuts extend the Zulip webapp\'s')} <span id="open-hotkeys-link"> ${t.__('keyboard shortcuts')}</span>.</p></div>
<div class="title">${t.__('Application Shortcuts')}</div> <div class="title">${t.__('Application Shortcuts')}</div>
@@ -181,10 +184,10 @@ export default class ShortcutsSection extends BaseSection {
`; `;
} }
templateWinLin(): string { templateWinLinHTML(): string {
const userOSKey = 'Ctrl'; const userOSKey = 'Ctrl';
return ` return htmlEscape`
<div class="settings-pane"> <div class="settings-pane">
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>${t.__('Tip')}: </b>${t.__('These desktop app shortcuts extend the Zulip webapp\'s')} <span id="open-hotkeys-link"> ${t.__('keyboard shortcuts')}</span>.</p></div> <div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>${t.__('Tip')}: </b>${t.__('These desktop app shortcuts extend the Zulip webapp\'s')} <span id="open-hotkeys-link"> ${t.__('keyboard shortcuts')}</span>.</p></div>
<div class="title">${t.__('Application Shortcuts')}</div> <div class="title">${t.__('Application Shortcuts')}</div>
@@ -339,7 +342,7 @@ export default class ShortcutsSection extends BaseSection {
init(): void { init(): void {
this.props.$root.innerHTML = (process.platform === 'darwin') ? this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin(); this.templateMacHTML() : this.templateWinLinHTML();
this.openHotkeysExternalLink(); this.openHotkeysExternalLink();
} }
} }

View File

@@ -3,18 +3,20 @@ import fs from 'fs';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import electron_bridge from './electron-bridge';
import {loadBots} from './notification/helpers';
import * as NetworkError from './pages/network'; import * as NetworkError from './pages/network';
// eslint-disable-next-line import/no-unassigned-import
import './notification';
// Prevent drag and drop event in main process which prevents remote code executaion // Prevent drag and drop event in main process which prevents remote code executaion
// eslint-disable-next-line import/no-unassigned-import // eslint-disable-next-line import/no-unassigned-import
import './shared/preventdrag'; import './shared/preventdrag';
import electron_bridge from './electron-bridge';
contextBridge.exposeInMainWorld('raw_electron_bridge', electron_bridge); contextBridge.exposeInMainWorld('raw_electron_bridge', electron_bridge);
electron_bridge.once('zulip-loaded', async () => {
await loadBots();
});
ipcRenderer.on('logout', () => { ipcRenderer.on('logout', () => {
// Create the menu for the below // Create the menu for the below
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle'); const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');

View File

@@ -1,6 +1,6 @@
import {ipcRenderer, remote, WebviewTag, NativeImage} from 'electron'; import {ipcRenderer, remote, WebviewTag, NativeImage} from 'electron';
import path from 'path'; import path from 'path';
import * as ConfigUtil from './utils/config-util'; import * as ConfigUtil from './utils/config-util';
const {Tray, Menu, nativeImage, BrowserWindow} = remote; const {Tray, Menu, nativeImage, BrowserWindow} = remote;

View File

@@ -1,9 +1,10 @@
import electron from 'electron'; import electron from 'electron';
import {JsonDB} from 'node-json-db';
import {initSetUp} from './default-util';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {JsonDB} from 'node-json-db';
import {initSetUp} from './default-util';
import Logger from './logger-util'; import Logger from './logger-util';
const {app, dialog} = const {app, dialog} =

View File

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

View File

@@ -1,10 +1,11 @@
import {JsonDB} from 'node-json-db'; import electron from 'electron';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import electron from 'electron';
import Logger from './logger-util'; import {JsonDB} from 'node-json-db';
import * as EnterpriseUtil from './enterprise-util'; import * as EnterpriseUtil from './enterprise-util';
import Logger from './logger-util';
const logger = new Logger({ const logger = new Logger({
file: 'config-util.log', file: 'config-util.log',

View File

@@ -1,12 +1,13 @@
import {JsonDB} from 'node-json-db'; import {remote, ipcRenderer} from 'electron';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import Logger from './logger-util';
import {remote, ipcRenderer} from 'electron'; import {JsonDB} from 'node-json-db';
import * as Messages from '../../../resources/messages';
import * as EnterpriseUtil from './enterprise-util'; import * as EnterpriseUtil from './enterprise-util';
import * as Messages from '../../../resources/messages'; import Logger from './logger-util';
const {app, dialog} = remote; const {app, dialog} = remote;

View File

@@ -1,9 +1,10 @@
import {shell} from 'electron'; import {shell} from 'electron';
import escape from 'escape-html';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import {htmlEscape} from 'escape-goat';
export function isUploadsUrl(server: string, url: URL): boolean { export function isUploadsUrl(server: string, url: URL): boolean {
return url.origin === server && url.pathname.startsWith('/user_uploads/'); return url.origin === server && url.pathname.startsWith('/user_uploads/');
} }
@@ -18,12 +19,12 @@ export async function openBrowser(url: URL): Promise<void> {
path.join(os.tmpdir(), 'zulip-redirect-') path.join(os.tmpdir(), 'zulip-redirect-')
); );
const file = path.join(dir, 'redirect.html'); const file = path.join(dir, 'redirect.html');
fs.writeFileSync(file, `\ fs.writeFileSync(file, htmlEscape`\
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="Refresh" content="0; url=${escape(url.href)}" /> <meta http-equiv="Refresh" content="0; url=${url.href}" />
<title>Redirecting</title> <title>Redirecting</title>
<style> <style>
html { html {
@@ -32,11 +33,11 @@ export async function openBrowser(url: URL): Promise<void> {
</style> </style>
</head> </head>
<body> <body>
<p>Opening <a href="${escape(url.href)}">${escape(url.href)}</a>…</p> <p>Opening <a href="${url.href}">${url.href}</a>…</p>
</body> </body>
</html> </html>
`); `);
shell.openItem(file); await shell.openPath(file);
setTimeout(() => { setTimeout(() => {
fs.unlinkSync(file); fs.unlinkSync(file);
fs.rmdirSync(dir); fs.rmdirSync(dir);

View File

@@ -1,8 +1,9 @@
import {JsonDB} from 'node-json-db'; import electron from 'electron';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import electron from 'electron';
import {JsonDB} from 'node-json-db';
import Logger from './logger-util'; import Logger from './logger-util';
const remote = const remote =

View File

@@ -1,11 +1,12 @@
import {Console} from 'console'; // eslint-disable-line node/prefer-global/console import {Console} from 'console'; // eslint-disable-line node/prefer-global/console
import {initSetUp} from './default-util'; import electron from 'electron';
import {sentryInit, captureException} from './sentry-util';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import electron from 'electron';
import {initSetUp} from './default-util';
import {sentryInit, captureException} from './sentry-util';
interface LoggerOptions { interface LoggerOptions {
timestamp?: true | (() => string); timestamp?: true | (() => string);

View File

@@ -1,7 +1,10 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import type WebView from '../components/webview';
import backoff from 'backoff'; import backoff from 'backoff';
import {htmlEscape} from 'escape-goat';
import type WebView from '../components/webview';
import Logger from './logger-util'; import Logger from './logger-util';
const logger = new Logger({ const logger = new Logger({
@@ -58,7 +61,7 @@ export default class ReconnectUtil {
logger.log('There is no internet connection, try checking network cables, modem and router.'); logger.log('There is no internet connection, try checking network cables, modem and router.');
const errorMessageHolder = document.querySelector('#description'); const errorMessageHolder = document.querySelector('#description');
if (errorMessageHolder) { if (errorMessageHolder) {
errorMessageHolder.innerHTML = ` errorMessageHolder.innerHTML = htmlEscape`
<div>Your internet connection doesn't seem to work properly!</div> <div>Your internet connection doesn't seem to work properly!</div>
<div>Verify that it works and then click try again.</div>`; <div>Verify that it works and then click try again.</div>`;
} }

View File

@@ -1,5 +1,4 @@
import {init} from '@sentry/electron'; import {init} from '@sentry/electron';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
export const sentryInit = (): void => { export const sentryInit = (): void => {

View File

@@ -1,5 +1,4 @@
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
import os from 'os'; import os from 'os';
export const connectivityERR: string[] = [ export const connectivityERR: string[] = [

View File

@@ -1,5 +1,7 @@
import path from 'path'; import path from 'path';
import i18n from 'i18n'; import i18n from 'i18n';
import * as ConfigUtil from './config-util'; import * as ConfigUtil from './config-util';
i18n.configure({ i18n.configure({

Binary file not shown.

Binary file not shown.

View File

@@ -2,6 +2,44 @@
All notable changes to the Zulip desktop app are documented in this file. All notable changes to the Zulip desktop app are documented in this file.
### v5.4.2 --2020-08-12
**Potential Fixes**:
* macOS: Electron 9 upgrade is a potential fix for the ['grey screen issue'](https://chat.zulip.org/#narrow/stream/9-issues/topic/Grey.20Window.20on.20macOS) reported.
**Dependencies**:
* Upgrade all dependencies, including Electron 9.2.0.
### v5.4.1-beta --2020-07-29
**Fixes**:
* Resized the large application icon on macOS dock to be coherent with other icons.
**Potential Fixes**:
* macOS: Electron 9 upgrade is a potential fix for the ['grey screen issue'](https://chat.zulip.org/#narrow/stream/9-issues/topic/Grey.20Window.20on.20macOS) reported.
**Dependencies**:
* Upgrade all dependencies, including Electron 9.1.1.
### v5.4.0 --2020-07-21
**New features**:
* Added support for certificates from system store.
* Added support for Slovak as application language.
**Fixes**:
* Fix bug in *Copy Link* and add *Copy Email* option in context menu.
* Enable *Copy* option in context menu only when copying is possible.
* Remove leading and trailing separators in context menu on non-mac systems.
* ignoreCerts: Accommodate WebSocket URLs in certificate-error handler.
**Dependencies**:
* Upgrade all dependencies, including Electron 8.4.0.
**Deprecations**:
* This release supports certificates from Zulip store as well as system store. Zulip certificate store will be deprecated in the next release.
Users are hereby requested to move to system store. For more information, please see the [documentation](https://zulip.com/help/custom-certificates).
### v5.3.0 --2020-06-24 ### v5.3.0 --2020-06-24
**Security fixes**: **Security fixes**:

View File

@@ -1,16 +1,16 @@
'use strict'; 'use strict';
const gulp = require('gulp'); const {execSync} = require('child_process');
const electron = require('electron-connect').server.create({ const electron = require('electron-connect').server.create({
verbose: true verbose: true
}); });
const tape = require('gulp-tape');
const tapColorize = require('tap-colorize');
const ts = require('gulp-typescript');
const tsProject = ts.createProject('tsconfig.json');
const glob = require('glob'); const glob = require('glob');
const {execSync} = require('child_process'); const gulp = require('gulp');
const tape = require('gulp-tape');
const ts = require('gulp-typescript');
const tapColorize = require('tap-colorize');
const tsProject = ts.createProject('tsconfig.json');
const baseFilePattern = 'app/+(main|renderer)/**/*'; const baseFilePattern = 'app/+(main|renderer)/**/*';
const globOptions = {cwd: __dirname}; const globOptions = {cwd: __dirname};
const jsFiles = glob.sync(baseFilePattern + '.js', globOptions); const jsFiles = glob.sync(baseFilePattern + '.js', globOptions);

4351
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "zulip", "name": "zulip",
"productName": "Zulip", "productName": "Zulip",
"version": "5.4.0", "version": "5.4.3",
"main": "./app/main", "main": "./app/main",
"description": "Zulip Desktop App", "description": "Zulip Desktop App",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -50,7 +50,7 @@
"files": [ "files": [
"app/**/*" "app/**/*"
], ],
"copyright": "©2019 Kandra Labs, Inc.", "copyright": "©2020 Kandra Labs, Inc.",
"mac": { "mac": {
"category": "public.app-category.productivity", "category": "public.app-category.productivity",
"target": [ "target": [
@@ -146,20 +146,20 @@
], ],
"dependencies": { "dependencies": {
"@electron-elements/send-feedback": "^2.0.3", "@electron-elements/send-feedback": "^2.0.3",
"@sentry/electron": "^1.4.0", "@sentry/electron": "^2.0.0",
"@yaireo/tagify": "^3.15.3", "@yaireo/tagify": "^3.17.10",
"adm-zip": "^0.4.16", "adm-zip": "^0.4.16",
"auto-launch": "^5.0.5", "auto-launch": "^5.0.5",
"backoff": "^2.5.0", "backoff": "^2.5.0",
"electron-is-dev": "^1.2.0", "electron-is-dev": "^1.2.0",
"electron-log": "^4.2.2", "electron-log": "^4.2.4",
"electron-updater": "^4.3.1", "electron-updater": "^4.3.4",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"escape-html": "^1.0.3", "escape-goat": "^3.0.0",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"get-stream": "^5.1.0", "get-stream": "^6.0.0",
"i18n": "^0.10.0", "i18n": "^0.13.2",
"iso-639-1": "^2.1.3", "iso-639-1": "^2.1.4",
"nan": "^2.14.0", "nan": "^2.14.0",
"node-json-db": "^1.1.0", "node-json-db": "^1.1.0",
"semver": "^7.3.2" "semver": "^7.3.2"
@@ -171,17 +171,14 @@
"@types/adm-zip": "^0.4.33", "@types/adm-zip": "^0.4.33",
"@types/auto-launch": "^5.0.1", "@types/auto-launch": "^5.0.1",
"@types/backoff": "^2.5.1", "@types/backoff": "^2.5.1",
"@types/escape-html": "^1.0.0",
"@types/fs-extra": "^9.0.1", "@types/fs-extra": "^9.0.1",
"@types/i18n": "^0.8.6", "@types/i18n": "^0.8.7",
"@types/node": "^14.0.23", "@types/node": "^14.6.4",
"@types/requestidlecallback": "^0.3.1", "@types/requestidlecallback": "^0.3.1",
"@typescript-eslint/eslint-plugin": "^3.6.1",
"@typescript-eslint/parser": "^3.6.1",
"devtron": "^1.4.0", "devtron": "^1.4.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"electron": "^8.4.0", "electron": "^9.3.0",
"electron-builder": "^22.7.0", "electron-builder": "^22.8.0",
"electron-connect": "^0.6.3", "electron-connect": "^0.6.3",
"electron-notarize": "^1.0.0", "electron-notarize": "^1.0.0",
"glob": "^7.1.6", "glob": "^7.1.6",
@@ -192,18 +189,29 @@
"nodemon": "^2.0.4", "nodemon": "^2.0.4",
"pre-commit": "^1.2.2", "pre-commit": "^1.2.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"spectron": "^10.0.1", "spectron": "^11.1.0",
"stylelint": "^13.6.1", "stylelint": "^13.7.0",
"tap-colorize": "^1.2.0", "tap-colorize": "^1.2.0",
"tape": "^5.0.1", "tape": "^5.0.1",
"typescript": "^3.9.7", "typescript": "^4.0.2",
"xo": "^0.32.1" "xo": "^0.33.1"
}, },
"xo": { "xo": {
"rules": { "rules": {
"@typescript-eslint/no-dynamic-delete": "off", "@typescript-eslint/no-dynamic-delete": "off",
"@typescript-eslint/prefer-readonly-parameter-types": "off", "@typescript-eslint/prefer-readonly-parameter-types": "off",
"arrow-body-style": "error", "arrow-body-style": "error",
"import/first": "error",
"import/newline-after-import": "error",
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always"
}
],
"import/unambiguous": "error", "import/unambiguous": "error",
"max-lines": [ "max-lines": [
"warn", "warn",

View File

@@ -1,11 +1,11 @@
'use strict'; 'use strict';
const path = require('path'); const path = require('path');
const dotenv = require('dotenv'); const dotenv = require('dotenv');
const {notarize} = require('electron-notarize');
dotenv.config({path: path.join(__dirname, '/../.env')}); dotenv.config({path: path.join(__dirname, '/../.env')});
const {notarize} = require('electron-notarize');
exports.default = async function (context) { exports.default = async function (context) {
const {electronPlatformName, appOutDir} = context; const {electronPlatformName, appOutDir} = context;
if (electronPlatformName !== 'darwin') { if (electronPlatformName !== 'darwin') {

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
const test = require('tape'); const test = require('tape');
const setup = require('./setup'); const setup = require('./setup');
test('app runs', async t => { test('app runs', async t => {
@@ -9,7 +10,7 @@ test('app runs', async t => {
try { try {
await setup.waitForLoad(app, t); await setup.waitForLoad(app, t);
await app.client.windowByIndex(1); // Focus on webview await app.client.windowByIndex(1); // Focus on webview
await app.client.waitForExist('//*[@id="connect"]'); // Id of the connect button await (await app.client.$('//*[@id="connect"]')).waitForExist(); // Id of the connect button
await setup.endTest(app, t); await setup.endTest(app, t);
} catch (error) { } catch (error) {
await setup.endTest(app, t, error || 'error'); await setup.endTest(app, t, error || 'error');

View File

@@ -1,8 +1,9 @@
'use strict'; 'use strict';
const {Application} = require('spectron');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const rimraf = require('rimraf'); const rimraf = require('rimraf');
const {Application} = require('spectron');
const config = require('./config'); const config = require('./config');
@@ -66,6 +67,7 @@ async function wait(ms) {
// Quit the app, end the test, either in success (!err) or failure (err) // Quit the app, end the test, either in success (!err) or failure (err)
async function endTest(app, t, err) { async function endTest(app, t, err) {
await app.client.windowByIndex(0);
await app.stop(); await app.stop();
t.end(err); t.end(err);
} }

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
const test = require('tape'); const test = require('tape');
const setup = require('./setup'); const setup = require('./setup');
test('add-organization', async t => { test('add-organization', async t => {
@@ -9,12 +10,12 @@ test('add-organization', async t => {
try { try {
await setup.waitForLoad(app, t); await setup.waitForLoad(app, t);
await app.client.windowByIndex(1); // Focus on webview await app.client.windowByIndex(1); // Focus on webview
await app.client.setValue('.setting-input-value', 'chat.zulip.org'); await (await app.client.$('.setting-input-value')).setValue('chat.zulip.org');
await app.client.click('#connect'); await (await app.client.$('#connect')).click();
await setup.wait(5000); await setup.wait(5000);
await app.client.windowByIndex(0); // Switch focus back to main win await app.client.windowByIndex(0); // Switch focus back to main win
await app.client.windowByIndex(1); // Switch focus back to org webview await app.client.windowByIndex(1); // Switch focus back to org webview
await app.client.waitForExist('//*[@id="id_username"]'); await (await app.client.$('//*[@id="id_username"]')).waitForExist();
await setup.endTest(app, t); await setup.endTest(app, t);
} catch (error) { } catch (error) {
await setup.endTest(app, t, error || 'error'); await setup.endTest(app, t, error || 'error');

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
const test = require('tape'); const test = require('tape');
const setup = require('./setup'); const setup = require('./setup');
// Create new org link should open in the default browser [WIP] // Create new org link should open in the default browser [WIP]
@@ -11,7 +12,7 @@ test('new-org-link', async t => {
try { try {
await setup.waitForLoad(app, t); await setup.waitForLoad(app, t);
await app.client.windowByIndex(1); // Focus on webview await app.client.windowByIndex(1); // Focus on webview
await app.client.click('#open-create-org-link'); // Click on new org link button await (await app.client.$('#open-create-org-link')).click(); // Click on new org link button
await setup.wait(5000); await setup.wait(5000);
await setup.endTest(app, t); await setup.endTest(app, t);
} catch (error) { } catch (error) {