mirror of
				https://github.com/zulip/zulip-desktop.git
				synced 2025-11-04 05:53:21 +00:00 
			
		
		
		
	Compare commits
	
		
			55 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0cb82a6f5e | ||
| 
						 | 
					79808e8ee9 | ||
| 
						 | 
					2c38df10c8 | ||
| 
						 | 
					1ca15d44a0 | ||
| 
						 | 
					82450a91a9 | ||
| 
						 | 
					62edfa6f8b | ||
| 
						 | 
					fe86315ece | ||
| 
						 | 
					df3f719e89 | ||
| 
						 | 
					0632d8199f | ||
| 
						 | 
					047bf0ca45 | ||
| 
						 | 
					356c879668 | ||
| 
						 | 
					ba432d32b3 | ||
| 
						 | 
					c8ada3f47d | ||
| 
						 | 
					cd77fc6448 | ||
| 
						 | 
					a2f926c611 | ||
| 
						 | 
					6c5eb85a16 | ||
| 
						 | 
					cadb1c6eaa | ||
| 
						 | 
					73710319e6 | ||
| 
						 | 
					da91dc5595 | ||
| 
						 | 
					31d5e5a092 | ||
| 
						 | 
					13ee1d0990 | ||
| 
						 | 
					d5a9063378 | ||
| 
						 | 
					918064f35d | ||
| 
						 | 
					193b8326bc | ||
| 
						 | 
					9abb7f376e | ||
| 
						 | 
					ac338fa438 | ||
| 
						 | 
					f5b78ee845 | ||
| 
						 | 
					126bb26a6e | ||
| 
						 | 
					23e86abb5b | ||
| 
						 | 
					3a3714787f | ||
| 
						 | 
					bc57aabc97 | ||
| 
						 | 
					08df02a1ea | ||
| 
						 | 
					35ad6fbad0 | ||
| 
						 | 
					97f8fe71af | ||
| 
						 | 
					a9d59b3dcd | ||
| 
						 | 
					b7240e1c40 | ||
| 
						 | 
					62aa849657 | ||
| 
						 | 
					c302ebe282 | ||
| 
						 | 
					6404bed519 | ||
| 
						 | 
					8d4d168988 | ||
| 
						 | 
					d4d3805be8 | ||
| 
						 | 
					e853af40c4 | ||
| 
						 | 
					941200cf3b | ||
| 
						 | 
					cf1f659ebf | ||
| 
						 | 
					eb381a87bc | ||
| 
						 | 
					68bc0ae4a0 | ||
| 
						 | 
					178bc7f401 | ||
| 
						 | 
					0f1245b975 | ||
| 
						 | 
					960312a932 | ||
| 
						 | 
					0e00f3bbce | ||
| 
						 | 
					ec205f68a6 | ||
| 
						 | 
					5fe5989710 | ||
| 
						 | 
					69141b5395 | ||
| 
						 | 
					8d66f05924 | ||
| 
						 | 
					e7330dbff8 | 
@@ -1,12 +1,13 @@
 | 
				
			|||||||
# Zulip Desktop Client
 | 
					# Zulip Desktop Client
 | 
				
			||||||
[](https://travis-ci.org/zulip/zulip-desktop)
 | 
					[](https://travis-ci.com/github/zulip/zulip-desktop)
 | 
				
			||||||
[](https://ci.appveyor.com/project/zulip/zulip-desktop/branch/master)
 | 
					[](https://ci.appveyor.com/project/zulip/zulip-desktop/branch/master)
 | 
				
			||||||
[](https://github.com/sindresorhus/xo)
 | 
					[](https://github.com/sindresorhus/xo)
 | 
				
			||||||
[](https://chat.zulip.org)
 | 
					[](https://chat.zulip.org)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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"/>
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# 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).
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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> {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,19 @@
 | 
				
			|||||||
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 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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -68,7 +69,7 @@ const toggleApp = (): void => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function createMainWindow(): Electron.BrowserWindow {
 | 
					function createMainWindow(): Electron.BrowserWindow {
 | 
				
			||||||
	// Load the previous state with fallback to defaults
 | 
						// Load the previous state with fallback to defaults
 | 
				
			||||||
	const mainWindowState: windowStateKeeper.State = windowStateKeeper({
 | 
						mainWindowState = windowStateKeeper({
 | 
				
			||||||
		defaultWidth: 1100,
 | 
							defaultWidth: 1100,
 | 
				
			||||||
		defaultHeight: 720,
 | 
							defaultHeight: 720,
 | 
				
			||||||
		path: `${app.getPath('userData')}/config`
 | 
							path: `${app.getPath('userData')}/config`
 | 
				
			||||||
@@ -85,7 +86,8 @@ function createMainWindow(): Electron.BrowserWindow {
 | 
				
			|||||||
		minWidth: 500,
 | 
							minWidth: 500,
 | 
				
			||||||
		minHeight: 400,
 | 
							minHeight: 400,
 | 
				
			||||||
		webPreferences: {
 | 
							webPreferences: {
 | 
				
			||||||
			plugins: true,
 | 
								contextIsolation: false,
 | 
				
			||||||
 | 
								enableRemoteModule: true,
 | 
				
			||||||
			nodeIntegration: true,
 | 
								nodeIntegration: true,
 | 
				
			||||||
			partition: 'persist:webviewsession',
 | 
								partition: 'persist:webviewsession',
 | 
				
			||||||
			webviewTag: true
 | 
								webviewTag: true
 | 
				
			||||||
@@ -220,36 +222,9 @@ app.on('ready', () => {
 | 
				
			|||||||
		event: Event,
 | 
							event: Event,
 | 
				
			||||||
		webContents: Electron.WebContents,
 | 
							webContents: Electron.WebContents,
 | 
				
			||||||
		urlString: string,
 | 
							urlString: string,
 | 
				
			||||||
		error: string,
 | 
							error: string
 | 
				
			||||||
		certificate: Electron.Certificate,
 | 
						) => {
 | 
				
			||||||
		callback: (isTrusted: boolean) => void
 | 
					 | 
				
			||||||
	) /* eslint-disable-line max-params */ => {
 | 
					 | 
				
			||||||
		// TODO: The entire concept of selectively ignoring certificate errors
 | 
					 | 
				
			||||||
		// is ill-conceived, and this handler needs to be deleted.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const url = new URL(urlString);
 | 
							const url = new URL(urlString);
 | 
				
			||||||
		if (url.protocol === 'wss:') {
 | 
					 | 
				
			||||||
			url.protocol = 'https:';
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const filename = CertificateUtil.getCertificate(encodeURIComponent(url.origin));
 | 
					 | 
				
			||||||
		if (filename !== undefined) {
 | 
					 | 
				
			||||||
			try {
 | 
					 | 
				
			||||||
				const savedCertificate = fs.readFileSync(
 | 
					 | 
				
			||||||
					path.join(`${app.getPath('userData')}/certificates`, filename),
 | 
					 | 
				
			||||||
					'utf8'
 | 
					 | 
				
			||||||
				);
 | 
					 | 
				
			||||||
				if (certificate.data.replace(/[\r\n]/g, '') ===
 | 
					 | 
				
			||||||
					savedCertificate.replace(/[\r\n]/g, '')) {
 | 
					 | 
				
			||||||
					event.preventDefault();
 | 
					 | 
				
			||||||
					callback(true);
 | 
					 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			} catch (error) {
 | 
					 | 
				
			||||||
				console.error(`Error reading certificate file ${filename}:`, error);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		dialog.showErrorBox(
 | 
							dialog.showErrorBox(
 | 
				
			||||||
			'Certificate error',
 | 
								'Certificate error',
 | 
				
			||||||
			`The server presented an invalid certificate for ${url.origin}:
 | 
								`The server presented an invalid certificate for ${url.origin}:
 | 
				
			||||||
@@ -283,32 +258,6 @@ ${error}`
 | 
				
			|||||||
		app.quit();
 | 
							app.quit();
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Code to show pdf in a new BrowserWindow (currently commented out due to bug-upstream)
 | 
					 | 
				
			||||||
	// ipcMain.on('pdf-view', (event, url) => {
 | 
					 | 
				
			||||||
	// 	// Paddings for pdfWindow so that it fits into the main browserWindow
 | 
					 | 
				
			||||||
	// 	const paddingWidth = 55;
 | 
					 | 
				
			||||||
	// 	const paddingHeight = 22;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 	// Get the config of main browserWindow
 | 
					 | 
				
			||||||
	// 	const mainWindowState = global.mainWindowState;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 	// Window to view the pdf file
 | 
					 | 
				
			||||||
	// 	const pdfWindow = new electron.BrowserWindow({
 | 
					 | 
				
			||||||
	// 		x: mainWindowState.x + paddingWidth,
 | 
					 | 
				
			||||||
	// 		y: mainWindowState.y + paddingHeight,
 | 
					 | 
				
			||||||
	// 		width: mainWindowState.width - paddingWidth,
 | 
					 | 
				
			||||||
	// 		height: mainWindowState.height - paddingHeight,
 | 
					 | 
				
			||||||
	// 		webPreferences: {
 | 
					 | 
				
			||||||
	// 			plugins: true,
 | 
					 | 
				
			||||||
	// 			partition: 'persist:webviewsession'
 | 
					 | 
				
			||||||
	// 		}
 | 
					 | 
				
			||||||
	// 	});
 | 
					 | 
				
			||||||
	// 	pdfWindow.loadURL(url);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 	// We don't want to have the menu bar in pdf window
 | 
					 | 
				
			||||||
	// 	pdfWindow.setMenu(null);
 | 
					 | 
				
			||||||
	// });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Reload full app not just webview, useful in debugging
 | 
						// Reload full app not just webview, useful in debugging
 | 
				
			||||||
	ipcMain.on('reload-full-app', () => {
 | 
						ipcMain.on('reload-full-app', () => {
 | 
				
			||||||
		mainWindow.reload();
 | 
							mainWindow.reload();
 | 
				
			||||||
@@ -353,7 +302,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}`);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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({
 | 
				
			||||||
@@ -36,7 +38,7 @@ export async function linuxUpdateNotification(session: Electron.session): Promis
 | 
				
			|||||||
				LinuxUpdateUtil.setUpdateItem(latestVersion, true);
 | 
									LinuxUpdateUtil.setUpdateItem(latestVersion, true);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error: unknown) {
 | 
				
			||||||
		logger.error('Linux update error.');
 | 
							logger.error('Linux update error.');
 | 
				
			||||||
		logger.error(error);
 | 
							logger.error(error);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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'
 | 
				
			||||||
@@ -306,7 +308,7 @@ function getDarwinTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
 | 
				
			|||||||
			enabled: enableMenu,
 | 
								enabled: enableMenu,
 | 
				
			||||||
			click(_item, focusedWindow) {
 | 
								click(_item, focusedWindow) {
 | 
				
			||||||
				if (focusedWindow) {
 | 
									if (focusedWindow) {
 | 
				
			||||||
					sendAction('shortcut');
 | 
										sendAction('show-keyboard-shortcuts');
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}, {
 | 
							}, {
 | 
				
			||||||
@@ -448,7 +450,7 @@ function getOtherTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
 | 
				
			|||||||
			enabled: enableMenu,
 | 
								enabled: enableMenu,
 | 
				
			||||||
			click(_item, focusedWindow) {
 | 
								click(_item, focusedWindow) {
 | 
				
			||||||
				if (focusedWindow) {
 | 
									if (focusedWindow) {
 | 
				
			||||||
					sendAction('shortcut');
 | 
										sendAction('show-keyboard-shortcuts');
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}, {
 | 
							}, {
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -86,7 +87,7 @@ export const _saveServerIcon = async (url: string, session: Electron.session): P
 | 
				
			|||||||
		const filePath = generateFilePath(url);
 | 
							const filePath = generateFilePath(url);
 | 
				
			||||||
		await pipeline(response, fs.createWriteStream(filePath));
 | 
							await pipeline(response, fs.createWriteStream(filePath));
 | 
				
			||||||
		return filePath;
 | 
							return filePath;
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error: unknown) {
 | 
				
			||||||
		logger.log('Could not get server icon.');
 | 
							logger.log('Could not get server icon.');
 | 
				
			||||||
		logger.log(error);
 | 
							logger.log(error);
 | 
				
			||||||
		logger.reportSentry(error);
 | 
							logger.reportSentry(error);
 | 
				
			||||||
@@ -104,7 +105,7 @@ export const _isOnline = async (url: string, session: Electron.session): Promise
 | 
				
			|||||||
		}));
 | 
							}));
 | 
				
			||||||
		const isValidResponse = response.statusCode >= 200 && response.statusCode < 400;
 | 
							const isValidResponse = response.statusCode >= 200 && response.statusCode < 400;
 | 
				
			||||||
		return isValidResponse;
 | 
							return isValidResponse;
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error: unknown) {
 | 
				
			||||||
		logger.log(error);
 | 
							logger.log(error);
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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> => {
 | 
				
			||||||
@@ -18,11 +19,7 @@ export const setAutoLaunch = async (AutoLaunchValue: boolean): Promise<void> =>
 | 
				
			|||||||
			name: 'Zulip',
 | 
								name: 'Zulip',
 | 
				
			||||||
			isHidden: false
 | 
								isHidden: false
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		if (autoLaunchOption) {
 | 
							await (autoLaunchOption ? ZulipAutoLauncher.enable() : ZulipAutoLauncher.disable());
 | 
				
			||||||
			await ZulipAutoLauncher.enable();
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			await ZulipAutoLauncher.disable();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		app.setLoginItemSettings({
 | 
							app.setLoginItemSettings({
 | 
				
			||||||
			openAtLogin: autoLaunchOption,
 | 
								openAtLogin: autoLaunchOption,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,8 +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>
 | 
					 | 
				
			||||||
    </body>
 | 
					    </body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -622,10 +622,6 @@ input.toggle-round:checked + label::after {
 | 
				
			|||||||
    max-width: 100%;
 | 
					    max-width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#add-certificate-button {
 | 
					 | 
				
			||||||
    margin: 10px 10px 10px 37px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.tip {
 | 
					.tip {
 | 
				
			||||||
    background: none;
 | 
					    background: none;
 | 
				
			||||||
    padding: 0;
 | 
					    padding: 0;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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="server-tab-badge close-button">
 | 
								<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
 | 
				
			||||||
						<i class="material-icons">close</i>
 | 
									<div class="server-tab-badge close-button">
 | 
				
			||||||
					</div>
 | 
										<i class="material-icons">close</i>
 | 
				
			||||||
					<div class="server-tab">
 | 
									</div>
 | 
				
			||||||
						<i class="material-icons">${this.props.materialIcon}</i>
 | 
									<div class="server-tab">
 | 
				
			||||||
					</div>
 | 
										<i class="material-icons">${this.props.materialIcon}</i>
 | 
				
			||||||
				</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];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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="server-tooltip" style="display:none">${this.props.name}</div>
 | 
								<div class="tab" data-tab-id="${this.props.tabIndex}">
 | 
				
			||||||
					<div class="server-tab-badge"></div>
 | 
									<div class="server-tooltip" style="display:none">${this.props.name}</div>
 | 
				
			||||||
					<div class="server-tab">
 | 
									<div class="server-tab-badge"></div>
 | 
				
			||||||
					<img class="server-icons" src='${this.props.icon}'/>
 | 
									<div class="server-tab">
 | 
				
			||||||
					</div>
 | 
										<img class="server-icons" src="${this.props.icon}"/>
 | 
				
			||||||
					<div class="server-tab-shortcut">${this.generateShortcutText()}</div>
 | 
									</div>
 | 
				
			||||||
				</div>`;
 | 
									<div class="server-tab-shortcut">${this.generateShortcutText()}</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');
 | 
				
			||||||
@@ -49,11 +54,7 @@ export default class ServerTab extends Tab {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		let shortcutText = '';
 | 
							let shortcutText = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (SystemUtil.getOS() === 'Mac') {
 | 
							shortcutText = SystemUtil.getOS() === 'Mac' ? `⌘ ${shownIndex}` : `Ctrl+${shownIndex}`;
 | 
				
			||||||
			shortcutText = `⌘ ${shownIndex}`;
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			shortcutText = `Ctrl+${shownIndex}`;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Array index == Shown index - 1
 | 
							// Array index == Shown index - 1
 | 
				
			||||||
		ipcRenderer.send('switch-server-tab', shownIndex - 1);
 | 
							ipcRenderer.send('switch-server-tab', shownIndex - 1);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,25 +52,26 @@ 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`
 | 
				
			||||||
					class="disabled"
 | 
								<webview
 | 
				
			||||||
					data-tab-id="${this.props.tabIndex}"
 | 
									class="disabled"
 | 
				
			||||||
					src="${this.props.url}"
 | 
									data-tab-id="${this.props.tabIndex}"
 | 
				
			||||||
					${this.props.nodeIntegration ? 'nodeIntegration' : ''}
 | 
									src="${this.props.url}"
 | 
				
			||||||
					${this.props.preload ? 'preload="js/preload.js"' : ''}
 | 
									` + (this.props.nodeIntegration ? 'nodeIntegration' : '') + htmlEscape`
 | 
				
			||||||
					partition="persist:webviewsession"
 | 
									` + (this.props.preload ? 'preload="js/preload.js"' : '') + htmlEscape`
 | 
				
			||||||
					name="${this.props.name}"
 | 
									partition="persist:webviewsession"
 | 
				
			||||||
					webpreferences="
 | 
									name="${this.props.name}"
 | 
				
			||||||
						${this.props.nodeIntegration ? '' : 'contextIsolation,'}
 | 
									webpreferences="
 | 
				
			||||||
						${ConfigUtil.getConfigItem('enableSpellchecker') ? 'spellcheck,' : ''}
 | 
										contextIsolation=${!this.props.nodeIntegration},
 | 
				
			||||||
						javascript
 | 
										spellcheck=${Boolean(ConfigUtil.getConfigItem('enableSpellchecker'))}
 | 
				
			||||||
					">
 | 
									">
 | 
				
			||||||
				</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);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
@@ -261,8 +265,8 @@ export default class WebView extends BaseComponent {
 | 
				
			|||||||
		ipcRenderer.sendTo(this.$el.getWebContentsId(), 'logout');
 | 
							ipcRenderer.sendTo(this.$el.getWebContentsId(), 'logout');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	showShortcut(): void {
 | 
						showKeyboardShortcuts(): void {
 | 
				
			||||||
		ipcRenderer.sendTo(this.$el.getWebContentsId(), 'shortcut');
 | 
							ipcRenderer.sendTo(this.$el.getWebContentsId(), 'show-keyboard-shortcuts');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	openDevTools(): void {
 | 
						openDevTools(): void {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,69 +1,61 @@
 | 
				
			|||||||
import {ipcRenderer} from 'electron';
 | 
					import {ipcRenderer} from 'electron';
 | 
				
			||||||
 | 
					 | 
				
			||||||
import {EventEmitter} from 'events';
 | 
					import {EventEmitter} from 'events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import isDev from 'electron-is-dev';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {ClipboardDecrypterImpl} from './clipboard-decrypter';
 | 
					import {ClipboardDecrypterImpl} from './clipboard-decrypter';
 | 
				
			||||||
import {NotificationData, newNotification} from './notification';
 | 
					import {NotificationData, newNotification} from './notification';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ListenerType = ((...args: any[]) => void);
 | 
					type ListenerType = ((...args: any[]) => void);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ElectronBridgeImpl extends EventEmitter implements ElectronBridge {
 | 
					let notificationReplySupported = false;
 | 
				
			||||||
	send_notification_reply_message_supported: boolean;
 | 
					// Indicates if the user is idle or not
 | 
				
			||||||
	idle_on_system: boolean;
 | 
					let idle = false;
 | 
				
			||||||
	last_active_on_system: number;
 | 
					// Indicates the time at which user was last active
 | 
				
			||||||
 | 
					let lastActive = Date.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	constructor() {
 | 
					export const bridgeEvents = new EventEmitter();
 | 
				
			||||||
		super();
 | 
					 | 
				
			||||||
		this.send_notification_reply_message_supported = false;
 | 
					 | 
				
			||||||
		// Indicates if the user is idle or not
 | 
					 | 
				
			||||||
		this.idle_on_system = false;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Indicates the time at which user was last active
 | 
					const electron_bridge: ElectronBridge = {
 | 
				
			||||||
		this.last_active_on_system = Date.now();
 | 
						send_event: (eventName: string | symbol, ...args: unknown[]): boolean =>
 | 
				
			||||||
	}
 | 
							bridgeEvents.emit(eventName, ...args),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	send_event = (eventName: string | symbol, ...args: unknown[]): void => {
 | 
						on_event: (eventName: string, listener: ListenerType): void => {
 | 
				
			||||||
		this.emit(eventName, ...args);
 | 
							bridgeEvents.on(eventName, listener);
 | 
				
			||||||
	};
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	on_event = (eventName: string, listener: ListenerType): void => {
 | 
						new_notification: (
 | 
				
			||||||
		this.on(eventName, listener);
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	new_notification = (
 | 
					 | 
				
			||||||
		title: string,
 | 
							title: string,
 | 
				
			||||||
		options: NotificationOptions | undefined,
 | 
							options: NotificationOptions | undefined,
 | 
				
			||||||
		dispatch: (type: string, eventInit: EventInit) => boolean
 | 
							dispatch: (type: string, eventInit: EventInit) => boolean
 | 
				
			||||||
	): NotificationData =>
 | 
						): NotificationData =>
 | 
				
			||||||
		newNotification(title, options, dispatch);
 | 
							newNotification(title, options, dispatch),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	get_idle_on_system = (): boolean => this.idle_on_system;
 | 
						get_idle_on_system: (): boolean => idle,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	get_last_active_on_system = (): number => this.last_active_on_system;
 | 
						get_last_active_on_system: (): number => lastActive,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	get_send_notification_reply_message_supported = (): boolean =>
 | 
						get_send_notification_reply_message_supported: (): boolean =>
 | 
				
			||||||
		this.send_notification_reply_message_supported;
 | 
							notificationReplySupported,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	set_send_notification_reply_message_supported = (value: boolean): void => {
 | 
						set_send_notification_reply_message_supported: (value: boolean): void => {
 | 
				
			||||||
		this.send_notification_reply_message_supported = value;
 | 
							notificationReplySupported = value;
 | 
				
			||||||
	};
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	decrypt_clipboard = (version: number): ClipboardDecrypterImpl =>
 | 
						decrypt_clipboard: (version: number): ClipboardDecrypterImpl =>
 | 
				
			||||||
		new ClipboardDecrypterImpl(version);
 | 
							new ClipboardDecrypterImpl(version)
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const electron_bridge = new ElectronBridgeImpl();
 | 
					bridgeEvents.on('total_unread_count', (...args) => {
 | 
				
			||||||
 | 
					 | 
				
			||||||
electron_bridge.on('total_unread_count', (...args) => {
 | 
					 | 
				
			||||||
	ipcRenderer.send('unread-count', ...args);
 | 
						ipcRenderer.send('unread-count', ...args);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
electron_bridge.on('realm_name', realmName => {
 | 
					bridgeEvents.on('realm_name', realmName => {
 | 
				
			||||||
	const serverURL = location.origin;
 | 
						const serverURL = location.origin;
 | 
				
			||||||
	ipcRenderer.send('realm-name-changed', serverURL, realmName);
 | 
						ipcRenderer.send('realm-name-changed', serverURL, realmName);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
electron_bridge.on('realm_icon_url', (iconURL: unknown) => {
 | 
					bridgeEvents.on('realm_icon_url', (iconURL: unknown) => {
 | 
				
			||||||
	if (typeof iconURL !== 'string') {
 | 
						if (typeof iconURL !== 'string') {
 | 
				
			||||||
		throw new TypeError('Expected string for iconURL');
 | 
							throw new TypeError('Expected string for iconURL');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -73,6 +65,25 @@ electron_bridge.on('realm_icon_url', (iconURL: unknown) => {
 | 
				
			|||||||
	ipcRenderer.send('realm-icon-changed', serverURL, iconURL);
 | 
						ipcRenderer.send('realm-icon-changed', serverURL, iconURL);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Set user as active and update the time of last activity
 | 
				
			||||||
 | 
					ipcRenderer.on('set-active', () => {
 | 
				
			||||||
 | 
						if (isDev) {
 | 
				
			||||||
 | 
							console.log('active');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						idle = false;
 | 
				
			||||||
 | 
						lastActive = Date.now();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Set user as idle and time of last activity is left unchanged
 | 
				
			||||||
 | 
					ipcRenderer.on('set-idle', () => {
 | 
				
			||||||
 | 
						if (isDev) {
 | 
				
			||||||
 | 
							console.log('idle');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						idle = true;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// This follows node's idiomatic implementation of event
 | 
					// This follows node's idiomatic implementation of event
 | 
				
			||||||
// emitters to make event handling more simpler instead of using
 | 
					// emitters to make event handling more simpler instead of using
 | 
				
			||||||
// functions zulip side will emit event using ElectronBrigde.send_event
 | 
					// functions zulip side will emit event using ElectronBrigde.send_event
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,13 +9,7 @@ interface CompatElectronBridge extends ElectronBridge {
 | 
				
			|||||||
(() => {
 | 
					(() => {
 | 
				
			||||||
	const zulipWindow = window as typeof window & {
 | 
						const zulipWindow = window as typeof window & {
 | 
				
			||||||
		electron_bridge: CompatElectronBridge;
 | 
							electron_bridge: CompatElectronBridge;
 | 
				
			||||||
		narrow: {
 | 
							page_params?: unknown;
 | 
				
			||||||
			by_subject?: (target_id: number, opts: {trigger?: string}) => void;
 | 
					 | 
				
			||||||
			by_topic?: (target_id: number, opts: {trigger?: string}) => void;
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
		page_params?: {
 | 
					 | 
				
			||||||
			default_language?: string;
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
		raw_electron_bridge: ElectronBridge;
 | 
							raw_electron_bridge: ElectronBridge;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -41,27 +35,6 @@ interface CompatElectronBridge extends ElectronBridge {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	zulipWindow.electron_bridge = electron_bridge;
 | 
						zulipWindow.electron_bridge = electron_bridge;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	(async () => {
 | 
					 | 
				
			||||||
		if (document.readyState === 'loading') {
 | 
					 | 
				
			||||||
			await new Promise(resolve => {
 | 
					 | 
				
			||||||
				document.addEventListener('DOMContentLoaded', () => {
 | 
					 | 
				
			||||||
					resolve();
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const {page_params} = zulipWindow;
 | 
					 | 
				
			||||||
		if (page_params) {
 | 
					 | 
				
			||||||
			electron_bridge.send_event('zulip-loaded');
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	electron_bridge.on_event('narrow-by-topic', (id: number) => {
 | 
					 | 
				
			||||||
		const {narrow} = zulipWindow;
 | 
					 | 
				
			||||||
		const narrowByTopic = narrow.by_topic || narrow.by_subject;
 | 
					 | 
				
			||||||
		narrowByTopic(id, {trigger: 'notification'});
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	function attributeListener<T extends EventTarget>(type: string): PropertyDescriptor {
 | 
						function attributeListener<T extends EventTarget>(type: string): PropertyDescriptor {
 | 
				
			||||||
		const symbol = Symbol('on' + type);
 | 
							const symbol = Symbol('on' + type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
@@ -154,19 +161,15 @@ class ServerManagerView {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
 | 
							const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
 | 
				
			||||||
		if (proxyEnabled) {
 | 
							await session.fromPartition('persist:webviewsession').setProxy(proxyEnabled ? {
 | 
				
			||||||
			await session.fromPartition('persist:webviewsession').setProxy({
 | 
								pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
 | 
				
			||||||
				pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
 | 
								proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
 | 
				
			||||||
				proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
 | 
								proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
 | 
				
			||||||
				proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
 | 
							} : {
 | 
				
			||||||
			});
 | 
								pacScript: '',
 | 
				
			||||||
		} else {
 | 
								proxyRules: '',
 | 
				
			||||||
			await session.fromPartition('persist:webviewsession').setProxy({
 | 
								proxyBypassRules: ''
 | 
				
			||||||
				pacScript: '',
 | 
							});
 | 
				
			||||||
				proxyRules: '',
 | 
					 | 
				
			||||||
				proxyBypassRules: ''
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Settings are initialized only when user clicks on General/Server/Network section settings
 | 
						// Settings are initialized only when user clicks on General/Server/Network section settings
 | 
				
			||||||
@@ -248,7 +251,7 @@ class ServerManagerView {
 | 
				
			|||||||
			const serverConf = await DomainUtil.checkDomain(domain);
 | 
								const serverConf = await DomainUtil.checkDomain(domain);
 | 
				
			||||||
			await DomainUtil.addDomain(serverConf);
 | 
								await DomainUtil.addDomain(serverConf);
 | 
				
			||||||
			return true;
 | 
								return true;
 | 
				
			||||||
		} catch (error) {
 | 
							} catch (error: unknown) {
 | 
				
			||||||
			logger.error(error);
 | 
								logger.error(error);
 | 
				
			||||||
			logger.error(`Could not add ${domain}. Please contact your system administrator.`);
 | 
								logger.error(`Could not add ${domain}. Please contact your system administrator.`);
 | 
				
			||||||
			return false;
 | 
								return false;
 | 
				
			||||||
@@ -349,7 +352,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 +365,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 +457,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 +493,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 +593,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 +678,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> {
 | 
				
			||||||
@@ -801,7 +798,7 @@ class ServerManagerView {
 | 
				
			|||||||
			['zoomOut', webview => webview.zoomOut()],
 | 
								['zoomOut', webview => webview.zoomOut()],
 | 
				
			||||||
			['zoomActualSize', webview => webview.zoomActualSize()],
 | 
								['zoomActualSize', webview => webview.zoomActualSize()],
 | 
				
			||||||
			['log-out', webview => webview.logOut()],
 | 
								['log-out', webview => webview.logOut()],
 | 
				
			||||||
			['shortcut', webview => webview.showShortcut()],
 | 
								['show-keyboard-shortcuts', webview => webview.showKeyboardShortcuts()],
 | 
				
			||||||
			['tab-devtools', webview => webview.openDevTools()]
 | 
								['tab-devtools', webview => webview.openDevTools()]
 | 
				
			||||||
		];
 | 
							];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -926,11 +923,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 +968,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();
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,111 +0,0 @@
 | 
				
			|||||||
import {ipcRenderer} from 'electron';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
	appId, customReply, focusCurrentServer, parseReply
 | 
					 | 
				
			||||||
} 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 ClickHandler = () => void;
 | 
					 | 
				
			||||||
let replyHandler: ReplyHandler;
 | 
					 | 
				
			||||||
let clickHandler: ClickHandler;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface NotificationHandlerArgs {
 | 
					 | 
				
			||||||
	response: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class DarwinNotification {
 | 
					 | 
				
			||||||
	tag: number;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	constructor(title: string, options: NotificationOptions) {
 | 
					 | 
				
			||||||
		const silent: boolean = ConfigUtil.getConfigItem('silent') || false;
 | 
					 | 
				
			||||||
		const {icon} = options;
 | 
					 | 
				
			||||||
		const profilePic = new URL(icon, location.href).href;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		this.tag = Number.parseInt(options.tag, 10);
 | 
					 | 
				
			||||||
		const notification = new MacNotifier(title, Object.assign(options, {
 | 
					 | 
				
			||||||
			bundleId: appId,
 | 
					 | 
				
			||||||
			canReply: true,
 | 
					 | 
				
			||||||
			silent,
 | 
					 | 
				
			||||||
			icon: profilePic
 | 
					 | 
				
			||||||
		}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		notification.addEventListener('click', () => {
 | 
					 | 
				
			||||||
			// Focus to the server who sent the
 | 
					 | 
				
			||||||
			// notification if not focused already
 | 
					 | 
				
			||||||
			if (clickHandler) {
 | 
					 | 
				
			||||||
				clickHandler();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			focusCurrentServer();
 | 
					 | 
				
			||||||
			ipcRenderer.send('focus-app');
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		notification.addEventListener('reply', this.notificationHandler);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	static requestPermission(): void {
 | 
					 | 
				
			||||||
		// Do nothing
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Override default Notification permission
 | 
					 | 
				
			||||||
	static get permission(): NotificationPermission {
 | 
					 | 
				
			||||||
		return ConfigUtil.getConfigItem('showNotification') ? 'granted' : 'denied';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	get onreply(): ReplyHandler {
 | 
					 | 
				
			||||||
		return replyHandler;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	set onreply(handler: ReplyHandler) {
 | 
					 | 
				
			||||||
		replyHandler = handler;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	get onclick(): ClickHandler {
 | 
					 | 
				
			||||||
		return clickHandler;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	set onclick(handler: ClickHandler) {
 | 
					 | 
				
			||||||
		clickHandler = handler;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Not something that is common or
 | 
					 | 
				
			||||||
	// used by zulip server but added to be
 | 
					 | 
				
			||||||
	// future proff.
 | 
					 | 
				
			||||||
	addEventListener(event: string, handler: ClickHandler | ReplyHandler): void {
 | 
					 | 
				
			||||||
		if (event === 'click') {
 | 
					 | 
				
			||||||
			clickHandler = handler as ClickHandler;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (event === 'reply') {
 | 
					 | 
				
			||||||
			replyHandler = handler as ReplyHandler;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async notificationHandler({response}: NotificationHandlerArgs): Promise<void> {
 | 
					 | 
				
			||||||
		response = await parseReply(response);
 | 
					 | 
				
			||||||
		focusCurrentServer();
 | 
					 | 
				
			||||||
		if (electron_bridge.send_notification_reply_message_supported) {
 | 
					 | 
				
			||||||
			electron_bridge.send_event('send_notification_reply_message', this.tag, response);
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		electron_bridge.emit('narrow-by-topic', this.tag);
 | 
					 | 
				
			||||||
		if (replyHandler) {
 | 
					 | 
				
			||||||
			replyHandler(response);
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		customReply(response);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Method specific to notification api
 | 
					 | 
				
			||||||
	// used by zulip
 | 
					 | 
				
			||||||
	close(): void {
 | 
					 | 
				
			||||||
		// Do nothing
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module.exports = DarwinNotification;
 | 
					 | 
				
			||||||
@@ -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) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,69 +1,8 @@
 | 
				
			|||||||
import {remote} from 'electron';
 | 
					import {remote} from 'electron';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Logger from '../utils/logger-util';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const logger = new Logger({
 | 
					 | 
				
			||||||
	file: 'errors.log',
 | 
					 | 
				
			||||||
	timestamp: true
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Do not change this
 | 
					// Do not change this
 | 
				
			||||||
export const appId = 'org.zulip.zulip-electron';
 | 
					export const appId = 'org.zulip.zulip-electron';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type BotListItem = [string, string];
 | 
					 | 
				
			||||||
const botsList: BotListItem[] = [];
 | 
					 | 
				
			||||||
let botsListLoaded = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// This function load list of bots from the server
 | 
					 | 
				
			||||||
// in case botsList isn't already completely loaded when required in parseRely
 | 
					 | 
				
			||||||
export async function loadBots(): Promise<void> {
 | 
					 | 
				
			||||||
	botsList.length = 0;
 | 
					 | 
				
			||||||
	const response = await fetch('/json/users');
 | 
					 | 
				
			||||||
	if (response.ok) {
 | 
					 | 
				
			||||||
		const {members} = await response.json();
 | 
					 | 
				
			||||||
		members.forEach(({is_bot, full_name}: {[key: string]: unknown}) => {
 | 
					 | 
				
			||||||
			if (is_bot && typeof full_name === 'string') {
 | 
					 | 
				
			||||||
				const bot = `@${full_name}`;
 | 
					 | 
				
			||||||
				const mention = `@**${bot.replace(/^@/, '')}**`;
 | 
					 | 
				
			||||||
				botsList.push([bot, mention]);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
		botsListLoaded = true;
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		logger.log('Load bots request failed: ', await response.text());
 | 
					 | 
				
			||||||
		logger.log('Load bots request status: ', response.status);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function checkElements(...elements: unknown[]): boolean {
 | 
					 | 
				
			||||||
	let status = true;
 | 
					 | 
				
			||||||
	elements.forEach(element => {
 | 
					 | 
				
			||||||
		if (element === null || element === undefined) {
 | 
					 | 
				
			||||||
			status = false;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
	return status;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function customReply(reply: string): void {
 | 
					 | 
				
			||||||
	// Server does not support notification reply yet.
 | 
					 | 
				
			||||||
	const buttonSelector = '.messagebox #send_controls button[type=submit]';
 | 
					 | 
				
			||||||
	const messageboxSelector = '.selected_message .messagebox .messagebox-border .messagebox-content';
 | 
					 | 
				
			||||||
	const textarea: HTMLInputElement = document.querySelector('#compose-textarea');
 | 
					 | 
				
			||||||
	const messagebox: HTMLButtonElement = document.querySelector(messageboxSelector);
 | 
					 | 
				
			||||||
	const sendButton: HTMLButtonElement = document.querySelector(buttonSelector);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Sanity check for old server versions
 | 
					 | 
				
			||||||
	const elementsExists = checkElements(textarea, messagebox, sendButton);
 | 
					 | 
				
			||||||
	if (!elementsExists) {
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	textarea.value = reply;
 | 
					 | 
				
			||||||
	messagebox.click();
 | 
					 | 
				
			||||||
	sendButton.click();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const currentWindow = remote.getCurrentWindow();
 | 
					const currentWindow = remote.getCurrentWindow();
 | 
				
			||||||
const webContents = remote.getCurrentWebContents();
 | 
					const webContents = remote.getCurrentWebContents();
 | 
				
			||||||
const webContentsId = webContents.id;
 | 
					const webContentsId = webContents.id;
 | 
				
			||||||
@@ -73,65 +12,3 @@ const webContentsId = webContents.id;
 | 
				
			|||||||
export function focusCurrentServer(): void {
 | 
					export function focusCurrentServer(): void {
 | 
				
			||||||
	currentWindow.webContents.send('focus-webview-with-id', webContentsId);
 | 
						currentWindow.webContents.send('focus-webview-with-id', webContentsId);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
// This function parses the reply from to notification
 | 
					 | 
				
			||||||
// making it easier to reply from notification eg
 | 
					 | 
				
			||||||
// @username in reply will be converted to @**username**
 | 
					 | 
				
			||||||
// #stream in reply will be converted to #**stream**
 | 
					 | 
				
			||||||
// bot mentions are not yet supported
 | 
					 | 
				
			||||||
export async function parseReply(reply: string): Promise<string> {
 | 
					 | 
				
			||||||
	const usersDiv = document.querySelectorAll('#user_presences li');
 | 
					 | 
				
			||||||
	const streamHolder = document.querySelectorAll('#stream_filters li');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	type UsersItem = BotListItem;
 | 
					 | 
				
			||||||
	type StreamsItem = BotListItem;
 | 
					 | 
				
			||||||
	const users: UsersItem[] = [];
 | 
					 | 
				
			||||||
	const streams: StreamsItem[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	usersDiv.forEach(userRow => {
 | 
					 | 
				
			||||||
		const anchor = userRow.querySelector('span a');
 | 
					 | 
				
			||||||
		if (anchor !== null) {
 | 
					 | 
				
			||||||
			const user = `@${anchor.textContent.trim()}`;
 | 
					 | 
				
			||||||
			const mention = `@**${user.replace(/^@/, '')}**`;
 | 
					 | 
				
			||||||
			users.push([user, mention]);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	streamHolder.forEach(stream => {
 | 
					 | 
				
			||||||
		const streamAnchor = stream.querySelector('div a');
 | 
					 | 
				
			||||||
		if (streamAnchor !== null) {
 | 
					 | 
				
			||||||
			const streamName = `#${streamAnchor.textContent.trim()}`;
 | 
					 | 
				
			||||||
			const streamMention = `#**${streamName.replace(/^#/, '')}**`;
 | 
					 | 
				
			||||||
			streams.push([streamName, streamMention]);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	users.forEach(([user, mention]) => {
 | 
					 | 
				
			||||||
		if (reply.includes(user)) {
 | 
					 | 
				
			||||||
			const regex = new RegExp(user, 'g');
 | 
					 | 
				
			||||||
			reply = reply.replace(regex, mention);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	streams.forEach(([stream, streamMention]) => {
 | 
					 | 
				
			||||||
		const regex = new RegExp(stream, 'g');
 | 
					 | 
				
			||||||
		reply = reply.replace(regex, streamMention);
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// If botsList isn't completely loaded yet, make a request for list
 | 
					 | 
				
			||||||
	if (!botsListLoaded) {
 | 
					 | 
				
			||||||
		await loadBots();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Iterate for every bot name and replace in reply
 | 
					 | 
				
			||||||
	// @botname with @**botname**
 | 
					 | 
				
			||||||
	botsList.forEach(([bot, mention]) => {
 | 
					 | 
				
			||||||
		if (reply.includes(bot)) {
 | 
					 | 
				
			||||||
			const regex = new RegExp(bot, 'g');
 | 
					 | 
				
			||||||
			reply = reply.replace(regex, mention);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	reply = reply.replace(/\\n/, '\n');
 | 
					 | 
				
			||||||
	return reply;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,14 @@
 | 
				
			|||||||
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
 | 
				
			||||||
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
 | 
					// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
 | 
				
			||||||
app.setAppUserModelId(appId);
 | 
					app.setAppUserModelId(appId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let Notification = DefaultNotification;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if (process.platform === 'darwin') {
 | 
					 | 
				
			||||||
	Notification = require('./darwin-notifications');
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface NotificationData {
 | 
					export interface NotificationData {
 | 
				
			||||||
	close: () => void;
 | 
						close: () => void;
 | 
				
			||||||
	title: string;
 | 
						title: string;
 | 
				
			||||||
@@ -39,7 +33,7 @@ export function newNotification(
 | 
				
			|||||||
	options: NotificationOptions | undefined,
 | 
						options: NotificationOptions | undefined,
 | 
				
			||||||
	dispatch: (type: string, eventInit: EventInit) => boolean
 | 
						dispatch: (type: string, eventInit: EventInit) => boolean
 | 
				
			||||||
): NotificationData {
 | 
					): NotificationData {
 | 
				
			||||||
	const notification = new Notification(title, options);
 | 
						const notification = new DefaultNotification(title, options);
 | 
				
			||||||
	for (const type of ['click', 'close', 'error', 'show']) {
 | 
						for (const type of ['click', 'close', 'error', 'show']) {
 | 
				
			||||||
		notification.addEventListener(type, (ev: Event) => {
 | 
							notification.addEventListener(type, (ev: Event) => {
 | 
				
			||||||
			if (!dispatch(type, ev)) {
 | 
								if (!dispatch(type, ev)) {
 | 
				
			||||||
@@ -67,7 +61,3 @@ export function newNotification(
 | 
				
			|||||||
		actions: notification.actions
 | 
							actions: notification.actions
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
electron_bridge.once('zulip-loaded', async () => {
 | 
					 | 
				
			||||||
	await loadBots();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,99 +0,0 @@
 | 
				
			|||||||
'use-strict';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import {remote, OpenDialogOptions} from 'electron';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import path from 'path';
 | 
					 | 
				
			||||||
import BaseComponent from '../../components/base';
 | 
					 | 
				
			||||||
import * as CertificateUtil from '../../utils/certificate-util';
 | 
					 | 
				
			||||||
import * as DomainUtil from '../../utils/domain-util';
 | 
					 | 
				
			||||||
import * as t from '../../utils/translation-util';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface AddCertificateProps {
 | 
					 | 
				
			||||||
	$root: Element;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const {dialog} = remote;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default class AddCertificate extends BaseComponent {
 | 
					 | 
				
			||||||
	props: AddCertificateProps;
 | 
					 | 
				
			||||||
	_certFile: string;
 | 
					 | 
				
			||||||
	$addCertificate: Element | null;
 | 
					 | 
				
			||||||
	addCertificateButton: Element | null;
 | 
					 | 
				
			||||||
	serverUrl: HTMLInputElement | null;
 | 
					 | 
				
			||||||
	constructor(props: AddCertificateProps) {
 | 
					 | 
				
			||||||
		super();
 | 
					 | 
				
			||||||
		this.props = props;
 | 
					 | 
				
			||||||
		this._certFile = '';
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	template(): string {
 | 
					 | 
				
			||||||
		return `
 | 
					 | 
				
			||||||
			<div class="settings-card certificates-card">
 | 
					 | 
				
			||||||
				<div class="certificate-input">
 | 
					 | 
				
			||||||
					<div>${t.__('Organization URL')}</div>
 | 
					 | 
				
			||||||
					<input class="setting-input-value" autofocus placeholder="your-organization.zulipchat.com or zulip.your-organization.com"/>
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
				<div class="certificate-input">
 | 
					 | 
				
			||||||
					<div>${t.__('Certificate file')}</div>
 | 
					 | 
				
			||||||
					<button class="green" id="add-certificate-button">${t.__('Upload')}</button>
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
			</div>
 | 
					 | 
				
			||||||
		`;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	init(): void {
 | 
					 | 
				
			||||||
		this.$addCertificate = this.generateNodeFromTemplate(this.template());
 | 
					 | 
				
			||||||
		this.props.$root.append(this.$addCertificate);
 | 
					 | 
				
			||||||
		this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button');
 | 
					 | 
				
			||||||
		this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
 | 
					 | 
				
			||||||
		this.initListeners();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async validateAndAdd(): Promise<void> {
 | 
					 | 
				
			||||||
		const certificate = this._certFile;
 | 
					 | 
				
			||||||
		const serverUrl = this.serverUrl.value;
 | 
					 | 
				
			||||||
		if (certificate !== '' && serverUrl !== '') {
 | 
					 | 
				
			||||||
			const server = encodeURIComponent(DomainUtil.formatUrl(serverUrl));
 | 
					 | 
				
			||||||
			const fileName = path.basename(certificate);
 | 
					 | 
				
			||||||
			const copy = CertificateUtil.copyCertificate(server, certificate, fileName);
 | 
					 | 
				
			||||||
			if (!copy) {
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			CertificateUtil.setCertificate(server, fileName);
 | 
					 | 
				
			||||||
			this.serverUrl.value = '';
 | 
					 | 
				
			||||||
			await dialog.showMessageBox({
 | 
					 | 
				
			||||||
				title: 'Success',
 | 
					 | 
				
			||||||
				message: 'Certificate saved!'
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			dialog.showErrorBox('Error', `Please, ${serverUrl === '' ?
 | 
					 | 
				
			||||||
				'Enter an Organization URL' : 'Choose certificate file'}`);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async addHandler(): Promise<void> {
 | 
					 | 
				
			||||||
		const showDialogOptions: OpenDialogOptions = {
 | 
					 | 
				
			||||||
			title: 'Select file',
 | 
					 | 
				
			||||||
			properties: ['openFile'],
 | 
					 | 
				
			||||||
			filters: [{name: 'crt, pem', extensions: ['crt', 'pem']}]
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
		const {filePaths, canceled} = await dialog.showOpenDialog(showDialogOptions);
 | 
					 | 
				
			||||||
		if (!canceled) {
 | 
					 | 
				
			||||||
			this._certFile = filePaths[0] || '';
 | 
					 | 
				
			||||||
			await this.validateAndAdd();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	initListeners(): void {
 | 
					 | 
				
			||||||
		this.addCertificateButton.addEventListener('click', async () => {
 | 
					 | 
				
			||||||
			await this.addHandler();
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		this.serverUrl.addEventListener('keypress', async event => {
 | 
					 | 
				
			||||||
			if (event.key === 'Enter') {
 | 
					 | 
				
			||||||
				await this.addHandler();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -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,39 +25,39 @@ 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>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/* 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: Record<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 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,14 @@
 | 
				
			|||||||
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 BaseSection from './base-section';
 | 
				
			||||||
 | 
					import FindAccounts from './find-accounts';
 | 
				
			||||||
 | 
					import ServerInfoForm from './server-info-form';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ConnectedOrgSectionProps {
 | 
					interface ConnectedOrgSectionProps {
 | 
				
			||||||
	$root: Element;
 | 
						$root: Element;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -16,22 +18,19 @@ export default class ConnectedOrgSection extends BaseSection {
 | 
				
			|||||||
	$serverInfoContainer: Element | null;
 | 
						$serverInfoContainer: Element | null;
 | 
				
			||||||
	$existingServers: Element | null;
 | 
						$existingServers: Element | null;
 | 
				
			||||||
	$newOrgButton: HTMLButtonElement | null;
 | 
						$newOrgButton: HTMLButtonElement | null;
 | 
				
			||||||
	$addCertificateContainer: Element | null;
 | 
					 | 
				
			||||||
	$findAccountsContainer: Element | null;
 | 
						$findAccountsContainer: Element | null;
 | 
				
			||||||
	constructor(props: ConnectedOrgSectionProps) {
 | 
						constructor(props: ConnectedOrgSectionProps) {
 | 
				
			||||||
		super();
 | 
							super();
 | 
				
			||||||
		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>
 | 
				
			||||||
				<div id="server-info-container"></div>
 | 
									<div id="server-info-container"></div>
 | 
				
			||||||
				<div id="new-org-button"><button class="green sea w-250">${t.__('Connect to another organization')}</button></div>
 | 
									<div id="new-org-button"><button class="green sea w-250">${t.__('Connect to another organization')}</button></div>
 | 
				
			||||||
				<div class="page-title">${t.__('Add Custom Certificates')}</div>
 | 
					 | 
				
			||||||
				<div id="add-certificate-container"></div>
 | 
					 | 
				
			||||||
				<div class="page-title">${t.__('Find accounts by email')}</div>
 | 
									<div class="page-title">${t.__('Find accounts by email')}</div>
 | 
				
			||||||
				<div id="find-accounts-container"></div>
 | 
									<div id="find-accounts-container"></div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
@@ -43,20 +42,19 @@ 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');
 | 
				
			||||||
		this.$newOrgButton = document.querySelector('#new-org-button');
 | 
							this.$newOrgButton = document.querySelector('#new-org-button');
 | 
				
			||||||
		this.$addCertificateContainer = document.querySelector('#add-certificate-container');
 | 
					 | 
				
			||||||
		this.$findAccountsContainer = document.querySelector('#find-accounts-container');
 | 
							this.$findAccountsContainer = document.querySelector('#find-accounts-container');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		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({
 | 
				
			||||||
@@ -71,16 +69,9 @@ export default class ConnectedOrgSection extends BaseSection {
 | 
				
			|||||||
			ipcRenderer.send('forward-message', 'open-org-tab');
 | 
								ipcRenderer.send('forward-message', 'open-org-tab');
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.initAddCertificate();
 | 
					 | 
				
			||||||
		this.initFindAccounts();
 | 
							this.initFindAccounts();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	initAddCertificate(): void {
 | 
					 | 
				
			||||||
		new AddCertificate({
 | 
					 | 
				
			||||||
			$root: this.$addCertificateContainer
 | 
					 | 
				
			||||||
		}).init();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	initFindAccounts(): void {
 | 
						initFindAccounts(): void {
 | 
				
			||||||
		new FindAccounts({
 | 
							new FindAccounts({
 | 
				
			||||||
			$root: this.$findAccountsContainer
 | 
								$root: this.$findAccountsContainer
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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">
 | 
				
			||||||
@@ -146,7 +148,7 @@ export default class GeneralSection extends BaseSection {
 | 
				
			|||||||
				<div class="title">${t.__('Factory Reset Data')}</div>
 | 
									<div class="title">${t.__('Factory Reset Data')}</div>
 | 
				
			||||||
                <div class="settings-card">
 | 
					                <div class="settings-card">
 | 
				
			||||||
					<div class="setting-row" id="factory-reset-option">
 | 
										<div class="setting-row" id="factory-reset-option">
 | 
				
			||||||
						<div class="setting-description">${t.__('Reset the application, thus deleting all the connected organizations, accounts, and certificates.')}
 | 
											<div class="setting-description">${t.__('Reset the application, thus deleting all the connected organizations and accounts.')}
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
						<button class="factory-reset-button red w-150">${t.__('Factory Reset')}</button>
 | 
											<button class="factory-reset-button red w-150">${t.__('Factory Reset')}</button>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
@@ -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 => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,23 +58,24 @@ 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.trim());
 | 
				
			||||||
		} catch (error) {
 | 
							} catch (error: unknown) {
 | 
				
			||||||
			this.$saveServerButton.innerHTML = 'Connect';
 | 
								this.$saveServerButton.textContent = 'Connect';
 | 
				
			||||||
			await dialog.showMessageBox({
 | 
								await dialog.showMessageBox({
 | 
				
			||||||
				type: 'error',
 | 
									type: 'error',
 | 
				
			||||||
				message: error.toString(),
 | 
									message: error instanceof Error ?
 | 
				
			||||||
 | 
										`${error.name}: ${error.message}` : 'Unknown error',
 | 
				
			||||||
				buttons: ['OK']
 | 
									buttons: ['OK']
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,16 +17,16 @@ 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">
 | 
				
			||||||
					<div class="page-title">${t.__('Add a Zulip organization')}</div>
 | 
											<div class="page-title">${t.__('Add a Zulip organization')}</div>
 | 
				
			||||||
					<div id="new-server-container"></div>
 | 
											<div id="new-server-container"></div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
					 | 
				
			||||||
		`;
 | 
							`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -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();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +1,16 @@
 | 
				
			|||||||
import {contextBridge, ipcRenderer, webFrame} from 'electron';
 | 
					import {contextBridge, ipcRenderer, webFrame} from 'electron';
 | 
				
			||||||
import fs from 'fs';
 | 
					import fs from 'fs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import isDev from 'electron-is-dev';
 | 
					import electron_bridge, {bridgeEvents} from './electron-bridge';
 | 
				
			||||||
 | 
					 | 
				
			||||||
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
 | 
					 | 
				
			||||||
// eslint-disable-next-line import/no-unassigned-import
 | 
					 | 
				
			||||||
import './shared/preventdrag';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import electron_bridge from './electron-bridge';
 | 
					 | 
				
			||||||
contextBridge.exposeInMainWorld('raw_electron_bridge', electron_bridge);
 | 
					contextBridge.exposeInMainWorld('raw_electron_bridge', electron_bridge);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ipcRenderer.on('logout', () => {
 | 
					ipcRenderer.on('logout', () => {
 | 
				
			||||||
 | 
						if (bridgeEvents.emit('logout')) {
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create the menu for the below
 | 
						// Create the menu for the below
 | 
				
			||||||
	const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
 | 
						const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
 | 
				
			||||||
	dropdown.click();
 | 
						dropdown.click();
 | 
				
			||||||
@@ -24,7 +19,11 @@ ipcRenderer.on('logout', () => {
 | 
				
			|||||||
	nodes[nodes.length - 1].click();
 | 
						nodes[nodes.length - 1].click();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ipcRenderer.on('shortcut', () => {
 | 
					ipcRenderer.on('show-keyboard-shortcuts', () => {
 | 
				
			||||||
 | 
						if (bridgeEvents.emit('show-keyboard-shortcuts')) {
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create the menu for the below
 | 
						// Create the menu for the below
 | 
				
			||||||
	const node: HTMLElement = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
 | 
						const node: HTMLElement = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
 | 
				
			||||||
	// Additional check
 | 
						// Additional check
 | 
				
			||||||
@@ -38,6 +37,10 @@ ipcRenderer.on('shortcut', () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ipcRenderer.on('show-notification-settings', () => {
 | 
					ipcRenderer.on('show-notification-settings', () => {
 | 
				
			||||||
 | 
						if (bridgeEvents.emit('show-notification-settings')) {
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create the menu for the below
 | 
						// Create the menu for the below
 | 
				
			||||||
	const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
 | 
						const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
 | 
				
			||||||
	dropdown.click();
 | 
						dropdown.click();
 | 
				
			||||||
@@ -53,16 +56,6 @@ ipcRenderer.on('show-notification-settings', () => {
 | 
				
			|||||||
	}, 100);
 | 
						}, 100);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
electron_bridge.once('zulip-loaded', () => {
 | 
					 | 
				
			||||||
	// Redirect users to network troubleshooting page
 | 
					 | 
				
			||||||
	const getRestartButton = document.querySelector('.restart_get_events_button');
 | 
					 | 
				
			||||||
	if (getRestartButton) {
 | 
					 | 
				
			||||||
		getRestartButton.addEventListener('click', () => {
 | 
					 | 
				
			||||||
			ipcRenderer.send('forward-message', 'reload-viewer');
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
window.addEventListener('load', (event: any): void => {
 | 
					window.addEventListener('load', (event: any): void => {
 | 
				
			||||||
	if (!event.target.URL.includes('app/renderer/network.html')) {
 | 
						if (!event.target.URL.includes('app/renderer/network.html')) {
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
@@ -89,25 +82,6 @@ document.addEventListener('keydown', event => {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Set user as active and update the time of last activity
 | 
					 | 
				
			||||||
ipcRenderer.on('set-active', () => {
 | 
					 | 
				
			||||||
	if (isDev) {
 | 
					 | 
				
			||||||
		console.log('active');
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	electron_bridge.idle_on_system = false;
 | 
					 | 
				
			||||||
	electron_bridge.last_active_on_system = Date.now();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Set user as idle and time of last activity is left unchanged
 | 
					 | 
				
			||||||
ipcRenderer.on('set-idle', () => {
 | 
					 | 
				
			||||||
	if (isDev) {
 | 
					 | 
				
			||||||
		console.log('idle');
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	electron_bridge.idle_on_system = true;
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
(async () => webFrame.executeJavaScript(
 | 
					(async () => webFrame.executeJavaScript(
 | 
				
			||||||
	fs.readFileSync(require.resolve('./injected'), 'utf8')
 | 
						fs.readFileSync(require.resolve('./injected'), 'utf8')
 | 
				
			||||||
))();
 | 
					))();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +0,0 @@
 | 
				
			|||||||
// This is a security fix. Following function prevents drag and drop event in the app
 | 
					 | 
				
			||||||
// so that attackers can't execute any remote code within the app
 | 
					 | 
				
			||||||
// It doesn't affect the compose box so that users can still
 | 
					 | 
				
			||||||
// use drag and drop event to share files etc
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const preventDragAndDrop = (): void => {
 | 
					 | 
				
			||||||
	const preventEvents = ['dragover', 'drop'];
 | 
					 | 
				
			||||||
	preventEvents.forEach(dragEvents => {
 | 
					 | 
				
			||||||
		document.addEventListener(dragEvents, event => {
 | 
					 | 
				
			||||||
			event.preventDefault();
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
preventDragAndDrop();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export {};
 | 
					 | 
				
			||||||
@@ -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;
 | 
				
			||||||
@@ -21,6 +21,8 @@ const iconPath = (): string => {
 | 
				
			|||||||
	return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'macOSTemplate.png');
 | 
						return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'macOSTemplate.png');
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const winUnreadTrayIconPath = (): string => APP_ICON + 'unread.ico';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let unread = 0;
 | 
					let unread = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const trayIconSize = (): number => {
 | 
					const trayIconSize = (): number => {
 | 
				
			||||||
@@ -97,6 +99,10 @@ const renderCanvas = function (arg: number): HTMLCanvasElement {
 | 
				
			|||||||
 * @return the native image
 | 
					 * @return the native image
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const renderNativeImage = function (arg: number): NativeImage {
 | 
					const renderNativeImage = function (arg: number): NativeImage {
 | 
				
			||||||
 | 
						if (process.platform === 'win32') {
 | 
				
			||||||
 | 
							return nativeImage.createFromPath(winUnreadTrayIconPath());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const canvas = renderCanvas(arg);
 | 
						const canvas = renderCanvas(arg);
 | 
				
			||||||
	const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
 | 
						const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
 | 
				
			||||||
	return nativeImage.createFromBuffer(pngData, {
 | 
						return nativeImage.createFromBuffer(pngData, {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,78 +0,0 @@
 | 
				
			|||||||
import electron from 'electron';
 | 
					 | 
				
			||||||
import {JsonDB} from 'node-json-db';
 | 
					 | 
				
			||||||
import {initSetUp} from './default-util';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import fs from 'fs';
 | 
					 | 
				
			||||||
import path from 'path';
 | 
					 | 
				
			||||||
import Logger from './logger-util';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const {app, dialog} =
 | 
					 | 
				
			||||||
	process.type === 'renderer' ? electron.remote : electron;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
initSetUp();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const logger = new Logger({
 | 
					 | 
				
			||||||
	file: 'certificate-util.log',
 | 
					 | 
				
			||||||
	timestamp: true
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const certificatesDir = `${app.getPath('userData')}/certificates`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
let db: JsonDB;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
reloadDB();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function getCertificate(server: string): string | undefined {
 | 
					 | 
				
			||||||
	reloadDB();
 | 
					 | 
				
			||||||
	return db.getData('/')[server];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Function to copy the certificate to userData folder
 | 
					 | 
				
			||||||
export function copyCertificate(_server: string, location: string, fileName: string): boolean {
 | 
					 | 
				
			||||||
	let copied = false;
 | 
					 | 
				
			||||||
	const filePath = `${certificatesDir}/${fileName}`;
 | 
					 | 
				
			||||||
	try {
 | 
					 | 
				
			||||||
		fs.copyFileSync(location, filePath);
 | 
					 | 
				
			||||||
		copied = true;
 | 
					 | 
				
			||||||
	} catch (error) {
 | 
					 | 
				
			||||||
		dialog.showErrorBox(
 | 
					 | 
				
			||||||
			'Error saving certificate',
 | 
					 | 
				
			||||||
			'We encountered error while saving the certificate.'
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
		logger.error('Error while copying the certificate to certificates folder.');
 | 
					 | 
				
			||||||
		logger.error(error);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return copied;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function setCertificate(server: string, fileName: string): void {
 | 
					 | 
				
			||||||
	const filePath = `${fileName}`;
 | 
					 | 
				
			||||||
	db.push(`/${server}`, filePath, true);
 | 
					 | 
				
			||||||
	reloadDB();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function removeCertificate(server: string): void {
 | 
					 | 
				
			||||||
	db.delete(`/${server}`);
 | 
					 | 
				
			||||||
	reloadDB();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function reloadDB(): void {
 | 
					 | 
				
			||||||
	const settingsJsonPath = path.join(app.getPath('userData'), '/config/certificates.json');
 | 
					 | 
				
			||||||
	try {
 | 
					 | 
				
			||||||
		const file = fs.readFileSync(settingsJsonPath, 'utf8');
 | 
					 | 
				
			||||||
		JSON.parse(file);
 | 
					 | 
				
			||||||
	} catch (error) {
 | 
					 | 
				
			||||||
		if (fs.existsSync(settingsJsonPath)) {
 | 
					 | 
				
			||||||
			fs.unlinkSync(settingsJsonPath);
 | 
					 | 
				
			||||||
			dialog.showErrorBox(
 | 
					 | 
				
			||||||
				'Error saving settings',
 | 
					 | 
				
			||||||
				'We encountered error while saving the certificate.'
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
			logger.error('Error while JSON parsing certificates.json: ');
 | 
					 | 
				
			||||||
			logger.error(error);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	db = new JsonDB(settingsJsonPath, true, true);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -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',
 | 
				
			||||||
@@ -31,7 +32,7 @@ reloadDB();
 | 
				
			|||||||
export function getConfigItem(key: string, defaultValue: unknown = null): any {
 | 
					export function getConfigItem(key: string, defaultValue: unknown = null): any {
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		db.reload();
 | 
							db.reload();
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error: unknown) {
 | 
				
			||||||
		logger.error('Error while reloading settings.json: ');
 | 
							logger.error('Error while reloading settings.json: ');
 | 
				
			||||||
		logger.error(error);
 | 
							logger.error(error);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -59,7 +60,7 @@ export function getConfigString(key: string, defaultValue: string): string {
 | 
				
			|||||||
export function isConfigItemExists(key: string): boolean {
 | 
					export function isConfigItemExists(key: string): boolean {
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		db.reload();
 | 
							db.reload();
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error: unknown) {
 | 
				
			||||||
		logger.error('Error while reloading settings.json: ');
 | 
							logger.error('Error while reloading settings.json: ');
 | 
				
			||||||
		logger.error(error);
 | 
							logger.error(error);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -88,7 +89,7 @@ function reloadDB(): void {
 | 
				
			|||||||
	try {
 | 
						try {
 | 
				
			||||||
		const file = fs.readFileSync(settingsJsonPath, 'utf8');
 | 
							const file = fs.readFileSync(settingsJsonPath, 'utf8');
 | 
				
			||||||
		JSON.parse(file);
 | 
							JSON.parse(file);
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error: unknown) {
 | 
				
			||||||
		if (fs.existsSync(settingsJsonPath)) {
 | 
							if (fs.existsSync(settingsJsonPath)) {
 | 
				
			||||||
			fs.unlinkSync(settingsJsonPath);
 | 
								fs.unlinkSync(settingsJsonPath);
 | 
				
			||||||
			dialog.showErrorBox(
 | 
								dialog.showErrorBox(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,11 @@
 | 
				
			|||||||
import electron from 'electron';
 | 
					import electron from 'electron';
 | 
				
			||||||
import fs from 'fs';
 | 
					import fs from 'fs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let app: Electron.App = null;
 | 
					const app = process.type === 'renderer' ? electron.remote.app : electron.app;
 | 
				
			||||||
let setupCompleted = false;
 | 
					let setupCompleted = false;
 | 
				
			||||||
if (process.type === 'renderer') {
 | 
					 | 
				
			||||||
	app = electron.remote.app;
 | 
					 | 
				
			||||||
} else {
 | 
					 | 
				
			||||||
	app = electron.app;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const zulipDir = app.getPath('userData');
 | 
					const zulipDir = app.getPath('userData');
 | 
				
			||||||
const logDir = `${zulipDir}/Logs/`;
 | 
					const logDir = `${zulipDir}/Logs/`;
 | 
				
			||||||
const certificatesDir = `${zulipDir}/certificates/`;
 | 
					 | 
				
			||||||
const configDir = `${zulipDir}/config/`;
 | 
					const configDir = `${zulipDir}/config/`;
 | 
				
			||||||
export const initSetUp = (): void => {
 | 
					export const initSetUp = (): void => {
 | 
				
			||||||
	// If it is the first time the app is running
 | 
						// If it is the first time the app is running
 | 
				
			||||||
@@ -26,16 +20,11 @@ export const initSetUp = (): void => {
 | 
				
			|||||||
			fs.mkdirSync(logDir);
 | 
								fs.mkdirSync(logDir);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!fs.existsSync(certificatesDir)) {
 | 
					 | 
				
			||||||
			fs.mkdirSync(certificatesDir);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Migrate config files from app data folder to config folder inside app
 | 
							// Migrate config files from app data folder to config folder inside app
 | 
				
			||||||
		// data folder. This will be done once when a user updates to the new version.
 | 
							// data folder. This will be done once when a user updates to the new version.
 | 
				
			||||||
		if (!fs.existsSync(configDir)) {
 | 
							if (!fs.existsSync(configDir)) {
 | 
				
			||||||
			fs.mkdirSync(configDir);
 | 
								fs.mkdirSync(configDir);
 | 
				
			||||||
			const domainJson = `${zulipDir}/domain.json`;
 | 
								const domainJson = `${zulipDir}/domain.json`;
 | 
				
			||||||
			const certificatesJson = `${zulipDir}/certificates.json`;
 | 
					 | 
				
			||||||
			const settingsJson = `${zulipDir}/settings.json`;
 | 
								const settingsJson = `${zulipDir}/settings.json`;
 | 
				
			||||||
			const updatesJson = `${zulipDir}/updates.json`;
 | 
								const updatesJson = `${zulipDir}/updates.json`;
 | 
				
			||||||
			const windowStateJson = `${zulipDir}/window-state.json`;
 | 
								const windowStateJson = `${zulipDir}/window-state.json`;
 | 
				
			||||||
@@ -44,10 +33,6 @@ export const initSetUp = (): void => {
 | 
				
			|||||||
					path: domainJson,
 | 
										path: domainJson,
 | 
				
			||||||
					fileName: 'domain.json'
 | 
										fileName: 'domain.json'
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					path: certificatesJson,
 | 
					 | 
				
			||||||
					fileName: 'certificates.json'
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				{
 | 
									{
 | 
				
			||||||
					path: settingsJson,
 | 
										path: settingsJson,
 | 
				
			||||||
					fileName: 'settings.json'
 | 
										fileName: 'settings.json'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -90,27 +91,6 @@ export function duplicateDomain(domain: string): boolean {
 | 
				
			|||||||
	return getDomains().some(server => server.url === domain);
 | 
						return getDomains().some(server => server.url === domain);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function checkCertError(domain: string, serverConf: ServerConf, error: any, silent: boolean): Promise<ServerConf> {
 | 
					 | 
				
			||||||
	if (silent) {
 | 
					 | 
				
			||||||
		// Since getting server settings has already failed
 | 
					 | 
				
			||||||
		return serverConf;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Report error to sentry to get idea of possible certificate errors
 | 
					 | 
				
			||||||
	// users get when adding the servers
 | 
					 | 
				
			||||||
	logger.reportSentry(error);
 | 
					 | 
				
			||||||
	const certErrorMessage = Messages.certErrorMessage(domain, error);
 | 
					 | 
				
			||||||
	const certErrorDetail = Messages.certErrorDetail();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	await dialog.showMessageBox({
 | 
					 | 
				
			||||||
		type: 'error',
 | 
					 | 
				
			||||||
		buttons: ['OK'],
 | 
					 | 
				
			||||||
		message: certErrorMessage,
 | 
					 | 
				
			||||||
		detail: certErrorDetail
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
	throw new Error('Untrusted certificate.');
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function checkDomain(domain: string, silent = false): Promise<ServerConf> {
 | 
					export async function checkDomain(domain: string, silent = false): Promise<ServerConf> {
 | 
				
			||||||
	if (!silent && duplicateDomain(domain)) {
 | 
						if (!silent && duplicateDomain(domain)) {
 | 
				
			||||||
		// Do not check duplicate in silent mode
 | 
							// Do not check duplicate in silent mode
 | 
				
			||||||
@@ -119,25 +99,9 @@ export async function checkDomain(domain: string, silent = false): Promise<Serve
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	domain = formatUrl(domain);
 | 
						domain = formatUrl(domain);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const serverConf = {
 | 
					 | 
				
			||||||
		icon: defaultIconUrl,
 | 
					 | 
				
			||||||
		url: domain,
 | 
					 | 
				
			||||||
		alias: domain
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		return await getServerSettings(domain);
 | 
							return await getServerSettings(domain);
 | 
				
			||||||
	} catch (error_) {
 | 
						} catch {
 | 
				
			||||||
		// Make sure that error is an error or string not undefined
 | 
					 | 
				
			||||||
		// so validation does not throw error.
 | 
					 | 
				
			||||||
		const error = error_ || '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const certsError = error.toString().includes('certificate');
 | 
					 | 
				
			||||||
		if (certsError) {
 | 
					 | 
				
			||||||
			const result = await checkCertError(domain, serverConf, error, silent);
 | 
					 | 
				
			||||||
			return result;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		throw new Error(Messages.invalidZulipServerError(domain));
 | 
							throw new Error(Messages.invalidZulipServerError(domain));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -161,7 +125,7 @@ export async function updateSavedServer(url: string, index: number): Promise<voi
 | 
				
			|||||||
			updateDomain(index, newServerConf);
 | 
								updateDomain(index, newServerConf);
 | 
				
			||||||
			reloadDB();
 | 
								reloadDB();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error: unknown) {
 | 
				
			||||||
		logger.log('Could not update server icon.');
 | 
							logger.log('Could not update server icon.');
 | 
				
			||||||
		logger.log(error);
 | 
							logger.log(error);
 | 
				
			||||||
		logger.reportSentry(error);
 | 
							logger.reportSentry(error);
 | 
				
			||||||
@@ -173,7 +137,7 @@ function reloadDB(): void {
 | 
				
			|||||||
	try {
 | 
						try {
 | 
				
			||||||
		const file = fs.readFileSync(domainJsonPath, 'utf8');
 | 
							const file = fs.readFileSync(domainJsonPath, 'utf8');
 | 
				
			||||||
		JSON.parse(file);
 | 
							JSON.parse(file);
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error: unknown) {
 | 
				
			||||||
		if (fs.existsSync(domainJsonPath)) {
 | 
							if (fs.existsSync(domainJsonPath)) {
 | 
				
			||||||
			fs.unlinkSync(domainJsonPath);
 | 
								fs.unlinkSync(domainJsonPath);
 | 
				
			||||||
			dialog.showErrorBox(
 | 
								dialog.showErrorBox(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ const logger = new Logger({
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO: replace enterpriseSettings type with an interface once settings are final
 | 
					// TODO: replace enterpriseSettings type with an interface once settings are final
 | 
				
			||||||
let enterpriseSettings: {[key: string]: unknown};
 | 
					let enterpriseSettings: Record<string, unknown>;
 | 
				
			||||||
let configFile: boolean;
 | 
					let configFile: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
reloadDB();
 | 
					reloadDB();
 | 
				
			||||||
@@ -26,7 +26,7 @@ function reloadDB(): void {
 | 
				
			|||||||
		try {
 | 
							try {
 | 
				
			||||||
			const file = fs.readFileSync(enterpriseFile, 'utf8');
 | 
								const file = fs.readFileSync(enterpriseFile, 'utf8');
 | 
				
			||||||
			enterpriseSettings = JSON.parse(file);
 | 
								enterpriseSettings = JSON.parse(file);
 | 
				
			||||||
		} catch (error) {
 | 
							} catch (error: unknown) {
 | 
				
			||||||
			logger.log('Error while JSON parsing global_config.json: ');
 | 
								logger.log('Error while JSON parsing global_config.json: ');
 | 
				
			||||||
			logger.log(error);
 | 
								logger.log(error);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 =
 | 
				
			||||||
@@ -46,7 +47,7 @@ function reloadDB(): void {
 | 
				
			|||||||
	try {
 | 
						try {
 | 
				
			||||||
		const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
 | 
							const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
 | 
				
			||||||
		JSON.parse(file);
 | 
							JSON.parse(file);
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error: unknown) {
 | 
				
			||||||
		if (fs.existsSync(linuxUpdateJsonPath)) {
 | 
							if (fs.existsSync(linuxUpdateJsonPath)) {
 | 
				
			||||||
			fs.unlinkSync(linuxUpdateJsonPath);
 | 
								fs.unlinkSync(linuxUpdateJsonPath);
 | 
				
			||||||
			dialog.showErrorBox(
 | 
								dialog.showErrorBox(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
@@ -132,7 +133,7 @@ export default class Logger {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	trimLog(file: string): void{
 | 
						trimLog(file: string): void {
 | 
				
			||||||
		fs.readFile(file, 'utf8', (err, data) => {
 | 
							fs.readFile(file, 'utf8', (err, data) => {
 | 
				
			||||||
			if (err) {
 | 
								if (err) {
 | 
				
			||||||
				throw err;
 | 
									throw err;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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[] = [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -61,5 +61,4 @@
 | 
				
			|||||||
        // it messes up require module path resolution
 | 
					        // it messes up require module path resolution
 | 
				
			||||||
        require('./js/main');
 | 
					        require('./js/main');
 | 
				
			||||||
    </script>
 | 
					    </script>
 | 
				
			||||||
    <script>require('./js/shared/preventdrag.js')</script>
 | 
					 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
@@ -16,6 +16,5 @@
 | 
				
			|||||||
    <script>
 | 
					    <script>
 | 
				
			||||||
        document.querySelector('#tagify-css').href = require.resolve('@yaireo/tagify/dist/tagify.css');
 | 
					        document.querySelector('#tagify-css').href = require.resolve('@yaireo/tagify/dist/tagify.css');
 | 
				
			||||||
        require('./js/pages/preference/preference.js');
 | 
					        require('./js/pages/preference/preference.js');
 | 
				
			||||||
        require('./js/shared/preventdrag.js')
 | 
					 | 
				
			||||||
    </script>
 | 
					    </script>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
 
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							@@ -8,7 +8,7 @@ export function invalidZulipServerError(domain: string): string {
 | 
				
			|||||||
		\n • You can connect to that URL in a web browser.\
 | 
							\n • You can connect to that URL in a web browser.\
 | 
				
			||||||
		\n • If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.\
 | 
							\n • If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.\
 | 
				
			||||||
		\n • It's a Zulip server. (The oldest supported version is 1.6).\
 | 
							\n • It's a Zulip server. (The oldest supported version is 1.6).\
 | 
				
			||||||
		\n • The server has a valid certificate. (You can add custom certificates in Settings > Organizations). \
 | 
							\n • The server has a valid certificate. \
 | 
				
			||||||
		\n • The SSL is correctly configured for the certificate. Check out the SSL troubleshooting guide -
 | 
							\n • The SSL is correctly configured for the certificate. Check out the SSL troubleshooting guide -
 | 
				
			||||||
		\n https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`;
 | 
							\n https://zulip.readthedocs.io/en/stable/production/ssl-certificates.html`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -18,15 +18,6 @@ export function noOrgsError(domain: string): string {
 | 
				
			|||||||
	\nPlease contact your server administrator.`;
 | 
						\nPlease contact your server administrator.`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function certErrorMessage(domain: string, error: string): string {
 | 
					 | 
				
			||||||
	return `Certificate error for ${domain}\n${error}`;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function certErrorDetail(): string {
 | 
					 | 
				
			||||||
	return `The organization you're connecting to is either someone impersonating the Zulip server you entered, or the server you're trying to connect to is configured in an insecure way.
 | 
					 | 
				
			||||||
	\nIf you have a valid certificate please add it from Settings>Organizations and try to add the organization again.`;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function enterpriseOrgError(length: number, domains: string[]): DialogBoxError {
 | 
					export function enterpriseOrgError(length: number, domains: string[]): DialogBoxError {
 | 
				
			||||||
	let domainList = '';
 | 
						let domainList = '';
 | 
				
			||||||
	for (const domain of domains) {
 | 
						for (const domain of domains) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								app/resources/tray/trayunread.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/resources/tray/trayunread.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 91 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								build/icon.icns
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								build/icon.icns
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										54
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								changelog.md
									
									
									
									
									
								
							@@ -2,6 +2,60 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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.5.0 --2020-12-01
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Removed features**:
 | 
				
			||||||
 | 
					* Removed legacy handling of custom certificates. Custom certificates can be configured in the same system certificate store that Chrome uses ([instructions](https://zulip.com/help/custom-certificates#desktop)).
 | 
				
			||||||
 | 
					* Removed the unmaintained notification inline replies feature on macOS. We believe the `node-mac-notifier` library used by this feature had been responsible for the grey screen crash issue.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Fixes**:
 | 
				
			||||||
 | 
					* Fixed a regression with the factory reset function.
 | 
				
			||||||
 | 
					* Fixed the grey screen crash issue on macOS ([#1016](https://github.com/zulip/zulip-desktop/issues/1016)).
 | 
				
			||||||
 | 
					* Whitespace is now stripped from the organization URL when adding a new organization.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Dependencies**:
 | 
				
			||||||
 | 
					* Upgraded all dependencies, including Electron 11.0.3.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### v5.4.3 --2020-09-10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Security fixes**:
 | 
				
			||||||
 | 
					* CVE-2020-24582: Escape all strings interpolated into HTML to close cross-site scripting vulnerabilities that a malicious server could exploit.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Dependencies**:
 | 
				
			||||||
 | 
					* Upgrade dependencies, including Electron 9.3.0.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### v5.4.2 --2020-08-12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**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**:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								gulpfile.js
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								gulpfile.js
									
									
									
									
									
								
							@@ -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);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5996
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5996
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										70
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								package.json
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "zulip",
 | 
					  "name": "zulip",
 | 
				
			||||||
  "productName": "Zulip",
 | 
					  "productName": "Zulip",
 | 
				
			||||||
  "version": "5.4.0",
 | 
					  "version": "5.5.0",
 | 
				
			||||||
  "main": "./app/main",
 | 
					  "main": "./app/main",
 | 
				
			||||||
  "description": "Zulip Desktop App",
 | 
					  "description": "Zulip Desktop App",
 | 
				
			||||||
  "license": "Apache-2.0",
 | 
					  "license": "Apache-2.0",
 | 
				
			||||||
@@ -24,7 +24,7 @@
 | 
				
			|||||||
    "start": "tsc && electron .",
 | 
					    "start": "tsc && electron .",
 | 
				
			||||||
    "clean-ts-files": "git clean app/*.js -e node_modules -xf",
 | 
					    "clean-ts-files": "git clean app/*.js -e node_modules -xf",
 | 
				
			||||||
    "watch-ts": "tsc -w",
 | 
					    "watch-ts": "tsc -w",
 | 
				
			||||||
    "reinstall": "node ./tools/reinstall-node-modules.js",
 | 
					    "reinstall": "rimraf node_modules && npm install",
 | 
				
			||||||
    "postinstall": "electron-builder install-app-deps",
 | 
					    "postinstall": "electron-builder install-app-deps",
 | 
				
			||||||
    "lint-css": "stylelint app/renderer/css/*.css",
 | 
					    "lint-css": "stylelint app/renderer/css/*.css",
 | 
				
			||||||
    "lint-html": "./node_modules/.bin/htmlhint \"app/renderer/*.html\" ",
 | 
					    "lint-html": "./node_modules/.bin/htmlhint \"app/renderer/*.html\" ",
 | 
				
			||||||
@@ -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,64 +146,67 @@
 | 
				
			|||||||
  ],
 | 
					  ],
 | 
				
			||||||
  "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.4",
 | 
				
			||||||
    "@yaireo/tagify": "^3.15.3",
 | 
					    "@yaireo/tagify": "^3.21.5",
 | 
				
			||||||
    "adm-zip": "^0.4.16",
 | 
					    "adm-zip": "^0.5.1",
 | 
				
			||||||
    "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.3.0",
 | 
				
			||||||
    "electron-updater": "^4.3.1",
 | 
					    "electron-updater": "^4.3.5",
 | 
				
			||||||
    "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",
 | 
					 | 
				
			||||||
    "node-json-db": "^1.1.0",
 | 
					    "node-json-db": "^1.1.0",
 | 
				
			||||||
    "semver": "^7.3.2"
 | 
					    "semver": "^7.3.4"
 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "optionalDependencies": {
 | 
					 | 
				
			||||||
    "node-mac-notifier": "^1.1.0"
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@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.4",
 | 
				
			||||||
    "@types/fs-extra": "^9.0.1",
 | 
					    "@types/i18n": "^0.8.8",
 | 
				
			||||||
    "@types/i18n": "^0.8.6",
 | 
					    "@types/node": "^14.14.10",
 | 
				
			||||||
    "@types/node": "^14.0.23",
 | 
					 | 
				
			||||||
    "@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",
 | 
					 | 
				
			||||||
    "dotenv": "^8.2.0",
 | 
					    "dotenv": "^8.2.0",
 | 
				
			||||||
    "electron": "^8.4.0",
 | 
					    "electron": "^11.0.3",
 | 
				
			||||||
    "electron-builder": "^22.7.0",
 | 
					    "electron-builder": "^22.9.1",
 | 
				
			||||||
    "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",
 | 
				
			||||||
    "gulp": "^4.0.2",
 | 
					    "gulp": "^4.0.2",
 | 
				
			||||||
    "gulp-tape": "^1.0.0",
 | 
					    "gulp-tape": "^1.0.0",
 | 
				
			||||||
    "gulp-typescript": "^6.0.0-alpha.1",
 | 
					    "gulp-typescript": "^6.0.0-alpha.1",
 | 
				
			||||||
    "htmlhint": "^0.14.1",
 | 
					    "htmlhint": "^0.14.2",
 | 
				
			||||||
    "nodemon": "^2.0.4",
 | 
					    "nodemon": "^2.0.6",
 | 
				
			||||||
    "pre-commit": "^1.2.2",
 | 
					    "pre-commit": "^1.2.2",
 | 
				
			||||||
    "rimraf": "^3.0.2",
 | 
					    "rimraf": "^3.0.2",
 | 
				
			||||||
    "spectron": "^10.0.1",
 | 
					    "spectron": "^13.0.0",
 | 
				
			||||||
    "stylelint": "^13.6.1",
 | 
					    "stylelint": "^13.8.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.1.2",
 | 
				
			||||||
    "xo": "^0.32.1"
 | 
					    "xo": "^0.35.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "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",
 | 
				
			||||||
@@ -226,8 +229,7 @@
 | 
				
			|||||||
          "app/renderer/js/injected.ts",
 | 
					          "app/renderer/js/injected.ts",
 | 
				
			||||||
          "gulpfile.js",
 | 
					          "gulpfile.js",
 | 
				
			||||||
          "scripts/notarize.js",
 | 
					          "scripts/notarize.js",
 | 
				
			||||||
          "tests/**/*.js",
 | 
					          "tests/**/*.js"
 | 
				
			||||||
          "tools/reinstall-node-modules.js"
 | 
					 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "parserOptions": {
 | 
					        "parserOptions": {
 | 
				
			||||||
          "sourceType": "script"
 | 
					          "sourceType": "script"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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') {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +0,0 @@
 | 
				
			|||||||
#!/bin/bash
 | 
					 | 
				
			||||||
set -e
 | 
					 | 
				
			||||||
set -x
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
echo "Removing node_modules"
 | 
					 | 
				
			||||||
rm -rf node_modules
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
echo "node_modules removed reinstalling npm packages"
 | 
					 | 
				
			||||||
npm i
 | 
					 | 
				
			||||||
@@ -1,7 +0,0 @@
 | 
				
			|||||||
@echo off
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
echo "Removing node_modules"
 | 
					 | 
				
			||||||
rmdir /s /q node_modules
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
echo "node_modules removed reinstalling npm packages"
 | 
					 | 
				
			||||||
npm i
 | 
					 | 
				
			||||||
@@ -1,19 +0,0 @@
 | 
				
			|||||||
#!/usr/bin/env node
 | 
					 | 
				
			||||||
'use strict';
 | 
					 | 
				
			||||||
const {exec} = require('child_process');
 | 
					 | 
				
			||||||
const path = require('path');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const isWindows = process.platform === 'win32';
 | 
					 | 
				
			||||||
const command = path.join(__dirname, `reinstall-node-modules${isWindows ? '.cmd' : ''}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const proc = exec(command, error => {
 | 
					 | 
				
			||||||
	if (error) {
 | 
					 | 
				
			||||||
		console.error(error);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
proc.stdout.on('data', data => console.log(data.toString()));
 | 
					 | 
				
			||||||
proc.stderr.on('data', data => console.error(data.toString()));
 | 
					 | 
				
			||||||
proc.on('exit', code => {
 | 
					 | 
				
			||||||
	process.exit(code);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
							
								
								
									
										4
									
								
								typings.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								typings.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -21,8 +21,6 @@ declare module '@electron-elements/send-feedback' {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
declare module 'electron-connect';
 | 
					declare module 'electron-connect';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare module 'node-mac-notifier';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
declare module '@yaireo/tagify';
 | 
					declare module '@yaireo/tagify';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ClipboardDecrypter {
 | 
					interface ClipboardDecrypter {
 | 
				
			||||||
@@ -32,7 +30,7 @@ interface ClipboardDecrypter {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ElectronBridge {
 | 
					interface ElectronBridge {
 | 
				
			||||||
	send_event: (eventName: string | symbol, ...args: unknown[]) => void;
 | 
						send_event: (eventName: string | symbol, ...args: unknown[]) => boolean;
 | 
				
			||||||
	on_event: (eventName: string, listener: ListenerType) => void;
 | 
						on_event: (eventName: string, listener: ListenerType) => void;
 | 
				
			||||||
	new_notification: (
 | 
						new_notification: (
 | 
				
			||||||
		title: string,
 | 
							title: string,
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user