Split electron-main into smaller chunks (#377)

* Split electron-main into smaller chunks

* Affix @types/node version and upgrade electron-store

* Iterate PR

* tidy up

* Actually run the split out code
This commit is contained in:
Michael Telatynski 2022-07-01 20:17:40 +01:00 committed by GitHub
parent b7a0402de5
commit 389f6f4334
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 794 additions and 691 deletions

View file

@ -42,7 +42,7 @@
"dependencies": {
"auto-launch": "^5.0.5",
"counterpart": "^0.18.6",
"electron-store": "^6.0.1",
"electron-store": "^8.0.2",
"electron-window-state": "^5.0.3",
"minimist": "^1.2.6",
"png-to-ico": "^2.1.1",
@ -88,6 +88,9 @@
"matrix-seshat": "^2.3.3",
"keytar": "^7.9.0"
},
"resolutions": {
"@types/node": "16.11.38"
},
"build": {
"appId": "im.riot.app",
"asarUnpack": "**/*.node",

View file

@ -1,5 +1,5 @@
/*
Copyright 2021 New Vector Ltd
Copyright 2021 - 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,12 +15,32 @@ limitations under the License.
*/
import { BrowserWindow } from "electron";
import Store from "electron-store";
import AutoLaunch from "auto-launch";
import { AppLocalization } from "../language-helper";
declare global {
namespace NodeJS {
interface Global {
mainWindow: BrowserWindow;
appQuitting: boolean;
appLocalization: AppLocalization;
launcher: AutoLaunch;
vectorConfig: Record<string, any>;
trayConfig: {
// eslint-disable-next-line camelcase
icon_path: string;
brand: string;
};
store: Store<{
warnBeforeExit?: boolean;
minimizeToTray?: boolean;
spellCheckerEnabled?: boolean;
autoHideMenuBar?: boolean;
locale?: string | string[];
disableHardwareAcceleration?: boolean;
}>;
}
}
}

View file

@ -21,90 +21,42 @@ limitations under the License.
import "./squirrelhooks";
import {
app,
ipcMain,
powerSaveBlocker,
BrowserWindow,
Menu,
autoUpdater,
protocol,
dialog,
desktopCapturer,
} from "electron";
import AutoLaunch from "auto-launch";
import path from "path";
import windowStateKeeper from 'electron-window-state';
import Store from 'electron-store';
import fs, { promises as afs } from "fs";
import crypto from "crypto";
import { URL } from "url";
import minimist from "minimist";
import type * as Keytar from "keytar"; // Hak dependency type
import type {
Seshat as SeshatType,
SeshatRecovery as SeshatRecoveryType,
ReindexError as ReindexErrorType,
} from "matrix-seshat"; // Hak dependency type
import "./ipc";
import "./keytar";
import "./seshat";
import "./settings";
import * as tray from "./tray";
import { buildMenuTemplate } from './vectormenu';
import webContentsHandler from './webcontents-handler';
import * as updater from './updater';
import { getProfileFromDeeplink, protocolInit, recordSSOSession } from './protocol';
import { getProfileFromDeeplink, protocolInit } from './protocol';
import { _t, AppLocalization } from './language-helper';
import Input = Electron.Input;
import IpcMainEvent = Electron.IpcMainEvent;
const argv = minimist(process.argv, {
alias: { help: "h" },
});
let keytar: typeof Keytar;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
keytar = require('keytar');
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
console.log("Keytar isn't installed; secure key storage is disabled.");
} else {
console.warn("Keytar unexpected error:", e);
}
}
let seshatSupported = false;
let Seshat: typeof SeshatType;
let SeshatRecovery: typeof SeshatRecoveryType;
let ReindexError: typeof ReindexErrorType;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const seshatModule = require('matrix-seshat');
Seshat = seshatModule.Seshat;
SeshatRecovery = seshatModule.SeshatRecovery;
ReindexError = seshatModule.ReindexError;
seshatSupported = true;
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
console.log("Seshat isn't installed, event indexing is disabled.");
} else {
console.warn("Seshat unexpected error:", e);
}
}
// Things we need throughout the file but need to be created
// async to are initialised in setupGlobals()
let asarPath: string;
let resPath: string;
let iconPath: string;
let vectorConfig: Record<string, any>;
let trayConfig: {
// eslint-disable-next-line camelcase
icon_path: string;
brand: string;
};
let launcher: AutoLaunch;
let appLocalization: AppLocalization;
if (argv["help"]) {
console.log("Options:");
console.log(" --profile-dir {path}: Path to where to store the profile.");
@ -199,13 +151,13 @@ async function setupGlobals(): Promise<void> {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
vectorConfig = require(asarPath + 'config.json');
global.vectorConfig = require(asarPath + 'config.json');
} catch (e) {
// it would be nice to check the error code here and bail if the config
// is unparsable, but we get MODULE_NOT_FOUND in the case of a missing
// file or invalid json, so node is just very unhelpful.
// Continue with the defaults (ie. an empty config)
vectorConfig = {};
global.vectorConfig = {};
}
try {
@ -219,19 +171,19 @@ async function setupGlobals(): Promise<void> {
const homeserverProps = ['default_is_url', 'default_hs_url', 'default_server_name', 'default_server_config'];
if (Object.keys(localConfig).find(k => homeserverProps.includes(k))) {
// Rip out all the homeserver options from the vector config
vectorConfig = Object.keys(vectorConfig)
global.vectorConfig = Object.keys(global.vectorConfig)
.filter(k => !homeserverProps.includes(k))
.reduce((obj, key) => {obj[key] = vectorConfig[key]; return obj;}, {});
.reduce((obj, key) => {obj[key] = global.vectorConfig[key]; return obj;}, {});
}
vectorConfig = Object.assign(vectorConfig, localConfig);
global.vectorConfig = Object.assign(global.vectorConfig, localConfig);
} catch (e) {
if (e instanceof SyntaxError) {
dialog.showMessageBox({
type: "error",
title: `Your ${vectorConfig.brand || 'Element'} is misconfigured`,
message: `Your custom ${vectorConfig.brand || 'Element'} configuration contains invalid JSON. ` +
`Please correct the problem and reopen ${vectorConfig.brand || 'Element'}.`,
title: `Your ${global.vectorConfig.brand || 'Element'} is misconfigured`,
message: `Your custom ${global.vectorConfig.brand || 'Element'} configuration contains invalid JSON. ` +
`Please correct the problem and reopen ${global.vectorConfig.brand || 'Element'}.`,
detail: e.message || "",
});
}
@ -243,14 +195,14 @@ async function setupGlobals(): Promise<void> {
// It's important to call `path.join` so we don't end up with the packaged asar in the final path.
const iconFile = `element.${process.platform === 'win32' ? 'ico' : 'png'}`;
iconPath = path.join(resPath, "img", iconFile);
trayConfig = {
global.trayConfig = {
icon_path: iconPath,
brand: vectorConfig.brand || 'Element',
brand: global.vectorConfig.brand || 'Element',
};
// launcher
launcher = new AutoLaunch({
name: vectorConfig.brand || 'Element',
global.launcher = new AutoLaunch({
name: global.vectorConfig.brand || 'Element',
isHidden: true,
mac: {
useLaunchAgent: true,
@ -261,7 +213,7 @@ async function setupGlobals(): Promise<void> {
async function moveAutoLauncher(): Promise<void> {
// Look for an auto-launcher under 'Riot' and if we find one, port it's
// enabled/disabled-ness over to the new 'Element' launcher
if (!vectorConfig.brand || vectorConfig.brand === 'Element') {
if (!global.vectorConfig.brand || global.vectorConfig.brand === 'Element') {
const oldLauncher = new AutoLaunch({
name: 'Riot',
isHidden: true,
@ -272,24 +224,13 @@ async function moveAutoLauncher(): Promise<void> {
const wasEnabled = await oldLauncher.isEnabled();
if (wasEnabled) {
await oldLauncher.disable();
await launcher.enable();
await global.launcher.enable();
}
}
}
const eventStorePath = path.join(app.getPath('userData'), 'EventStore');
const store = new Store<{
warnBeforeExit?: boolean;
minimizeToTray?: boolean;
spellCheckerEnabled?: boolean;
autoHideMenuBar?: boolean;
locale?: string | string[];
disableHardwareAcceleration?: boolean;
}>({ name: "electron-config" });
global.store = new Store({ name: "electron-config" });
let eventIndex: SeshatType = null;
let mainWindow: BrowserWindow = null;
global.appQuitting = false;
const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
@ -299,12 +240,12 @@ const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
];
const warnBeforeExit = (event: Event, input: Input): void => {
const shouldWarnBeforeExit = store.get('warnBeforeExit', true);
const shouldWarnBeforeExit = global.store.get('warnBeforeExit', true);
const exitShortcutPressed =
input.type === 'keyDown' && exitShortcuts.some(shortcutFn => shortcutFn(input, process.platform));
if (shouldWarnBeforeExit && exitShortcutPressed) {
const shouldCancelCloseRequest = dialog.showMessageBoxSync(mainWindow, {
const shouldCancelCloseRequest = dialog.showMessageBoxSync(global.mainWindow, {
type: "question",
buttons: [_t("Cancel"), _t("Close Element")],
message: _t("Are you sure you want to quit?"),
@ -318,25 +259,6 @@ const warnBeforeExit = (event: Event, input: Input): void => {
}
};
const deleteContents = async (p: string): Promise<void> => {
for (const entry of await afs.readdir(p)) {
const curPath = path.join(p, entry);
await afs.unlink(curPath);
}
};
async function randomArray(size: number): Promise<string> {
return new Promise((resolve, reject) => {
crypto.randomBytes(size, (err, buf) => {
if (err) {
reject(err);
} else {
resolve(buf.toString("base64").replace(/=+$/g, ''));
}
});
});
}
// handle uncaught errors otherwise it displays
// stack traces in popup dialogs, which is terrible (which
// it will do any time the auto update poke fails, and there's
@ -347,510 +269,6 @@ process.on('uncaughtException', function(error: Error): void {
console.log('Unhandled exception', error);
});
let focusHandlerAttached = false;
ipcMain.on('setBadgeCount', function(_ev: IpcMainEvent, count: number): void {
if (process.platform !== 'win32') {
// only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron
// has some Windows support too, and in some Windows environments this leads to two badges rendering atop
// each other. See https://github.com/vector-im/element-web/issues/16942
app.badgeCount = count;
}
if (count === 0 && mainWindow) {
mainWindow.flashFrame(false);
}
});
ipcMain.on('loudNotification', function(): void {
if (process.platform === 'win32' && mainWindow && !mainWindow.isFocused() && !focusHandlerAttached) {
mainWindow.flashFrame(true);
mainWindow.once('focus', () => {
mainWindow.flashFrame(false);
focusHandlerAttached = false;
});
focusHandlerAttached = true;
}
});
let powerSaveBlockerId: number = null;
ipcMain.on('app_onAction', function(_ev: IpcMainEvent, payload) {
switch (payload.action) {
case 'call_state':
if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) {
if (payload.state === 'ended') {
powerSaveBlocker.stop(powerSaveBlockerId);
powerSaveBlockerId = null;
}
} else {
if (powerSaveBlockerId === null && payload.state === 'connected') {
powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep');
}
}
break;
}
});
interface Setting {
read(): Promise<any>;
write(value: any): Promise<void>;
}
const settings: Record<string, Setting> = {
"Electron.autoLaunch": {
async read(): Promise<any> {
return launcher.isEnabled();
},
async write(value: any): Promise<void> {
if (value) {
return launcher.enable();
} else {
return launcher.disable();
}
},
},
"Electron.warnBeforeExit": {
async read(): Promise<any> {
return store.get("warnBeforeExit", true);
},
async write(value: any): Promise<void> {
store.set("warnBeforeExit", value);
},
},
"Electron.alwaysShowMenuBar": { // not supported on macOS
async read(): Promise<any> {
return !global.mainWindow.autoHideMenuBar;
},
async write(value: any): Promise<void> {
store.set('autoHideMenuBar', !value);
global.mainWindow.autoHideMenuBar = !value;
global.mainWindow.setMenuBarVisibility(value);
},
},
"Electron.showTrayIcon": { // not supported on macOS
async read(): Promise<any> {
return tray.hasTray();
},
async write(value: any): Promise<void> {
if (value) {
// Create trayIcon icon
tray.create(trayConfig);
} else {
tray.destroy();
}
store.set('minimizeToTray', value);
},
},
"Electron.enableHardwareAcceleration": {
async read(): Promise<any> {
return !store.get('disableHardwareAcceleration', false);
},
async write(value: any): Promise<void> {
store.set('disableHardwareAcceleration', !value);
},
},
};
ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
if (!mainWindow) return;
const args = payload.args || [];
let ret: any;
switch (payload.name) {
case 'getUpdateFeedUrl':
ret = autoUpdater.getFeedURL();
break;
case 'getSettingValue': {
const [settingName] = args;
const setting = settings[settingName];
ret = await setting.read();
break;
}
case 'setSettingValue': {
const [settingName, value] = args;
const setting = settings[settingName];
await setting.write(value);
break;
}
case 'setLanguage':
appLocalization.setAppLocale(args[0]);
break;
case 'getAppVersion':
ret = app.getVersion();
break;
case 'focusWindow':
if (mainWindow.isMinimized()) {
mainWindow.restore();
} else if (!mainWindow.isVisible()) {
mainWindow.show();
} else {
mainWindow.focus();
}
break;
case 'getConfig':
ret = vectorConfig;
break;
case 'navigateBack':
if (mainWindow.webContents.canGoBack()) {
mainWindow.webContents.goBack();
}
break;
case 'navigateForward':
if (mainWindow.webContents.canGoForward()) {
mainWindow.webContents.goForward();
}
break;
case 'setSpellCheckLanguages':
if (args[0] && args[0].length > 0) {
mainWindow.webContents.session.setSpellCheckerEnabled(true);
store.set("spellCheckerEnabled", true);
try {
mainWindow.webContents.session.setSpellCheckerLanguages(args[0]);
} catch (er) {
console.log("There were problems setting the spellcheck languages", er);
}
} else {
mainWindow.webContents.session.setSpellCheckerEnabled(false);
store.set("spellCheckerEnabled", false);
}
break;
case 'getSpellCheckLanguages':
if (store.get("spellCheckerEnabled", true)) {
ret = mainWindow.webContents.session.getSpellCheckerLanguages();
} else {
ret = [];
}
break;
case 'getAvailableSpellCheckLanguages':
ret = mainWindow.webContents.session.availableSpellCheckerLanguages;
break;
case 'startSSOFlow':
recordSSOSession(args[0]);
break;
case 'getPickleKey':
try {
ret = await keytar.getPassword("element.io", `${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
if (ret === null) {
ret = await keytar.getPassword("riot.im", `${args[0]}|${args[1]}`);
}
} catch (e) {
// if an error is thrown (e.g. keytar can't connect to the keychain),
// then return null, which means the default pickle key will be used
ret = null;
}
break;
case 'createPickleKey':
try {
const pickleKey = await randomArray(32);
await keytar.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey);
ret = pickleKey;
} catch (e) {
ret = null;
}
break;
case 'destroyPickleKey':
try {
await keytar.deletePassword("element.io", `${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
await keytar.deletePassword("riot.im", `${args[0]}|${args[1]}`);
} catch (e) {}
break;
case 'getDesktopCapturerSources':
ret = (await desktopCapturer.getSources(args[0])).map((source) => ({
id: source.id,
name: source.name,
thumbnailURL: source.thumbnail.toDataURL(),
}));
break;
default:
mainWindow.webContents.send('ipcReply', {
id: payload.id,
error: "Unknown IPC Call: " + payload.name,
});
return;
}
mainWindow.webContents.send('ipcReply', {
id: payload.id,
reply: ret,
});
});
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
async function getOrCreatePassphrase(key: string): Promise<string> {
if (keytar) {
try {
const storedPassphrase = await keytar.getPassword("element.io", key);
if (storedPassphrase !== null) {
return storedPassphrase;
} else {
const newPassphrase = await randomArray(32);
await keytar.setPassword("element.io", key, newPassphrase);
return newPassphrase;
}
} catch (e) {
console.log("Error getting the event index passphrase out of the secret store", e);
}
} else {
return seshatDefaultPassphrase;
}
}
ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
if (!mainWindow) return;
const sendError = (id, e) => {
const error = {
message: e.message,
};
mainWindow.webContents.send('seshatReply', {
id: id,
error: error,
});
};
const args = payload.args || [];
let ret: any;
switch (payload.name) {
case 'supportsEventIndexing':
ret = seshatSupported;
break;
case 'initEventIndex':
if (eventIndex === null) {
const userId = args[0];
const deviceId = args[1];
const passphraseKey = `seshat|${userId}|${deviceId}`;
const passphrase = await getOrCreatePassphrase(passphraseKey);
try {
await afs.mkdir(eventStorePath, { recursive: true });
eventIndex = new Seshat(eventStorePath, { passphrase });
} catch (e) {
if (e instanceof ReindexError) {
// If this is a reindex error, the index schema
// changed. Try to open the database in recovery mode,
// reindex the database and finally try to open the
// database again.
const recoveryIndex = new SeshatRecovery(eventStorePath, {
passphrase,
});
const userVersion = await recoveryIndex.getUserVersion();
// If our user version is 0 we'll delete the db
// anyways so reindexing it is a waste of time.
if (userVersion === 0) {
await recoveryIndex.shutdown();
try {
await deleteContents(eventStorePath);
} catch (e) {
}
} else {
await recoveryIndex.reindex();
}
eventIndex = new Seshat(eventStorePath, { passphrase });
} else {
sendError(payload.id, e);
return;
}
}
}
break;
case 'closeEventIndex':
if (eventIndex !== null) {
const index = eventIndex;
eventIndex = null;
try {
await index.shutdown();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'deleteEventIndex':
{
try {
await deleteContents(eventStorePath);
} catch (e) {
}
}
break;
case 'isEventIndexEmpty':
if (eventIndex === null) ret = true;
else ret = await eventIndex.isEmpty();
break;
case 'isRoomIndexed':
if (eventIndex === null) ret = false;
else ret = await eventIndex.isRoomIndexed(args[0]);
break;
case 'addEventToIndex':
try {
eventIndex.addEvent(args[0], args[1]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'deleteEvent':
try {
ret = await eventIndex.deleteEvent(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'commitLiveEvents':
try {
ret = await eventIndex.commit();
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'searchEventIndex':
try {
ret = await eventIndex.search(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'addHistoricEvents':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addHistoricEvents(
args[0], args[1], args[2]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'getStats':
if (eventIndex === null) ret = 0;
else {
try {
ret = await eventIndex.getStats();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'removeCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.removeCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'addCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'loadFileEvents':
if (eventIndex === null) ret = [];
else {
try {
ret = await eventIndex.loadFileEvents(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'loadCheckpoints':
if (eventIndex === null) ret = [];
else {
try {
ret = await eventIndex.loadCheckpoints();
} catch (e) {
ret = [];
}
}
break;
case 'setUserVersion':
if (eventIndex === null) break;
else {
try {
await eventIndex.setUserVersion(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'getUserVersion':
if (eventIndex === null) ret = 0;
else {
try {
ret = await eventIndex.getUserVersion();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
default:
mainWindow.webContents.send('seshatReply', {
id: payload.id,
error: "Unknown IPC Call: " + payload.name,
});
return;
}
mainWindow.webContents.send('seshatReply', {
id: payload.id,
reply: ret,
});
});
app.commandLine.appendSwitch('--enable-usermedia-screen-capturing');
if (!app.commandLine.hasSwitch('enable-features')) {
app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer');
@ -894,7 +312,7 @@ app.enableSandbox();
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
// Disable hardware acceleration if the setting has been set.
if (store.get('disableHardwareAcceleration', false) === true) {
if (global.store.get('disableHardwareAcceleration', false) === true) {
console.log("Disabling hardware acceleration.");
app.disableHardwareAcceleration();
}
@ -982,9 +400,9 @@ app.on('ready', async () => {
if (argv['no-update']) {
console.log('Auto update disabled via command line flag "--no-update"');
} else if (vectorConfig['update_base_url']) {
console.log(`Starting auto update with base URL: ${vectorConfig['update_base_url']}`);
updater.start(vectorConfig['update_base_url']);
} else if (global.vectorConfig['update_base_url']) {
console.log(`Starting auto update with base URL: ${global.vectorConfig['update_base_url']}`);
updater.start(global.vectorConfig['update_base_url']);
} else {
console.log('No update_base_url is defined: auto update is disabled');
}
@ -996,13 +414,13 @@ app.on('ready', async () => {
});
const preloadScript = path.normalize(`${__dirname}/preload.js`);
mainWindow = global.mainWindow = new BrowserWindow({
global.mainWindow = new BrowserWindow({
// https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
backgroundColor: '#fff',
icon: iconPath,
show: false,
autoHideMenuBar: store.get('autoHideMenuBar', true),
autoHideMenuBar: global.store.get('autoHideMenuBar', true),
x: mainWindowState.x,
y: mainWindowState.y,
@ -1016,32 +434,32 @@ app.on('ready', async () => {
webgl: true,
},
});
mainWindow.loadURL('vector://vector/webapp/');
global.mainWindow.loadURL('vector://vector/webapp/');
// Handle spellchecker
// For some reason spellCheckerEnabled isn't persisted so we have to use the store here
mainWindow.webContents.session.setSpellCheckerEnabled(store.get("spellCheckerEnabled", true));
// For some reason spellCheckerEnabled isn't persisted, so we have to use the store here
global.mainWindow.webContents.session.setSpellCheckerEnabled(global.store.get("spellCheckerEnabled", true));
// Create trayIcon icon
if (store.get('minimizeToTray', true)) tray.create(trayConfig);
if (global.store.get('minimizeToTray', true)) tray.create(global.trayConfig);
mainWindow.once('ready-to-show', () => {
mainWindowState.manage(mainWindow);
global.mainWindow.once('ready-to-show', () => {
mainWindowState.manage(global.mainWindow);
if (!argv['hidden']) {
mainWindow.show();
global.mainWindow.show();
} else {
// hide here explicitly because window manage above sometimes shows it
mainWindow.hide();
global.mainWindow.hide();
}
});
mainWindow.webContents.on('before-input-event', warnBeforeExit);
global.mainWindow.webContents.on('before-input-event', warnBeforeExit);
mainWindow.on('closed', () => {
mainWindow = global.mainWindow = null;
global.mainWindow.on('closed', () => {
global.mainWindow = null;
});
mainWindow.on('close', async (e) => {
global.mainWindow.on('close', async (e) => {
// If we are not quitting and have a tray icon then minimize to tray
if (!global.appQuitting && (tray.hasTray() || process.platform === 'darwin')) {
// On Mac, closing the window just hides it
@ -1049,12 +467,12 @@ app.on('ready', async () => {
// behave, eg. Mail.app)
e.preventDefault();
if (mainWindow.isFullScreen()) {
mainWindow.once('leave-full-screen', () => mainWindow.hide());
if (global.mainWindow.isFullScreen()) {
global.mainWindow.once('leave-full-screen', () => global.mainWindow.hide());
mainWindow.setFullScreen(false);
global.mainWindow.setFullScreen(false);
} else {
mainWindow.hide();
global.mainWindow.hide();
}
return false;
@ -1063,19 +481,19 @@ app.on('ready', async () => {
if (process.platform === 'win32') {
// Handle forward/backward mouse buttons in Windows
mainWindow.on('app-command', (e, cmd) => {
if (cmd === 'browser-backward' && mainWindow.webContents.canGoBack()) {
mainWindow.webContents.goBack();
} else if (cmd === 'browser-forward' && mainWindow.webContents.canGoForward()) {
mainWindow.webContents.goForward();
global.mainWindow.on('app-command', (e, cmd) => {
if (cmd === 'browser-backward' && global.mainWindow.webContents.canGoBack()) {
global.mainWindow.webContents.goBack();
} else if (cmd === 'browser-forward' && global.mainWindow.webContents.canGoForward()) {
global.mainWindow.webContents.goForward();
}
});
}
webContentsHandler(mainWindow.webContents);
webContentsHandler(global.mainWindow.webContents);
appLocalization = new AppLocalization({
store,
global.appLocalization = new AppLocalization({
store: global.store,
components: [
() => tray.initApplicationMenu(),
() => Menu.setApplicationMenu(buildMenuTemplate()),
@ -1088,14 +506,12 @@ app.on('window-all-closed', () => {
});
app.on('activate', () => {
mainWindow.show();
global.mainWindow.show();
});
function beforeQuit(): void {
global.appQuitting = true;
if (mainWindow) {
mainWindow.webContents.send('before-quit');
}
global.mainWindow?.webContents.send('before-quit');
}
app.on('before-quit', beforeQuit);
@ -1106,10 +522,10 @@ app.on('second-instance', (ev, commandLine, workingDirectory) => {
if (commandLine.includes('--hidden')) return;
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (!mainWindow.isVisible()) mainWindow.show();
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
if (global.mainWindow) {
if (!global.mainWindow.isVisible()) global.mainWindow.show();
if (global.mainWindow.isMinimized()) global.mainWindow.restore();
global.mainWindow.focus();
}
});

202
src/ipc.ts Normal file
View file

@ -0,0 +1,202 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { app, autoUpdater, desktopCapturer, ipcMain, powerSaveBlocker } from "electron";
import IpcMainEvent = Electron.IpcMainEvent;
import { recordSSOSession } from "./protocol";
import { randomArray } from "./utils";
import { Settings } from "./settings";
import { keytar } from "./keytar";
ipcMain.on('setBadgeCount', function(_ev: IpcMainEvent, count: number): void {
if (process.platform !== 'win32') {
// only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron
// has some Windows support too, and in some Windows environments this leads to two badges rendering atop
// each other. See https://github.com/vector-im/element-web/issues/16942
app.badgeCount = count;
}
if (count === 0) {
global.mainWindow?.flashFrame(false);
}
});
let focusHandlerAttached = false;
ipcMain.on('loudNotification', function(): void {
if (process.platform === 'win32' && global.mainWindow && !global.mainWindow.isFocused() && !focusHandlerAttached) {
global.mainWindow.flashFrame(true);
global.mainWindow.once('focus', () => {
global.mainWindow.flashFrame(false);
focusHandlerAttached = false;
});
focusHandlerAttached = true;
}
});
let powerSaveBlockerId: number = null;
ipcMain.on('app_onAction', function(_ev: IpcMainEvent, payload) {
switch (payload.action) {
case 'call_state': {
if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) {
if (payload.state === 'ended') {
powerSaveBlocker.stop(powerSaveBlockerId);
powerSaveBlockerId = null;
}
} else {
if (powerSaveBlockerId === null && payload.state === 'connected') {
powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep');
}
}
break;
}
}
});
ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
if (!global.mainWindow) return;
const args = payload.args || [];
let ret: any;
switch (payload.name) {
case 'getUpdateFeedUrl':
ret = autoUpdater.getFeedURL();
break;
case 'getSettingValue': {
const [settingName] = args;
const setting = Settings[settingName];
ret = await setting.read();
break;
}
case 'setSettingValue': {
const [settingName, value] = args;
const setting = Settings[settingName];
await setting.write(value);
break;
}
case 'setLanguage':
global.appLocalization.setAppLocale(args[0]);
break;
case 'getAppVersion':
ret = app.getVersion();
break;
case 'focusWindow':
if (global.mainWindow.isMinimized()) {
global.mainWindow.restore();
} else if (!global.mainWindow.isVisible()) {
global.mainWindow.show();
} else {
global.mainWindow.focus();
}
break;
case 'getConfig':
ret = global.vectorConfig;
break;
case 'navigateBack':
if (global.mainWindow.webContents.canGoBack()) {
global.mainWindow.webContents.goBack();
}
break;
case 'navigateForward':
if (global.mainWindow.webContents.canGoForward()) {
global.mainWindow.webContents.goForward();
}
break;
case 'setSpellCheckLanguages':
if (args[0] && args[0].length > 0) {
global.mainWindow.webContents.session.setSpellCheckerEnabled(true);
global.store.set("spellCheckerEnabled", true);
try {
global.mainWindow.webContents.session.setSpellCheckerLanguages(args[0]);
} catch (er) {
console.log("There were problems setting the spellcheck languages", er);
}
} else {
global.mainWindow.webContents.session.setSpellCheckerEnabled(false);
global.store.set("spellCheckerEnabled", false);
}
break;
case 'getSpellCheckLanguages':
if (global.store.get("spellCheckerEnabled", true)) {
ret = global.mainWindow.webContents.session.getSpellCheckerLanguages();
} else {
ret = [];
}
break;
case 'getAvailableSpellCheckLanguages':
ret = global.mainWindow.webContents.session.availableSpellCheckerLanguages;
break;
case 'startSSOFlow':
recordSSOSession(args[0]);
break;
case 'getPickleKey':
try {
ret = await keytar.getPassword("element.io", `${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
if (ret === null) {
ret = await keytar.getPassword("riot.im", `${args[0]}|${args[1]}`);
}
} catch (e) {
// if an error is thrown (e.g. keytar can't connect to the keychain),
// then return null, which means the default pickle key will be used
ret = null;
}
break;
case 'createPickleKey':
try {
const pickleKey = await randomArray(32);
await keytar.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey);
ret = pickleKey;
} catch (e) {
ret = null;
}
break;
case 'destroyPickleKey':
try {
await keytar.deletePassword("element.io", `${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
await keytar.deletePassword("riot.im", `${args[0]}|${args[1]}`);
} catch (e) {}
break;
case 'getDesktopCapturerSources':
ret = (await desktopCapturer.getSources(args[0])).map((source) => ({
id: source.id,
name: source.name,
thumbnailURL: source.thumbnail.toDataURL(),
}));
break;
default:
global.mainWindow.webContents.send('ipcReply', {
id: payload.id,
error: "Unknown IPC Call: " + payload.name,
});
return;
}
global.mainWindow.webContents.send('ipcReply', {
id: payload.id,
reply: ret,
});
});

31
src/keytar.ts Normal file
View file

@ -0,0 +1,31 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type * as Keytar from "keytar"; // Hak dependency type
let keytar: typeof Keytar | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
keytar = require('keytar');
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
console.log("Keytar isn't installed; secure key storage is disabled.");
} else {
console.warn("Keytar unexpected error:", e);
}
}
export { keytar };

325
src/seshat.ts Normal file
View file

@ -0,0 +1,325 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { app, ipcMain } from "electron";
import { promises as afs } from "fs";
import path from "path";
import type {
Seshat as SeshatType,
SeshatRecovery as SeshatRecoveryType,
ReindexError as ReindexErrorType,
} from "matrix-seshat"; // Hak dependency type
import IpcMainEvent = Electron.IpcMainEvent;
import { randomArray } from "./utils";
import { keytar } from "./keytar";
let seshatSupported = false;
let Seshat: typeof SeshatType;
let SeshatRecovery: typeof SeshatRecoveryType;
let ReindexError: typeof ReindexErrorType;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const seshatModule = require('matrix-seshat');
Seshat = seshatModule.Seshat;
SeshatRecovery = seshatModule.SeshatRecovery;
ReindexError = seshatModule.ReindexError;
seshatSupported = true;
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
console.log("Seshat isn't installed, event indexing is disabled.");
} else {
console.warn("Seshat unexpected error:", e);
}
}
const eventStorePath = path.join(app.getPath('userData'), 'EventStore');
let eventIndex: SeshatType = null;
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
async function getOrCreatePassphrase(key: string): Promise<string> {
if (keytar) {
try {
const storedPassphrase = await keytar.getPassword("element.io", key);
if (storedPassphrase !== null) {
return storedPassphrase;
} else {
const newPassphrase = await randomArray(32);
await keytar.setPassword("element.io", key, newPassphrase);
return newPassphrase;
}
} catch (e) {
console.log("Error getting the event index passphrase out of the secret store", e);
}
} else {
return seshatDefaultPassphrase;
}
}
const deleteContents = async (p: string): Promise<void> => {
for (const entry of await afs.readdir(p)) {
const curPath = path.join(p, entry);
await afs.unlink(curPath);
}
};
ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
if (!global.mainWindow) return;
const sendError = (id, e) => {
const error = {
message: e.message,
};
global.mainWindow.webContents.send('seshatReply', {
id: id,
error: error,
});
};
const args = payload.args || [];
let ret: any;
switch (payload.name) {
case 'supportsEventIndexing':
ret = seshatSupported;
break;
case 'initEventIndex':
if (eventIndex === null) {
const userId = args[0];
const deviceId = args[1];
const passphraseKey = `seshat|${userId}|${deviceId}`;
const passphrase = await getOrCreatePassphrase(passphraseKey);
try {
await afs.mkdir(eventStorePath, { recursive: true });
eventIndex = new Seshat(eventStorePath, { passphrase });
} catch (e) {
if (e instanceof ReindexError) {
// If this is a reindex error, the index schema
// changed. Try to open the database in recovery mode,
// reindex the database and finally try to open the
// database again.
const recoveryIndex = new SeshatRecovery(eventStorePath, {
passphrase,
});
const userVersion = await recoveryIndex.getUserVersion();
// If our user version is 0 we'll delete the db
// anyways so reindexing it is a waste of time.
if (userVersion === 0) {
await recoveryIndex.shutdown();
try {
await deleteContents(eventStorePath);
} catch (e) {
}
} else {
await recoveryIndex.reindex();
}
eventIndex = new Seshat(eventStorePath, { passphrase });
} else {
sendError(payload.id, e);
return;
}
}
}
break;
case 'closeEventIndex':
if (eventIndex !== null) {
const index = eventIndex;
eventIndex = null;
try {
await index.shutdown();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'deleteEventIndex': {
try {
await deleteContents(eventStorePath);
} catch (e) {
}
break;
}
case 'isEventIndexEmpty':
if (eventIndex === null) ret = true;
else ret = await eventIndex.isEmpty();
break;
case 'isRoomIndexed':
if (eventIndex === null) ret = false;
else ret = await eventIndex.isRoomIndexed(args[0]);
break;
case 'addEventToIndex':
try {
eventIndex.addEvent(args[0], args[1]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'deleteEvent':
try {
ret = await eventIndex.deleteEvent(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'commitLiveEvents':
try {
ret = await eventIndex.commit();
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'searchEventIndex':
try {
ret = await eventIndex.search(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
break;
case 'addHistoricEvents':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addHistoricEvents(
args[0], args[1], args[2]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'getStats':
if (eventIndex === null) ret = 0;
else {
try {
ret = await eventIndex.getStats();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'removeCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.removeCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'addCrawlerCheckpoint':
if (eventIndex === null) ret = false;
else {
try {
ret = await eventIndex.addCrawlerCheckpoint(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'loadFileEvents':
if (eventIndex === null) ret = [];
else {
try {
ret = await eventIndex.loadFileEvents(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'loadCheckpoints':
if (eventIndex === null) ret = [];
else {
try {
ret = await eventIndex.loadCheckpoints();
} catch (e) {
ret = [];
}
}
break;
case 'setUserVersion':
if (eventIndex === null) break;
else {
try {
await eventIndex.setUserVersion(args[0]);
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
case 'getUserVersion':
if (eventIndex === null) ret = 0;
else {
try {
ret = await eventIndex.getUserVersion();
} catch (e) {
sendError(payload.id, e);
return;
}
}
break;
default:
global.mainWindow.webContents.send('seshatReply', {
id: payload.id,
error: "Unknown IPC Call: " + payload.name,
});
return;
}
global.mainWindow.webContents.send('seshatReply', {
id: payload.id,
reply: ret,
});
});

77
src/settings.ts Normal file
View file

@ -0,0 +1,77 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as tray from "./tray";
interface Setting {
read(): Promise<any>;
write(value: any): Promise<void>;
}
export const Settings: Record<string, Setting> = {
"Electron.autoLaunch": {
async read(): Promise<any> {
return global.launcher.isEnabled();
},
async write(value: any): Promise<void> {
if (value) {
return global.launcher.enable();
} else {
return global.launcher.disable();
}
},
},
"Electron.warnBeforeExit": {
async read(): Promise<any> {
return global.store.get("warnBeforeExit", true);
},
async write(value: any): Promise<void> {
global.store.set("warnBeforeExit", value);
},
},
"Electron.alwaysShowMenuBar": { // not supported on macOS
async read(): Promise<any> {
return !global.mainWindow.autoHideMenuBar;
},
async write(value: any): Promise<void> {
global.store.set('autoHideMenuBar', !value);
global.mainWindow.autoHideMenuBar = !value;
global.mainWindow.setMenuBarVisibility(value);
},
},
"Electron.showTrayIcon": { // not supported on macOS
async read(): Promise<any> {
return tray.hasTray();
},
async write(value: any): Promise<void> {
if (value) {
// Create trayIcon icon
tray.create(global.trayConfig);
} else {
tray.destroy();
}
global.store.set('minimizeToTray', value);
},
},
"Electron.enableHardwareAcceleration": {
async read(): Promise<any> {
return !global.store.get('disableHardwareAcceleration', false);
},
async write(value: any): Promise<void> {
global.store.set('disableHardwareAcceleration', !value);
},
},
};

29
src/utils.ts Normal file
View file

@ -0,0 +1,29 @@
/*
Copyright 2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import crypto from "crypto";
export async function randomArray(size: number): Promise<string> {
return new Promise((resolve, reject) => {
crypto.randomBytes(size, (err, buf) => {
if (err) {
reject(err);
} else {
resolve(buf.toString("base64").replace(/=+$/g, ''));
}
});
});
}

View file

@ -10,6 +10,7 @@
"outDir": "./lib",
"rootDir": "./src",
"declaration": true,
"typeRoots": ["src/@types"],
"lib": [
"es2019",
"dom"

View file

@ -839,25 +839,10 @@
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*":
version "18.0.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a"
integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==
"@types/node@16.9.1":
version "16.9.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
"@types/node@^16.11.26":
version "16.11.41"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.41.tgz#88eb485b1bfdb4c224d878b7832239536aa2f813"
integrity sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ==
"@types/node@^17.0.12":
version "17.0.45"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190"
integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==
"@types/node@*", "@types/node@16.11.38", "@types/node@16.9.1", "@types/node@^16.11.26", "@types/node@^17.0.12":
version "16.11.38"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.38.tgz#be0edd097b23eace6c471c525a74b3f98803017f"
integrity sha512-hjO/0K140An3GWDw2HJfq7gko3wWeznbjXgg+rzPdVzhe198hp4x2i1dgveAOEiFKd8sOilAxzoSJiVv5P/CUg==
"@types/npm-package-arg@*":
version "6.1.1"
@ -1059,12 +1044,19 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ajv-formats@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==
dependencies:
ajv "^8.0.0"
ajv-keywords@^3.4.1:
version "3.5.2"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4:
ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.3, ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@ -1074,7 +1066,7 @@ ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^8.0.1:
ajv@^8.0.0, ajv@^8.0.1, ajv@^8.6.3:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
@ -1371,7 +1363,7 @@ at-least-node@^1.0.0:
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
atomically@^1.3.1:
atomically@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe"
integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==
@ -1851,21 +1843,21 @@ concat-stream@^1.6.2:
readable-stream "^2.2.2"
typedarray "^0.0.6"
conf@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/conf/-/conf-7.1.2.tgz#d9678a9d8f04de8bf5cd475105da8fdae49c2ec4"
integrity sha512-r8/HEoWPFn4CztjhMJaWNAe5n+gPUCSaJ0oufbqDLFKsA1V8JjAG7G+p0pgoDFAws9Bpk2VtVLLXqOBA7WxLeg==
conf@^10.1.2:
version "10.1.2"
resolved "https://registry.yarnpkg.com/conf/-/conf-10.1.2.tgz#50132158f388756fa9dea3048f6b47935315c14e"
integrity sha512-o9Fv1Mv+6A0JpoayQ8JleNp3hhkbOJP/Re/Q+QqxMPHPkABVsRjQGWZn9A5GcqLiTNC6d89p2PB5ZhHVDSMwyg==
dependencies:
ajv "^6.12.2"
atomically "^1.3.1"
ajv "^8.6.3"
ajv-formats "^2.1.1"
atomically "^1.7.0"
debounce-fn "^4.0.0"
dot-prop "^5.2.0"
env-paths "^2.2.0"
dot-prop "^6.0.1"
env-paths "^2.2.1"
json-schema-typed "^7.0.3"
make-dir "^3.1.0"
onetime "^5.1.0"
onetime "^5.1.2"
pkg-up "^3.1.0"
semver "^7.3.2"
semver "^7.3.5"
config-chain@^1.1.11:
version "1.1.13"
@ -2141,6 +2133,13 @@ dot-prop@^5.2.0:
dependencies:
is-obj "^2.0.0"
dot-prop@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083"
integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==
dependencies:
is-obj "^2.0.0"
dotenv-expand@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
@ -2257,13 +2256,13 @@ electron-publish@22.14.13:
lazy-val "^1.0.5"
mime "^2.5.2"
electron-store@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-6.0.1.tgz#2178b9dc37aeb749d99cf9d1d1bc090890b922dc"
integrity sha512-8rdM0XEmDGsLuZM2oRABzsLX+XmD5x3rwxPMEPv0MrN9/BWanyy3ilb2v+tCrKtIZVF3MxUiZ9Bfqe8e0popKQ==
electron-store@^8.0.2:
version "8.0.2"
resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.0.2.tgz#95c8cf81c1e1cf48b24f3ceeea24b921c1ff62d7"
integrity sha512-9GwUMv51w8ydbkaG7X0HrPlElXLApg63zYy1/VZ/a08ndl0gfm4iCoD3f0E1JvP3V16a+7KxqriCI0c122stiA==
dependencies:
conf "^7.1.2"
type-fest "^0.16.0"
conf "^10.1.2"
type-fest "^2.12.2"
electron-window-state@^5.0.3:
version "5.0.3"
@ -2313,7 +2312,7 @@ enquirer@^2.3.5:
dependencies:
ansi-colors "^4.1.1"
env-paths@^2.2.0:
env-paths@^2.2.0, env-paths@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
@ -3785,7 +3784,7 @@ lru-queue@^0.1.0:
dependencies:
es5-ext "~0.10.2"
make-dir@^3.0.0, make-dir@^3.1.0:
make-dir@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@ -4272,7 +4271,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
onetime@^5.1.0:
onetime@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
@ -5314,11 +5313,6 @@ type-fest@^0.13.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==
type-fest@^0.16.0:
version "0.16.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860"
integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==
type-fest@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
@ -5329,6 +5323,11 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
type-fest@^2.12.2:
version "2.13.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.13.0.tgz#d1ecee38af29eb2e863b22299a3d68ef30d2abfb"
integrity sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==
type@^1.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"