Compare commits

...

55 Commits

Author SHA1 Message Date
Anders Kaseorg
0cb82a6f5e release: New release v5.5.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 19:53:20 -08:00
Anders Kaseorg
79808e8ee9 preload: Provide hooks for server to robustly replace logout et al.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 18:11:45 -08:00
Anders Kaseorg
2c38df10c8 electron-bridge: Expose boolean return from emit.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 17:59:59 -08:00
Anders Kaseorg
1ca15d44a0 electron-bridge: Move mutable state out of electron_bridge.
Only the initial value of a mutable field is exposed via
exposeInMainWorld, which is why we have a bunch of setter and getter
functions.  It’s better to avoid the possibility for this confusion.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 17:54:21 -08:00
Anders Kaseorg
82450a91a9 preload: Remove retry button redirection hack.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 17:18:09 -08:00
Anders Kaseorg
62edfa6f8b Remove macOS notification inline replies feature.
node-mac-notifier no longer builds on macOS with Electron 11 (error:
no template named 'remove_cv_t' in namespace 'std').  It was
previously implicated in crashes on macOS (#1016).  And we no longer
have any macOS developers that seem to be maintaining this
feature (e.g. #1022 is stalled).

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 17:06:11 -08:00
Anders Kaseorg
fe86315ece main: Be explicit about disabling contextIsolation for the main window.
We have been relying on the default here, but the default will be
changing in Electron 12.  (We already enable contextIsolation in the
webviews that load remote content.)

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 16:34:41 -08:00
Anders Kaseorg
df3f719e89 Upgrade dependencies, including Electron 11.0.3.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 16:02:41 -08:00
Anders Kaseorg
0632d8199f injected: Condition narrow-by-topic handler on page_params.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-12-01 16:02:32 -08:00
Anders Kaseorg
047bf0ca45 webview: Pass webPreferences values as explicit booleans
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-11-30 12:39:35 -08:00
Anders Kaseorg
356c879668 Remove Devtron.
Devtron is unmaintained and no longer works.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-11-18 15:25:33 -08:00
Anders Kaseorg
ba432d32b3 Remove preventdrag script.
This was not a security feature; security is enforced using context
isolation and the same-origin policy.

Furthermore, navigation on drag-and-drop was already disabled by
default in Electron 3.0.

https://www.electronjs.org/blog/electron-3-0#breaking-api-changes

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-11-17 16:10:47 -08:00
Anders Kaseorg
c8ada3f47d Rewrite reinstall script to avoid auxilliary script files.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-11-17 15:41:46 -08:00
aryanshridhar
cd77fc6448 new-server-form: Strip whitespace from added organization URL.
Fixes #1037.
2020-11-15 19:56:53 -08:00
Anders Kaseorg
a2f926c611 README: Migrate Travis badge to travis-ci.com.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-27 15:49:22 -07:00
Anders Kaseorg
6c5eb85a16 README: Use Markdown for screenshot display.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-27 15:45:55 -07:00
Anders Kaseorg
cadb1c6eaa Upgrade dependencies, including Electron 10.1.5.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
73710319e6 xo: Fix unicorn/prevent-abbreviations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
da91dc5595 xo: Fix @typescript-eslint/consistent-indexed-object-style.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
31d5e5a092 xo: Fix unicorn/prefer-ternary, I guess.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
13ee1d0990 logger-util: Add missing space.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
d5a9063378 typescript: Fix implicit any in catch clauses.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:13 -07:00
Anders Kaseorg
918064f35d checkDomain: Remove special handling for “certificate” error strings.
The fragile check has been broken by changing strings, and the default
invalidZulipServerError message is fine.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:47:01 -07:00
Anders Kaseorg
193b8326bc injected: Check if narrow is defined.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:32:05 -07:00
Anders Kaseorg
9abb7f376e injected: Remove unused default_language from zulipWindow type.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-24 15:32:05 -07:00
Anders Kaseorg
ac338fa438 Upgrade dependencies, including Electron 10.1.3.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-01 15:54:59 -07:00
Anders Kaseorg
f5b78ee845 Set enableRemoteModule.
We would like to disable the remote module for improved sandboxing
(#915), but until then this is required for Electron 10, which
disables the remote module by default.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-10-01 15:54:59 -07:00
Aryan Shridhar
126bb26a6e Tray Icon : Changed Unread tray icon in Windows.
Replaced unread messages icon in the lower tray bar in windows with a new icon.
Fixed #506.
2020-09-17 16:07:23 +05:30
Anders Kaseorg
23e86abb5b Remove support for custom certificate exceptions.
Version 5.4.0 and later uses electron.net for all network
requests (#993), so custom certificates can now be configured in the
same system certificate store that Chrome uses.

https://zulip.com/help/custom-certificates#desktop

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-11 22:25:28 -07:00
Anders Kaseorg
3a3714787f main: Fix mainWindowState scope.
Fixes a regression with the factory reset function introduced by
commit cf9d0c8aa2.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-10 21:11:41 -07:00
Anders Kaseorg
bc57aabc97 Disable unused Chromium plugins; delete old commented PDF code.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-10 18:28:35 -07:00
Anders Kaseorg
08df02a1ea changelog: Update for 5.4.3 release.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-09 23:18:59 -07:00
Akash Nimare
35ad6fbad0 release: New release v5.4.3. 2020-09-09 12:24:43 +05:30
Anders Kaseorg
97f8fe71af Escape all strings inserted into CSS selectors.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-04 22:52:42 -07:00
Anders Kaseorg
a9d59b3dcd CVE-2020-24582: Escape all strings interpolated into HTML.
Also fix various variable names to consistently indicate which strings
contain HTML.

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

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

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

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

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

View File

@@ -1,12 +1,13 @@
# Zulip Desktop Client
[![Build Status](https://travis-ci.org/zulip/zulip-desktop.svg?branch=master)](https://travis-ci.org/zulip/zulip-desktop)
[![Build Status](https://travis-ci.com/zulip/zulip-desktop.svg?branch=master)](https://travis-ci.com/github/zulip/zulip-desktop)
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/zulip/zulip-desktop?branch=master&svg=true)](https://ci.appveyor.com/project/zulip/zulip-desktop/branch/master)
[![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)
[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://chat.zulip.org)
Desktop client for Zulip. Available for Mac, Linux, and Windows.
<img src="http://i.imgur.com/ChzTq4F.png"/>
![screenshot](https://i.imgur.com/s1o6TRA.png)
![screenshot](https://i.imgur.com/vekKnW4.png)
# Download
Please see the [installation guide](https://zulip.com/help/desktop-app-install-guide).

View File

@@ -1,13 +1,15 @@
import {app, dialog, session} from 'electron';
import {UpdateDownloadedEvent, UpdateInfo, autoUpdater} from 'electron-updater';
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 log from 'electron-log';
import {UpdateDownloadedEvent, UpdateInfo, autoUpdater} from 'electron-updater';
import * as ConfigUtil from '../renderer/js/utils/config-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);
export async function appUpdater(updateFromMenu = false): Promise<void> {

View File

@@ -1,18 +1,19 @@
import {sentryInit} from '../renderer/js/utils/sentry-util';
import {appUpdater} from './autoupdater';
import {setAutoLaunch} from './startup';
import electron, {app, dialog, ipcMain, session} from 'electron';
import fs from 'fs';
import path from 'path';
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 CertificateUtil from '../renderer/js/utils/certificate-util';
import * as ConfigUtil from '../renderer/js/utils/config-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 {setAutoLaunch} from './startup';
let mainWindowState: windowStateKeeper.State;
@@ -68,7 +69,7 @@ const toggleApp = (): void => {
function createMainWindow(): Electron.BrowserWindow {
// Load the previous state with fallback to defaults
const mainWindowState: windowStateKeeper.State = windowStateKeeper({
mainWindowState = windowStateKeeper({
defaultWidth: 1100,
defaultHeight: 720,
path: `${app.getPath('userData')}/config`
@@ -85,7 +86,8 @@ function createMainWindow(): Electron.BrowserWindow {
minWidth: 500,
minHeight: 400,
webPreferences: {
plugins: true,
contextIsolation: false,
enableRemoteModule: true,
nodeIntegration: true,
partition: 'persist:webviewsession',
webviewTag: true
@@ -220,36 +222,9 @@ app.on('ready', () => {
event: Event,
webContents: Electron.WebContents,
urlString: 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.
error: string
) => {
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(
'Certificate error',
`The server presented an invalid certificate for ${url.origin}:
@@ -283,32 +258,6 @@ ${error}`
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
ipcMain.on('reload-full-app', () => {
mainWindow.reload();
@@ -353,7 +302,7 @@ ${error}`
AppMenu.setMenu(props);
const activeTab = props.tabs[props.activeTabIndex];
if (activeTab) {
mainWindow.setTitle(`Zulip - ${activeTab.webview.props.name}`);
mainWindow.setTitle(`Zulip - ${activeTab.webviewName}`);
}
});

View File

@@ -2,9 +2,11 @@ import {app, Notification, net} from 'electron';
import getStream from 'get-stream';
import semver from 'semver';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as LinuxUpdateUtil from '../renderer/js/utils/linux-update-util';
import Logger from '../renderer/js/utils/logger-util';
import {fetchResponse} from './request';
const logger = new Logger({
@@ -36,7 +38,7 @@ export async function linuxUpdateNotification(session: Electron.session): Promis
LinuxUpdateUtil.setUpdateItem(latestVersion, true);
}
}
} catch (error) {
} catch (error: unknown) {
logger.error('Linux update error.');
logger.error(error);
}

View File

@@ -1,15 +1,17 @@
import {app, shell, BrowserWindow, Menu} from 'electron';
import {appUpdater} from './autoupdater';
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 DNDUtil from '../renderer/js/utils/dnd-util';
import * as LinkUtil from '../renderer/js/utils/link-util';
import * as t from '../renderer/js/utils/translation-util';
import type {ServerOrFunctionalTab} from '../renderer/js/main';
import {appUpdater} from './autoupdater';
export interface MenuProps {
tabs: ServerOrFunctionalTab[];
tabs: TabData[];
activeTabIndex?: number;
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[] = [{
label: t.__('Minimize'),
role: 'minimize'
@@ -229,17 +231,17 @@ function getWindowSubmenu(tabs: ServerOrFunctionalTab[], activeTabIndex: number)
});
tabs.forEach(tab => {
// 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;
}
initialSubmenu.push({
label: tab.props.name,
accelerator: tab.props.role === 'function' ? '' : `${ShortcutKey} + ${tab.props.index + 1}`,
checked: tab.props.index === activeTabIndex,
label: tab.name,
accelerator: tab.role === 'function' ? '' : `${ShortcutKey} + ${tab.index + 1}`,
checked: tab.index === activeTabIndex,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('switch-server-tab', tab.props.index);
sendAction('switch-server-tab', tab.index);
}
},
type: 'checkbox'
@@ -306,7 +308,7 @@ function getDarwinTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('shortcut');
sendAction('show-keyboard-shortcuts');
}
}
}, {
@@ -448,7 +450,7 @@ function getOtherTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
enabled: enableMenu,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('shortcut');
sendAction('show-keyboard-shortcuts');
}
}
}, {
@@ -545,20 +547,20 @@ async function checkForUpdate(): Promise<void> {
await appUpdater(true);
}
function getNextServer(tabs: ServerOrFunctionalTab[], activeTabIndex: number): number {
function getNextServer(tabs: TabData[], activeTabIndex: number): number {
do {
activeTabIndex = (activeTabIndex + 1) % tabs.length;
}
while (tabs[activeTabIndex].props.role !== 'server');
while (tabs[activeTabIndex].role !== 'server');
return activeTabIndex;
}
function getPreviousServer(tabs: ServerOrFunctionalTab[], activeTabIndex: number): number {
function getPreviousServer(tabs: TabData[], activeTabIndex: number): number {
do {
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
}
while (tabs[activeTabIndex].props.role !== 'server');
while (tabs[activeTabIndex].role !== 'server');
return activeTabIndex;
}

View File

@@ -3,12 +3,13 @@ import fs from 'fs';
import path from 'path';
import stream from 'stream';
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 {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> {
return new Promise((resolve, reject) => {
request.on('response', resolve);
@@ -71,7 +72,7 @@ export const _getServerSettings = async (domain: string, session: Electron.sessi
// Following check handles both the cases
icon: realm_icon.startsWith('/') ? realm_uri + realm_icon : realm_icon,
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);
await pipeline(response, fs.createWriteStream(filePath));
return filePath;
} catch (error) {
} catch (error: unknown) {
logger.log('Could not get server icon.');
logger.log(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;
return isValidResponse;
} catch (error) {
} catch (error: unknown) {
logger.log(error);
return false;
}

View File

@@ -2,6 +2,7 @@ import {app} from 'electron';
import AutoLaunch from 'auto-launch';
import isDev from 'electron-is-dev';
import * as ConfigUtil from '../renderer/js/utils/config-util';
export const setAutoLaunch = async (AutoLaunchValue: boolean): Promise<void> => {
@@ -18,11 +19,7 @@ export const setAutoLaunch = async (AutoLaunchValue: boolean): Promise<void> =>
name: 'Zulip',
isHidden: false
});
if (autoLaunchOption) {
await ZulipAutoLauncher.enable();
} else {
await ZulipAutoLauncher.disable();
}
await (autoLaunchOption ? ZulipAutoLauncher.enable() : ZulipAutoLauncher.disable());
} else {
app.setLoginItemSettings({
openAtLogin: autoLaunchOption,

View File

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

View File

@@ -622,10 +622,6 @@ input.toggle-round:checked + label::after {
max-width: 100%;
}
#add-certificate-button {
margin: 10px 10px 10px 37px;
}
.tip {
background: none;
padding: 0;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,15 @@
import {ipcRenderer, remote} from 'electron';
import path from 'path';
import fs from 'fs';
import path from 'path';
import {htmlEscape} from 'escape-goat';
import * as ConfigUtil from '../utils/config-util';
import * as SystemUtil from '../utils/system-util';
import BaseComponent from './base';
import handleExternalLink from './handle-external-link';
import {contextMenu} from './context-menu';
import handleExternalLink from './handle-external-link';
const {app, dialog} = remote;
@@ -49,25 +52,26 @@ export default class WebView extends BaseComponent {
this.$webviewsContainer = document.querySelector('#webviews-container').classList;
}
template(): string {
return `<webview
class="disabled"
data-tab-id="${this.props.tabIndex}"
src="${this.props.url}"
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession"
name="${this.props.name}"
webpreferences="
${this.props.nodeIntegration ? '' : 'contextIsolation,'}
${ConfigUtil.getConfigItem('enableSpellchecker') ? 'spellcheck,' : ''}
javascript
">
</webview>`;
templateHTML(): string {
return htmlEscape`
<webview
class="disabled"
data-tab-id="${this.props.tabIndex}"
src="${this.props.url}"
` + (this.props.nodeIntegration ? 'nodeIntegration' : '') + htmlEscape`
` + (this.props.preload ? 'preload="js/preload.js"' : '') + htmlEscape`
partition="persist:webviewsession"
name="${this.props.name}"
webpreferences="
contextIsolation=${!this.props.nodeIntegration},
spellcheck=${Boolean(ConfigUtil.getConfigItem('enableSpellchecker'))}
">
</webview>
`;
}
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.$el.addEventListener('dom-ready', () => resolve(), true);
});
@@ -261,8 +265,8 @@ export default class WebView extends BaseComponent {
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'logout');
}
showShortcut(): void {
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'shortcut');
showKeyboardShortcuts(): void {
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'show-keyboard-shortcuts');
}
openDevTools(): void {

View File

@@ -1,69 +1,61 @@
import {ipcRenderer} from 'electron';
import {EventEmitter} from 'events';
import isDev from 'electron-is-dev';
import {ClipboardDecrypterImpl} from './clipboard-decrypter';
import {NotificationData, newNotification} from './notification';
type ListenerType = ((...args: any[]) => void);
class ElectronBridgeImpl extends EventEmitter implements ElectronBridge {
send_notification_reply_message_supported: boolean;
idle_on_system: boolean;
last_active_on_system: number;
let notificationReplySupported = false;
// Indicates if the user is idle or not
let idle = false;
// Indicates the time at which user was last active
let lastActive = Date.now();
constructor() {
super();
this.send_notification_reply_message_supported = false;
// Indicates if the user is idle or not
this.idle_on_system = false;
export const bridgeEvents = new EventEmitter();
// Indicates the time at which user was last active
this.last_active_on_system = Date.now();
}
const electron_bridge: ElectronBridge = {
send_event: (eventName: string | symbol, ...args: unknown[]): boolean =>
bridgeEvents.emit(eventName, ...args),
send_event = (eventName: string | symbol, ...args: unknown[]): void => {
this.emit(eventName, ...args);
};
on_event: (eventName: string, listener: ListenerType): void => {
bridgeEvents.on(eventName, listener);
},
on_event = (eventName: string, listener: ListenerType): void => {
this.on(eventName, listener);
};
new_notification = (
new_notification: (
title: string,
options: NotificationOptions | undefined,
dispatch: (type: string, eventInit: EventInit) => boolean
): 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 =>
this.send_notification_reply_message_supported;
get_send_notification_reply_message_supported: (): boolean =>
notificationReplySupported,
set_send_notification_reply_message_supported = (value: boolean): void => {
this.send_notification_reply_message_supported = value;
};
set_send_notification_reply_message_supported: (value: boolean): void => {
notificationReplySupported = value;
},
decrypt_clipboard = (version: number): ClipboardDecrypterImpl =>
new ClipboardDecrypterImpl(version);
}
decrypt_clipboard: (version: number): ClipboardDecrypterImpl =>
new ClipboardDecrypterImpl(version)
};
const electron_bridge = new ElectronBridgeImpl();
electron_bridge.on('total_unread_count', (...args) => {
bridgeEvents.on('total_unread_count', (...args) => {
ipcRenderer.send('unread-count', ...args);
});
electron_bridge.on('realm_name', realmName => {
bridgeEvents.on('realm_name', realmName => {
const serverURL = location.origin;
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') {
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);
});
// 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
// emitters to make event handling more simpler instead of using
// functions zulip side will emit event using ElectronBrigde.send_event

View File

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

View File

@@ -9,13 +9,7 @@ interface CompatElectronBridge extends ElectronBridge {
(() => {
const zulipWindow = window as typeof window & {
electron_bridge: CompatElectronBridge;
narrow: {
by_subject?: (target_id: number, opts: {trigger?: string}) => void;
by_topic?: (target_id: number, opts: {trigger?: string}) => void;
};
page_params?: {
default_language?: string;
};
page_params?: unknown;
raw_electron_bridge: ElectronBridge;
};
@@ -41,27 +35,6 @@ interface CompatElectronBridge extends ElectronBridge {
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 {
const symbol = Symbol('on' + type);

View File

@@ -1,27 +1,27 @@
import {ipcRenderer, remote, clipboard} from 'electron';
import {feedbackHolder} from './feedback';
import path from 'path';
import escape from 'escape-html';
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
import './tray';
import * as DomainUtil from './utils/domain-util';
import WebView from './components/webview';
import ServerTab from './components/server-tab';
import FunctionalTab from './components/functional-tab';
import * as ConfigUtil from './utils/config-util';
import * as DNDUtil from './utils/dnd-util';
import ReconnectUtil from './utils/reconnect-util';
import Logger from './utils/logger-util';
import * as CommonUtil from './utils/common-util';
import * as EnterpriseUtil from './utils/enterprise-util';
import * as LinkUtil from './utils/link-util';
import * as Messages from '../../resources/messages';
import type {DNDSettings} from './utils/dnd-util';
const {session, app, Menu, dialog} = remote;
interface FunctionalTabProps {
name: string;
@@ -59,7 +59,14 @@ const logger = new Logger({
});
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 {
$addServerButton: HTMLButtonElement;
@@ -116,7 +123,7 @@ class ServerManagerView {
this.$fullscreenPopup = document.querySelector('#fullscreen-popup');
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.activeTabIndex = -1;
@@ -154,19 +161,15 @@ class ServerManagerView {
}
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
if (proxyEnabled) {
await session.fromPartition('persist:webviewsession').setProxy({
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
});
} else {
await session.fromPartition('persist:webviewsession').setProxy({
pacScript: '',
proxyRules: '',
proxyBypassRules: ''
});
}
await session.fromPartition('persist:webviewsession').setProxy(proxyEnabled ? {
pacScript: ConfigUtil.getConfigItem('proxyPAC', ''),
proxyRules: ConfigUtil.getConfigItem('proxyRules', ''),
proxyBypassRules: ConfigUtil.getConfigItem('proxyBypass', '')
} : {
pacScript: '',
proxyRules: '',
proxyBypassRules: ''
});
}
// 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);
await DomainUtil.addDomain(serverConf);
return true;
} catch (error) {
} catch (error: unknown) {
logger.error(error);
logger.error(`Could not add ${domain}. Please contact your system administrator.`);
return false;
@@ -349,7 +352,7 @@ class ServerManagerView {
this.tabs.push(new ServerTab({
role: 'server',
icon: server.icon,
name: CommonUtil.decodeString(server.alias),
name: server.alias,
$root: this.$tabsContainer,
onClick: this.activateLastTab.bind(this, index),
index,
@@ -362,7 +365,7 @@ class ServerManagerView {
tabIndex,
url: server.url,
role: 'server',
name: CommonUtil.decodeString(server.alias),
name: server.alias,
hasPermission: (origin: string, permission: string) =>
origin === server.url && permission === 'notifications',
isActive: () => index === this.activeTabIndex,
@@ -454,7 +457,7 @@ class ServerManagerView {
const $parent = $img.parentElement;
const $container = $parent.parentElement;
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');
if (realmName === null) {
@@ -490,7 +493,7 @@ class ServerManagerView {
}
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.
this.$serverIconTooltip[index].removeAttribute('style');
// 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
// ipcRenderer. Something about webview, and props.webview
// properties in ServerTab causes the app to crash.
get tabsForIpc(): ServerOrFunctionalTab[] {
const tabs: ServerOrFunctionalTab[] = [];
this.tabs.forEach((tab: ServerOrFunctionalTab) => {
const proto = Object.create(Object.getPrototypeOf(tab));
const tabClone = Object.assign(proto, tab);
tabClone.webview = {props: {}};
tabClone.webview.props.name = tab.webview.props.name;
delete tabClone.props.webview;
tabs.push(tabClone);
});
return tabs;
get tabsForIpc(): TabData[] {
return this.tabs.map(tab => ({
role: tab.props.role,
name: tab.props.name,
index: tab.props.index,
webviewName: tab.webview.props.name
}));
}
activateTab(index: number, hideOldTab = true): void {
@@ -681,8 +678,8 @@ class ServerManagerView {
this.functionalTabs.clear();
// Clear DOM elements
this.$tabsContainer.innerHTML = '';
this.$webviewsContainer.innerHTML = '';
this.$tabsContainer.textContent = '';
this.$webviewsContainer.textContent = '';
}
async reloadView(): Promise<void> {
@@ -801,7 +798,7 @@ class ServerManagerView {
['zoomOut', webview => webview.zoomOut()],
['zoomActualSize', webview => webview.zoomActualSize()],
['log-out', webview => webview.logOut()],
['shortcut', webview => webview.showShortcut()],
['show-keyboard-shortcuts', webview => webview.showKeyboardShortcuts()],
['tab-devtools', webview => webview.openDevTools()]
];
@@ -926,11 +923,11 @@ class ServerManagerView {
if (domain.url.includes(serverURL)) {
const serverTooltipSelector = '.tab .server-tooltip';
const serverTooltips = document.querySelectorAll(serverTooltipSelector);
serverTooltips[index].innerHTML = escape(realmName);
this.tabs[index].props.name = escape(realmName);
serverTooltips[index].textContent = realmName;
this.tabs[index].props.name = realmName;
this.tabs[index].webview.props.name = realmName;
domain.alias = escape(realmName);
domain.alias = realmName;
DomainUtil.updateDomain(index, domain);
// Update the realm name also on the Window menu
ipcRenderer.send('update-menu', {
@@ -971,7 +968,7 @@ class ServerManagerView {
webviews.forEach(webview => {
const currentId = webview.getWebContentsId();
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) {
concurrentTab.click();
}

View File

@@ -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;

View File

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

View File

@@ -1,69 +1,8 @@
import {remote} from 'electron';
import Logger from '../utils/logger-util';
const logger = new Logger({
file: 'errors.log',
timestamp: true
});
// Do not change this
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 webContents = remote.getCurrentWebContents();
const webContentsId = webContents.id;
@@ -73,65 +12,3 @@ const webContentsId = webContents.id;
export function focusCurrentServer(): void {
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;
}

View File

@@ -1,20 +1,14 @@
import {remote} from 'electron';
import electron_bridge from '../electron-bridge';
import {appId, loadBots} from './helpers';
import DefaultNotification from './default-notification';
import {appId} from './helpers';
const {app} = remote;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
// On windows 8 we have to explicitly set the appUserModelId otherwise notification won't work.
app.setAppUserModelId(appId);
let Notification = DefaultNotification;
if (process.platform === 'darwin') {
Notification = require('./darwin-notifications');
}
export interface NotificationData {
close: () => void;
title: string;
@@ -39,7 +33,7 @@ export function newNotification(
options: NotificationOptions | undefined,
dispatch: (type: string, eventInit: EventInit) => boolean
): NotificationData {
const notification = new Notification(title, options);
const notification = new DefaultNotification(title, options);
for (const type of ['click', 'close', 'error', 'show']) {
notification.addEventListener(type, (ev: Event) => {
if (!dispatch(type, ev)) {
@@ -67,7 +61,3 @@ export function newNotification(
actions: notification.actions
};
}
electron_bridge.once('zulip-loaded', async () => {
await loadBots();
});

View File

@@ -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();
}
});
}
}

View File

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

View File

@@ -1,12 +1,14 @@
import {ipcRenderer} from 'electron';
import BaseSection from './base-section';
import {htmlEscape} from 'escape-goat';
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 BaseSection from './base-section';
import FindAccounts from './find-accounts';
import ServerInfoForm from './server-info-form';
interface ConnectedOrgSectionProps {
$root: Element;
}
@@ -16,22 +18,19 @@ export default class ConnectedOrgSection extends BaseSection {
$serverInfoContainer: Element | null;
$existingServers: Element | null;
$newOrgButton: HTMLButtonElement | null;
$addCertificateContainer: Element | null;
$findAccountsContainer: Element | null;
constructor(props: ConnectedOrgSectionProps) {
super();
this.props = props;
}
template(): string {
return `
templateHTML(): string {
return htmlEscape`
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">${t.__('Connected organizations')}</div>
<div class="title" id="existing-servers">${t.__('All the connected orgnizations will appear here.')}</div>
<div id="server-info-container"></div>
<div id="new-org-button"><button class="green sea w-250">${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 id="find-accounts-container"></div>
</div>
@@ -43,20 +42,19 @@ export default class ConnectedOrgSection extends BaseSection {
}
initServers(): void {
this.props.$root.innerHTML = '';
this.props.$root.textContent = '';
const servers = DomainUtil.getDomains();
this.props.$root.innerHTML = this.template();
this.props.$root.innerHTML = this.templateHTML();
this.$serverInfoContainer = document.querySelector('#server-info-container');
this.$existingServers = document.querySelector('#existing-servers');
this.$newOrgButton = document.querySelector('#new-org-button');
this.$addCertificateContainer = document.querySelector('#add-certificate-container');
this.$findAccountsContainer = document.querySelector('#find-accounts-container');
const noServerText = t.__('All the connected orgnizations will appear here');
// Show noServerText if no servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? noServerText : '';
this.$existingServers.textContent = servers.length === 0 ? noServerText : '';
for (const [i, server] of servers.entries()) {
new ServerInfoForm({
@@ -71,16 +69,9 @@ export default class ConnectedOrgSection extends BaseSection {
ipcRenderer.send('forward-message', 'open-org-tab');
});
this.initAddCertificate();
this.initFindAccounts();
}
initAddCertificate(): void {
new AddCertificate({
$root: this.$addCertificateContainer
}).init();
}
initFindAccounts(): void {
new FindAccounts({
$root: this.$findAccountsContainer

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,15 @@
import {ipcRenderer} from 'electron';
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 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;
export default class PreferenceView extends BaseComponent {

View File

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

View File

@@ -1,6 +1,9 @@
import {htmlEscape} from 'escape-goat';
import * as t from '../../utils/translation-util';
import BaseSection from './base-section';
import NewServerForm from './new-server-form';
import * as t from '../../utils/translation-util';
interface ServersSectionProps {
$root: Element;
@@ -14,16 +17,16 @@ export default class ServersSection extends BaseSection {
this.props = props;
}
template(): string {
return `
<div class="add-server-modal">
<div class="modal-container">
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">${t.__('Add a Zulip organization')}</div>
<div id="new-server-container"></div>
templateHTML(): string {
return htmlEscape`
<div class="add-server-modal">
<div class="modal-container">
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">${t.__('Add a Zulip organization')}</div>
<div id="new-server-container"></div>
</div>
</div>
</div>
</div>
`;
}
@@ -32,9 +35,9 @@ export default class ServersSection extends BaseSection {
}
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.initNewServerForm();

View File

@@ -1,7 +1,10 @@
import BaseSection from './base-section';
import {htmlEscape} from 'escape-goat';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
import BaseSection from './base-section';
interface ShortcutsSectionProps {
$root: Element;
}
@@ -13,14 +16,14 @@ export default class ShortcutsSection extends BaseSection {
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
// 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 +.
templateMac(): string {
templateMacHTML(): string {
const userOSKey = '⌘';
return `
return htmlEscape`
<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="title">${t.__('Application Shortcuts')}</div>
@@ -181,10 +184,10 @@ export default class ShortcutsSection extends BaseSection {
`;
}
templateWinLin(): string {
templateWinLinHTML(): string {
const userOSKey = 'Ctrl';
return `
return htmlEscape`
<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="title">${t.__('Application Shortcuts')}</div>
@@ -339,7 +342,7 @@ export default class ShortcutsSection extends BaseSection {
init(): void {
this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin();
this.templateMacHTML() : this.templateWinLinHTML();
this.openHotkeysExternalLink();
}
}

View File

@@ -1,21 +1,16 @@
import {contextBridge, ipcRenderer, webFrame} from 'electron';
import fs from 'fs';
import isDev from 'electron-is-dev';
import electron_bridge, {bridgeEvents} from './electron-bridge';
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);
ipcRenderer.on('logout', () => {
if (bridgeEvents.emit('logout')) {
return;
}
// Create the menu for the below
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click();
@@ -24,7 +19,11 @@ ipcRenderer.on('logout', () => {
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
const node: HTMLElement = document.querySelector('a[data-overlay-trigger=keyboard-shortcuts]');
// Additional check
@@ -38,6 +37,10 @@ ipcRenderer.on('shortcut', () => {
});
ipcRenderer.on('show-notification-settings', () => {
if (bridgeEvents.emit('show-notification-settings')) {
return;
}
// Create the menu for the below
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');
dropdown.click();
@@ -53,16 +56,6 @@ ipcRenderer.on('show-notification-settings', () => {
}, 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 => {
if (!event.target.URL.includes('app/renderer/network.html')) {
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(
fs.readFileSync(require.resolve('./injected'), 'utf8')
))();

View File

@@ -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 {};

View File

@@ -1,6 +1,6 @@
import {ipcRenderer, remote, WebviewTag, NativeImage} from 'electron';
import path from 'path';
import * as ConfigUtil from './utils/config-util';
const {Tray, Menu, nativeImage, BrowserWindow} = remote;
@@ -21,6 +21,8 @@ const iconPath = (): string => {
return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'macOSTemplate.png');
};
const winUnreadTrayIconPath = (): string => APP_ICON + 'unread.ico';
let unread = 0;
const trayIconSize = (): number => {
@@ -97,6 +99,10 @@ const renderCanvas = function (arg: number): HTMLCanvasElement {
* @return the native image
*/
const renderNativeImage = function (arg: number): NativeImage {
if (process.platform === 'win32') {
return nativeImage.createFromPath(winUnreadTrayIconPath());
}
const canvas = renderCanvas(arg);
const pngData = nativeImage.createFromDataURL(canvas.toDataURL('image/png')).toPNG();
return nativeImage.createFromBuffer(pngData, {

View File

@@ -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);
}

View File

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

View File

@@ -1,10 +1,11 @@
import {JsonDB} from 'node-json-db';
import electron from 'electron';
import fs from 'fs';
import 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 Logger from './logger-util';
const logger = new Logger({
file: 'config-util.log',
@@ -31,7 +32,7 @@ reloadDB();
export function getConfigItem(key: string, defaultValue: unknown = null): any {
try {
db.reload();
} catch (error) {
} catch (error: unknown) {
logger.error('Error while reloading settings.json: ');
logger.error(error);
}
@@ -59,7 +60,7 @@ export function getConfigString(key: string, defaultValue: string): string {
export function isConfigItemExists(key: string): boolean {
try {
db.reload();
} catch (error) {
} catch (error: unknown) {
logger.error('Error while reloading settings.json: ');
logger.error(error);
}
@@ -88,7 +89,7 @@ function reloadDB(): void {
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
} catch (error) {
} catch (error: unknown) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(

View File

@@ -1,17 +1,11 @@
import electron from 'electron';
import fs from 'fs';
let app: Electron.App = null;
const app = process.type === 'renderer' ? electron.remote.app : electron.app;
let setupCompleted = false;
if (process.type === 'renderer') {
app = electron.remote.app;
} else {
app = electron.app;
}
const zulipDir = app.getPath('userData');
const logDir = `${zulipDir}/Logs/`;
const certificatesDir = `${zulipDir}/certificates/`;
const configDir = `${zulipDir}/config/`;
export const initSetUp = (): void => {
// If it is the first time the app is running
@@ -26,16 +20,11 @@ export const initSetUp = (): void => {
fs.mkdirSync(logDir);
}
if (!fs.existsSync(certificatesDir)) {
fs.mkdirSync(certificatesDir);
}
// 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.
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir);
const domainJson = `${zulipDir}/domain.json`;
const certificatesJson = `${zulipDir}/certificates.json`;
const settingsJson = `${zulipDir}/settings.json`;
const updatesJson = `${zulipDir}/updates.json`;
const windowStateJson = `${zulipDir}/window-state.json`;
@@ -44,10 +33,6 @@ export const initSetUp = (): void => {
path: domainJson,
fileName: 'domain.json'
},
{
path: certificatesJson,
fileName: 'certificates.json'
},
{
path: settingsJson,
fileName: 'settings.json'

View File

@@ -1,12 +1,13 @@
import {JsonDB} from 'node-json-db';
import {remote, ipcRenderer} from 'electron';
import fs from 'fs';
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 Messages from '../../../resources/messages';
import Logger from './logger-util';
const {app, dialog} = remote;
@@ -90,27 +91,6 @@ export function duplicateDomain(domain: string): boolean {
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> {
if (!silent && duplicateDomain(domain)) {
// Do not check duplicate in silent mode
@@ -119,25 +99,9 @@ export async function checkDomain(domain: string, silent = false): Promise<Serve
domain = formatUrl(domain);
const serverConf = {
icon: defaultIconUrl,
url: domain,
alias: domain
};
try {
return await getServerSettings(domain);
} catch (error_) {
// 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;
}
} catch {
throw new Error(Messages.invalidZulipServerError(domain));
}
}
@@ -161,7 +125,7 @@ export async function updateSavedServer(url: string, index: number): Promise<voi
updateDomain(index, newServerConf);
reloadDB();
}
} catch (error) {
} catch (error: unknown) {
logger.log('Could not update server icon.');
logger.log(error);
logger.reportSentry(error);
@@ -173,7 +137,7 @@ function reloadDB(): void {
try {
const file = fs.readFileSync(domainJsonPath, 'utf8');
JSON.parse(file);
} catch (error) {
} catch (error: unknown) {
if (fs.existsSync(domainJsonPath)) {
fs.unlinkSync(domainJsonPath);
dialog.showErrorBox(

View File

@@ -9,7 +9,7 @@ const logger = new Logger({
});
// TODO: replace enterpriseSettings type with an interface once settings are final
let enterpriseSettings: {[key: string]: unknown};
let enterpriseSettings: Record<string, unknown>;
let configFile: boolean;
reloadDB();
@@ -26,7 +26,7 @@ function reloadDB(): void {
try {
const file = fs.readFileSync(enterpriseFile, 'utf8');
enterpriseSettings = JSON.parse(file);
} catch (error) {
} catch (error: unknown) {
logger.log('Error while JSON parsing global_config.json: ');
logger.log(error);
}

View File

@@ -1,9 +1,10 @@
import {shell} from 'electron';
import escape from 'escape-html';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {htmlEscape} from 'escape-goat';
export function isUploadsUrl(server: string, url: URL): boolean {
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-')
);
const file = path.join(dir, 'redirect.html');
fs.writeFileSync(file, `\
fs.writeFileSync(file, htmlEscape`\
<!DOCTYPE html>
<html>
<head>
<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>
<style>
html {
@@ -32,11 +33,11 @@ export async function openBrowser(url: URL): Promise<void> {
</style>
</head>
<body>
<p>Opening <a href="${escape(url.href)}">${escape(url.href)}</a>…</p>
<p>Opening <a href="${url.href}">${url.href}</a>…</p>
</body>
</html>
`);
shell.openItem(file);
await shell.openPath(file);
setTimeout(() => {
fs.unlinkSync(file);
fs.rmdirSync(dir);

View File

@@ -1,8 +1,9 @@
import {JsonDB} from 'node-json-db';
import electron from 'electron';
import fs from 'fs';
import path from 'path';
import electron from 'electron';
import {JsonDB} from 'node-json-db';
import Logger from './logger-util';
const remote =
@@ -46,7 +47,7 @@ function reloadDB(): void {
try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
JSON.parse(file);
} catch (error) {
} catch (error: unknown) {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(

View File

@@ -1,11 +1,12 @@
import {Console} from 'console'; // eslint-disable-line node/prefer-global/console
import {initSetUp} from './default-util';
import {sentryInit, captureException} from './sentry-util';
import electron from 'electron';
import fs from 'fs';
import os from 'os';
import isDev from 'electron-is-dev';
import electron from 'electron';
import {initSetUp} from './default-util';
import {sentryInit, captureException} from './sentry-util';
interface LoggerOptions {
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) => {
if (err) {
throw err;

View File

@@ -1,7 +1,10 @@
import {ipcRenderer} from 'electron';
import type WebView from '../components/webview';
import backoff from 'backoff';
import {htmlEscape} from 'escape-goat';
import type WebView from '../components/webview';
import Logger from './logger-util';
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.');
const errorMessageHolder = document.querySelector('#description');
if (errorMessageHolder) {
errorMessageHolder.innerHTML = `
errorMessageHolder.innerHTML = htmlEscape`
<div>Your internet connection doesn't seem to work properly!</div>
<div>Verify that it works and then click try again.</div>`;
}

View File

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

View File

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

View File

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

View File

@@ -61,5 +61,4 @@
// it messes up require module path resolution
require('./js/main');
</script>
<script>require('./js/shared/preventdrag.js')</script>
</html>

View File

@@ -16,6 +16,5 @@
<script>
document.querySelector('#tagify-css').href = require.resolve('@yaireo/tagify/dist/tagify.css');
require('./js/pages/preference/preference.js');
require('./js/shared/preventdrag.js')
</script>
</html>

Binary file not shown.

View File

@@ -8,7 +8,7 @@ export function invalidZulipServerError(domain: string): string {
\n • You can connect to that URL in a web browser.\
\n • If you need a proxy to connect to the Internet, that you've configured your proxy in the Network settings.\
\n • It's a Zulip server. (The oldest supported version is 1.6).\
\n • The server has a valid certificate. (You can add custom certificates in Settings > Organizations). \
\n • The server has a valid certificate. \
\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`;
}
@@ -18,15 +18,6 @@ export function noOrgsError(domain: string): string {
\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 {
let domainList = '';
for (const domain of domains) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

View File

@@ -2,6 +2,60 @@
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
**Security fixes**:

View File

@@ -1,16 +1,16 @@
'use strict';
const gulp = require('gulp');
const {execSync} = require('child_process');
const electron = require('electron-connect').server.create({
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 {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 globOptions = {cwd: __dirname};
const jsFiles = glob.sync(baseFilePattern + '.js', globOptions);

5996
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "5.4.0",
"version": "5.5.0",
"main": "./app/main",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
@@ -24,7 +24,7 @@
"start": "tsc && electron .",
"clean-ts-files": "git clean app/*.js -e node_modules -xf",
"watch-ts": "tsc -w",
"reinstall": "node ./tools/reinstall-node-modules.js",
"reinstall": "rimraf node_modules && npm install",
"postinstall": "electron-builder install-app-deps",
"lint-css": "stylelint app/renderer/css/*.css",
"lint-html": "./node_modules/.bin/htmlhint \"app/renderer/*.html\" ",
@@ -50,7 +50,7 @@
"files": [
"app/**/*"
],
"copyright": "©2019 Kandra Labs, Inc.",
"copyright": "©2020 Kandra Labs, Inc.",
"mac": {
"category": "public.app-category.productivity",
"target": [
@@ -146,64 +146,67 @@
],
"dependencies": {
"@electron-elements/send-feedback": "^2.0.3",
"@sentry/electron": "^1.4.0",
"@yaireo/tagify": "^3.15.3",
"adm-zip": "^0.4.16",
"@sentry/electron": "^2.0.4",
"@yaireo/tagify": "^3.21.5",
"adm-zip": "^0.5.1",
"auto-launch": "^5.0.5",
"backoff": "^2.5.0",
"electron-is-dev": "^1.2.0",
"electron-log": "^4.2.2",
"electron-updater": "^4.3.1",
"electron-log": "^4.3.0",
"electron-updater": "^4.3.5",
"electron-window-state": "^5.0.3",
"escape-html": "^1.0.3",
"escape-goat": "^3.0.0",
"fs-extra": "^9.0.1",
"get-stream": "^5.1.0",
"i18n": "^0.10.0",
"iso-639-1": "^2.1.3",
"nan": "^2.14.0",
"get-stream": "^6.0.0",
"i18n": "^0.13.2",
"iso-639-1": "^2.1.4",
"node-json-db": "^1.1.0",
"semver": "^7.3.2"
},
"optionalDependencies": {
"node-mac-notifier": "^1.1.0"
"semver": "^7.3.4"
},
"devDependencies": {
"@types/adm-zip": "^0.4.33",
"@types/auto-launch": "^5.0.1",
"@types/backoff": "^2.5.1",
"@types/escape-html": "^1.0.0",
"@types/fs-extra": "^9.0.1",
"@types/i18n": "^0.8.6",
"@types/node": "^14.0.23",
"@types/fs-extra": "^9.0.4",
"@types/i18n": "^0.8.8",
"@types/node": "^14.14.10",
"@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",
"electron": "^8.4.0",
"electron-builder": "^22.7.0",
"electron": "^11.0.3",
"electron-builder": "^22.9.1",
"electron-connect": "^0.6.3",
"electron-notarize": "^1.0.0",
"glob": "^7.1.6",
"gulp": "^4.0.2",
"gulp-tape": "^1.0.0",
"gulp-typescript": "^6.0.0-alpha.1",
"htmlhint": "^0.14.1",
"nodemon": "^2.0.4",
"htmlhint": "^0.14.2",
"nodemon": "^2.0.6",
"pre-commit": "^1.2.2",
"rimraf": "^3.0.2",
"spectron": "^10.0.1",
"stylelint": "^13.6.1",
"spectron": "^13.0.0",
"stylelint": "^13.8.0",
"tap-colorize": "^1.2.0",
"tape": "^5.0.1",
"typescript": "^3.9.7",
"xo": "^0.32.1"
"typescript": "^4.1.2",
"xo": "^0.35.0"
},
"xo": {
"rules": {
"@typescript-eslint/no-dynamic-delete": "off",
"@typescript-eslint/prefer-readonly-parameter-types": "off",
"arrow-body-style": "error",
"import/first": "error",
"import/newline-after-import": "error",
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always"
}
],
"import/unambiguous": "error",
"max-lines": [
"warn",
@@ -226,8 +229,7 @@
"app/renderer/js/injected.ts",
"gulpfile.js",
"scripts/notarize.js",
"tests/**/*.js",
"tools/reinstall-node-modules.js"
"tests/**/*.js"
],
"parserOptions": {
"sourceType": "script"

View File

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

View File

@@ -1,5 +1,6 @@
'use strict';
const test = require('tape');
const setup = require('./setup');
test('app runs', async t => {
@@ -9,7 +10,7 @@ test('app runs', async t => {
try {
await setup.waitForLoad(app, t);
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);
} catch (error) {
await setup.endTest(app, t, error || 'error');

View File

@@ -1,8 +1,9 @@
'use strict';
const {Application} = require('spectron');
const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');
const {Application} = require('spectron');
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)
async function endTest(app, t, err) {
await app.client.windowByIndex(0);
await app.stop();
t.end(err);
}

View File

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

View File

@@ -1,5 +1,6 @@
'use strict';
const test = require('tape');
const setup = require('./setup');
// Create new org link should open in the default browser [WIP]
@@ -11,7 +12,7 @@ test('new-org-link', async t => {
try {
await setup.waitForLoad(app, t);
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.endTest(app, t);
} catch (error) {

View File

@@ -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

View File

@@ -1,7 +0,0 @@
@echo off
echo "Removing node_modules"
rmdir /s /q node_modules
echo "node_modules removed reinstalling npm packages"
npm i

View File

@@ -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
View File

@@ -21,8 +21,6 @@ declare module '@electron-elements/send-feedback' {
declare module 'electron-connect';
declare module 'node-mac-notifier';
declare module '@yaireo/tagify';
interface ClipboardDecrypter {
@@ -32,7 +30,7 @@ interface ClipboardDecrypter {
}
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;
new_notification: (
title: string,