Compare commits

...

38 Commits

Author SHA1 Message Date
Anders Kaseorg
e03de26137 release: New release v5.2.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-05 12:54:35 -07:00
Anders Kaseorg
983254c310 CVE-2020-12637: Do not ignore certificate errors without ignoreCerts.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-05 12:54:35 -07:00
Anders Kaseorg
b6059077d8 Generalize permission-response mechanism to renderer-callback.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-05 12:54:35 -07:00
Anders Kaseorg
cafff9a008 new-server-form: Add missing string conversion for error message.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-05 12:54:35 -07:00
Anders Kaseorg
190204b2e5 notarize: Fix lint errors.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-05 12:54:09 -07:00
Akash Nimare
4c25c99abc notarization: Pass team short name info in code-signing. 2020-05-06 01:13:29 +05:30
Anders Kaseorg
55be93b906 dependencies: Upgrade dependencies.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-04 01:56:35 -07:00
Anders Kaseorg
34e2b3a3d0 typescript: Eradicate most any annotations.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-04 01:08:05 -07:00
Anders Kaseorg
e5ece8db9e package.json: Fix ‘npm start’ to do something useful.
The old run-dev script raced electron and tsc against each other in
parallel, which usually meant electron would run stale JS files.  We
should overhaul the build process at some point, but whatever we do,
this script isn’t going to be part of it.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 20:35:21 -07:00
Anders Kaseorg
40b26dbb0e dnd-util: Refine typing.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 20:03:41 -07:00
Anders Kaseorg
ae4f03f4ba logger-util: Clean up typing disaster zone.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 20:03:41 -07:00
Anders Kaseorg
8ea32a7a96 feedback: Stub send-feedback types.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 19:33:52 -07:00
Anders Kaseorg
6b7cce0366 ServerManagerView: Convert loading from object to Set.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 19:16:05 -07:00
Anders Kaseorg
73fec72e6d registerIpcs: Convert webviewListeners to array with functions.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 19:09:22 -07:00
Anders Kaseorg
920adfb169 CI: Clean unused steps from CI configuration.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-03 15:02:58 -07:00
Manav Mehta
98174fdcaf help: Update Help Center URL.
Earlier the menu option used to redirect to *.zulipchat.com/help which is already reachable from the webapp.
This will enable the desktop app to redirect only to zulipchat.com/help instead of the help page of the corresponding organization.

Fixes: #948.
2020-05-02 20:17:25 +05:30
Anders Kaseorg
a0c033431e electron-bridge: Add decrypt_clipboard helper.
This one helper allows us to implement browser-based social login
entirely on the server side.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-05-01 15:00:02 -07:00
Abhigyan Khaund
82421d843a downloadFiles: Fix issue of showing two Save As dialog box.
Currently, there are two dialog boxes shown while downloading files (in Ubuntu). One by default behavior of electron and other by the dialog box for save as feature.
This PR fixes this issue by using electron's save as dialog box.

Fixes: #947.
2020-05-01 17:55:46 +05:30
Anders Kaseorg
d9afee3330 ServerManagerView: Convert functionalTabs from object to Map.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
a46f2ed618 xo: Enable import/no-mutable-exports.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
9f3b4ff408 xo: Remove unicorn/string-content exclusion.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
fb800f7862 xo: Enable @typescript-eslint/no-unused-vars.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
ba191c3699 xo: Enable object-curly-spacing.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
e49a880ed6 xo: Enable capitalized-comments.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
4bfa7c9265 xo: Enable no-else-return.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
39c6fa4ace xo: Enable @typescript-eslint/member-ordering.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
963c2e5388 xo: Enable @typescript-eslint/restrict-plus-operands.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
849df4adaf xo: Enable @typescript-eslint/restrict-template-expressions.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
fc6ff83485 xo: Enable no-alert.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
5ae2a717fa xo: Enable padding-line-between-statements.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
cfdc08a038 xo: Enable unicorn/catch-error-name.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
b76467529d xo: Upgrade xo to 0.30.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
bb88a7b7a8 xo: Handle floating promises.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
0225778050 typescript: Annotate queueDomain.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
2154b191c8 typescript: Annotate url in WebviewProps.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
4093304b4d typescript: Annotate electron-updater events.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Anders Kaseorg
2e03f779e8 typescript: Annotate setMenu props.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-30 13:48:18 -07:00
Manav Mehta
9464390070 docs: Update changelog for release 5.1.0. 2020-04-30 00:48:31 +05:30
61 changed files with 1426 additions and 1646 deletions

View File

@@ -1,6 +1,3 @@
sudo: required
dist: trusty
os:
- osx
- linux
@@ -18,20 +15,9 @@ node_js:
- '10'
- '12'
before_install:
- ./scripts/travis-xvfb.sh
- npm install -g gulp
- npm ci
cache:
directories:
- node_modules
script:
- npm run travis
notifications:
webhooks:
urls:
- https://zulip.org/zulipbot/travis
on_success: always
on_failure: always
- npm run test

View File

@@ -1,6 +1,6 @@
import { app, dialog } from 'electron';
import { autoUpdater } from 'electron-updater';
import { linuxUpdateNotification } from './linuxupdater'; // Required only in case of linux
import {app, dialog} from 'electron';
import {UpdateDownloadedEvent, UpdateInfo, autoUpdater} from 'electron-updater';
import {linuxUpdateNotification} from './linuxupdater'; // Required only in case of linux
import log from 'electron-log';
import isDev from 'electron-is-dev';
@@ -34,57 +34,59 @@ export function appUpdater(updateFromMenu = false): void {
autoUpdater.allowPrerelease = isBetaUpdate || false;
const eventsListenerRemove = ['update-available', 'update-not-available'];
autoUpdater.on('update-available', info => {
autoUpdater.on('update-available', async (info: UpdateInfo) => {
if (updateFromMenu) {
dialog.showMessageBox({
message: `A new version ${info.version}, of Zulip Desktop is available`,
detail: 'The update will be downloaded in the background. You will be notified when it is ready to be installed.'
});
updateAvailable = true;
// This is to prevent removal of 'update-downloaded' and 'error' event listener.
eventsListenerRemove.forEach(event => {
autoUpdater.removeAllListeners(event);
});
await dialog.showMessageBox({
message: `A new version ${info.version}, of Zulip Desktop is available`,
detail: 'The update will be downloaded in the background. You will be notified when it is ready to be installed.'
});
}
});
autoUpdater.on('update-not-available', () => {
autoUpdater.on('update-not-available', async () => {
if (updateFromMenu) {
dialog.showMessageBox({
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
await dialog.showMessageBox({
message: 'No updates available',
detail: `You are running the latest version of Zulip Desktop.\nVersion: ${app.getVersion()}`
});
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
}
});
autoUpdater.on('error', async error => {
autoUpdater.on('error', async (error: Error) => {
if (updateFromMenu) {
const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates');
const { response } = await dialog.showMessageBox({
type: 'error',
buttons: ['Manual Download', 'Cancel'],
message: messageText,
detail: (error).toString() + `\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulipchat.com/apps/.\n
Current Version: ${app.getVersion()}`
});
if (response === 0) {
LinkUtil.openBrowser(new URL('https://zulipchat.com/apps/'));
}
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
// listeners don't trigger multiple times.
autoUpdater.removeAllListeners();
const messageText = (updateAvailable) ? ('Unable to download the updates') : ('Unable to check for updates');
const {response} = await dialog.showMessageBox({
type: 'error',
buttons: ['Manual Download', 'Cancel'],
message: messageText,
detail: `Error: ${error.message}\n\nThe latest version of Zulip Desktop is available at -\nhttps://zulipchat.com/apps/.\n
Current Version: ${app.getVersion()}`
});
if (response === 0) {
await LinkUtil.openBrowser(new URL('https://zulipchat.com/apps/'));
}
}
});
// Ask the user if update is available
autoUpdater.on('update-downloaded', async event => {
autoUpdater.on('update-downloaded', async (event: UpdateDownloadedEvent) => {
// Ask user to update the app
const { response } = await dialog.showMessageBox({
const {response} = await dialog.showMessageBox({
type: 'question',
buttons: ['Install and Relaunch', 'Install Later'],
defaultId: 0,
@@ -94,11 +96,11 @@ export function appUpdater(updateFromMenu = false): void {
if (response === 0) {
setTimeout(() => {
autoUpdater.quitAndInstall();
// force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
// Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
app.quit();
}, 1000);
}
});
// Init for updates
autoUpdater.checkForUpdates();
(async () => autoUpdater.checkForUpdates())();
}

View File

@@ -1,14 +1,15 @@
import { sentryInit } from '../renderer/js/utils/sentry-util';
import { appUpdater } from './autoupdater';
import { setAutoLaunch } from './startup';
import {sentryInit} from '../renderer/js/utils/sentry-util';
import {appUpdater} from './autoupdater';
import {setAutoLaunch} from './startup';
import windowStateKeeper from 'electron-window-state';
import path from 'path';
import fs from 'fs';
import electron, { app, ipcMain, session, dialog } from 'electron';
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';
@@ -42,13 +43,26 @@ if (singleInstanceLock) {
app.quit();
}
const rendererCallbacks = new Map();
let nextRendererCallbackId = 0;
ipcMain.on('renderer-callback', (event: Event, rendererCallbackId: number, ...args: any[]) => {
rendererCallbacks.get(rendererCallbackId)(...args);
rendererCallbacks.delete(rendererCallbackId);
});
function makeRendererCallback(callback: (...args: any[]) => void): number {
rendererCallbacks.set(nextRendererCallbackId, callback);
return nextRendererCallbackId++;
}
const APP_ICON = path.join(__dirname, '../resources', 'Icon');
const iconPath = (): string => {
return APP_ICON + (process.platform === 'win32' ? '.ico' : '.png');
};
// toggle the app window
// Toggle the app window
const toggleApp = (): void => {
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
mainWindow.show();
@@ -91,13 +105,14 @@ function createMainWindow(): Electron.BrowserWindow {
win.webContents.send('focus');
});
win.loadURL(mainURL);
(async () => win.loadURL(mainURL))();
// Keep the app running in background on close event
win.on('close', event => {
if (ConfigUtil.getConfigItem('quitOnClose')) {
app.quit();
}
if (!isQuitting) {
event.preventDefault();
@@ -141,16 +156,10 @@ app.disableHardwareAcceleration();
// More info here - https://github.com/electron/electron/issues/10732
app.commandLine.appendSwitch('force-color-profile', 'srgb');
// eslint-disable-next-line max-params
app.on('certificate-error', (event: Event, _webContents: Electron.WebContents, _url: string, _error: string, _certificate: any, callback) => {
event.preventDefault();
callback(true);
});
// this event is only available on macOS. Triggers when you click on the dock icon.
// This event is only available on macOS. Triggers when you click on the dock icon.
app.on('activate', () => {
if (mainWindow) {
// if there is already a window show it
// If there is already a window show it
mainWindow.show();
} else {
mainWindow = createMainWindow();
@@ -182,7 +191,7 @@ app.on('ready', () => {
const isSystemProxy = ConfigUtil.getConfigItem('useSystemProxy');
if (isSystemProxy) {
ProxyUtil.resolveSystemProxy(mainWindow);
(async () => ProxyUtil.resolveSystemProxy(mainWindow))();
}
const page = mainWindow.webContents;
@@ -206,25 +215,62 @@ app.on('ready', () => {
}
});
const permissionCallbacks = new Map();
let nextPermissionId = 0;
app.on('certificate-error', (
event: Event,
webContents: Electron.WebContents,
url: 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.
event.preventDefault();
const {origin} = new URL(url);
const filename = CertificateUtil.getCertificate(encodeURIComponent(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, '')) {
callback(true);
return;
}
} catch (error) {
console.error(`Error reading certificate file ${filename}:`, error);
}
}
page.send(
'certificate-error',
webContents.id === mainWindow.webContents.id ? null : webContents.id,
makeRendererCallback(ignore => {
callback(ignore);
if (!ignore) {
dialog.showErrorBox(
'Certificate error',
`The server presented an invalid certificate for ${origin}:
${error}`
);
}
})
);
});
page.session.setPermissionRequestHandler((webContents, permission, callback, details) => {
const {origin} = new URL(details.requestingUrl);
permissionCallbacks.set(nextPermissionId, callback);
page.send('permission-request', nextPermissionId, {
page.send('permission-request', {
webContentsId: webContents.id === mainWindow.webContents.id ?
null :
webContents.id,
origin,
permission
});
nextPermissionId++;
});
ipcMain.on('permission-response', (event: Event, permissionId: number, grant: boolean) => {
permissionCallbacks.get(permissionId)(grant);
permissionCallbacks.delete(permissionId);
}, makeRendererCallback(callback));
});
// Temporarily remove this event
@@ -303,11 +349,11 @@ app.on('ready', () => {
BadgeSettings.updateTaskbarIcon(data, text, mainWindow);
});
ipcMain.on('forward-message', (_event: Electron.IpcMainEvent, listener: string, ...parameters: any[]) => {
ipcMain.on('forward-message', (_event: Electron.IpcMainEvent, listener: string, ...parameters: unknown[]) => {
page.send(listener, ...parameters);
});
ipcMain.on('update-menu', (_event: Electron.IpcMainEvent, props: any) => {
ipcMain.on('update-menu', (_event: Electron.IpcMainEvent, props: AppMenu.MenuProps) => {
AppMenu.setMenu(props);
const activeTab = props.tabs[props.activeTabIndex];
if (activeTab) {
@@ -315,27 +361,18 @@ app.on('ready', () => {
}
});
ipcMain.on('toggleAutoLauncher', (_event: Electron.IpcMainEvent, AutoLaunchValue: boolean) => {
setAutoLaunch(AutoLaunchValue);
ipcMain.on('toggleAutoLauncher', async (_event: Electron.IpcMainEvent, AutoLaunchValue: boolean) => {
await setAutoLaunch(AutoLaunchValue);
});
ipcMain.on('downloadFile', (_event: Electron.IpcMainEvent, url: string, downloadPath: string) => {
page.downloadURL(url);
page.session.once('will-download', async (_event: Event, item) => {
let setFilePath: string;
let shortFileName: string;
if (ConfigUtil.getConfigItem('promptDownload', false)) {
const showDialogOptions: object = {
defaultPath: path.join(downloadPath, item.getFilename())
};
const result = await dialog.showSaveDialog(mainWindow, showDialogOptions);
if (result.canceled) {
item.cancel();
return;
}
setFilePath = result.filePath;
shortFileName = path.basename(setFilePath);
item.setSaveDialogOptions(showDialogOptions);
} else {
const getTimeStamp = (): number => {
const date = new Date();
@@ -352,12 +389,10 @@ app.on('ready', () => {
// Update the name and path of the file if it already exists
const updatedFilePath = path.join(downloadPath, formatFile(filePath));
setFilePath = fs.existsSync(filePath) ? updatedFilePath : filePath;
shortFileName = fs.existsSync(filePath) ? formatFile(filePath) : item.getFilename();
const setFilePath: string = fs.existsSync(filePath) ? updatedFilePath : filePath;
item.setSavePath(setFilePath);
}
item.setSavePath(setFilePath);
const updatedListener = (_event: Event, state: string): void => {
switch (state) {
case 'interrupted': {
@@ -366,26 +401,31 @@ app.on('ready', () => {
item.cancel();
break;
}
case 'progressing': {
if (item.isPaused()) {
item.cancel();
}
// This event can also be used to show progress in percentage in future.
break;
}
default: {
console.info('Unknown updated state of download item');
}
}
};
item.on('updated', updatedListener);
item.once('done', (_event: Event, state) => {
if (state === 'completed') {
page.send('downloadFileCompleted', item.getSavePath(), shortFileName);
page.send('downloadFileCompleted', item.getSavePath(), path.basename(item.getSavePath()));
} else {
console.log('Download failed state:', state);
page.send('downloadFileFailed');
page.send('downloadFileFailed', state);
}
// To stop item for listening to updated events of this file
item.removeListener('updated', updatedListener);
});

View File

@@ -1,4 +1,4 @@
import { app, Notification } from 'electron';
import {app, Notification} from 'electron';
import request from 'request';
import semver from 'semver';
@@ -24,20 +24,24 @@ export function linuxUpdateNotification(): void {
ecdhCurve: 'auto'
};
request(options, (error: any, response: any, body: any) => {
request(options, (error, response: request.Response, body: string) => {
if (error) {
logger.error('Linux update error.');
logger.error(error);
return;
}
if (response.statusCode < 400) {
const data = JSON.parse(body);
const latestVersion = ConfigUtil.getConfigItem('betaUpdate') ? data[0].tag_name : data.tag_name;
if (typeof latestVersion !== 'string') {
throw new TypeError('Expected string for tag_name');
}
if (semver.gt(latestVersion, app.getVersion())) {
const notified = LinuxUpdateUtil.getUpdateItem(latestVersion);
if (notified === null) {
new Notification({title: 'Zulip Update', body: 'A new version ' + latestVersion + ' is available. Please update using your package manager.'}).show();
new Notification({title: 'Zulip Update', body: `A new version ${latestVersion} is available. Please update using your package manager.`}).show();
LinuxUpdateUtil.setUpdateItem(latestVersion, true);
}
}

View File

@@ -1,5 +1,5 @@
import { app, shell, BrowserWindow, Menu, dialog } from 'electron';
import { appUpdater } from './autoupdater';
import {app, shell, BrowserWindow, Menu, dialog} from 'electron';
import {appUpdater} from './autoupdater';
import AdmZip from 'adm-zip';
import fs from 'fs-extra';
@@ -9,6 +9,13 @@ import Logger from '../renderer/js/utils/logger-util';
import * as ConfigUtil from '../renderer/js/utils/config-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';
export interface MenuProps {
tabs: ServerOrFunctionalTab[];
activeTabIndex?: number;
enableMenu?: boolean;
}
const appName = app.name;
@@ -22,7 +29,7 @@ function getHistorySubmenu(enableMenu: boolean): Electron.MenuItemConstructorOpt
label: t.__('Back'),
accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left',
enabled: enableMenu,
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('back');
}
@@ -31,7 +38,7 @@ function getHistorySubmenu(enableMenu: boolean): Electron.MenuItemConstructorOpt
label: t.__('Forward'),
accelerator: process.platform === 'darwin' ? 'Command+Right' : 'Alt+Right',
enabled: enableMenu,
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('forward');
}
@@ -48,8 +55,8 @@ function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
},
{
label: t.__('Release Notes'),
click() {
LinkUtil.openBrowser(new URL(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`));
async click() {
await LinkUtil.openBrowser(new URL(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`));
}
},
{
@@ -58,8 +65,8 @@ function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
{
label: t.__('Factory Reset'),
accelerator: process.platform === 'darwin' ? 'Command+Shift+D' : 'Ctrl+Shift+D',
click() {
resetAppSettings();
async click() {
await resetAppSettings();
}
},
{
@@ -87,7 +94,7 @@ function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
{
label: t.__('Toggle DevTools for Zulip App'),
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.openDevTools({mode: 'undocked'});
}
@@ -96,7 +103,7 @@ function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
{
label: t.__('Toggle DevTools for Active Tab'),
accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('tab-devtools');
}
@@ -108,7 +115,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
return [{
label: t.__('Reload'),
accelerator: 'CommandOrControl+R',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('reload-current-viewer');
}
@@ -116,7 +123,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
}, {
label: t.__('Hard Reload'),
accelerator: 'CommandOrControl+Shift+R',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('hard-reload');
}
@@ -129,7 +136,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
}, {
label: t.__('Zoom In'),
role: 'zoomIn',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('zoomIn');
}
@@ -138,7 +145,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
label: t.__('Zoom Out'),
role: 'zoomOut',
accelerator: 'CommandOrControl+-',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('zoomOut');
}
@@ -147,7 +154,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
label: t.__('Actual Size'),
role: 'resetZoom',
accelerator: 'CommandOrControl+0',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('zoomActualSize');
}
@@ -156,7 +163,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
type: 'separator'
}, {
label: t.__('Toggle Tray Icon'),
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.send('toggletray');
}
@@ -164,7 +171,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
}, {
label: t.__('Toggle Sidebar'),
accelerator: 'CommandOrControl+Shift+S',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem('showSidebar');
focusedWindow.webContents.send('toggle-sidebar', newValue);
@@ -175,7 +182,7 @@ function getViewSubmenu(): Electron.MenuItemConstructorOptions[] {
label: t.__('Auto hide Menu bar'),
checked: ConfigUtil.getConfigItem('autoHideMenubar', false),
visible: process.platform !== 'darwin',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
const newValue = !ConfigUtil.getConfigItem('autoHideMenubar');
focusedWindow.autoHideMenuBar = newValue;
@@ -196,7 +203,7 @@ function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
},
{
label: t.__('About Zulip'),
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('open-about');
}
@@ -213,7 +220,7 @@ function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
{
label: t.__('Report an Issue'),
click() {
// the goal is to notify the main.html BrowserWindow
// The goal is to notify the main.html BrowserWindow
// which may not be the focused window.
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('open-feedback-modal');
@@ -223,7 +230,7 @@ function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
];
}
function getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boolean): Electron.MenuItemConstructorOptions[] {
function getWindowSubmenu(tabs: ServerOrFunctionalTab[], activeTabIndex: number): Electron.MenuItemConstructorOptions[] {
const initialSubmenu: Electron.MenuItemConstructorOptions[] = [{
label: t.__('Minimize'),
role: 'minimize'
@@ -247,7 +254,7 @@ function getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boole
label: tab.props.name,
accelerator: tab.props.role === 'function' ? '' : `${ShortcutKey} + ${tab.props.index + 1}`,
checked: tab.props.index === activeTabIndex,
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('switch-server-tab', tab.props.index);
}
@@ -262,7 +269,7 @@ function getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boole
label: t.__('Switch to Next Organization'),
accelerator: 'Ctrl+Tab',
enabled: tabs.length > 1,
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('switch-server-tab', getNextServer(tabs, activeTabIndex));
}
@@ -271,7 +278,7 @@ function getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boole
label: t.__('Switch to Previous Organization'),
accelerator: 'Ctrl+Shift+Tab',
enabled: tabs.length > 1,
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('switch-server-tab', getPreviousServer(tabs, activeTabIndex));
}
@@ -282,15 +289,15 @@ function getWindowSubmenu(tabs: any[], activeTabIndex: number, enableMenu: boole
return initialSubmenu;
}
function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
const { tabs, activeTabIndex, enableMenu } = props;
function getDarwinTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu} = props;
return [{
label: app.name,
submenu: [{
label: t.__('Add Organization'),
accelerator: 'Cmd+Shift+N',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('new-server');
}
@@ -305,7 +312,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
}, {
label: t.__('Desktop Settings'),
accelerator: 'Cmd+,',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('open-settings');
}
@@ -314,7 +321,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Keyboard Shortcuts'),
accelerator: 'Cmd+Shift+K',
enabled: enableMenu,
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('shortcut');
}
@@ -325,7 +332,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Copy Zulip URL'),
accelerator: 'Cmd+Shift+C',
enabled: enableMenu,
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('copy-zulip-url');
}
@@ -334,7 +341,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Log Out of Organization'),
accelerator: 'Cmd+L',
enabled: enableMenu,
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('log-out');
}
@@ -373,7 +380,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
submenu: [{
label: t.__('Undo'),
accelerator: 'Cmd+Z',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('undo');
}
@@ -381,7 +388,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
}, {
label: t.__('Redo'),
accelerator: 'Cmd+Shift+Z',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('redo');
}
@@ -412,7 +419,7 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
submenu: getHistorySubmenu(enableMenu)
}, {
label: t.__('Window'),
submenu: getWindowSubmenu(tabs, activeTabIndex, enableMenu)
submenu: getWindowSubmenu(tabs, activeTabIndex)
}, {
label: t.__('Tools'),
submenu: getToolsSubmenu()
@@ -423,14 +430,14 @@ function getDarwinTpl(props: any): Electron.MenuItemConstructorOptions[] {
}];
}
function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
const { tabs, activeTabIndex, enableMenu } = props;
function getOtherTpl(props: MenuProps): Electron.MenuItemConstructorOptions[] {
const {tabs, activeTabIndex, enableMenu} = props;
return [{
label: t.__('File'),
submenu: [{
label: t.__('Add Organization'),
accelerator: 'Ctrl+Shift+N',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('new-server');
}
@@ -447,7 +454,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
}, {
label: t.__('Desktop Settings'),
accelerator: 'Ctrl+,',
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('open-settings');
}
@@ -456,7 +463,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Keyboard Shortcuts'),
accelerator: 'Ctrl+Shift+K',
enabled: enableMenu,
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('shortcut');
}
@@ -467,7 +474,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Copy Zulip URL'),
accelerator: 'Ctrl+Shift+C',
enabled: enableMenu,
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('copy-zulip-url');
}
@@ -476,7 +483,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
label: t.__('Log Out of Organization'),
accelerator: 'Ctrl+L',
enabled: enableMenu,
click(_item: any, focusedWindow: any) {
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('log-out');
}
@@ -530,7 +537,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
submenu: getHistorySubmenu(enableMenu)
}, {
label: t.__('Window'),
submenu: getWindowSubmenu(tabs, activeTabIndex, enableMenu)
submenu: getWindowSubmenu(tabs, activeTabIndex)
}, {
label: t.__('Tools'),
submenu: getToolsSubmenu()
@@ -541,7 +548,7 @@ function getOtherTpl(props: any): Electron.MenuItemConstructorOptions[] {
}];
}
function sendAction(action: string, ...parameters: any[]): void {
function sendAction(action: string, ...parameters: unknown[]): void {
const win = BrowserWindow.getAllWindows()[0];
if (process.platform === 'darwin') {
@@ -555,19 +562,21 @@ function checkForUpdate(): void {
appUpdater(true);
}
function getNextServer(tabs: any[], activeTabIndex: number): number {
function getNextServer(tabs: ServerOrFunctionalTab[], activeTabIndex: number): number {
do {
activeTabIndex = (activeTabIndex + 1) % tabs.length;
}
while (tabs[activeTabIndex].props.role !== 'server');
return activeTabIndex;
}
function getPreviousServer(tabs: any[], activeTabIndex: number): number {
function getPreviousServer(tabs: ServerOrFunctionalTab[], activeTabIndex: number): number {
do {
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
}
while (tabs[activeTabIndex].props.role !== 'server');
return activeTabIndex;
}
@@ -577,7 +586,7 @@ async function resetAppSettings(): Promise<void> {
// We save App's settings/configurations in following files
const settingFiles = ['config/window-state.json', 'config/domain.json', 'config/settings.json', 'config/certificates.json'];
const { response } = await dialog.showMessageBox({
const {response} = await dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
@@ -587,7 +596,7 @@ async function resetAppSettings(): Promise<void> {
if (response === 0) {
settingFiles.forEach(settingFileName => {
const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
fs.access(getSettingFilesPath, (error: any) => {
fs.access(getSettingFilesPath, (error: NodeJS.ErrnoException) => {
if (error) {
logger.error('Error while resetting app settings.');
logger.error(error);
@@ -601,7 +610,7 @@ async function resetAppSettings(): Promise<void> {
}
}
export function setMenu(props: any): void {
export function setMenu(props: MenuProps): void {
const tpl = process.platform === 'darwin' ? getDarwinTpl(props) : getOtherTpl(props);
const menu = Menu.buildFromTemplate(tpl);
Menu.setApplicationMenu(menu);

View File

@@ -1,10 +1,10 @@
import { app } from 'electron';
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 = (AutoLaunchValue: boolean): void => {
export const setAutoLaunch = async (AutoLaunchValue: boolean): Promise<void> => {
// Don't run this in development
if (isDev) {
return;
@@ -12,16 +12,16 @@ export const setAutoLaunch = (AutoLaunchValue: boolean): void => {
const autoLaunchOption = ConfigUtil.getConfigItem('startAtLogin', AutoLaunchValue);
// setLoginItemSettings doesn't support linux
// `setLoginItemSettings` doesn't support linux
if (process.platform === 'linux') {
const ZulipAutoLauncher = new AutoLaunch({
name: 'Zulip',
isHidden: false
});
if (autoLaunchOption) {
ZulipAutoLauncher.enable();
await ZulipAutoLauncher.enable();
} else {
ZulipAutoLauncher.disable();
await ZulipAutoLauncher.disable();
}
} else {
app.setLoginItemSettings({

View File

@@ -0,0 +1,82 @@
import {clipboard} from 'electron';
import crypto from 'crypto';
// This helper is exposed via electron_bridge for use in the social
// login flow.
//
// It consists of a key and a promised token. The in-app page sends
// the key to the server, and opens the users browser to a page where
// they can log in and get a token encrypted to that key. When the
// user copies the encrypted token from their browser to the
// clipboard, we decrypt it and resolve the promise. The in-app page
// then uses the decrypted token to log the user in within the app.
//
// The encryption is authenticated (AES-GCM) to guarantee that we
// dont leak anything from the users clipboard other than the token
// intended for us.
export class ClipboardDecrypterImpl implements ClipboardDecrypter {
version: number;
key: Uint8Array;
pasted: Promise<string>;
constructor(_: number) {
// At this time, the only version is 1.
this.version = 1;
this.key = crypto.randomBytes(32);
this.pasted = new Promise(resolve => {
let interval: NodeJS.Timeout | null = null;
const startPolling = () => {
if (interval === null) {
interval = setInterval(poll, 1000);
}
poll();
};
const stopPolling = () => {
if (interval !== null) {
clearInterval(interval);
interval = null;
}
};
const poll = () => {
let plaintext;
try {
const data = Buffer.from(clipboard.readText(), 'hex');
const iv = data.slice(0, 12);
const ciphertext = data.slice(12, -16);
const authTag = data.slice(-16);
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
this.key,
iv,
{authTagLength: 16}
);
decipher.setAuthTag(authTag);
plaintext =
decipher.update(ciphertext, undefined, 'utf8') +
decipher.final('utf8');
} catch (_) {
// If the parsing or decryption failed in any way,
// the correct token hasnt been copied yet; try
// again next time.
return;
}
window.removeEventListener('focus', startPolling);
window.removeEventListener('blur', stopPolling);
stopPolling();
resolve(plaintext);
};
window.addEventListener('focus', startPolling);
window.addEventListener('blur', stopPolling);
if (document.hasFocus()) {
startPolling();
}
});
}
}

View File

@@ -1,7 +1,13 @@
import Tab, { TabProps } from './tab';
import Tab, {TabProps} from './tab';
export default class FunctionalTab extends Tab {
$closeButton: Element;
constructor(props: TabProps) {
super(props);
this.init();
}
template(): string {
return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tab-badge close-button">
@@ -13,11 +19,6 @@ export default class FunctionalTab extends Tab {
</div>`;
}
constructor(props: TabProps) {
super(props);
this.init();
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template());
if (this.props.name !== 'Settings') {

View File

@@ -1,10 +1,10 @@
import { ipcRenderer, remote } from 'electron';
import {ipcRenderer, remote} from 'electron';
import * as LinkUtil from '../utils/link-util';
import * as ConfigUtil from '../utils/config-util';
import type WebView from './webview';
const { shell, app } = remote;
const {shell, app} = remote;
const dingSound = new Audio('../resources/sounds/ding.ogg');
@@ -16,41 +16,45 @@ export default function handleExternalLink(this: WebView, event: Electron.NewWin
if (LinkUtil.isUploadsUrl(this.props.url, url)) {
ipcRenderer.send('downloadFile', url.href, downloadPath);
ipcRenderer.once('downloadFileCompleted', (_event: Event, filePath: string, fileName: string) => {
ipcRenderer.once('downloadFileCompleted', async (_event: Event, filePath: string, fileName: string) => {
const downloadNotification = new Notification('Download Complete', {
body: `Click to show ${fileName} in folder`,
silent: true // We'll play our own sound - ding.ogg
});
// Play sound to indicate download complete
if (!ConfigUtil.getConfigItem('silent')) {
dingSound.play();
}
downloadNotification.addEventListener('click', () => {
// Reveal file in download folder
shell.showItemInFolder(filePath);
});
ipcRenderer.removeAllListeners('downloadFileFailed');
// Play sound to indicate download complete
if (!ConfigUtil.getConfigItem('silent')) {
await dingSound.play();
}
});
ipcRenderer.once('downloadFileFailed', () => {
ipcRenderer.once('downloadFileFailed', (_event: Event, state: string) => {
// Automatic download failed, so show save dialog prompt and download
// through webview
// Only do this if it is the automatic download, otherwise show an error (so we aren't showing two save
// prompts right after each other)
if (ConfigUtil.getConfigItem('promptDownload', false)) {
// We need to create a "new Notification" to display it, but just `Notification(...)` on its own
// doesn't work
new Notification('Download Complete', { // eslint-disable-line no-new
body: 'Download failed'
});
} else {
this.$el.downloadURL(url.href);
// Check that the download is not cancelled by user
if (state !== 'cancelled') {
if (ConfigUtil.getConfigItem('promptDownload', false)) {
// We need to create a "new Notification" to display it, but just `Notification(...)` on its own
// doesn't work
new Notification('Download Complete', { // eslint-disable-line no-new
body: 'Download failed'
});
} else {
this.$el.downloadURL(url.href);
}
}
ipcRenderer.removeAllListeners('downloadFileCompleted');
});
} else {
LinkUtil.openBrowser(url);
(async () => LinkUtil.openBrowser(url))();
}
}

View File

@@ -1,11 +1,16 @@
import { ipcRenderer } from 'electron';
import {ipcRenderer} from 'electron';
import Tab, { TabProps } from './tab';
import Tab, {TabProps} from './tab';
import * as SystemUtil from '../utils/system-util';
export default class ServerTab extends Tab {
$badge: Element;
constructor(props: TabProps) {
super(props);
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>
@@ -17,11 +22,6 @@ export default class ServerTab extends Tab {
</div>`;
}
constructor(props: TabProps) {
super(props);
this.init();
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template());
this.props.$root.append(this.$el);

View File

@@ -48,7 +48,7 @@ export default class Tab extends BaseComponent {
}
destroy(): void {
this.$el.parentNode.removeChild(this.$el);
this.webview.$el.parentNode.removeChild(this.webview.$el);
this.$el.remove();
this.webview.$el.remove();
}
}

View File

@@ -1,4 +1,4 @@
import { ipcRenderer, remote } from 'electron';
import {ipcRenderer, remote} from 'electron';
import path from 'path';
import fs from 'fs';
@@ -7,7 +7,7 @@ import * as SystemUtil from '../utils/system-util';
import BaseComponent from './base';
import handleExternalLink from './handle-external-link';
const { app, dialog } = remote;
const {app, dialog} = remote;
const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
@@ -15,15 +15,16 @@ interface WebViewProps {
$root: Element;
index: number;
tabIndex: number;
url: any;
url: string;
role: string;
name: string;
isActive: () => boolean;
switchLoading: (loading: any, url: string) => void;
switchLoading: (loading: boolean, url: string) => void;
onNetworkError: (index: number) => void;
nodeIntegration: boolean;
preload: boolean;
onTitleChange: any;
onTitleChange: () => void;
ignoreCerts?: boolean;
hasPermission?: (origin: string, permission: string) => boolean;
}
@@ -37,10 +38,6 @@ export default class WebView extends BaseComponent {
$el: Electron.WebviewTag;
domReady?: Promise<void>;
// This is required because in main.js we access WebView.method as
// webview[method].
[key: string]: any;
constructor(props: WebViewProps) {
super();
@@ -87,7 +84,7 @@ export default class WebView extends BaseComponent {
}
this.$el.addEventListener('page-title-updated', event => {
const { title } = event;
const {title} = event;
this.badgeCount = this.getBadgeCount(title);
this.props.onTitleChange();
});
@@ -97,6 +94,7 @@ export default class WebView extends BaseComponent {
if (isSettingPage) {
return;
}
this.canGoBackButton();
});
@@ -105,14 +103,14 @@ export default class WebView extends BaseComponent {
});
this.$el.addEventListener('page-favicon-updated', event => {
const { favicons } = event;
const {favicons} = event;
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') {
// This api is only supported on macOS
app.dock.setBadge('●');
// bounce the dock
// Bounce the dock
if (ConfigUtil.getConfigItem('dockBouncing')) {
app.dock.bounce();
}
@@ -123,6 +121,7 @@ export default class WebView extends BaseComponent {
if (this.props.role === 'server') {
this.$el.classList.add('onload');
}
this.loading = false;
this.props.switchLoading(false, this.props.url);
this.show();
@@ -134,7 +133,7 @@ export default class WebView extends BaseComponent {
});
this.$el.addEventListener('did-fail-load', event => {
const { errorDescription } = event;
const {errorDescription} = event;
const hasConnectivityErr = SystemUtil.connectivityERR.includes(errorDescription);
if (hasConnectivityErr) {
console.error('error', errorDescription);
@@ -188,9 +187,9 @@ export default class WebView extends BaseComponent {
this.focus();
this.props.onTitleChange();
// Injecting preload css in webview to override some css rules
this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8'));
(async () => this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8')))();
// get customCSS again from config util to avoid warning user again
// Get customCSS again from config util to avoid warning user again
this.customCSS = ConfigUtil.getConfigItem('customCSS');
if (this.customCSS) {
if (!fs.existsSync(this.customCSS)) {
@@ -202,12 +201,12 @@ export default class WebView extends BaseComponent {
return;
}
this.$el.insertCSS(fs.readFileSync(path.resolve(__dirname, this.customCSS), 'utf8'));
(async () => this.$el.insertCSS(fs.readFileSync(path.resolve(__dirname, this.customCSS), 'utf8')))();
}
}
focus(): void {
// focus Webview and it's contents when Window regain focus.
// Focus Webview and it's contents when Window regain focus.
const webContents = this.$el.getWebContents();
// HACK: webContents.isFocused() seems to be true even without the element
// being in focus. So, we check against `document.activeElement`.
@@ -296,8 +295,8 @@ export default class WebView extends BaseComponent {
this.init();
}
async send(channel: string, ...parameters: any[]): Promise<void> {
async send(channel: string, ...parameters: unknown[]): Promise<void> {
await this.domReady;
this.$el.send(channel, ...parameters);
await this.$el.send(channel, ...parameters);
}
}

View File

@@ -1,12 +1,13 @@
import { ipcRenderer } from 'electron';
import {ipcRenderer} from 'electron';
import { EventEmitter } from 'events';
import {EventEmitter} from 'events';
import { NotificationData, newNotification } from './notification';
import {ClipboardDecrypterImpl} from './clipboard-decrypter';
import {NotificationData, newNotification} from './notification';
type ListenerType = ((...args: any[]) => void);
class ElectronBridge extends EventEmitter {
class ElectronBridgeImpl extends EventEmitter implements ElectronBridge {
send_notification_reply_message_supported: boolean;
idle_on_system: boolean;
last_active_on_system: number;
@@ -21,7 +22,7 @@ class ElectronBridge extends EventEmitter {
this.last_active_on_system = Date.now();
}
send_event = (eventName: string | symbol, ...args: any[]): void => {
send_event = (eventName: string | symbol, ...args: unknown[]): void => {
this.emit(eventName, ...args);
};
@@ -46,9 +47,12 @@ class ElectronBridge extends EventEmitter {
set_send_notification_reply_message_supported = (value: boolean): void => {
this.send_notification_reply_message_supported = value;
};
decrypt_clipboard = (version: number): ClipboardDecrypterImpl =>
new ClipboardDecrypterImpl(version);
}
const electron_bridge = new ElectronBridge();
const electron_bridge = new ElectronBridgeImpl();
electron_bridge.on('total_unread_count', (...args) => {
ipcRenderer.send('unread-count', ...args);
@@ -59,13 +63,17 @@ electron_bridge.on('realm_name', realmName => {
ipcRenderer.send('realm-name-changed', serverURL, realmName);
});
electron_bridge.on('realm_icon_url', iconURL => {
electron_bridge.on('realm_icon_url', (iconURL: unknown) => {
if (typeof iconURL !== 'string') {
throw new TypeError('Expected string for iconURL');
}
const serverURL = location.origin;
iconURL = iconURL.includes('http') ? iconURL : `${serverURL}${iconURL}`;
ipcRenderer.send('realm-icon-changed', serverURL, iconURL);
});
// this follows node's idiomatic implementation of event
// This follows node's idiomatic implementation of event
// emitters to make event handling more simpler instead of using
// functions zulip side will emit event using ElectronBrigde.send_event
// which is alias of .emit and on this side we can handle the data by adding

View File

@@ -1,19 +1,17 @@
import { remote } from 'electron';
import {remote} from 'electron';
import SendFeedback from '@electron-elements/send-feedback';
import path from 'path';
import fs from 'fs';
const { app } = remote;
const {app} = remote;
interface SendFeedback extends HTMLElement {
[key: string]: any;
}
customElements.define('send-feedback', SendFeedback);
export const sendFeedback: SendFeedback = document.querySelector('send-feedback');
export const feedbackHolder = sendFeedback.parentElement;
type SendFeedbackType = SendFeedback;
// make the button color match zulip app's theme
SendFeedback.customStyles = `
// Make the button color match zulip app's theme
sendFeedback.customStyles = `
button:hover, button:focus {
border-color: #4EBFAC;
color: #4EBFAC;
@@ -30,10 +28,6 @@ button {
}
`;
customElements.define('send-feedback', SendFeedback);
export const sendFeedback: SendFeedbackType = document.querySelector('send-feedback');
export const feedbackHolder = sendFeedback.parentElement;
/* eslint-disable no-multi-str */
// customize the fields of custom elements
@@ -62,7 +56,7 @@ sendFeedback.useReporter('emailReporter', {
});
feedbackHolder.addEventListener('click', (event: Event) => {
// only remove the class if the grey out faded
// Only remove the class if the grey out faded
// part is clicked and not the feedback element itself
if (event.target === event.currentTarget) {
feedbackHolder.classList.remove('show');

View File

@@ -1,25 +1,36 @@
'use strict';
interface CompatElectronBridge extends ElectronBridge {
readonly idle_on_system: boolean;
readonly last_active_on_system: number;
send_notification_reply_message_supported: boolean;
}
(() => {
const zulipWindow = window as typeof window & {
electron_bridge: any;
narrow: any;
page_params: any;
raw_electron_bridge: any;
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;
};
raw_electron_bridge: ElectronBridge;
};
const electron_bridge = {
const electron_bridge: CompatElectronBridge = {
...zulipWindow.raw_electron_bridge,
get idle_on_system() {
get idle_on_system(): boolean {
return this.get_idle_on_system();
},
get last_active_on_system() {
get last_active_on_system(): number {
return this.get_last_active_on_system();
},
get send_notification_reply_message_supported() {
get send_notification_reply_message_supported(): boolean {
return this.get_send_notification_reply_message_supported();
},
@@ -39,7 +50,7 @@
});
}
const { page_params } = zulipWindow;
const {page_params} = zulipWindow;
if (page_params) {
electron_bridge.send_event('zulip-loaded', {
serverLanguage: page_params.default_language
@@ -47,10 +58,10 @@
}
})();
electron_bridge.on_event('narrow-by-topic', (id: string) => {
const { narrow } = zulipWindow;
electron_bridge.on_event('narrow-by-topic', (id: number) => {
const {narrow} = zulipWindow;
const narrowByTopic = narrow.by_topic || narrow.by_subject;
narrowByTopic(id, { trigger: 'notification' });
narrowByTopic(id, {trigger: 'notification'});
});
function attributeListener<T extends EventTarget>(type: string): PropertyDescriptor {
@@ -73,6 +84,7 @@
if (!(symbol in this)) {
this.addEventListener(type, listener);
}
(this as any)[symbol] = value;
} else if (symbol in this) {
this.removeEventListener(type, listener);
@@ -107,6 +119,7 @@
if (callback) {
callback(await Promise.resolve(NativeNotification.permission));
}
return NativeNotification.permission;
}
}

View File

@@ -1,10 +1,10 @@
import { ipcRenderer, remote, clipboard } from 'electron';
import { feedbackHolder } from './feedback';
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;
const {session, app, Menu, dialog} = remote;
// eslint-disable-next-line import/no-unassigned-import
import './tray';
@@ -21,6 +21,7 @@ import * as CommonUtil from './utils/common-util';
import * as EnterpriseUtil from './utils/enterprise-util';
import * as LinkUtil from './utils/link-util';
import * as Messages from '../../resources/messages';
import type {DNDSettings} from './utils/dnd-util';
interface FunctionalTabProps {
name: string;
@@ -28,11 +29,7 @@ interface FunctionalTabProps {
url: string;
}
interface AnyObject {
[key: string]: any;
}
interface SettingsOptions {
interface SettingsOptions extends DNDSettings {
autoHideMenubar: boolean;
trayIcon: boolean;
useManualProxy: boolean;
@@ -42,25 +39,17 @@ interface SettingsOptions {
startAtLogin: boolean;
startMinimized: boolean;
enableSpellchecker: boolean;
showNotification: boolean;
autoUpdate: boolean;
betaUpdate: boolean;
errorReporting: boolean;
customCSS: boolean;
silent: boolean;
lastActiveTab: number;
dnd: boolean;
dndPreviousSettings: {
showNotification: boolean;
silent: boolean;
flashTaskbarOnMessage?: boolean;
};
dndPreviousSettings: DNDSettings;
downloadsPath: string;
quitOnClose: boolean;
promptDownload: boolean;
flashTaskbarOnMessage?: boolean;
dockBouncing?: boolean;
loading?: AnyObject;
}
const logger = new Logger({
@@ -69,7 +58,7 @@ const logger = new Logger({
});
const rendererDirectory = path.resolve(__dirname, '..');
type ServerOrFunctionalTab = ServerTab | FunctionalTab;
export type ServerOrFunctionalTab = ServerTab | FunctionalTab;
class ServerManagerView {
$addServerButton: HTMLButtonElement;
@@ -90,10 +79,10 @@ class ServerManagerView {
$sidebar: Element;
$fullscreenPopup: Element;
$fullscreenEscapeKey: string;
loading: AnyObject;
loading: Set<string>;
activeTabIndex: number;
tabs: ServerOrFunctionalTab[];
functionalTabs: AnyObject;
functionalTabs: Map<string, number>;
tabIndex: number;
presetOrgs: string[];
constructor() {
@@ -128,11 +117,11 @@ class ServerManagerView {
this.$fullscreenEscapeKey = process.platform === 'darwin' ? '^⌘F' : 'F11';
this.$fullscreenPopup.innerHTML = `Press ${this.$fullscreenEscapeKey} to exit full screen`;
this.loading = {};
this.loading = new Set();
this.activeTabIndex = -1;
this.tabs = [];
this.presetOrgs = [];
this.functionalTabs = {};
this.functionalTabs = new Map();
this.tabIndex = 0;
}
@@ -141,10 +130,11 @@ class ServerManagerView {
this.initDefaultSettings();
this.initSidebar();
this.removeUAfromDisk();
if (EnterpriseUtil.configFile) {
this.initPresetOrgs();
if (EnterpriseUtil.hasConfigFile()) {
await this.initPresetOrgs();
}
this.initTabs();
await this.initTabs();
this.initActions();
this.registerIpcs();
}
@@ -157,6 +147,7 @@ class ServerManagerView {
if (proxyEnableOldState) {
ConfigUtil.setConfigItem('useManualProxy', true);
}
ConfigUtil.removeConfigItem('useProxy');
}
@@ -226,7 +217,7 @@ class ServerManagerView {
}
for (const [setting, value] of Object.entries(settingOptions)) {
// give preference to defaults defined in global_config.json
// Give preference to defaults defined in global_config.json
if (EnterpriseUtil.configItemExists(setting)) {
ConfigUtil.setConfigItem(setting, EnterpriseUtil.getConfigItem(setting), true);
} else if (ConfigUtil.getConfigItem(setting) === null) {
@@ -246,41 +237,43 @@ class ServerManagerView {
ConfigUtil.removeConfigItem('userAgent');
}
async queueDomain(domain: any): Promise<boolean> {
// allows us to start adding multiple domains to the app simultaneously
async queueDomain(domain: string): Promise<boolean> {
// Allows us to start adding multiple domains to the app simultaneously
// promise of addition resolves in both cases, but we consider it rejected
// if the resolved value is false
try {
const serverConf = await DomainUtil.checkDomain(domain);
await DomainUtil.addDomain(serverConf);
return true;
} catch (err) {
logger.error(err);
logger.error('Could not add ' + domain + '. Please contact your system administrator.');
} catch (error) {
logger.error(error);
logger.error(`Could not add ${domain}. Please contact your system administrator.`);
return false;
}
}
async initPresetOrgs(): Promise<void> {
// read preset organizations from global_config.json and queues them
// Read preset organizations from global_config.json and queues them
// for addition to the app's domains
const preAddedDomains = DomainUtil.getDomains();
this.presetOrgs = EnterpriseUtil.getConfigItem('presetOrganizations', []);
// set to true if at least one new domain is added
// Set to true if at least one new domain is added
const domainPromises = [];
for (const url of this.presetOrgs) {
if (DomainUtil.duplicateDomain(url)) {
continue;
}
domainPromises.push(this.queueDomain(url));
}
const domainsAdded = await Promise.all(domainPromises);
if (domainsAdded.includes(true)) {
// at least one domain was resolved
// At least one domain was resolved
if (preAddedDomains.length > 0) {
// user already has servers added
// User already has servers added
// ask them before reloading the app
const { response } = await dialog.showMessageBox({
const {response} = await dialog.showMessageBox({
type: 'question',
buttons: ['Yes', 'Later'],
defaultId: 0,
@@ -293,57 +286,62 @@ class ServerManagerView {
ipcRenderer.send('reload-full-app');
}
} else if (domainsAdded.length > 0) {
// find all orgs that failed
// Find all orgs that failed
const failedDomains: string[] = [];
for (const org of this.presetOrgs) {
if (DomainUtil.duplicateDomain(org)) {
continue;
}
failedDomains.push(org);
}
const { title, content } = Messages.enterpriseOrgError(domainsAdded.length, failedDomains);
const {title, content} = Messages.enterpriseOrgError(domainsAdded.length, failedDomains);
dialog.showErrorBox(title, content);
if (DomainUtil.getDomains().length === 0) {
// no orgs present, stop showing loading gif
// No orgs present, stop showing loading gif
await this.openSettings('AddServer');
}
}
}
initTabs(): void {
async initTabs(): Promise<void> {
const servers = DomainUtil.getDomains();
if (servers.length > 0) {
for (const [i, server] of servers.entries()) {
this.initServer(server, i);
}
// Open last active tab
let lastActiveTab = ConfigUtil.getConfigItem('lastActiveTab');
if (lastActiveTab >= servers.length) {
lastActiveTab = 0;
}
// checkDomain() and webview.load() for lastActiveTab before the others
DomainUtil.updateSavedServer(servers[lastActiveTab].url, lastActiveTab);
// `checkDomain()` and `webview.load()` for lastActiveTab before the others
await DomainUtil.updateSavedServer(servers[lastActiveTab].url, lastActiveTab);
this.activateTab(lastActiveTab);
for (const [i, server] of servers.entries()) {
// after the lastActiveTab is activated, we load the others in the background
await Promise.all(servers.map(async (server, i) => {
// After the lastActiveTab is activated, we load the others in the background
// without activating them, to prevent flashing of server icons
if (i === lastActiveTab) {
continue;
return;
}
DomainUtil.updateSavedServer(server.url, i);
await DomainUtil.updateSavedServer(server.url, i);
this.tabs[i].webview.load();
}
}));
// Remove focus from the settings icon at sidebar bottom
this.$settingsButton.classList.remove('active');
} else if (this.presetOrgs.length === 0) {
// not attempting to add organisations in the background
this.openSettings('AddServer');
// Not attempting to add organisations in the background
await this.openSettings('AddServer');
} else {
this.showLoading(true);
}
}
initServer(server: any, index: number): void {
initServer(server: DomainUtil.ServerConf, index: number): void {
const tabIndex = this.getTabIndex();
this.tabs.push(new ServerTab({
role: 'server',
@@ -362,18 +360,20 @@ class ServerManagerView {
url: server.url,
role: 'server',
name: CommonUtil.decodeString(server.alias),
ignoreCerts: server.ignoreCerts,
hasPermission: (origin: string, permission: string) =>
origin === server.url && permission === 'notifications',
isActive: () => {
return index === this.activeTabIndex;
},
switchLoading: (loading: boolean, url: string) => {
if (!loading && this.loading[url]) {
this.loading[url] = false;
} else if (loading && !this.loading[url]) {
this.loading[url] = true;
if (loading) {
this.loading.add(url);
} else {
this.loading.delete(url);
}
this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]);
this.showLoading(this.loading.has(this.tabs[this.activeTabIndex].webview.props.url));
},
onNetworkError: (index: number) => this.openNetworkTroubleshooting(index),
onTitleChange: this.updateBadge.bind(this),
@@ -381,7 +381,7 @@ class ServerManagerView {
preload: true
})
}));
this.loading[server.url] = true;
this.loading.add(server.url);
}
initActions(): void {
@@ -397,6 +397,7 @@ class ServerManagerView {
if ($serverImg.src.includes('img/icon.png')) {
this.displayInitialCharLogo($serverImg, index);
}
$serverImg.addEventListener('error', () => {
this.displayInitialCharLogo($serverImg, index);
});
@@ -411,11 +412,11 @@ class ServerManagerView {
this.$reloadButton.addEventListener('click', () => {
this.tabs[this.activeTabIndex].webview.reload();
});
this.$addServerButton.addEventListener('click', () => {
this.openSettings('AddServer');
this.$addServerButton.addEventListener('click', async () => {
await this.openSettings('AddServer');
});
this.$settingsButton.addEventListener('click', () => {
this.openSettings('General');
this.$settingsButton.addEventListener('click', async () => {
await this.openSettings('General');
});
this.$backButton.addEventListener('click', () => {
this.tabs[this.activeTabIndex].webview.back();
@@ -445,10 +446,9 @@ class ServerManagerView {
}
displayInitialCharLogo($img: HTMLImageElement, index: number): void {
/*
index parameter needed because webview[data-tab-id] can increment
beyond size of sidebar org array and throw error
*/
// The index parameter is needed because webview[data-tab-id] can
// increment beyond the size of the sidebar org array and throw an
// error
const $altIcon = document.createElement('div');
const $parent = $img.parentElement;
@@ -480,8 +480,8 @@ class ServerManagerView {
// as that of its parent element.
// This needs to handled only for the add server tooltip and not others.
if (addServer) {
const { top } = SidebarButton.getBoundingClientRect();
SidebarTooltip.style.top = top + 'px';
const {top} = SidebarButton.getBoundingClientRect();
SidebarTooltip.style.top = `${top}px`;
}
});
SidebarButton.addEventListener('mouseout', () => {
@@ -490,14 +490,14 @@ class ServerManagerView {
}
onHover(index: number): void {
// this.$serverIconTooltip[index].innerHTML already has realm name, so we are just
// `this.$serverIconTooltip[index].innerHTML` 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
// This could not be handled using CSS, hence the top of the tooltip is made same
// as that of its parent element.
const { top } = this.$serverIconTooltip[index].parentElement.getBoundingClientRect();
this.$serverIconTooltip[index].style.top = top + 'px';
const {top} = this.$serverIconTooltip[index].parentElement.getBoundingClientRect();
this.$serverIconTooltip[index].style.top = `${top}px`;
}
onHoverOut(index: number): void {
@@ -505,12 +505,12 @@ class ServerManagerView {
}
openFunctionalTab(tabProps: FunctionalTabProps): void {
if (this.functionalTabs[tabProps.name] !== undefined) {
this.activateTab(this.functionalTabs[tabProps.name]);
if (this.functionalTabs.has(tabProps.name)) {
this.activateTab(this.functionalTabs.get(tabProps.name));
return;
}
this.functionalTabs[tabProps.name] = this.tabs.length;
this.functionalTabs.set(tabProps.name, this.tabs.length);
const tabIndex = this.getTabIndex();
@@ -519,27 +519,28 @@ class ServerManagerView {
materialIcon: tabProps.materialIcon,
name: tabProps.name,
$root: this.$tabsContainer,
index: this.functionalTabs[tabProps.name],
index: this.functionalTabs.get(tabProps.name),
tabIndex,
onClick: this.activateTab.bind(this, this.functionalTabs[tabProps.name]),
onDestroy: this.destroyTab.bind(this, tabProps.name, this.functionalTabs[tabProps.name]),
onClick: this.activateTab.bind(this, this.functionalTabs.get(tabProps.name)),
onDestroy: this.destroyTab.bind(this, tabProps.name, this.functionalTabs.get(tabProps.name)),
webview: new WebView({
$root: this.$webviewsContainer,
index: this.functionalTabs[tabProps.name],
index: this.functionalTabs.get(tabProps.name),
tabIndex,
url: tabProps.url,
role: 'function',
name: tabProps.name,
isActive: () => {
return this.functionalTabs[tabProps.name] === this.activeTabIndex;
return this.functionalTabs.get(tabProps.name) === this.activeTabIndex;
},
switchLoading: (loading: AnyObject, url: string) => {
if (!loading && this.loading[url]) {
this.loading[url] = false;
} else if (loading && !this.loading[url]) {
this.loading[url] = true;
switchLoading: (loading: boolean, url: string) => {
if (loading) {
this.loading.add(url);
} else {
this.loading.delete(url);
}
this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]);
this.showLoading(this.loading.has(this.tabs[this.activeTabIndex].webview.props.url));
},
onNetworkError: (index: number) => this.openNetworkTroubleshooting(index),
onTitleChange: this.updateBadge.bind(this),
@@ -552,7 +553,7 @@ class ServerManagerView {
// closed when the functional tab DOM is ready, handled in webview.js
this.$webviewsContainer.classList.remove('loaded');
this.activateTab(this.functionalTabs[tabProps.name]);
this.activateTab(this.functionalTabs.get(tabProps.name));
}
async openSettings(nav = 'General'): Promise<void> {
@@ -562,7 +563,7 @@ class ServerManagerView {
url: `file://${rendererDirectory}/preference.html#${nav}`
});
this.$settingsButton.classList.add('active');
await this.tabs[this.functionalTabs.Settings].webview.send('switch-settings-nav', nav);
await this.tabs[this.functionalTabs.get('Settings')].webview.send('switch-settings-nav', nav);
}
openAbout(): void {
@@ -587,7 +588,7 @@ class ServerManagerView {
ipcRenderer.send('save-last-tab', index);
}
// returns this.tabs in an way that does
// Returns this.tabs in an way that does
// not crash app when this.tabs is passed into
// ipcRenderer. Something about webview, and props.webview
// properties in ServerTab causes the app to crash.
@@ -597,7 +598,7 @@ class ServerManagerView {
const proto = Object.create(Object.getPrototypeOf(tab));
const tabClone = Object.assign(proto, tab);
tabClone.webview = { props: {} };
tabClone.webview = {props: {}};
tabClone.webview.props.name = tab.webview.props.name;
delete tabClone.props.webview;
tabs.push(tabClone);
@@ -614,24 +615,27 @@ class ServerManagerView {
if (this.activeTabIndex !== -1) {
if (this.activeTabIndex === index) {
return;
} else if (hideOldTab) {
}
if (hideOldTab) {
// If old tab is functional tab Settings, remove focus from the settings icon at sidebar bottom
if (this.tabs[this.activeTabIndex].props.role === 'function' && this.tabs[this.activeTabIndex].props.name === 'Settings') {
this.$settingsButton.classList.remove('active');
}
this.tabs[this.activeTabIndex].deactivate();
}
}
try {
this.tabs[index].webview.canGoBackButton();
} catch (err) {
} catch (_) {
}
this.activeTabIndex = index;
this.tabs[index].activate();
this.showLoading(this.loading[this.tabs[this.activeTabIndex].webview.props.url]);
this.showLoading(this.loading.has(this.tabs[this.activeTabIndex].webview.props.url));
ipcRenderer.send('update-menu', {
// JSON stringify this.tabs to avoid a crash
@@ -661,7 +665,7 @@ class ServerManagerView {
this.tabs[index].destroy();
delete this.tabs[index];
delete this.functionalTabs[name];
this.functionalTabs.delete(name);
// Issue #188: If the functional tab was not focused, do not activate another tab.
if (this.activeTabIndex === index) {
@@ -676,21 +680,21 @@ class ServerManagerView {
// Clear global variables
this.activeTabIndex = -1;
this.tabs = [];
this.functionalTabs = {};
this.functionalTabs.clear();
// Clear DOM elements
this.$tabsContainer.innerHTML = '';
this.$webviewsContainer.innerHTML = '';
}
reloadView(): void {
async reloadView(): Promise<void> {
// Save and remember the index of last active tab so that we can use it later
const lastActiveTab = this.tabs[this.activeTabIndex].props.index;
ConfigUtil.setConfigItem('lastActiveTab', lastActiveTab);
// Destroy the current view and re-initiate it
this.destroyView();
this.initTabs();
await this.initTabs();
this.initServerActions();
}
@@ -713,7 +717,7 @@ class ServerManagerView {
ipcRenderer.send('update-badge', messageCountAll);
}
updateGeneralSettings(setting: string, value: any): void {
updateGeneralSettings(setting: string, value: unknown): void {
if (this.getActiveWebview()) {
const webContents = this.getActiveWebview().getWebContents();
webContents.send(setting, value);
@@ -752,7 +756,7 @@ class ServerManagerView {
{
label: 'Disconnect organization',
click: async () => {
const { response } = await dialog.showMessageBox({
const {response} = await dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
@@ -762,7 +766,7 @@ class ServerManagerView {
if (DomainUtil.removeDomain(index)) {
ipcRenderer.send('reload-full-app');
} else {
const { title, content } = Messages.orgRemovalError(DomainUtil.getDomain(index).url);
const {title, content} = Messages.orgRemovalError(DomainUtil.getDomain(index).url);
dialog.showErrorBox(title, content);
}
}
@@ -772,7 +776,7 @@ class ServerManagerView {
label: 'Notification settings',
enabled: this.isLoggedIn(index),
click: () => {
// switch to tab whose icon was right-clicked
// Switch to tab whose icon was right-clicked
this.activateTab(index);
this.tabs[index].webview.showNotificationSettings();
}
@@ -785,46 +789,62 @@ class ServerManagerView {
}
];
const contextMenu = Menu.buildFromTemplate(template);
contextMenu.popup({ window: remote.getCurrentWindow() });
contextMenu.popup({window: remote.getCurrentWindow()});
});
}
registerIpcs(): void {
const webviewListeners: AnyObject = {
'webview-reload': 'reload',
back: 'back',
focus: 'focus',
forward: 'forward',
zoomIn: 'zoomIn',
zoomOut: 'zoomOut',
zoomActualSize: 'zoomActualSize',
'log-out': 'logOut',
shortcut: 'showShortcut',
'tab-devtools': 'openDevTools'
};
const webviewListeners: Array<[string, (webview: WebView) => void]> = [
['webview-reload', webview => webview.reload()],
['back', webview => webview.back()],
['focus', webview => webview.focus()],
['forward', webview => webview.forward()],
['zoomIn', webview => webview.zoomIn()],
['zoomOut', webview => webview.zoomOut()],
['zoomActualSize', webview => webview.zoomActualSize()],
['log-out', webview => webview.logOut()],
['shortcut', webview => webview.showShortcut()],
['tab-devtools', webview => webview.openDevTools()]
];
for (const [key, method] of Object.entries(webviewListeners)) {
ipcRenderer.on(key, () => {
for (const [channel, listener] of webviewListeners) {
ipcRenderer.on(channel, () => {
const activeWebview = this.tabs[this.activeTabIndex].webview;
if (activeWebview) {
activeWebview[method]();
listener(activeWebview);
}
});
}
ipcRenderer.on('certificate-error', (
event: Event,
webContentsId: number | null,
rendererCallbackId: number
) => {
const ignore = webContentsId !== null &&
this.tabs.some(
({webview}) =>
!webview.loading &&
webview.$el.getWebContentsId() === webContentsId &&
webview.props.ignoreCerts
);
ipcRenderer.send('renderer-callback', rendererCallbackId, ignore);
});
ipcRenderer.on('permission-request', (
event: Event,
permissionId: number,
{webContentsId, origin, permission}: {
webContentsId: number | null;
origin: string;
permission: string;
}
},
rendererCallbackId: number
) => {
const grant = webContentsId === null ?
origin === 'null' && permission === 'notifications' :
this.tabs.some(
({webview}) =>
!webview.loading &&
webview.$el.getWebContentsId() === webContentsId &&
webview.props.hasPermission?.(origin, permission)
);
@@ -832,22 +852,22 @@ class ServerManagerView {
grant ? 'Granted' : 'Denied', 'permissions request for',
permission, 'from', origin
);
ipcRenderer.send('permission-response', permissionId, grant);
ipcRenderer.send('renderer-callback', rendererCallbackId, grant);
});
ipcRenderer.on('show-network-error', (event: Event, index: number) => {
this.openNetworkTroubleshooting(index);
});
ipcRenderer.on('open-settings', (event: Event, settingNav: string) => {
this.openSettings(settingNav);
ipcRenderer.on('open-settings', async (event: Event, settingNav: string) => {
await this.openSettings(settingNav);
});
ipcRenderer.on('open-about', this.openAbout.bind(this));
ipcRenderer.on('open-help', () => {
ipcRenderer.on('open-help', async () => {
// Open help page of current active server
LinkUtil.openBrowser(new URL('/help', this.getCurrentActiveServer()));
await LinkUtil.openBrowser(new URL('https://zulipchat.com/help/'));
});
ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index));
@@ -866,14 +886,17 @@ class ServerManagerView {
this.activateLastTab(index);
});
ipcRenderer.on('open-org-tab', () => {
this.openSettings('AddServer');
ipcRenderer.on('open-org-tab', async () => {
await this.openSettings('AddServer');
});
ipcRenderer.on('reload-proxy', async (event: Event, showAlert: boolean) => {
await this.loadProxy();
if (showAlert) {
alert('Proxy settings saved!');
await dialog.showMessageBox({
message: 'Proxy settings saved!',
buttons: ['OK']
});
ipcRenderer.send('reload-full-app');
}
});
@@ -891,8 +914,8 @@ class ServerManagerView {
webviews.forEach(webview => {
try {
webview.setAudioMuted(state);
} catch (err) {
// webview is not ready yet
} catch (_) {
// Webview is not ready yet
webview.addEventListener('dom-ready', () => {
webview.setAudioMuted(state);
});
@@ -908,10 +931,11 @@ class ServerManagerView {
});
return;
}
this.updateGeneralSettings('toggle-menubar-setting', autoHideMenubar);
});
ipcRenderer.on('toggle-dnd', (event: Event, state: boolean, newSettings: SettingsOptions) => {
ipcRenderer.on('toggle-dnd', (event: Event, state: boolean, newSettings: DNDSettings) => {
this.toggleDNDButton(state);
ipcRenderer.send('forward-message', 'toggle-silent', newSettings.silent);
const webContents = this.getActiveWebview().getWebContents();
@@ -928,8 +952,7 @@ class ServerManagerView {
this.tabs[index].webview.props.name = realmName;
domain.alias = escape(realmName);
DomainUtil.db.push(`/domains[${index}]`, domain, true);
DomainUtil.reloadDB();
DomainUtil.updateDomain(index, domain);
// Update the realm name also on the Window menu
ipcRenderer.send('update-menu', {
tabs: this.tabsForIpc,
@@ -950,8 +973,7 @@ class ServerManagerView {
const serverImgs: NodeListOf<HTMLImageElement> = document.querySelectorAll(serverImgsSelector);
serverImgs[index].src = localIconUrl;
domain.icon = localIconUrl;
DomainUtil.db.push(`/domains[${index}]`, domain, true);
DomainUtil.reloadDB();
DomainUtil.updateDomain(index, domain);
}
});
});
@@ -1001,8 +1023,10 @@ class ServerManagerView {
ctx.font = '85px Helvetica';
ctx.fillText(String(Math.min(99, messageCount)), 64, 90);
}
return canvas;
}
ipcRenderer.send('update-taskbar-icon', createOverlayIcon(messageCount).toDataURL(), String(messageCount));
});
@@ -1014,8 +1038,8 @@ class ServerManagerView {
clipboard.writeText(this.getCurrentActiveServer());
});
ipcRenderer.on('new-server', () => {
this.openSettings('AddServer');
ipcRenderer.on('new-server', async () => {
await this.openSettings('AddServer');
});
// Redo and undo functionality since the default API doesn't work on macOS
@@ -1027,37 +1051,33 @@ class ServerManagerView {
return this.getActiveWebview().redo();
});
ipcRenderer.on('set-active', () => {
ipcRenderer.on('set-active', async () => {
const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => {
webview.send('set-active');
});
await Promise.all([...webviews].map(async webview => webview.send('set-active')));
});
ipcRenderer.on('set-idle', () => {
ipcRenderer.on('set-idle', async () => {
const webviews: NodeListOf<Electron.WebviewTag> = document.querySelectorAll('webview');
webviews.forEach(webview => {
webview.send('set-idle');
});
await Promise.all([...webviews].map(async webview => webview.send('set-idle')));
});
ipcRenderer.on('open-network-settings', () => {
this.openSettings('Network');
ipcRenderer.on('open-network-settings', async () => {
await this.openSettings('Network');
});
}
}
window.addEventListener('load', () => {
const serverManagerView = new ServerManagerView();
serverManagerView.init();
// only start electron-connect (auto reload on change) when its ran
window.addEventListener('load', async () => {
// Only start electron-connect (auto reload on change) when its ran
// from `npm run dev` or `gulp dev` and not from `npm start` when
// app is started `npm start` main process's proces.argv will have
// `--no-electron-connect`
const mainProcessArgv = remote.getGlobal('process').argv;
if (isDev && !mainProcessArgv.includes('--no-electron-connect')) {
if (isDev && !remote.getGlobal('process').argv.includes('--no-electron-connect')) {
require('electron-connect').client.create();
}
const serverManagerView = new ServerManagerView();
await serverManagerView.init();
});
export {};

View File

@@ -1,4 +1,4 @@
import { ipcRenderer } from 'electron';
import {ipcRenderer} from 'electron';
import {
appId, customReply, focusCurrentServer, parseReply
} from './helpers';
@@ -17,14 +17,14 @@ interface NotificationHandlerArgs {
}
class DarwinNotification {
tag: string;
tag: number;
constructor(title: string, options: NotificationOptions) {
const silent: boolean = ConfigUtil.getConfigItem('silent') || false;
const { icon } = options;
const {icon} = options;
const profilePic = new URL(icon, location.href).href;
this.tag = options.tag;
this.tag = Number.parseInt(options.tag, 10);
const notification = new MacNotifier(title, Object.assign(options, {
bundleId: appId,
canReply: true,
@@ -33,7 +33,7 @@ class DarwinNotification {
}));
notification.addEventListener('click', () => {
// focus to the server who sent the
// Focus to the server who sent the
// notification if not focused already
if (clickHandler) {
clickHandler();
@@ -71,7 +71,7 @@ class DarwinNotification {
clickHandler = handler;
}
// not something that is common or
// Not something that is common or
// used by zulip server but added to be
// future proff.
addEventListener(event: string, handler: ClickHandler | ReplyHandler): void {
@@ -84,7 +84,7 @@ class DarwinNotification {
}
}
async notificationHandler({ response }: NotificationHandlerArgs): Promise<void> {
async notificationHandler({response}: NotificationHandlerArgs): Promise<void> {
response = await parseReply(response);
focusCurrentServer();
if (electron_bridge.send_notification_reply_message_supported) {
@@ -101,7 +101,7 @@ class DarwinNotification {
customReply(response);
}
// method specific to notification api
// Method specific to notification api
// used by zulip
close(): void {
return; // eslint-disable-line no-useless-return

View File

@@ -1,5 +1,5 @@
import { ipcRenderer } from 'electron';
import { focusCurrentServer } from './helpers';
import {ipcRenderer} from 'electron';
import {focusCurrentServer} from './helpers';
import * as ConfigUtil from '../utils/config-util';
@@ -10,7 +10,7 @@ export default class BaseNotification extends NativeNotification {
super(title, options);
this.addEventListener('click', () => {
// focus to the server who sent the
// Focus to the server who sent the
// notification if not focused already
focusCurrentServer();
ipcRenderer.send('focus-app');

View File

@@ -1,4 +1,4 @@
import { remote } from 'electron';
import {remote} from 'electron';
import Logger from '../utils/logger-util';
@@ -14,16 +14,16 @@ export type BotListItem = [string, string];
const botsList: BotListItem[] = [];
let botsListLoaded = false;
// this function load list of bots from the server
// 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((membersRow: any) => {
if (membersRow.is_bot) {
const bot = `@${membersRow.full_name}`;
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]);
}
@@ -35,7 +35,7 @@ export async function loadBots(): Promise<void> {
}
}
export function checkElements(...elements: any[]): boolean {
export function checkElements(...elements: unknown[]): boolean {
let status = true;
elements.forEach(element => {
if (element === null || element === undefined) {
@@ -46,14 +46,14 @@ export function checkElements(...elements: any[]): boolean {
}
export function customReply(reply: string): void {
// server does not support notification reply yet.
// 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
// Sanity check for old server versions
const elementsExists = checkElements(textarea, messagebox, sendButton);
if (!elementsExists) {
return;
@@ -68,12 +68,13 @@ const currentWindow = remote.getCurrentWindow();
const webContents = remote.getCurrentWebContents();
const webContentsId = webContents.id;
// this function will focus the server that sent
// This function will focus the server that sent
// the notification. Main function implemented in main.js
export function focusCurrentServer(): void {
currentWindow.webContents.send('focus-webview-with-id', webContentsId);
}
// this function parses the reply from to notification
// 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**

View File

@@ -1,9 +1,9 @@
import { remote } from 'electron';
import {remote} from 'electron';
import electron_bridge from '../electron-bridge';
import { appId, loadBots } from './helpers';
import {appId, loadBots} from './helpers';
import DefaultNotification from './default-notification';
const { app } = remote;
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.
@@ -16,7 +16,7 @@ if (process.platform === 'darwin') {
}
export interface NotificationData {
close(): void;
close: () => void;
title: string;
dir: NotificationDirection;
lang: string;
@@ -47,6 +47,7 @@ export function newNotification(
}
});
}
return {
close: () => notification.close(),
title: notification.title,
@@ -67,6 +68,6 @@ export function newNotification(
};
}
electron_bridge.once('zulip-loaded', () => {
loadBots();
electron_bridge.once('zulip-loaded', async () => {
await loadBots();
});

View File

@@ -1,4 +1,4 @@
import { ipcRenderer } from 'electron';
import {ipcRenderer} from 'electron';
export function init($reconnectButton: Element, $settingsButton: Element): void {
$reconnectButton.addEventListener('click', () => {

View File

@@ -1,6 +1,6 @@
'use-strict';
import { remote, OpenDialogOptions } from 'electron';
import {remote, OpenDialogOptions} from 'electron';
import path from 'path';
import BaseComponent from '../../components/base';
@@ -12,7 +12,7 @@ interface AddCertificateProps {
$root: Element;
}
const { dialog } = remote;
const {dialog} = remote;
export default class AddCertificate extends BaseComponent {
props: AddCertificateProps;
@@ -49,7 +49,7 @@ export default class AddCertificate extends BaseComponent {
this.initListeners();
}
validateAndAdd(): void {
async validateAndAdd(): Promise<void> {
const certificate = this._certFile;
const serverUrl = this.serverUrl.value;
if (certificate !== '' && serverUrl !== '') {
@@ -59,12 +59,13 @@ export default class AddCertificate extends BaseComponent {
if (!copy) {
return;
}
CertificateUtil.setCertificate(server, fileName);
dialog.showMessageBox({
this.serverUrl.value = '';
await dialog.showMessageBox({
title: 'Success',
message: 'Certificate saved!'
});
this.serverUrl.value = '';
} else {
dialog.showErrorBox('Error', `Please, ${serverUrl === '' ?
'Enter an Organization URL' : 'Choose certificate file'}`);
@@ -75,23 +76,23 @@ export default class AddCertificate extends BaseComponent {
const showDialogOptions: OpenDialogOptions = {
title: 'Select file',
properties: ['openFile'],
filters: [{ name: 'crt, pem', extensions: ['crt', 'pem'] }]
filters: [{name: 'crt, pem', extensions: ['crt', 'pem']}]
};
const { filePaths, canceled } = await dialog.showOpenDialog(showDialogOptions);
const {filePaths, canceled} = await dialog.showOpenDialog(showDialogOptions);
if (!canceled) {
this._certFile = filePaths[0] || '';
this.validateAndAdd();
await this.validateAndAdd();
}
}
initListeners(): void {
this.addCertificateButton.addEventListener('click', () => {
this.addHandler();
this.addCertificateButton.addEventListener('click', async () => {
await this.addHandler();
});
this.serverUrl.addEventListener('keypress', event => {
this.serverUrl.addEventListener('keypress', async event => {
if (event.key === 'Enter') {
this.addHandler();
await this.addHandler();
}
});
}

View File

@@ -1,4 +1,4 @@
import electron, { app } from 'electron';
import electron, {app} from 'electron';
import * as ConfigUtil from '../../utils/config-util';
@@ -30,6 +30,7 @@ function updateOverlayIcon(messageCount: number, mainWindow: electron.BrowserWin
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(ConfigUtil.getConfigItem('flashTaskbarOnMessage'));
}
if (messageCount === 0) {
mainWindow.setOverlayIcon(null, '');
} else {

View File

@@ -1,4 +1,4 @@
import { ipcRenderer } from 'electron';
import {ipcRenderer} from 'electron';
import escape from 'escape-html';
import BaseComponent from '../../components/base';
@@ -35,8 +35,9 @@ export default class BaseSection extends BaseComponent {
</div>
</div>
`;
} else {
return `
}
return `
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox">
@@ -44,10 +45,9 @@ export default class BaseSection extends BaseComponent {
</div>
</div>
`;
}
}
/* a method that in future can be used to create dropdown menus using <select> <option> tags.
/* A method that in future can be used to create dropdown menus using <select> <option> tags.
it needs an object which has ``key: value`` pairs and will return a string that can be appended to HTML
*/
generateSelectTemplate(options: {[key: string]: string}, className?: string, idName?: string): string {

View File

@@ -1,4 +1,4 @@
import { ipcRenderer } from 'electron';
import {ipcRenderer} from 'electron';
import BaseSection from './base-section';
import * as DomainUtil from '../../utils/domain-util';

View File

@@ -40,19 +40,21 @@ export default class FindAccounts extends BaseComponent {
this.initListeners();
}
findAccounts(url: string): void {
async findAccounts(url: string): Promise<void> {
if (!url) {
return;
}
if (!url.startsWith('http')) {
url = 'https://' + url;
}
LinkUtil.openBrowser(new URL('/accounts/find', url));
await LinkUtil.openBrowser(new URL('/accounts/find', url));
}
initListeners(): void {
this.$findAccountsButton.addEventListener('click', () => {
this.findAccounts(this.$serverUrlField.value);
this.$findAccountsButton.addEventListener('click', async () => {
await this.findAccounts(this.$serverUrlField.value);
});
this.$serverUrlField.addEventListener('click', () => {
@@ -61,9 +63,9 @@ export default class FindAccounts extends BaseComponent {
}
});
this.$serverUrlField.addEventListener('keypress', event => {
this.$serverUrlField.addEventListener('keypress', async event => {
if (event.key === 'Enter') {
this.findAccounts(this.$serverUrlField.value);
await this.findAccounts(this.$serverUrlField.value);
}
});

View File

@@ -1,9 +1,9 @@
import { ipcRenderer, remote, OpenDialogOptions } from 'electron';
import {ipcRenderer, remote, OpenDialogOptions} from 'electron';
import path from 'path';
import fs from 'fs-extra';
const { app, dialog } = remote;
const {app, dialog} = remote;
const currentBrowserWindow = remote.getCurrentWindow();
import BaseSection from './base-section';
@@ -101,7 +101,7 @@ export default class GeneralSection extends BaseSection {
<div class="setting-description">${t.__('Enable error reporting (requires restart)')}</div>
<div class="setting-control"></div>
</div>
<div class="setting-row" id="app-language">
<div class="setting-description">${t.__('App language (requires restart)')}</div>
<div id="lang-div" class="lang-div"></div>
@@ -115,7 +115,7 @@ export default class GeneralSection extends BaseSection {
</div>
<div class="setting-row" id="remove-custom-css">
<div class="setting-description">
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigItem('customCSS')}</div>
<div class="selected-css-path" id="custom-css-path">${ConfigUtil.getConfigString('customCSS', '')}</div>
</div>
<div class="action red" id="css-delete-action">
<i class="material-icons">indeterminate_check_box</i>
@@ -130,7 +130,7 @@ export default class GeneralSection extends BaseSection {
</div>
<div class="setting-row">
<div class="setting-description">
<div class="download-folder-path">${ConfigUtil.getConfigItem('downloadsPath', `${app.getPath('downloads')}`)}</div>
<div class="download-folder-path">${ConfigUtil.getConfigString('downloadsPath', app.getPath('downloads'))}</div>
</div>
</div>
<div class="setting-row" id="prompt-download">
@@ -178,6 +178,7 @@ export default class GeneralSection extends BaseSection {
if (process.platform === 'win32') {
this.updateFlashTaskbar();
}
// Dock bounce on macOS
if (process.platform === 'darwin') {
this.updateDockBouncing();
@@ -264,6 +265,7 @@ export default class GeneralSection extends BaseSection {
ConfigUtil.setConfigItem('betaUpdate', false);
this.betaUpdateOption();
}
this.autoUpdateOption();
}
});
@@ -374,7 +376,7 @@ export default class GeneralSection extends BaseSection {
const clearAppDataMessage = 'By clicking proceed you will be removing all added accounts and preferences from Zulip. When the application restarts, it will be as if you are starting Zulip for the first time.';
const getAppPath = path.join(app.getPath('appData'), app.name);
const { response } = await dialog.showMessageBox({
const {response} = await dialog.showMessageBox({
type: 'warning',
buttons: ['YES', 'NO'],
defaultId: 0,
@@ -382,8 +384,8 @@ export default class GeneralSection extends BaseSection {
detail: clearAppDataMessage
});
if (response === 0) {
fs.remove(getAppPath);
setTimeout(() => ipcRenderer.send('forward-message', 'hard-reload'), 1000);
await fs.remove(getAppPath);
ipcRenderer.send('forward-message', 'hard-reload');
}
}
@@ -391,10 +393,10 @@ export default class GeneralSection extends BaseSection {
const showDialogOptions: OpenDialogOptions = {
title: 'Select file',
properties: ['openFile'],
filters: [{ name: 'CSS file', extensions: ['css'] }]
filters: [{name: 'CSS file', extensions: ['css']}]
};
const { filePaths, canceled } = await dialog.showOpenDialog(showDialogOptions);
const {filePaths, canceled} = await dialog.showOpenDialog(showDialogOptions);
if (!canceled) {
ConfigUtil.setConfigItem('customCSS', filePaths[0]);
ipcRenderer.send('forward-message', 'hard-reload');
@@ -403,8 +405,8 @@ export default class GeneralSection extends BaseSection {
updateResetDataOption(): void {
const resetDataButton = document.querySelector('#resetdata-option .reset-data-button');
resetDataButton.addEventListener('click', () => {
this.clearAppDataDialog();
resetDataButton.addEventListener('click', async () => {
await this.clearAppDataDialog();
});
}
@@ -413,7 +415,7 @@ export default class GeneralSection extends BaseSection {
// This path is for the JSON file that stores key: value pairs for supported locales
const langList = this.generateSelectTemplate(supportedLocales, 'lang-menu');
langDiv.innerHTML += langList;
// langMenu is the select-option dropdown menu formed after executing the previous command
// `langMenu` is the select-option dropdown menu formed after executing the previous command
const langMenu: HTMLSelectElement = document.querySelector('.lang-menu');
// The next three lines set the selected language visible on the dropdown button
@@ -440,8 +442,8 @@ export default class GeneralSection extends BaseSection {
addCustomCSS(): void {
const customCSSButton = document.querySelector('#add-custom-css .custom-css-button');
customCSSButton.addEventListener('click', () => {
this.customCssDialog();
customCSSButton.addEventListener('click', async () => {
await this.customCssDialog();
});
}
@@ -466,7 +468,7 @@ export default class GeneralSection extends BaseSection {
properties: ['openDirectory']
};
const { filePaths, canceled } = await dialog.showOpenDialog(showDialogOptions);
const {filePaths, canceled} = await dialog.showOpenDialog(showDialogOptions);
if (!canceled) {
ConfigUtil.setConfigItem('downloadsPath', filePaths[0]);
const downloadFolderPath: HTMLElement = document.querySelector('.download-folder-path');
@@ -476,8 +478,8 @@ export default class GeneralSection extends BaseSection {
downloadFolder(): void {
const downloadFolder = document.querySelector('#download-folder .download-folder-button');
downloadFolder.addEventListener('click', () => {
this.downloadFolderDialog();
downloadFolder.addEventListener('click', async () => {
await this.downloadFolderDialog();
});
}

View File

@@ -1,4 +1,4 @@
import { ipcRenderer } from 'electron';
import {ipcRenderer} from 'electron';
import BaseSection from './base-section';
import * as ConfigUtil from '../../utils/config-util';
@@ -105,11 +105,13 @@ export default class NetworkSection extends BaseSection {
ConfigUtil.setConfigItem('useManualProxy', !manualProxyValue);
this.toggleManualProxySettings(!manualProxyValue);
}
if (!newValue) {
// Remove proxy system proxy settings
ConfigUtil.setConfigItem('proxyRules', '');
ipcRenderer.send('forward-message', 'reload-proxy', false);
}
ConfigUtil.setConfigItem('useSystemProxy', newValue);
this.updateProxyOption();
}
@@ -124,6 +126,7 @@ export default class NetworkSection extends BaseSection {
if (systemProxyValue && newValue) {
ConfigUtil.setConfigItem('useSystemProxy', !systemProxyValue);
}
ConfigUtil.setConfigItem('proxyRules', '');
ConfigUtil.setConfigItem('useManualProxy', newValue);
// Reload app only when turning manual proxy off, hence !newValue

View File

@@ -1,10 +1,12 @@
import { ipcRenderer } from 'electron';
import {ipcRenderer, remote} from 'electron';
import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
const {dialog} = remote;
interface NewServerFormProps {
$root: Element;
onChange: () => void;
@@ -66,11 +68,16 @@ export default class NewServerForm extends BaseComponent {
let serverConf;
try {
serverConf = await DomainUtil.checkDomain(this.$newServerUrl.value);
} catch (errorMessage) {
} catch (error) {
this.$saveServerButton.innerHTML = 'Connect';
alert(errorMessage);
await dialog.showMessageBox({
type: 'error',
message: error.toString(),
buttons: ['OK']
});
return;
}
await DomainUtil.addDomain(serverConf);
this.props.onChange();
}
@@ -78,8 +85,8 @@ export default class NewServerForm extends BaseComponent {
openCreateNewOrgExternalLink(): void {
const link = 'https://zulipchat.com/new/';
const externalCreateNewOrgElement = document.querySelector('#open-create-org-link');
externalCreateNewOrgElement.addEventListener('click', () => {
LinkUtil.openBrowser(new URL(link));
externalCreateNewOrgElement.addEventListener('click', async () => {
await LinkUtil.openBrowser(new URL(link));
});
}
@@ -89,15 +96,15 @@ export default class NewServerForm extends BaseComponent {
}
initActions(): void {
this.$saveServerButton.addEventListener('click', () => {
this.submitFormHandler();
this.$saveServerButton.addEventListener('click', async () => {
await this.submitFormHandler();
});
this.$newServerUrl.addEventListener('keypress', event => {
this.$newServerUrl.addEventListener('keypress', async event => {
if (event.key === 'Enter') {
this.submitFormHandler();
await this.submitFormHandler();
}
});
// open create new org link in default browser
// Open create new org link in default browser
this.openCreateNewOrgExternalLink();
this.networkSettingsLink();
}

View File

@@ -1,4 +1,4 @@
import { ipcRenderer } from 'electron';
import {ipcRenderer} from 'electron';
import BaseComponent from '../../components/base';
import Nav from './nav';
@@ -7,6 +7,7 @@ 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';
type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection;
@@ -37,6 +38,7 @@ export default class PreferenceView extends BaseComponent {
if (hasTag) {
nav = hasTag.slice(1);
}
this.handleNavigation(nav);
}
@@ -49,32 +51,38 @@ export default class PreferenceView extends BaseComponent {
});
break;
}
case 'General': {
this.section = new GeneralSection({
$root: this.$settingsContainer
});
break;
}
case 'Organizations': {
this.section = new ConnectedOrgSection({
$root: this.$settingsContainer
});
break;
}
case 'Network': {
this.section = new NetworkSection({
$root: this.$settingsContainer
});
break;
}
case 'Shortcuts': {
this.section = new ShortcutsSection({
$root: this.$settingsContainer
});
break;
}
default: break;
}
this.section.init();
window.location.hash = `#${navItem}`;
}
@@ -105,7 +113,7 @@ export default class PreferenceView extends BaseComponent {
this.handleToggle('tray-option', state);
});
ipcRenderer.on('toggle-dnd', (_event: Event, _state: boolean, newSettings: any) => {
ipcRenderer.on('toggle-dnd', (_event: Event, _state: boolean, newSettings: DNDSettings) => {
this.handleToggle('show-notification-option', newSettings.showNotification);
this.handleToggle('silent-option', newSettings.silent);

View File

@@ -1,11 +1,11 @@
import { remote, ipcRenderer } from 'electron';
import {remote, ipcRenderer} from 'electron';
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;
const {dialog} = remote;
interface ServerInfoFormProps {
$root: Element;
@@ -66,7 +66,7 @@ export default class ServerInfoForm extends BaseComponent {
initActions(): void {
this.$deleteServerButton.addEventListener('click', async () => {
const { response } = await dialog.showMessageBox({
const {response} = await dialog.showMessageBox({
type: 'warning',
buttons: [t.__('YES'), t.__('NO')],
defaultId: 0,
@@ -76,7 +76,7 @@ export default class ServerInfoForm extends BaseComponent {
if (DomainUtil.removeDomain(this.props.index)) {
ipcRenderer.send('reload-full-app');
} else {
const { title, content } = Messages.orgRemovalError(DomainUtil.getDomain(this.props.index).url);
const {title, content} = Messages.orgRemovalError(DomainUtil.getDomain(this.props.index).url);
dialog.showErrorBox(title, content);
}
}

View File

@@ -332,8 +332,8 @@ export default class ShortcutsSection extends BaseSection {
openHotkeysExternalLink(): void {
const link = 'https://zulipchat.com/help/keyboard-shortcuts';
const externalCreateNewOrgElement = document.querySelector('#open-hotkeys-link');
externalCreateNewOrgElement.addEventListener('click', () => {
LinkUtil.openBrowser(new URL(link));
externalCreateNewOrgElement.addEventListener('click', async () => {
await LinkUtil.openBrowser(new URL(link));
});
}

View File

@@ -1,4 +1,4 @@
import { contextBridge, ipcRenderer, webFrame } from 'electron';
import {contextBridge, ipcRenderer, webFrame} from 'electron';
import fs from 'fs';
import * as SetupSpellChecker from './spellchecker';
@@ -48,19 +48,20 @@ ipcRenderer.on('show-notification-settings', () => {
const notificationItem: NodeListOf<HTMLElement> = document.querySelectorAll('.normal-settings-list li div');
// wait until the notification dom element shows up
// Wait until the notification dom element shows up
setTimeout(() => {
notificationItem[2].click();
}, 100);
});
electron_bridge.once('zulip-loaded', ({ serverLanguage }) => {
electron_bridge.once('zulip-loaded', ({serverLanguage}) => {
// Get the default language of the server
if (serverLanguage) {
// Init spellchecker
SetupSpellChecker.init(serverLanguage);
}
// redirect users to network troubleshooting page
// Redirect users to network troubleshooting page
const getRestartButton = document.querySelector('.restart_get_events_button');
if (getRestartButton) {
getRestartButton.addEventListener('click', () => {
@@ -79,12 +80,13 @@ window.addEventListener('load', (event: any): void => {
if (!event.target.URL.includes('app/renderer/network.html')) {
return;
}
const $reconnectButton = document.querySelector('#reconnect');
const $settingsButton = document.querySelector('#settings');
NetworkError.init($reconnectButton, $settingsButton);
});
// electron's globalShortcut can cause unexpected results
// Electron's globalShortcut can cause unexpected results
// so adding the reload shortcut in the old-school way
// Zoom from numpad keys is not supported by electron, so adding it through listeners.
document.addEventListener('keydown', event => {
@@ -105,6 +107,7 @@ ipcRenderer.on('set-active', () => {
if (isDev) {
console.log('active');
}
electron_bridge.idle_on_system = false;
electron_bridge.last_active_on_system = Date.now();
});
@@ -114,9 +117,10 @@ ipcRenderer.on('set-idle', () => {
if (isDev) {
console.log('idle');
}
electron_bridge.idle_on_system = true;
});
webFrame.executeJavaScript(
(async () => webFrame.executeJavaScript(
fs.readFileSync(require.resolve('./injected'), 'utf8')
);
))();

View File

@@ -1,5 +1,5 @@
import type { Subject } from 'rxjs';
import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker';
import type {Subject} from 'rxjs';
import {SpellCheckHandler, ContextMenuListener, ContextMenuBuilder} from 'electron-spellchecker';
import * as ConfigUtil from './utils/config-util';
import Logger from './utils/logger-util';
@@ -23,14 +23,15 @@ export function init(serverLanguage: string): void {
if (ConfigUtil.getConfigItem('enableSpellchecker')) {
enableSpellChecker();
}
enableContextMenu(serverLanguage);
}
function enableSpellChecker(): void {
try {
spellCheckHandler = new SpellCheckHandler();
} catch (err) {
logger.error(err);
} catch (error) {
logger.error(error);
}
}
@@ -53,6 +54,7 @@ export function unsubscribeSpellChecker(): void {
if (spellCheckHandler) {
spellCheckHandler.unsubscribe();
}
if (contextMenuListener) {
contextMenuListener.unsubscribe();
}

View File

@@ -1,13 +1,13 @@
import { ipcRenderer, remote, WebviewTag, NativeImage } from 'electron';
import {ipcRenderer, remote, WebviewTag, NativeImage} from 'electron';
import path from 'path';
import * as ConfigUtil from './utils/config-util';
const { Tray, Menu, nativeImage, BrowserWindow, nativeTheme } = remote;
const {Tray, Menu, nativeImage, BrowserWindow, nativeTheme} = remote;
let tray: Electron.Tray;
// get the theme on macOS
// Get the theme on macOS
const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
const ICON_DIR = process.platform === 'darwin' ? `../../resources/tray/${theme}` : '../../resources/tray';
@@ -20,6 +20,7 @@ const iconPath = (): string => {
if (process.platform === 'linux') {
return APP_ICON + 'linux.png';
}
return APP_ICON + (process.platform === 'win32' ? 'win.ico' : 'osx.png');
};
@@ -92,6 +93,7 @@ const renderCanvas = function (arg: number): HTMLCanvasElement {
return canvas;
};
/**
* Renders the tray icon as a native image
* @param arg: Unread count
@@ -168,6 +170,7 @@ ipcRenderer.on('tray', (_event: Event, arg: number): void => {
if (!tray) {
return;
}
// We don't want to create tray from unread messages on macOS since it already has dock badges.
if (process.platform === 'linux' || process.platform === 'win32') {
if (arg === 0) {
@@ -178,7 +181,7 @@ ipcRenderer.on('tray', (_event: Event, arg: number): void => {
unread = arg;
const image = renderNativeImage(arg);
tray.setImage(image);
tray.setToolTip(arg + ' unread messages');
tray.setToolTip(`${arg} unread messages`);
}
}
});
@@ -191,6 +194,7 @@ function toggleTray(): void {
if (tray.isDestroyed()) {
tray = null;
}
ConfigUtil.setConfigItem('trayIcon', false);
} else {
state = true;
@@ -198,10 +202,12 @@ function toggleTray(): void {
if (process.platform === 'linux' || process.platform === 'win32') {
const image = renderNativeImage(unread);
tray.setImage(image);
tray.setToolTip(unread + ' unread messages');
tray.setToolTip(`${unread} unread messages`);
}
ConfigUtil.setConfigItem('trayIcon', true);
}
const selector = 'webview:not([class*=disabled])';
const webview: WebviewTag = document.querySelector(selector);
const webContents = webview.getWebContents();

View File

@@ -1,12 +1,13 @@
import { remote } from 'electron';
import { JsonDB } from 'node-json-db';
import { initSetUp } from './default-util';
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 } = remote;
const {app, dialog} =
process.type === 'renderer' ? electron.remote : electron;
initSetUp();
@@ -21,14 +22,9 @@ let db: JsonDB;
reloadDB();
export function getCertificate(server: string, defaultValue: any = null): any {
export function getCertificate(server: string): string | undefined {
reloadDB();
const value = db.getData('/')[server];
if (value === undefined) {
return defaultValue;
} else {
return value;
}
return db.getData('/')[server];
}
// Function to copy the certificate to userData folder
@@ -38,14 +34,15 @@ export function copyCertificate(_server: string, location: string, fileName: str
try {
fs.copyFileSync(location, filePath);
copied = true;
} catch (err) {
} 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(err);
logger.error(error);
}
return copied;
}
@@ -65,7 +62,7 @@ function reloadDB(): void {
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
} catch (error) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
@@ -73,8 +70,9 @@ function reloadDB(): void {
'We encountered error while saving the certificate.'
);
logger.error('Error while JSON parsing certificates.json: ');
logger.error(err);
logger.error(error);
}
}
db = new JsonDB(settingsJsonPath, true, true);
}

View File

@@ -1,4 +1,4 @@
// unescape already encoded/escaped strings
// Unescape already encoded/escaped strings
export function decodeString(stringInput: string): string {
const parser = new DOMParser();
const dom = parser.parseFromString(

View File

@@ -1,4 +1,4 @@
import { JsonDB } from 'node-json-db';
import {JsonDB} from 'node-json-db';
import fs from 'fs';
import path from 'path';
@@ -16,7 +16,7 @@ let app: Electron.App = null;
/* To make the util runnable in both main and renderer process */
if (process.type === 'renderer') {
const { remote } = electron;
const {remote} = electron;
dialog = remote.dialog;
app = remote.app;
} else {
@@ -28,39 +28,52 @@ let db: JsonDB;
reloadDB();
export function getConfigItem(key: string, defaultValue: any = null): any {
export function getConfigItem(key: string, defaultValue: unknown = null): any {
try {
db.reload();
} catch (err) {
} catch (error) {
logger.error('Error while reloading settings.json: ');
logger.error(err);
logger.error(error);
}
const value = db.getData('/')[key];
if (value === undefined) {
setConfigItem(key, defaultValue);
return defaultValue;
} else {
}
return value;
}
export function getConfigString(key: string, defaultValue: string): string {
const value = getConfigItem(key, defaultValue);
if (typeof value === 'string') {
return value;
}
setConfigItem(key, defaultValue);
return defaultValue;
}
// This function returns whether a key exists in the configuration file (settings.json)
export function isConfigItemExists(key: string): boolean {
try {
db.reload();
} catch (err) {
} catch (error) {
logger.error('Error while reloading settings.json: ');
logger.error(err);
logger.error(error);
}
const value = db.getData('/')[key];
return (value !== undefined);
}
export function setConfigItem(key: string, value: any, override? : boolean): void {
export function setConfigItem(key: string, value: unknown, override? : boolean): void {
if (EnterpriseUtil.configItemExists(key) && !override) {
// if item is in global config and we're not trying to override
// If item is in global config and we're not trying to override
return;
}
db.push(`/${key}`, value, true);
db.save();
}
@@ -75,7 +88,7 @@ function reloadDB(): void {
try {
const file = fs.readFileSync(settingsJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
} catch (error) {
if (fs.existsSync(settingsJsonPath)) {
fs.unlinkSync(settingsJsonPath);
dialog.showErrorBox(
@@ -83,9 +96,10 @@ function reloadDB(): void {
'We encountered an error while saving the settings.'
);
logger.error('Error while JSON parsing settings.json: ');
logger.error(err);
logger.reportSentry(err);
logger.error(error);
logger.reportSentry(error);
}
}
db = new JsonDB(settingsJsonPath, true, true);
}

View File

@@ -14,7 +14,7 @@ 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
// If it is the first time the app is running
// create zulip dir in userData folder to
// avoid errors
if (!setupCompleted) {
@@ -63,7 +63,7 @@ export const initSetUp = (): void => {
fs.unlinkSync(data.path);
}
});
// window-state.json is only deleted not moved, as the electron-window-state
// `window-state.json` is only deleted not moved, as the electron-window-state
// package will recreate the file in the config folder.
if (fs.existsSync(windowStateJson)) {
fs.unlinkSync(windowStateJson);

View File

@@ -1,28 +1,28 @@
import * as ConfigUtil from './config-util';
// TODO: TypeScript - add to Setting interface
// the list of settings since we have fixed amount of them
// We want to do this by creating a new module that exports
// this interface
interface Setting {
[key: string]: boolean;
type SettingName = 'showNotification' | 'silent' | 'flashTaskbarOnMessage';
export interface DNDSettings {
showNotification?: boolean;
silent?: boolean;
flashTaskbarOnMessage?: boolean;
}
interface Toggle {
dnd: boolean;
newSettings: Setting;
newSettings: DNDSettings;
}
export function toggle(): Toggle {
const dnd = !ConfigUtil.getConfigItem('dnd', false);
const dndSettingList = ['showNotification', 'silent'];
const dndSettingList: SettingName[] = ['showNotification', 'silent'];
if (process.platform === 'win32') {
dndSettingList.push('flashTaskbarOnMessage');
}
let newSettings: Setting;
let newSettings: DNDSettings;
if (dnd) {
const oldSettings: Setting = {};
const oldSettings: DNDSettings = {};
newSettings = {};
// Iterate through the dndSettingList.

View File

@@ -1,17 +1,17 @@
import { JsonDB } from 'node-json-db';
import {JsonDB} from 'node-json-db';
import escape from 'escape-html';
import request from 'request';
import fs from 'fs';
import path from 'path';
import Logger from './logger-util';
import { remote } from 'electron';
import {remote} from 'electron';
import * as RequestUtil from './request-util';
import * as EnterpriseUtil from './enterprise-util';
import * as Messages from '../../../resources/messages';
const { app, dialog } = remote;
const {app, dialog} = remote;
export interface ServerConf {
url: string;
@@ -27,25 +27,27 @@ const logger = new Logger({
const defaultIconUrl = '../renderer/img/icon.png';
export let db: JsonDB;
let db: JsonDB;
reloadDB();
// Migrate from old schema
if (db.getData('/').domain) {
addDomain({
alias: 'Zulip',
url: db.getData('/domain')
});
db.delete('/domain');
(async () => {
await addDomain({
alias: 'Zulip',
url: db.getData('/domain')
});
db.delete('/domain');
})();
}
export function getDomains(): ServerConf[] {
reloadDB();
if (db.getData('/').domains === undefined) {
return [];
} else {
return db.getData('/domains');
}
return db.getData('/domains');
}
export function getDomain(index: number): ServerConf {
@@ -60,16 +62,17 @@ export function shouldIgnoreCerts(url: string): boolean {
return domain.ignoreCerts;
}
}
return null;
}
function updateDomain(index: number, server: ServerConf): void {
export function updateDomain(index: number, server: ServerConf): void {
reloadDB();
db.push(`/domains[${index}]`, server, true);
}
export async function addDomain(server: ServerConf): Promise<void> {
const { ignoreCerts } = server;
const {ignoreCerts} = server;
if (server.icon) {
const localIconUrl = await saveServerIcon(server, ignoreCerts);
server.icon = localIconUrl;
@@ -91,6 +94,7 @@ export function removeDomain(index: number): boolean {
if (EnterpriseUtil.isPresetOrg(getDomain(index).url)) {
return false;
}
db.delete(`/domains[${index}]`);
reloadDB();
return true;
@@ -102,38 +106,39 @@ export function duplicateDomain(domain: string): boolean {
return getDomains().some(server => server.url === domain);
}
async function checkCertError(domain: string, serverConf: ServerConf, error: string, silent: boolean): Promise<ServerConf> {
async function checkCertError(domain: string, serverConf: ServerConf, error: any, silent: boolean): Promise<ServerConf> {
if (silent) {
// since getting server settings has already failed
// Since getting server settings has already failed
return serverConf;
} else {
// Report error to sentry to get idea of possible certificate errors
// users get when adding the servers
logger.reportSentry(new Error(error).toString());
const certErrorMessage = Messages.certErrorMessage(domain, error);
const certErrorDetail = Messages.certErrorDetail();
}
const { response } = await dialog.showMessageBox({
type: 'warning',
buttons: ['Yes', 'No'],
defaultId: 1,
message: certErrorMessage,
detail: certErrorDetail
});
if (response === 0) {
// set ignoreCerts parameter to true in case user responds with yes
serverConf.ignoreCerts = true;
try {
return await getServerSettings(domain, serverConf.ignoreCerts);
} catch (_) {
if (error === Messages.noOrgsError(domain)) {
throw new Error(error);
}
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();
const {response} = await dialog.showMessageBox({
type: 'warning',
buttons: ['Yes', 'No'],
defaultId: 1,
message: certErrorMessage,
detail: certErrorDetail
});
if (response === 0) {
// Set ignoreCerts parameter to true in case user responds with yes
serverConf.ignoreCerts = true;
try {
return await getServerSettings(domain, serverConf.ignoreCerts);
} catch (_) {
if (error === Messages.noOrgsError(domain)) {
throw new Error(error);
}
} else {
throw new Error('Untrusted certificate.');
return serverConf;
}
} else {
throw new Error('Untrusted certificate.');
}
}
@@ -156,18 +161,18 @@ export async function checkDomain(domain: string, ignoreCerts = false, silent =
try {
return await getServerSettings(domain, serverConf.ignoreCerts);
} catch (err) {
} catch (error_) {
// Make sure that error is an error or string not undefined
// so validation does not throw error.
const error = err || '';
const error = error_ || '';
const certsError = error.toString().includes('certificate');
if (certsError) {
const result = await checkCertError(domain, serverConf, error, silent);
return result;
} else {
throw new Error(Messages.invalidZulipServerError(domain));
}
throw new Error(Messages.invalidZulipServerError(domain));
}
}
@@ -178,16 +183,20 @@ async function getServerSettings(domain: string, ignoreCerts = false): Promise<S
};
return new Promise((resolve, reject) => {
request(serverSettingsOptions, (error: string, response: any) => {
request(serverSettingsOptions, (error: Error, response: request.Response) => {
if (!error && response.statusCode === 200) {
const data = JSON.parse(response.body);
if (Object.prototype.hasOwnProperty.call(data, 'realm_icon') && data.realm_icon) {
const {realm_name, realm_uri, realm_icon} = JSON.parse(response.body);
if (
typeof realm_name === 'string' &&
typeof realm_uri === 'string' &&
typeof realm_icon === 'string'
) {
resolve({
// Some Zulip Servers use absolute URL for server icon whereas others use relative URL
// Following check handles both the cases
icon: data.realm_icon.startsWith('/') ? data.realm_uri + data.realm_icon : data.realm_icon,
url: data.realm_uri,
alias: escape(data.realm_name),
icon: realm_icon.startsWith('/') ? realm_uri + realm_icon : realm_icon,
url: realm_uri,
alias: escape(realm_name),
ignoreCerts
});
} else {
@@ -214,8 +223,8 @@ export async function saveServerIcon(server: ServerConf, ignoreCerts = false): P
const filePath = generateFilePath(url);
const file = fs.createWriteStream(filePath);
try {
request(serverIconOptions).on('response', (response: any) => {
response.on('error', (err: string) => {
request(serverIconOptions).on('response', (response: request.Response) => {
response.on('error', (err: Error) => {
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
@@ -224,16 +233,16 @@ export async function saveServerIcon(server: ServerConf, ignoreCerts = false): P
response.pipe(file).on('finish', () => {
resolve(filePath);
});
}).on('error', (err: string) => {
}).on('error', (err: Error) => {
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
resolve(defaultIconUrl);
});
} catch (err) {
} catch (error) {
logger.log('Could not get server icon.');
logger.log(err);
logger.reportSentry(err);
logger.log(error);
logger.reportSentry(error);
resolve(defaultIconUrl);
}
});
@@ -242,7 +251,7 @@ export async function saveServerIcon(server: ServerConf, ignoreCerts = false): P
export async function updateSavedServer(url: string, index: number): Promise<void> {
// Does not promise successful update
const oldIcon = getDomain(index).icon;
const { ignoreCerts } = getDomain(index);
const {ignoreCerts} = getDomain(index);
try {
const newServerConf = await checkDomain(url, ignoreCerts, true);
const localIconUrl = await saveServerIcon(newServerConf, ignoreCerts);
@@ -251,19 +260,19 @@ export async function updateSavedServer(url: string, index: number): Promise<voi
updateDomain(index, newServerConf);
reloadDB();
}
} catch (err) {
} catch (error) {
logger.log('Could not update server icon.');
logger.log(err);
logger.reportSentry(err);
logger.log(error);
logger.reportSentry(error);
}
}
export function reloadDB(): void {
function reloadDB(): void {
const domainJsonPath = path.join(app.getPath('userData'), 'config/domain.json');
try {
const file = fs.readFileSync(domainJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
} catch (error) {
if (fs.existsSync(domainJsonPath)) {
fs.unlinkSync(domainJsonPath);
dialog.showErrorBox(
@@ -272,10 +281,11 @@ export function reloadDB(): void {
'you may have to re-add your previous organizations back.'
);
logger.error('Error while JSON parsing domain.json: ');
logger.error(err);
logger.reportSentry(err);
logger.error(error);
logger.reportSentry(error);
}
}
db = new JsonDB(domainJsonPath, true, true);
}
@@ -284,7 +294,7 @@ function generateFilePath(url: string): string {
const extension = path.extname(url).split('?')[0];
let hash = 5381;
let { length } = url;
let {length} = url;
while (length) {
hash = (hash * 33) ^ url.charCodeAt(--length);
@@ -302,8 +312,10 @@ export function formatUrl(domain: string): string {
if (domain.startsWith('http://') || domain.startsWith('https://')) {
return domain;
}
if (domain.startsWith('localhost:')) {
return `http://${domain}`;
}
return `https://${domain}`;
}

View File

@@ -8,9 +8,9 @@ const logger = new Logger({
timestamp: true
});
// todo: replace enterpriseSettings type with an interface once settings are final
export let enterpriseSettings: any;
export let configFile: boolean;
// TODO: replace enterpriseSettings type with an interface once settings are final
let enterpriseSettings: {[key: string]: unknown};
let configFile: boolean;
reloadDB();
@@ -26,23 +26,29 @@ function reloadDB(): void {
try {
const file = fs.readFileSync(enterpriseFile, 'utf8');
enterpriseSettings = JSON.parse(file);
} catch (err) {
} catch (error) {
logger.log('Error while JSON parsing global_config.json: ');
logger.log(err);
logger.log(error);
}
} else {
configFile = false;
}
}
export function getConfigItem(key: string, defaultValue?: any): any {
export function hasConfigFile(): boolean {
return configFile;
}
export function getConfigItem(key: string, defaultValue?: unknown): any {
reloadDB();
if (!configFile) {
return defaultValue;
}
if (defaultValue === undefined) {
defaultValue = null;
}
return configItemExists(key) ? enterpriseSettings[key] : defaultValue;
}
@@ -51,6 +57,7 @@ export function configItemExists(key: string): boolean {
if (!configFile) {
return false;
}
return (enterpriseSettings[key] !== undefined);
}
@@ -58,11 +65,17 @@ export function isPresetOrg(url: string): boolean {
if (!configFile || !configItemExists('presetOrganizations')) {
return false;
}
const presetOrgs = enterpriseSettings.presetOrganizations;
if (!Array.isArray(presetOrgs)) {
throw new TypeError('Expected array for presetOrgs');
}
for (const org of presetOrgs) {
if (url.includes(org)) {
return true;
}
}
return false;
}

View File

@@ -1,4 +1,4 @@
import { shell } from 'electron';
import {shell} from 'electron';
import escape from 'escape-html';
import fs from 'fs';
import os from 'os';
@@ -8,9 +8,9 @@ export function isUploadsUrl(server: string, url: URL): boolean {
return url.origin === server && url.pathname.startsWith('/user_uploads/');
}
export function openBrowser(url: URL): void {
export async function openBrowser(url: URL): Promise<void> {
if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
shell.openExternal(url.href);
await shell.openExternal(url.href);
} else {
// For security, indirect links to non-whitelisted protocols
// through a real web browser via a local HTML file.

View File

@@ -1,4 +1,4 @@
import { JsonDB } from 'node-json-db';
import {JsonDB} from 'node-json-db';
import fs from 'fs';
import path from 'path';
@@ -14,24 +14,24 @@ const logger = new Logger({
});
/* To make the util runnable in both main and renderer process */
const { dialog, app } = remote;
const {dialog, app} = remote;
let db: JsonDB;
reloadDB();
export function getUpdateItem(key: string, defaultValue: any = null): any {
export function getUpdateItem(key: string, defaultValue: unknown = null): any {
reloadDB();
const value = db.getData('/')[key];
if (value === undefined) {
setUpdateItem(key, defaultValue);
return defaultValue;
} else {
return value;
}
return value;
}
export function setUpdateItem(key: string, value: any): void {
export function setUpdateItem(key: string, value: unknown): void {
db.push(`/${key}`, value, true);
reloadDB();
}
@@ -46,7 +46,7 @@ function reloadDB(): void {
try {
const file = fs.readFileSync(linuxUpdateJsonPath, 'utf8');
JSON.parse(file);
} catch (err) {
} catch (error) {
if (fs.existsSync(linuxUpdateJsonPath)) {
fs.unlinkSync(linuxUpdateJsonPath);
dialog.showErrorBox(
@@ -54,8 +54,9 @@ function reloadDB(): void {
'We encountered an error while saving the update notifications.'
);
logger.error('Error while JSON parsing updates.json: ');
logger.error(err);
logger.error(error);
}
}
db = new JsonDB(linuxUpdateJsonPath, true, true);
}

View File

@@ -1,16 +1,11 @@
import { Console as NodeConsole } from 'console'; // eslint-disable-line node/prefer-global/console
import { initSetUp } from './default-util';
import { sentryInit, captureException } from './sentry-util';
import {Console} from 'console'; // eslint-disable-line node/prefer-global/console
import {initSetUp} from './default-util';
import {sentryInit, captureException} from './sentry-util';
import fs from 'fs';
import os from 'os';
import isDev from 'electron-is-dev';
import electron from 'electron';
// this interface adds [key: string]: any so
// we can do console[type] later on in the code
interface PatchedConsole extends Console {
[key: string]: any;
}
interface LoggerOptions {
timestamp?: true | (() => string);
@@ -29,9 +24,9 @@ if (process.type === 'renderer') {
// Report Errors to Sentry only if it is enabled in settings
// Gets the value of reportErrors from config-util for renderer process
// For main process, sentryInit() is handled in index.js
const { ipcRenderer } = electron;
const {ipcRenderer} = electron;
ipcRenderer.send('error-reporting');
ipcRenderer.on('error-reporting-val', (_event: any, errorReporting: boolean) => {
ipcRenderer.on('error-reporting-val', (_event: Event, errorReporting: boolean) => {
reportErrors = errorReporting;
if (reportErrors) {
sentryInit();
@@ -41,15 +36,23 @@ if (process.type === 'renderer') {
app = electron.app;
}
const browserConsole: PatchedConsole = console;
const logDir = `${app.getPath('userData')}/Logs`;
type Level = 'log' | 'debug' | 'info' | 'warn' | 'error';
const levels: Level[] = ['log', 'debug', 'info', 'warn', 'error'];
type LogMethod = (...args: unknown[]) => void;
export default class Logger {
nodeConsole: PatchedConsole;
log: LogMethod;
debug: LogMethod;
info: LogMethod;
warn: LogMethod;
error: LogMethod;
nodeConsole: Console;
timestamp?: () => string;
level: boolean;
logInDevMode: boolean;
[key: string]: any;
constructor(options: LoggerOptions = {}) {
let {
@@ -71,8 +74,8 @@ export default class Logger {
process.nextTick(() => this.trimLog(file));
}
const fileStream = fs.createWriteStream(file, { flags: 'a' });
const nodeConsole = new NodeConsole(fileStream);
const fileStream = fs.createWriteStream(file, {flags: 'a'});
const nodeConsole = new Console(fileStream);
this.nodeConsole = nodeConsole;
this.timestamp = timestamp;
@@ -81,11 +84,10 @@ export default class Logger {
this.setUpConsole();
}
_log(type: string, ...args: any[]): void {
_log(type: Level, ...args: unknown[]): void {
const {
nodeConsole, timestamp, level, logInDevMode
} = this;
let nodeConsoleLog;
/* eslint-disable no-fallthrough */
switch (true) {
@@ -96,27 +98,23 @@ export default class Logger {
args.unshift(type.toUpperCase() + ' |');
case isDev || logInDevMode:
nodeConsoleLog = nodeConsole[type] || nodeConsole.log;
nodeConsoleLog.apply(null, args); // eslint-disable-line prefer-spread
nodeConsole[type](...args);
default: break;
}
/* eslint-enable no-fallthrough */
browserConsole[type].apply(null, args);
console[type](...args);
}
setUpConsole(): void {
for (const type of Object.keys(browserConsole)) {
for (const type of levels) {
this.setupConsoleMethod(type);
}
}
setupConsoleMethod(type: string): void {
this[type] = (...args: any[]) => {
const log = this._log.bind(this, type, ...args);
log();
};
setupConsoleMethod(type: Level): void {
this[type] = (...args: unknown[]) => this._log(type, ...args);
}
getTimestamp(): string {
@@ -127,7 +125,7 @@ export default class Logger {
return timestamp;
}
reportSentry(err: string): void {
reportSentry(err: unknown): void {
if (reportErrors) {
captureException(err);
}
@@ -138,6 +136,7 @@ export default class Logger {
if (err) {
throw err;
}
const MAX_LOG_FILE_LINES = 500;
const logs = data.split(os.EOL);
const logLength = logs.length - 1;

View File

@@ -10,7 +10,7 @@ export function getProxy(_uri: string): ProxyRule | void {
let uri;
try {
uri = new URL(_uri);
} catch (err) {
} catch (_) {
return;
}
@@ -55,12 +55,13 @@ export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Pr
const proxy = await ses.resolveProxy('http://' + resolveProxyUrl);
let httpString = '';
if (proxy !== 'DIRECT') {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// In case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
httpString = 'http=' + proxy.split('PROXY')[1] + ';';
}
}
return httpString;
})();
// Check HTTPS Proxy
@@ -68,12 +69,13 @@ export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Pr
const proxy = await ses.resolveProxy('https://' + resolveProxyUrl);
let httpsString = '';
if (proxy !== 'DIRECT' || proxy.includes('HTTPS')) {
// in case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// In case of proxy HTTPS url:port, windows gives first word as HTTPS while linux gives PROXY
// for all other HTTP or direct url:port both uses PROXY
if (proxy.includes('PROXY') || proxy.includes('HTTPS')) {
httpsString += 'https=' + proxy.split('PROXY')[1] + ';';
}
}
return httpsString;
})();
@@ -86,6 +88,7 @@ export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Pr
ftpString += 'ftp=' + proxy.split('PROXY')[1] + ';';
}
}
return ftpString;
})();
@@ -102,6 +105,7 @@ export async function resolveSystemProxy(mainWindow: Electron.BrowserWindow): Pr
socksString += 'socks=' + proxy.split('PROXY')[1] + ';';
}
}
return socksString;
})();

View File

@@ -1,4 +1,4 @@
import { ipcRenderer } from 'electron';
import {ipcRenderer} from 'electron';
import type WebView from '../components/webview';
import backoff from 'backoff';
@@ -39,19 +39,20 @@ export default class ReconnectUtil {
if (ignoreCerts === null) {
return;
}
request(
{
url: `${this.url}/static/favicon.ico`,
...RequestUtil.requestOptions(this.url, ignoreCerts)
},
(error: Error, response: any) => {
(error: Error, response: request.Response) => {
const isValidResponse =
!error && response.statusCode >= 200 && response.statusCode < 400;
resolve(isValidResponse);
}
);
} catch (err) {
logger.log(err);
} catch (error) {
logger.log(error);
}
});
}
@@ -71,11 +72,13 @@ export default class ReconnectUtil {
if (this.alreadyReloaded) {
return true;
}
if (await this.isOnline()) {
ipcRenderer.send('forward-message', 'reload-viewer');
logger.log('You\'re back online.');
return true;
}
logger.log('There is no internet connection, try checking network cables, modem and router.');
const errorMessageHolder = document.querySelector('#description');
if (errorMessageHolder) {
@@ -83,6 +86,7 @@ export default class ReconnectUtil {
<div>Your internet connection doesn't seem to work properly!</div>
<div>Verify that it works and then click try again.</div>`;
}
return false;
}
}

View File

@@ -1,4 +1,4 @@
import { remote } from 'electron';
import {remote} from 'electron';
import fs from 'fs';
import path from 'path';
@@ -8,7 +8,7 @@ import * as ProxyUtil from './proxy-util';
import * as CertificateUtil from './certificate-util';
import * as SystemUtil from './system-util';
const { app } = remote;
const {app} = remote;
const logger = new Logger({
file: 'request-util.log',
@@ -33,7 +33,7 @@ export function requestOptions(domain: string, ignoreCerts: boolean): RequestUti
let certificateFile = null;
if (certificate?.includes('/')) {
// certificate saved using old app version
// Certificate saved using old app version
certificateFile = certificate;
} else if (certificate) {
certificateFile = path.join(`${app.getPath('userData')}/certificates`, certificate);
@@ -44,10 +44,11 @@ export function requestOptions(domain: string, ignoreCerts: boolean): RequestUti
// To handle case where certificate has been moved from the location in certificates.json
try {
certificateLocation = fs.readFileSync(certificateFile, 'utf8');
} catch (err) {
logger.warn(`Error while trying to get certificate: ${err}`);
} catch (error) {
logger.warn('Error while trying to get certificate:', error);
}
}
const proxyEnabled = ConfigUtil.getConfigItem('useManualProxy') || ConfigUtil.getConfigItem('useSystemProxy');
// If certificate for the domain exists add it as a ca key in the request's parameter else consider only domain as the parameter for request
// Add proxy as a parameter if it is being used.
@@ -55,7 +56,7 @@ export function requestOptions(domain: string, ignoreCerts: boolean): RequestUti
ca: certificateLocation ? certificateLocation : '',
proxy: proxyEnabled ? ProxyUtil.getProxy(domain) : '',
ecdhCurve: 'auto',
headers: { 'User-Agent': SystemUtil.getUserAgent() },
headers: {'User-Agent': SystemUtil.getUserAgent()},
rejectUnauthorized: !ignoreCerts
};
}
@@ -64,7 +65,7 @@ function formatUrl(domain: string): string {
const hasPrefix = domain.startsWith('http', 0);
if (hasPrefix) {
return domain;
} else {
return domain.includes('localhost:') ? `http://${domain}` : `https://${domain}`;
}
return domain.includes('localhost:') ? `http://${domain}` : `https://${domain}`;
}

View File

@@ -1,4 +1,4 @@
import { init } from '@sentry/electron';
import {init} from '@sentry/electron';
import isDev from 'electron-is-dev';
@@ -10,9 +10,9 @@ export const sentryInit = (): void => {
// This error mainly comes from the console logs.
// This is a temp solution until Sentry supports disabling the console logs
ignoreErrors: ['does not appear to be a valid Zulip server']
// sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second
/// sendTimeout: 30 // wait 30 seconds before considering the sending capture to have failed, default is 1 second
});
}
};
export { captureException } from '@sentry/electron';
export {captureException} from '@sentry/electron';

View File

@@ -1,4 +1,4 @@
import { ipcRenderer } from 'electron';
import {ipcRenderer} from 'electron';
import os from 'os';
@@ -17,18 +17,23 @@ export function getOS(): string {
const platform = os.platform();
if (platform === 'darwin') {
return 'Mac';
} else if (platform === 'linux') {
return 'Linux';
} else if (platform === 'win32') {
if (parseFloat(os.release()) < 6.2) {
return 'Windows 7';
} else {
return 'Windows 10';
}
} else {
return '';
}
if (platform === 'linux') {
return 'Linux';
}
if (platform === 'win32') {
if (Number.parseFloat(os.release()) < 6.2) {
return 'Windows 7';
}
return 'Windows 10';
}
return '';
}
export function getUserAgent(): string {
return userAgent;
}

View File

@@ -6,10 +6,10 @@ i18n.configure({
directory: path.join(__dirname, '../../../translations/')
});
/* fetches the current appLocale from settings.json */
/* Fetches the current appLocale from settings.json */
const appLocale = ConfigUtil.getConfigItem('appLanguage');
/* if no locale present in the json, en is set default */
/* If no locale present in the json, en is set default */
export function __(phrase: string): string {
return i18n.__({ phrase, locale: appLocale ? appLocale : 'en' });
return i18n.__({phrase, locale: appLocale ? appLocale : 'en'});
}

View File

@@ -34,6 +34,7 @@ export function enterpriseOrgError(length: number, domains: string[]): DialogBox
for (const domain of domains) {
domainList += `${domain}\n`;
}
return {
title: `Could not add the following ${length === 1 ? 'organization' : 'organizations'}`,
content: `${domainList}\nPlease contact your system administrator.`

View File

@@ -14,8 +14,7 @@ install:
- node --version
- npm --version
- python --version
- npm install
- npm install -g gulp
- npm ci
build: off

View File

@@ -2,6 +2,34 @@
All notable changes to the Zulip desktop app are documented in this file.
### v5.2.0 --2020-05-04
**Security fixes**:
* CVE-2020-12637: Do not ignore certificate errors in webviews unless the (unsupported, deprecated) `ignoreCerts` option is enabled.
**Fixes**:
* Avoid opening the file chooser dialog twice when downloading a file.
**New features**:
* Provide clipboard decryption helper for use in new social login flow.
**Dependencies**:
* Upgrade all dependencies, including Electron 8.2.5.
### v5.1.0 --2020-04-29
**Fixes**:
* macOS: If the app is in foreground, the app will no longer hide upon clicking on dock again.
* Synchronise debian scripts with electron-builder 22.4.1, thus fixing SUID sandbox binary issues.
* Dock icon on macOS used to be larger than the other applications, which is now updated to the appropriate size.
* Upon catching error in updating the server icon, the app will log the error and make a sentry report instead of triggering user-facing network error
**New features**:
* User can now set application language without changing the language on their operating system.
**Dependencies**:
* Upgrade all dependencies, including Electron 8.2.3.
### v5.0.0 --2020-03-30
**Security fixes**:

1253
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.1.0",
"version": "5.2.0",
"main": "./app/main",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
@@ -21,7 +21,7 @@
"node": ">=10.0.0"
},
"scripts": {
"start": "node tools/run-dev",
"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",
@@ -36,7 +36,6 @@
"pack": "tsc && electron-builder --dir",
"dist": "tsc && electron-builder",
"mas": "tsc && electron-builder --mac mas",
"travis": "cd ./scripts && ./travis-build-test.sh",
"build-locales": "node tools/locale-helper"
},
"pre-commit": [
@@ -149,13 +148,13 @@
"auto-launch": "^5.0.5",
"backoff": "^2.5.0",
"electron-is-dev": "^1.2.0",
"electron-log": "^4.1.1",
"electron-log": "^4.1.2",
"electron-spellchecker": "^2.2.1",
"electron-updater": "^4.2.5",
"electron-updater": "^4.3.1",
"electron-window-state": "^5.0.3",
"escape-html": "^1.0.3",
"fs-extra": "^9.0.0",
"i18n": "^0.9.0",
"i18n": "^0.9.1",
"node-json-db": "^1.1.0",
"request": "^2.88.2",
"rxjs": "^5.5.12",
@@ -172,19 +171,19 @@
"@types/escape-html": "0.0.20",
"@types/fs-extra": "^8.1.0",
"@types/i18n": "^0.8.6",
"@types/node": "^13.13.2",
"@types/node": "^13.13.4",
"@types/request": "^2.48.4",
"@types/requestidlecallback": "^0.3.1",
"@typescript-eslint/eslint-plugin": "^2.29.0",
"@typescript-eslint/parser": "^2.29.0",
"@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0",
"@vitalets/google-translate-api": "^3.0.0",
"devtron": "^1.4.0",
"dotenv": "^8.2.0",
"electron": "^8.2.3",
"electron-builder": "^22.5.1",
"electron": "^8.2.5",
"electron-builder": "^22.6.0",
"electron-connect": "^0.6.3",
"electron-notarize": "^0.3.0",
"eslint-config-xo-typescript": "^0.26.0",
"eslint-config-xo-typescript": "^0.28.0",
"glob": "^7.1.6",
"gulp": "^4.0.2",
"gulp-tape": "^1.0.0",
@@ -198,17 +197,12 @@
"tap-colorize": "^1.2.0",
"tape": "^5.0.0",
"typescript": "^3.8.3",
"xo": "^0.28.3"
"xo": "^0.30.0"
},
"xo": {
"rules": {
"@typescript-eslint/member-ordering": "off",
"@typescript-eslint/no-dynamic-delete": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"capitalized-comments": "off",
"import/no-mutable-exports": "off",
"@typescript-eslint/prefer-readonly-parameter-types": "off",
"import/unambiguous": "error",
"max-lines": [
"warn",
@@ -218,14 +212,8 @@
"skipComments": true
}
],
"no-alert": "off",
"no-else-return": "off",
"no-warning-comments": "off",
"object-curly-spacing": "off",
"padding-line-between-statements": "off",
"strict": "error",
"unicorn/catch-error-name": "off",
"unicorn/string-content": "off"
"strict": "error"
},
"envs": [
"node",

View File

@@ -18,6 +18,7 @@ exports.default = async function (context) {
appBundleId: 'org.zulip.zulip-electron',
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASS
appleIdPassword: process.env.APPLE_ID_PASS,
ascProvider: process.env.ASC_PROVIDER // Team short name
});
};

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
# exit script if fails
set -e;
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
export {no_proxy,NO_PROXY}="127.0.0.1,localhost"
export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start
sleep 3
echo 'Travis Screen Resolution:'
xdpyinfo | grep dimensions
fi
npm run test
# if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
# npm run test-e2e
# fi

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env bash
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16
fi

View File

@@ -1,89 +0,0 @@
#!/usr/bin/env node
const path = require('path');
const glob = require('glob');
const chalk = require('chalk');
const { spawn } = require('child_process');
async function run(task, commandToRun, opts = {}) {
const args = commandToRun.split(' ');
let cmd = args[0];
args.splice(0, 1);
if (process.platform === 'win32' && /np(m|x)/.test(cmd)) {
cmd = cmd + '.cmd';
}
const defaults = {
cwd: path.resolve(__dirname, '..'),
env: process.env,
stdio: 'pipe'
};
opts = { ...defaults, ...opts };
task = ' ' + task + ' ';
const colors = [
{ bg: 'bold.bgGreen', color: 'bold.green' },
{ bg: 'bold.bgMagenta', color: 'bold.magenta' },
{ bg: 'bold.bgBlue', color: 'bold.blue' },
{ bg: 'bold.bgYellow', color: 'bold.yellow' },
{ bg: 'bold.bgCyan', color: 'bold.cyan' },
{ bg: 'bold.bgKeyword("rebeccapurple")', color: 'bold.keyword("rebeccapurple")' },
{ bg: 'bold.bgKeyword("darkslategray")', color: 'bold.keyword("darkslategray")' },
];
const randomColorIndex = Math.floor((Math.random() * (colors.length * 1000)) / 1000);
const randomColor = colors[randomColorIndex];
console.log(chalk`{${randomColor.bg} ${ task }} {${randomColor.color} ${commandToRun}}`);
const proc = spawn(cmd, args, opts);
proc.stderr.on('data', data => console.log(data.toString()));
proc.stdout.on('data', data => console.log(data.toString()));
return new Promise((resolve, reject) => {
function check(code) {
if (code !== 0) {
reject(chalk`{bgRed ERROR!} Running {red ${commandToRun}} failed with exit code: ${code}`);
} else {
resolve();
}
// we don't want to exit after compiling typescript files
// but instead want to do it if the tsc -w or electron app died.
if (commandToRun !== 'npx tsc') {
process.exit(0);
}
}
proc.on('exit', check);
proc.on('close', check);
proc.on('error', () => {
reject();
process.exit(0);
});
});
}
async function main() {
const baseFilePattern = 'app/+(main|renderer)/**/*';
const globOptions = { cwd: path.resolve(__dirname, '..') };
const jsFiles = glob.sync(baseFilePattern + '.js', globOptions);
const tsFiles = glob.sync(baseFilePattern + '.ts', globOptions);
// if the are missing compiled js file for typescript files
// run the typescript compiler before starting the app
if (jsFiles.length !== tsFiles.length) {
console.log('Compiling typescript files...');
await run('TypeScript', 'npx tsc');
}
await Promise.all([
run('Electron app', 'npx electron . --disable-http-cache --no-electron-connect'),
run('TypeScript watch mode', 'npx tsc --watch --pretty')
]);
}
main()
.catch(err => console.error(err));

37
typings.d.ts vendored
View File

@@ -1,2 +1,37 @@
declare module '@electron-elements/send-feedback';
declare module '@electron-elements/send-feedback' {
class SendFeedback extends HTMLElement {
customStyles: string;
titleLabel: string;
titlePlaceholder: string;
textareaLabel: string;
textareaPlaceholder: string;
buttonLabel: string;
loaderSuccessText: string;
logs: string[];
useReporter: (reporter: string, data: object) => void;
}
export = SendFeedback;
}
declare module 'node-mac-notifier';
interface ClipboardDecrypter {
version: number;
key: Uint8Array;
pasted: Promise<string>;
}
interface ElectronBridge {
send_event: (eventName: string | symbol, ...args: unknown[]) => void;
on_event: (eventName: string, listener: ListenerType) => void;
new_notification: (
title: string,
options: NotificationOptions | undefined,
dispatch: (type: string, eventInit: EventInit) => boolean
) => NotificationData;
get_idle_on_system: () => boolean;
get_last_active_on_system: () => number;
get_send_notification_reply_message_supported: () => boolean;
set_send_notification_reply_message_supported: (value: boolean) => void;
decrypt_clipboard: (version: number) => ClipboardDecrypter;
}