Format all files with prettier
This commit is contained in:
Vendored
+1
-1
@@ -50,5 +50,5 @@ declare module "keytar" {
|
||||
*
|
||||
* @returns A promise for the array of found credentials.
|
||||
*/
|
||||
export function findCredentials(service: string): Promise<Array<{ account: string, password: string}>>;
|
||||
export function findCredentials(service: string): Promise<Array<{ account: string; password: string }>>;
|
||||
}
|
||||
|
||||
+124
-129
@@ -19,18 +19,11 @@ limitations under the License.
|
||||
|
||||
// Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc.
|
||||
import "./squirrelhooks";
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
autoUpdater,
|
||||
protocol,
|
||||
dialog,
|
||||
} from "electron";
|
||||
import { app, BrowserWindow, Menu, autoUpdater, protocol, dialog } from "electron";
|
||||
import AutoLaunch from "auto-launch";
|
||||
import path from "path";
|
||||
import windowStateKeeper from 'electron-window-state';
|
||||
import Store from 'electron-store';
|
||||
import windowStateKeeper from "electron-window-state";
|
||||
import Store from "electron-store";
|
||||
import fs, { promises as afs } from "fs";
|
||||
import { URL } from "url";
|
||||
import minimist from "minimist";
|
||||
@@ -40,11 +33,11 @@ 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 } from './protocol';
|
||||
import { _t, AppLocalization } from './language-helper';
|
||||
import { buildMenuTemplate } from "./vectormenu";
|
||||
import webContentsHandler from "./webcontents-handler";
|
||||
import * as updater from "./updater";
|
||||
import { getProfileFromDeeplink, protocolInit } from "./protocol";
|
||||
import { _t, AppLocalization } from "./language-helper";
|
||||
import Input = Electron.Input;
|
||||
|
||||
const argv = minimist(process.argv, {
|
||||
@@ -65,8 +58,7 @@ if (argv["help"]) {
|
||||
console.log(" --no-update: Disable automatic updating.");
|
||||
console.log(" --hidden: Start the application hidden in the system tray.");
|
||||
console.log(" --help: Displays this help message.");
|
||||
console.log("And more such as --proxy, see:" +
|
||||
"https://electronjs.org/docs/api/command-line-switches");
|
||||
console.log("And more such as --proxy, see:" + "https://electronjs.org/docs/api/command-line-switches");
|
||||
app.exit();
|
||||
}
|
||||
|
||||
@@ -74,7 +66,7 @@ if (argv["help"]) {
|
||||
// as soon as the app path is set, so pick a random path in it that must exist if it's a
|
||||
// real user data directory.
|
||||
function isRealUserDataDir(d: string): boolean {
|
||||
return fs.existsSync(path.join(d, 'IndexedDB'));
|
||||
return fs.existsSync(path.join(d, "IndexedDB"));
|
||||
}
|
||||
|
||||
// check if we are passed a profile in the SSO callback url
|
||||
@@ -83,22 +75,22 @@ let userDataPath: string;
|
||||
const userDataPathInProtocol = getProfileFromDeeplink(argv["_"]);
|
||||
if (userDataPathInProtocol) {
|
||||
userDataPath = userDataPathInProtocol;
|
||||
} else if (argv['profile-dir']) {
|
||||
userDataPath = argv['profile-dir'];
|
||||
} else if (argv["profile-dir"]) {
|
||||
userDataPath = argv["profile-dir"];
|
||||
} else {
|
||||
let newUserDataPath = app.getPath('userData');
|
||||
if (argv['profile']) {
|
||||
newUserDataPath += '-' + argv['profile'];
|
||||
let newUserDataPath = app.getPath("userData");
|
||||
if (argv["profile"]) {
|
||||
newUserDataPath += "-" + argv["profile"];
|
||||
}
|
||||
const newUserDataPathExists = isRealUserDataDir(newUserDataPath);
|
||||
let oldUserDataPath = path.join(app.getPath('appData'), app.getName().replace('Element', 'Riot'));
|
||||
if (argv['profile']) {
|
||||
oldUserDataPath += '-' + argv['profile'];
|
||||
let oldUserDataPath = path.join(app.getPath("appData"), app.getName().replace("Element", "Riot"));
|
||||
if (argv["profile"]) {
|
||||
oldUserDataPath += "-" + argv["profile"];
|
||||
}
|
||||
|
||||
const oldUserDataPathExists = isRealUserDataDir(oldUserDataPath);
|
||||
console.log(newUserDataPath + " exists: " + (newUserDataPathExists ? 'yes' : 'no'));
|
||||
console.log(oldUserDataPath + " exists: " + (oldUserDataPathExists ? 'yes' : 'no'));
|
||||
console.log(newUserDataPath + " exists: " + (newUserDataPathExists ? "yes" : "no"));
|
||||
console.log(oldUserDataPath + " exists: " + (oldUserDataPathExists ? "yes" : "no"));
|
||||
if (!newUserDataPathExists && oldUserDataPathExists) {
|
||||
console.log("Using legacy user data path: " + oldUserDataPath);
|
||||
userDataPath = oldUserDataPath;
|
||||
@@ -106,54 +98,53 @@ if (userDataPathInProtocol) {
|
||||
userDataPath = newUserDataPath;
|
||||
}
|
||||
}
|
||||
app.setPath('userData', userDataPath);
|
||||
app.setPath("userData", userDataPath);
|
||||
|
||||
async function tryPaths(name: string, root: string, rawPaths: string[]): Promise<string> {
|
||||
// Make everything relative to root
|
||||
const paths = rawPaths.map(p => path.join(root, p));
|
||||
const paths = rawPaths.map((p) => path.join(root, p));
|
||||
|
||||
for (const p of paths) {
|
||||
try {
|
||||
await afs.stat(p);
|
||||
return p + '/';
|
||||
} catch (e) {
|
||||
}
|
||||
return p + "/";
|
||||
} catch (e) {}
|
||||
}
|
||||
console.log(`Couldn't find ${name} files in any of: `);
|
||||
for (const p of paths) {
|
||||
console.log("\t"+path.resolve(p));
|
||||
console.log("\t" + path.resolve(p));
|
||||
}
|
||||
throw new Error(`Failed to find ${name} files`);
|
||||
}
|
||||
|
||||
const homeserverProps = ['default_is_url', 'default_hs_url', 'default_server_name', 'default_server_config'] as const;
|
||||
const homeserverProps = ["default_is_url", "default_hs_url", "default_server_name", "default_server_config"] as const;
|
||||
|
||||
// Find the webapp resources and set up things that require them
|
||||
async function setupGlobals(): Promise<void> {
|
||||
// find the webapp asar.
|
||||
asarPath = await tryPaths("webapp", __dirname, [
|
||||
// If run from the source checkout, this will be in the directory above
|
||||
'../webapp.asar',
|
||||
"../webapp.asar",
|
||||
// but if run from a packaged application, electron-main.js will be in
|
||||
// a different asar file so it will be two levels above
|
||||
'../../webapp.asar',
|
||||
"../../webapp.asar",
|
||||
// also try without the 'asar' suffix to allow symlinking in a directory
|
||||
'../webapp',
|
||||
"../webapp",
|
||||
// from a packaged application
|
||||
'../../webapp',
|
||||
"../../webapp",
|
||||
]);
|
||||
|
||||
// we assume the resources path is in the same place as the asar
|
||||
resPath = await tryPaths("res", path.dirname(asarPath), [
|
||||
// If run from the source checkout
|
||||
'res',
|
||||
"res",
|
||||
// if run from packaged application
|
||||
'',
|
||||
"",
|
||||
]);
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
global.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
|
||||
@@ -165,15 +156,15 @@ async function setupGlobals(): Promise<void> {
|
||||
try {
|
||||
// Load local config and use it to override values from the one baked with the build
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const localConfig = require(path.join(app.getPath('userData'), 'config.json'));
|
||||
const localConfig = require(path.join(app.getPath("userData"), "config.json"));
|
||||
|
||||
// If the local config has a homeserver defined, don't use the homeserver from the build
|
||||
// config. This is to avoid a problem where Riot thinks there are multiple homeservers
|
||||
// defined, and panics as a result.
|
||||
if (Object.keys(localConfig).find(k => homeserverProps.includes(<any>k))) {
|
||||
if (Object.keys(localConfig).find((k) => homeserverProps.includes(<any>k))) {
|
||||
// Rip out all the homeserver options from the vector config
|
||||
global.vectorConfig = Object.keys(global.vectorConfig)
|
||||
.filter(k => !homeserverProps.includes(<any>k))
|
||||
.filter((k) => !homeserverProps.includes(<any>k))
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = global.vectorConfig[key];
|
||||
return obj;
|
||||
@@ -185,9 +176,10 @@ async function setupGlobals(): Promise<void> {
|
||||
if (e instanceof SyntaxError) {
|
||||
dialog.showMessageBox({
|
||||
type: "error",
|
||||
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'}.`,
|
||||
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 || "",
|
||||
});
|
||||
}
|
||||
@@ -197,16 +189,16 @@ async function setupGlobals(): Promise<void> {
|
||||
|
||||
// The tray icon
|
||||
// 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'}`;
|
||||
const iconFile = `element.${process.platform === "win32" ? "ico" : "png"}`;
|
||||
iconPath = path.join(resPath, "img", iconFile);
|
||||
global.trayConfig = {
|
||||
icon_path: iconPath,
|
||||
brand: global.vectorConfig.brand || 'Element',
|
||||
brand: global.vectorConfig.brand || "Element",
|
||||
};
|
||||
|
||||
// launcher
|
||||
global.launcher = new AutoLaunch({
|
||||
name: global.vectorConfig.brand || 'Element',
|
||||
name: global.vectorConfig.brand || "Element",
|
||||
isHidden: true,
|
||||
mac: {
|
||||
useLaunchAgent: true,
|
||||
@@ -217,9 +209,9 @@ 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 (!global.vectorConfig.brand || global.vectorConfig.brand === 'Element') {
|
||||
if (!global.vectorConfig.brand || global.vectorConfig.brand === "Element") {
|
||||
const oldLauncher = new AutoLaunch({
|
||||
name: 'Riot',
|
||||
name: "Riot",
|
||||
isHidden: true,
|
||||
mac: {
|
||||
useLaunchAgent: true,
|
||||
@@ -238,26 +230,30 @@ global.store = new Store({ name: "electron-config" });
|
||||
global.appQuitting = false;
|
||||
|
||||
const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
|
||||
(input, platform): boolean => platform !== 'darwin' && input.alt && input.key.toUpperCase() === 'F4',
|
||||
(input, platform): boolean => platform !== 'darwin' && input.control && input.key.toUpperCase() === 'Q',
|
||||
(input, platform): boolean => platform === 'darwin' && input.meta && input.key.toUpperCase() === 'Q',
|
||||
(input, platform): boolean => platform !== "darwin" && input.alt && input.key.toUpperCase() === "F4",
|
||||
(input, platform): boolean => platform !== "darwin" && input.control && input.key.toUpperCase() === "Q",
|
||||
(input, platform): boolean => platform === "darwin" && input.meta && input.key.toUpperCase() === "Q",
|
||||
];
|
||||
|
||||
const warnBeforeExit = (event: Event, input: Input): void => {
|
||||
const shouldWarnBeforeExit = global.store.get('warnBeforeExit', true);
|
||||
const shouldWarnBeforeExit = global.store.get("warnBeforeExit", true);
|
||||
const exitShortcutPressed =
|
||||
input.type === 'keyDown' && exitShortcuts.some(shortcutFn => shortcutFn(input, process.platform));
|
||||
input.type === "keyDown" && exitShortcuts.some((shortcutFn) => shortcutFn(input, process.platform));
|
||||
|
||||
if (shouldWarnBeforeExit && exitShortcutPressed && global.mainWindow) {
|
||||
const shouldCancelCloseRequest = dialog.showMessageBoxSync(global.mainWindow, {
|
||||
type: "question",
|
||||
buttons: [_t("Cancel"), _t("Close %(brand)s", {
|
||||
brand: global.vectorConfig.brand || 'Element',
|
||||
})],
|
||||
message: _t("Are you sure you want to quit?"),
|
||||
defaultId: 1,
|
||||
cancelId: 0,
|
||||
}) === 0;
|
||||
const shouldCancelCloseRequest =
|
||||
dialog.showMessageBoxSync(global.mainWindow, {
|
||||
type: "question",
|
||||
buttons: [
|
||||
_t("Cancel"),
|
||||
_t("Close %(brand)s", {
|
||||
brand: global.vectorConfig.brand || "Element",
|
||||
}),
|
||||
],
|
||||
message: _t("Are you sure you want to quit?"),
|
||||
defaultId: 1,
|
||||
cancelId: 0,
|
||||
}) === 0;
|
||||
|
||||
if (shouldCancelCloseRequest) {
|
||||
event.preventDefault();
|
||||
@@ -271,18 +267,18 @@ const warnBeforeExit = (event: Event, input: Input): void => {
|
||||
// no other way to catch this error).
|
||||
// Assuming we generally run from the console when developing,
|
||||
// this is far preferable.
|
||||
process.on('uncaughtException', function(error: Error): void {
|
||||
console.log('Unhandled exception', error);
|
||||
process.on("uncaughtException", function (error: Error): void {
|
||||
console.log("Unhandled exception", error);
|
||||
});
|
||||
|
||||
app.commandLine.appendSwitch('--enable-usermedia-screen-capturing');
|
||||
if (!app.commandLine.hasSwitch('enable-features')) {
|
||||
app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer');
|
||||
app.commandLine.appendSwitch("--enable-usermedia-screen-capturing");
|
||||
if (!app.commandLine.hasSwitch("enable-features")) {
|
||||
app.commandLine.appendSwitch("enable-features", "WebRTCPipeWireCapturer");
|
||||
}
|
||||
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
if (!gotLock) {
|
||||
console.log('Other instance detected: exiting');
|
||||
console.log("Other instance detected: exiting");
|
||||
app.exit();
|
||||
}
|
||||
|
||||
@@ -294,14 +290,16 @@ protocolInit();
|
||||
// work.
|
||||
// Also mark it as secure (ie. accessing resources from this
|
||||
// protocol and HTTPS won't trigger mixed content warnings).
|
||||
protocol.registerSchemesAsPrivileged([{
|
||||
scheme: 'vector',
|
||||
privileges: {
|
||||
standard: true,
|
||||
secure: true,
|
||||
supportFetchAPI: true,
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: "vector",
|
||||
privileges: {
|
||||
standard: true,
|
||||
secure: true,
|
||||
supportFetchAPI: true,
|
||||
},
|
||||
},
|
||||
}]);
|
||||
]);
|
||||
|
||||
// Turn the sandbox on for *all* windows we might generate. Doing this means we don't
|
||||
// have to specify a `sandbox: true` to each BrowserWindow.
|
||||
@@ -315,15 +313,15 @@ protocol.registerSchemesAsPrivileged([{
|
||||
app.enableSandbox();
|
||||
|
||||
// We disable media controls here. We do this because calls use audio and video elements and they sometimes capture the media keys. See https://github.com/vector-im/element-web/issues/15704
|
||||
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
|
||||
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");
|
||||
|
||||
// Disable hardware acceleration if the setting has been set.
|
||||
if (global.store.get('disableHardwareAcceleration', false) === true) {
|
||||
if (global.store.get("disableHardwareAcceleration", false) === true) {
|
||||
console.log("Disabling hardware acceleration.");
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
app.on('ready', async () => {
|
||||
app.on("ready", async () => {
|
||||
try {
|
||||
await setupGlobals();
|
||||
await moveAutoLauncher();
|
||||
@@ -337,51 +335,51 @@ app.on('ready', async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (argv['devtools']) {
|
||||
if (argv["devtools"]) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { default: installExt, REACT_DEVELOPER_TOOLS, REACT_PERF } = require('electron-devtools-installer');
|
||||
const { default: installExt, REACT_DEVELOPER_TOOLS, REACT_PERF } = require("electron-devtools-installer");
|
||||
installExt(REACT_DEVELOPER_TOOLS)
|
||||
.then((name: string) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err: unknown) => console.log('An error occurred: ', err));
|
||||
.catch((err: unknown) => console.log("An error occurred: ", err));
|
||||
installExt(REACT_PERF)
|
||||
.then((name: string) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err: unknown) => console.log('An error occurred: ', err));
|
||||
.catch((err: unknown) => console.log("An error occurred: ", err));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
protocol.registerFileProtocol('vector', (request, callback) => {
|
||||
if (request.method !== 'GET') {
|
||||
protocol.registerFileProtocol("vector", (request, callback) => {
|
||||
if (request.method !== "GET") {
|
||||
callback({ error: -322 }); // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(request.url);
|
||||
if (parsedUrl.protocol !== 'vector:') {
|
||||
if (parsedUrl.protocol !== "vector:") {
|
||||
callback({ error: -302 }); // UNKNOWN_URL_SCHEME
|
||||
return;
|
||||
}
|
||||
if (parsedUrl.host !== 'vector') {
|
||||
if (parsedUrl.host !== "vector") {
|
||||
callback({ error: -105 }); // NAME_NOT_RESOLVED
|
||||
return;
|
||||
}
|
||||
|
||||
const target = parsedUrl.pathname.split('/');
|
||||
const target = parsedUrl.pathname.split("/");
|
||||
|
||||
// path starts with a '/'
|
||||
if (target[0] !== '') {
|
||||
if (target[0] !== "") {
|
||||
callback({ error: -6 }); // FILE_NOT_FOUND
|
||||
return;
|
||||
}
|
||||
|
||||
if (target[target.length - 1] == '') {
|
||||
target[target.length - 1] = 'index.html';
|
||||
if (target[target.length - 1] == "") {
|
||||
target[target.length - 1] = "index.html";
|
||||
}
|
||||
|
||||
let baseDir: string;
|
||||
if (target[1] === 'webapp') {
|
||||
if (target[1] === "webapp") {
|
||||
baseDir = asarPath;
|
||||
} else {
|
||||
callback({ error: -6 }); // FILE_NOT_FOUND
|
||||
@@ -393,7 +391,7 @@ app.on('ready', async () => {
|
||||
baseDir = path.normalize(baseDir);
|
||||
|
||||
const relTarget = path.normalize(path.join(...target.slice(2)));
|
||||
if (relTarget.startsWith('..')) {
|
||||
if (relTarget.startsWith("..")) {
|
||||
callback({ error: -6 }); // FILE_NOT_FOUND
|
||||
return;
|
||||
}
|
||||
@@ -404,13 +402,13 @@ app.on('ready', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
if (argv['no-update']) {
|
||||
if (argv["no-update"]) {
|
||||
console.log('Auto update disabled via command line flag "--no-update"');
|
||||
} 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 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');
|
||||
console.log("No update_base_url is defined: auto update is disabled");
|
||||
}
|
||||
|
||||
// Load the previous window state with fallback to defaults
|
||||
@@ -422,11 +420,11 @@ app.on('ready', async () => {
|
||||
const preloadScript = path.normalize(`${__dirname}/preload.js`);
|
||||
global.mainWindow = new BrowserWindow({
|
||||
// https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: "#fff",
|
||||
|
||||
icon: iconPath,
|
||||
show: false,
|
||||
autoHideMenuBar: global.store.get('autoHideMenuBar', true),
|
||||
autoHideMenuBar: global.store.get("autoHideMenuBar", true),
|
||||
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
@@ -440,20 +438,20 @@ app.on('ready', async () => {
|
||||
webgl: true,
|
||||
},
|
||||
});
|
||||
global.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
|
||||
global.mainWindow.webContents.session.setSpellCheckerEnabled(global.store.get("spellCheckerEnabled", true));
|
||||
|
||||
// Create trayIcon icon
|
||||
if (global.store.get('minimizeToTray', true)) tray.create(global.trayConfig);
|
||||
if (global.store.get("minimizeToTray", true)) tray.create(global.trayConfig);
|
||||
|
||||
global.mainWindow.once('ready-to-show', () => {
|
||||
global.mainWindow.once("ready-to-show", () => {
|
||||
if (!global.mainWindow) return;
|
||||
mainWindowState.manage(global.mainWindow);
|
||||
|
||||
if (!argv['hidden']) {
|
||||
if (!argv["hidden"]) {
|
||||
global.mainWindow.show();
|
||||
} else {
|
||||
// hide here explicitly because window manage above sometimes shows it
|
||||
@@ -461,21 +459,21 @@ app.on('ready', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
global.mainWindow.webContents.on('before-input-event', warnBeforeExit);
|
||||
global.mainWindow.webContents.on("before-input-event", warnBeforeExit);
|
||||
|
||||
global.mainWindow.on('closed', () => {
|
||||
global.mainWindow.on("closed", () => {
|
||||
global.mainWindow = null;
|
||||
});
|
||||
global.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')) {
|
||||
if (!global.appQuitting && (tray.hasTray() || process.platform === "darwin")) {
|
||||
// On Mac, closing the window just hides it
|
||||
// (this is generally how single-window Mac apps
|
||||
// behave, eg. Mail.app)
|
||||
e.preventDefault();
|
||||
|
||||
if (global.mainWindow?.isFullScreen()) {
|
||||
global.mainWindow.once('leave-full-screen', () => global.mainWindow?.hide());
|
||||
global.mainWindow.once("leave-full-screen", () => global.mainWindow?.hide());
|
||||
|
||||
global.mainWindow.setFullScreen(false);
|
||||
} else {
|
||||
@@ -486,12 +484,12 @@ app.on('ready', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
if (process.platform === "win32") {
|
||||
// Handle forward/backward mouse buttons in Windows
|
||||
global.mainWindow.on('app-command', (e, cmd) => {
|
||||
if (cmd === 'browser-backward' && global.mainWindow?.webContents.canGoBack()) {
|
||||
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()) {
|
||||
} else if (cmd === "browser-forward" && global.mainWindow?.webContents.canGoForward()) {
|
||||
global.mainWindow.webContents.goForward();
|
||||
}
|
||||
});
|
||||
@@ -501,32 +499,29 @@ app.on('ready', async () => {
|
||||
|
||||
global.appLocalization = new AppLocalization({
|
||||
store: global.store,
|
||||
components: [
|
||||
(): void => tray.initApplicationMenu(),
|
||||
(): void => Menu.setApplicationMenu(buildMenuTemplate()),
|
||||
],
|
||||
components: [(): void => tray.initApplicationMenu(), (): void => Menu.setApplicationMenu(buildMenuTemplate())],
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
app.on("window-all-closed", () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
app.on("activate", () => {
|
||||
global.mainWindow?.show();
|
||||
});
|
||||
|
||||
function beforeQuit(): void {
|
||||
global.appQuitting = true;
|
||||
global.mainWindow?.webContents.send('before-quit');
|
||||
global.mainWindow?.webContents.send("before-quit");
|
||||
}
|
||||
|
||||
app.on('before-quit', beforeQuit);
|
||||
autoUpdater.on('before-quit-for-update', beforeQuit);
|
||||
app.on("before-quit", beforeQuit);
|
||||
autoUpdater.on("before-quit-for-update", beforeQuit);
|
||||
|
||||
app.on('second-instance', (ev, commandLine, workingDirectory) => {
|
||||
app.on("second-instance", (ev, commandLine, workingDirectory) => {
|
||||
// If other instance launched with --hidden then skip showing window
|
||||
if (commandLine.includes('--hidden')) return;
|
||||
if (commandLine.includes("--hidden")) return;
|
||||
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (global.mainWindow) {
|
||||
@@ -540,4 +535,4 @@ app.on('second-instance', (ev, commandLine, workingDirectory) => {
|
||||
// installer uses for the shortcut icon.
|
||||
// This makes notifications work on windows 8.1 (and is
|
||||
// a noop on other platforms).
|
||||
app.setAppUserModelId('com.squirrel.element-desktop.Element');
|
||||
app.setAppUserModelId("com.squirrel.element-desktop.Element");
|
||||
|
||||
+33
-34
@@ -22,8 +22,8 @@ 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') {
|
||||
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
|
||||
@@ -35,10 +35,10 @@ ipcMain.on('setBadgeCount', function(_ev: IpcMainEvent, count: number): void {
|
||||
});
|
||||
|
||||
let focusHandlerAttached = false;
|
||||
ipcMain.on('loudNotification', function(): void {
|
||||
if (process.platform === 'win32' && global.mainWindow && !global.mainWindow.isFocused() && !focusHandlerAttached) {
|
||||
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.once("focus", () => {
|
||||
global.mainWindow?.flashFrame(false);
|
||||
focusHandlerAttached = false;
|
||||
});
|
||||
@@ -47,17 +47,17 @@ ipcMain.on('loudNotification', function(): void {
|
||||
});
|
||||
|
||||
let powerSaveBlockerId: number | null = null;
|
||||
ipcMain.on('app_onAction', function(_ev: IpcMainEvent, payload) {
|
||||
ipcMain.on("app_onAction", function (_ev: IpcMainEvent, payload) {
|
||||
switch (payload.action) {
|
||||
case 'call_state': {
|
||||
case "call_state": {
|
||||
if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) {
|
||||
if (payload.state === 'ended') {
|
||||
if (payload.state === "ended") {
|
||||
powerSaveBlocker.stop(powerSaveBlockerId);
|
||||
powerSaveBlockerId = null;
|
||||
}
|
||||
} else {
|
||||
if (powerSaveBlockerId === null && payload.state === 'connected') {
|
||||
powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep');
|
||||
if (powerSaveBlockerId === null && payload.state === "connected") {
|
||||
powerSaveBlockerId = powerSaveBlocker.start("prevent-display-sleep");
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -65,35 +65,35 @@ ipcMain.on('app_onAction', function(_ev: IpcMainEvent, payload) {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
|
||||
ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
|
||||
if (!global.mainWindow) return;
|
||||
|
||||
const args = payload.args || [];
|
||||
let ret: any;
|
||||
|
||||
switch (payload.name) {
|
||||
case 'getUpdateFeedUrl':
|
||||
case "getUpdateFeedUrl":
|
||||
ret = autoUpdater.getFeedURL();
|
||||
break;
|
||||
case 'getSettingValue': {
|
||||
case "getSettingValue": {
|
||||
const [settingName] = args;
|
||||
const setting = Settings[settingName];
|
||||
ret = await setting.read();
|
||||
break;
|
||||
}
|
||||
case 'setSettingValue': {
|
||||
case "setSettingValue": {
|
||||
const [settingName, value] = args;
|
||||
const setting = Settings[settingName];
|
||||
await setting.write(value);
|
||||
break;
|
||||
}
|
||||
case 'setLanguage':
|
||||
case "setLanguage":
|
||||
global.appLocalization.setAppLocale(args[0]);
|
||||
break;
|
||||
case 'getAppVersion':
|
||||
case "getAppVersion":
|
||||
ret = app.getVersion();
|
||||
break;
|
||||
case 'focusWindow':
|
||||
case "focusWindow":
|
||||
if (global.mainWindow.isMinimized()) {
|
||||
global.mainWindow.restore();
|
||||
} else if (!global.mainWindow.isVisible()) {
|
||||
@@ -102,31 +102,31 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
|
||||
global.mainWindow.focus();
|
||||
}
|
||||
break;
|
||||
case 'getConfig':
|
||||
case "getConfig":
|
||||
ret = global.vectorConfig;
|
||||
break;
|
||||
case 'navigateBack':
|
||||
case "navigateBack":
|
||||
if (global.mainWindow.webContents.canGoBack()) {
|
||||
global.mainWindow.webContents.goBack();
|
||||
}
|
||||
break;
|
||||
case 'navigateForward':
|
||||
case "navigateForward":
|
||||
if (global.mainWindow.webContents.canGoForward()) {
|
||||
global.mainWindow.webContents.goForward();
|
||||
}
|
||||
break;
|
||||
case 'setSpellCheckEnabled':
|
||||
if (typeof args[0] !== 'boolean') return;
|
||||
case "setSpellCheckEnabled":
|
||||
if (typeof args[0] !== "boolean") return;
|
||||
|
||||
global.mainWindow.webContents.session.setSpellCheckerEnabled(args[0]);
|
||||
global.store.set("spellCheckerEnabled", args[0]);
|
||||
break;
|
||||
|
||||
case 'getSpellCheckEnabled':
|
||||
case "getSpellCheckEnabled":
|
||||
ret = global.store.get("spellCheckerEnabled", true);
|
||||
break;
|
||||
|
||||
case 'setSpellCheckLanguages':
|
||||
case "setSpellCheckLanguages":
|
||||
try {
|
||||
global.mainWindow.webContents.session.setSpellCheckerLanguages(args[0]);
|
||||
} catch (er) {
|
||||
@@ -134,18 +134,18 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'getSpellCheckLanguages':
|
||||
case "getSpellCheckLanguages":
|
||||
ret = global.mainWindow.webContents.session.getSpellCheckerLanguages();
|
||||
break;
|
||||
case 'getAvailableSpellCheckLanguages':
|
||||
case "getAvailableSpellCheckLanguages":
|
||||
ret = global.mainWindow.webContents.session.availableSpellCheckerLanguages;
|
||||
break;
|
||||
|
||||
case 'startSSOFlow':
|
||||
case "startSSOFlow":
|
||||
recordSSOSession(args[0]);
|
||||
break;
|
||||
|
||||
case 'getPickleKey':
|
||||
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
|
||||
@@ -160,7 +160,7 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'createPickleKey':
|
||||
case "createPickleKey":
|
||||
try {
|
||||
const pickleKey = await randomArray(32);
|
||||
await keytar?.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey);
|
||||
@@ -170,7 +170,7 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'destroyPickleKey':
|
||||
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
|
||||
@@ -178,7 +178,7 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
|
||||
await keytar?.deletePassword("riot.im", `${args[0]}|${args[1]}`);
|
||||
} catch (e) {}
|
||||
break;
|
||||
case 'getDesktopCapturerSources':
|
||||
case "getDesktopCapturerSources":
|
||||
ret = (await desktopCapturer.getSources(args[0])).map((source) => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
@@ -187,16 +187,15 @@ ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) {
|
||||
break;
|
||||
|
||||
default:
|
||||
global.mainWindow.webContents.send('ipcReply', {
|
||||
global.mainWindow.webContents.send("ipcReply", {
|
||||
id: payload.id,
|
||||
error: "Unknown IPC Call: " + payload.name,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
global.mainWindow.webContents.send('ipcReply', {
|
||||
global.mainWindow.webContents.send("ipcReply", {
|
||||
id: payload.id,
|
||||
reply: ret,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ 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');
|
||||
keytar = require("keytar");
|
||||
} catch (e) {
|
||||
if ((<NodeJS.ErrnoException>e).code === "MODULE_NOT_FOUND") {
|
||||
console.log("Keytar isn't installed; secure key storage is disabled.");
|
||||
|
||||
@@ -16,9 +16,9 @@ limitations under the License.
|
||||
|
||||
import counterpart from "counterpart";
|
||||
|
||||
import type Store from 'electron-store';
|
||||
import type Store from "electron-store";
|
||||
|
||||
const FALLBACK_LOCALE = 'en';
|
||||
const FALLBACK_LOCALE = "en";
|
||||
|
||||
export function _td(text: string): string {
|
||||
return text;
|
||||
@@ -44,11 +44,11 @@ export function _t(text: string, variables: IVariables = {}): string {
|
||||
Object.keys(variables).forEach((key) => {
|
||||
if (variables[key] === undefined) {
|
||||
console.warn("safeCounterpartTranslate called with undefined interpolation name: " + key);
|
||||
variables[key] = 'undefined';
|
||||
variables[key] = "undefined";
|
||||
}
|
||||
if (variables[key] === null) {
|
||||
console.warn("safeCounterpartTranslate called with null interpolation name: " + key);
|
||||
variables[key] = 'null';
|
||||
variables[key] = "null";
|
||||
}
|
||||
});
|
||||
let translated = counterpart.translate(text, variables);
|
||||
@@ -71,10 +71,10 @@ export class AppLocalization {
|
||||
private readonly store: TypedStore;
|
||||
private readonly localizedComponents?: Set<Component>;
|
||||
|
||||
public constructor({ store, components = [] }: { store: TypedStore, components: Component[] }) {
|
||||
public constructor({ store, components = [] }: { store: TypedStore; components: Component[] }) {
|
||||
counterpart.registerTranslations(FALLBACK_LOCALE, this.fetchTranslationJson("en_EN"));
|
||||
counterpart.setFallbackLocale(FALLBACK_LOCALE);
|
||||
counterpart.setSeparator('|');
|
||||
counterpart.setSeparator("|");
|
||||
|
||||
if (Array.isArray(components)) {
|
||||
this.localizedComponents = new Set(components);
|
||||
@@ -119,7 +119,7 @@ export class AppLocalization {
|
||||
locales = [locales];
|
||||
}
|
||||
|
||||
const loadedLocales = locales.filter(locale => {
|
||||
const loadedLocales = locales.filter((locale) => {
|
||||
const translations = this.fetchTranslationJson(locale);
|
||||
if (translations !== null) {
|
||||
counterpart.registerTranslations(locale, translations);
|
||||
@@ -135,7 +135,7 @@ export class AppLocalization {
|
||||
|
||||
public resetLocalizedUI(): void {
|
||||
console.log("Resetting the UI components after locale change");
|
||||
this.localizedComponents?.forEach(componentSetup => {
|
||||
this.localizedComponents?.forEach((componentSetup) => {
|
||||
if (typeof componentSetup === "function") {
|
||||
componentSetup();
|
||||
}
|
||||
|
||||
+16
-19
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ipcRenderer, contextBridge, IpcRendererEvent } from 'electron';
|
||||
import { ipcRenderer, contextBridge, IpcRendererEvent } from "electron";
|
||||
|
||||
// Expose only expected IPC wrapper APIs to the renderer process to avoid
|
||||
// handing out generalised messaging access.
|
||||
@@ -36,22 +36,19 @@ const CHANNELS = [
|
||||
"userDownloadAction",
|
||||
];
|
||||
|
||||
contextBridge.exposeInMainWorld(
|
||||
"electron",
|
||||
{
|
||||
on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): void {
|
||||
if (!CHANNELS.includes(channel)) {
|
||||
console.error(`Unknown IPC channel ${channel} ignored`);
|
||||
return;
|
||||
}
|
||||
ipcRenderer.on(channel, listener);
|
||||
},
|
||||
send(channel: string, ...args: any[]): void {
|
||||
if (!CHANNELS.includes(channel)) {
|
||||
console.error(`Unknown IPC channel ${channel} ignored`);
|
||||
return;
|
||||
}
|
||||
ipcRenderer.send(channel, ...args);
|
||||
},
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): void {
|
||||
if (!CHANNELS.includes(channel)) {
|
||||
console.error(`Unknown IPC channel ${channel} ignored`);
|
||||
return;
|
||||
}
|
||||
ipcRenderer.on(channel, listener);
|
||||
},
|
||||
);
|
||||
send(channel: string, ...args: any[]): void {
|
||||
if (!CHANNELS.includes(channel)) {
|
||||
console.error(`Unknown IPC channel ${channel} ignored`);
|
||||
return;
|
||||
}
|
||||
ipcRenderer.send(channel, ...args);
|
||||
},
|
||||
});
|
||||
|
||||
+11
-10
@@ -67,7 +67,7 @@ function writeStore(data: Record<string, string>): void {
|
||||
}
|
||||
|
||||
export function recordSSOSession(sessionID: string): void {
|
||||
const userDataPath = app.getPath('userData');
|
||||
const userDataPath = app.getPath("userData");
|
||||
const store = readStore();
|
||||
for (const key in store) {
|
||||
// ensure each instance only has one (the latest) session ID to prevent the file growing unbounded
|
||||
@@ -82,7 +82,7 @@ export function recordSSOSession(sessionID: string): void {
|
||||
|
||||
export function getProfileFromDeeplink(args: string[]): string | undefined {
|
||||
// check if we are passed a profile in the SSO callback url
|
||||
const deeplinkUrl = args.find(arg => arg.startsWith(PROTOCOL + '//'));
|
||||
const deeplinkUrl = args.find((arg) => arg.startsWith(PROTOCOL + "//"));
|
||||
if (deeplinkUrl?.includes(SEARCH_PARAM)) {
|
||||
const parsedUrl = new URL(deeplinkUrl);
|
||||
if (parsedUrl.protocol === PROTOCOL) {
|
||||
@@ -98,25 +98,26 @@ export function protocolInit(): void {
|
||||
// get all args except `hidden` as it'd mean the app would not get focused
|
||||
// XXX: passing args to protocol handlers only works on Windows, so unpackaged deep-linking
|
||||
// --profile/--profile-dir are passed via the SEARCH_PARAM var in the callback url
|
||||
const args = process.argv.slice(1).filter(arg => arg !== "--hidden" && arg !== "-hidden");
|
||||
const args = process.argv.slice(1).filter((arg) => arg !== "--hidden" && arg !== "-hidden");
|
||||
if (app.isPackaged) {
|
||||
app.setAsDefaultProtocolClient('element', process.execPath, args);
|
||||
} else if (process.platform === 'win32') { // on Mac/Linux this would just cause the electron binary to open
|
||||
app.setAsDefaultProtocolClient("element", process.execPath, args);
|
||||
} else if (process.platform === "win32") {
|
||||
// on Mac/Linux this would just cause the electron binary to open
|
||||
// special handler for running without being packaged, e.g `electron .` by passing our app path to electron
|
||||
app.setAsDefaultProtocolClient('element', process.execPath, [app.getAppPath(), ...args]);
|
||||
app.setAsDefaultProtocolClient("element", process.execPath, [app.getAppPath(), ...args]);
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
if (process.platform === "darwin") {
|
||||
// Protocol handler for macos
|
||||
app.on('open-url', function(ev, url) {
|
||||
app.on("open-url", function (ev, url) {
|
||||
ev.preventDefault();
|
||||
processUrl(url);
|
||||
});
|
||||
} else {
|
||||
// Protocol handler for win32/Linux
|
||||
app.on('second-instance', (ev, commandLine) => {
|
||||
app.on("second-instance", (ev, commandLine) => {
|
||||
const url = commandLine[commandLine.length - 1];
|
||||
if (!url.startsWith(PROTOCOL + '//')) return;
|
||||
if (!url.startsWith(PROTOCOL + "//")) return;
|
||||
processUrl(url);
|
||||
});
|
||||
}
|
||||
|
||||
+27
-31
@@ -34,7 +34,7 @@ let ReindexError: typeof ReindexErrorType;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const seshatModule = require('matrix-seshat');
|
||||
const seshatModule = require("matrix-seshat");
|
||||
Seshat = seshatModule.Seshat;
|
||||
SeshatRecovery = seshatModule.SeshatRecovery;
|
||||
ReindexError = seshatModule.ReindexError;
|
||||
@@ -75,29 +75,29 @@ const deleteContents = async (p: string): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
ipcMain.on("seshat", async function (_ev: IpcMainEvent, payload): Promise<void> {
|
||||
if (!global.mainWindow) return;
|
||||
|
||||
// We do this here to ensure we get the path after --profile has been resolved
|
||||
const eventStorePath = path.join(app.getPath('userData'), 'EventStore');
|
||||
const eventStorePath = path.join(app.getPath("userData"), "EventStore");
|
||||
|
||||
const sendError = (id: string, e: Error): void => {
|
||||
const error = {
|
||||
message: e.message,
|
||||
};
|
||||
|
||||
global.mainWindow?.webContents.send('seshatReply', { id, error });
|
||||
global.mainWindow?.webContents.send("seshatReply", { id, error });
|
||||
};
|
||||
|
||||
const args = payload.args || [];
|
||||
let ret: any;
|
||||
|
||||
switch (payload.name) {
|
||||
case 'supportsEventIndexing':
|
||||
case "supportsEventIndexing":
|
||||
ret = seshatSupported;
|
||||
break;
|
||||
|
||||
case 'initEventIndex':
|
||||
case "initEventIndex":
|
||||
if (eventIndex === null) {
|
||||
const userId = args[0];
|
||||
const deviceId = args[1];
|
||||
@@ -127,8 +127,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
|
||||
try {
|
||||
await deleteContents(eventStorePath);
|
||||
} catch (e) {
|
||||
}
|
||||
} catch (e) {}
|
||||
} else {
|
||||
await recoveryIndex.reindex();
|
||||
}
|
||||
@@ -142,7 +141,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'closeEventIndex':
|
||||
case "closeEventIndex":
|
||||
if (eventIndex !== null) {
|
||||
const index = eventIndex;
|
||||
eventIndex = null;
|
||||
@@ -156,26 +155,24 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'deleteEventIndex': {
|
||||
case "deleteEventIndex": {
|
||||
try {
|
||||
await deleteContents(eventStorePath);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
} catch (e) {}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'isEventIndexEmpty':
|
||||
case "isEventIndexEmpty":
|
||||
if (eventIndex === null) ret = true;
|
||||
else ret = await eventIndex.isEmpty();
|
||||
break;
|
||||
|
||||
case 'isRoomIndexed':
|
||||
case "isRoomIndexed":
|
||||
if (eventIndex === null) ret = false;
|
||||
else ret = await eventIndex.isRoomIndexed(args[0]);
|
||||
break;
|
||||
|
||||
case 'addEventToIndex':
|
||||
case "addEventToIndex":
|
||||
try {
|
||||
eventIndex?.addEvent(args[0], args[1]);
|
||||
} catch (e) {
|
||||
@@ -184,7 +181,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'deleteEvent':
|
||||
case "deleteEvent":
|
||||
try {
|
||||
ret = await eventIndex?.deleteEvent(args[0]);
|
||||
} catch (e) {
|
||||
@@ -193,7 +190,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'commitLiveEvents':
|
||||
case "commitLiveEvents":
|
||||
try {
|
||||
ret = await eventIndex?.commit();
|
||||
} catch (e) {
|
||||
@@ -202,7 +199,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'searchEventIndex':
|
||||
case "searchEventIndex":
|
||||
try {
|
||||
ret = await eventIndex?.search(args[0]);
|
||||
} catch (e) {
|
||||
@@ -211,12 +208,11 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'addHistoricEvents':
|
||||
case "addHistoricEvents":
|
||||
if (eventIndex === null) ret = false;
|
||||
else {
|
||||
try {
|
||||
ret = await eventIndex.addHistoricEvents(
|
||||
args[0], args[1], args[2]);
|
||||
ret = await eventIndex.addHistoricEvents(args[0], args[1], args[2]);
|
||||
} catch (e) {
|
||||
sendError(payload.id, <Error>e);
|
||||
return;
|
||||
@@ -224,7 +220,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'getStats':
|
||||
case "getStats":
|
||||
if (eventIndex === null) ret = 0;
|
||||
else {
|
||||
try {
|
||||
@@ -236,7 +232,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'removeCrawlerCheckpoint':
|
||||
case "removeCrawlerCheckpoint":
|
||||
if (eventIndex === null) ret = false;
|
||||
else {
|
||||
try {
|
||||
@@ -248,7 +244,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'addCrawlerCheckpoint':
|
||||
case "addCrawlerCheckpoint":
|
||||
if (eventIndex === null) ret = false;
|
||||
else {
|
||||
try {
|
||||
@@ -260,7 +256,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'loadFileEvents':
|
||||
case "loadFileEvents":
|
||||
if (eventIndex === null) ret = [];
|
||||
else {
|
||||
try {
|
||||
@@ -272,7 +268,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'loadCheckpoints':
|
||||
case "loadCheckpoints":
|
||||
if (eventIndex === null) ret = [];
|
||||
else {
|
||||
try {
|
||||
@@ -283,7 +279,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'setUserVersion':
|
||||
case "setUserVersion":
|
||||
if (eventIndex === null) break;
|
||||
else {
|
||||
try {
|
||||
@@ -295,7 +291,7 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'getUserVersion':
|
||||
case "getUserVersion":
|
||||
if (eventIndex === null) ret = 0;
|
||||
else {
|
||||
try {
|
||||
@@ -308,14 +304,14 @@ ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise<void> {
|
||||
break;
|
||||
|
||||
default:
|
||||
global.mainWindow.webContents.send('seshatReply', {
|
||||
global.mainWindow.webContents.send("seshatReply", {
|
||||
id: payload.id,
|
||||
error: "Unknown IPC Call: " + payload.name,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
global.mainWindow.webContents.send('seshatReply', {
|
||||
global.mainWindow.webContents.send("seshatReply", {
|
||||
id: payload.id,
|
||||
reply: ret,
|
||||
});
|
||||
|
||||
+8
-6
@@ -42,17 +42,19 @@ export const Settings: Record<string, Setting> = {
|
||||
global.store.set("warnBeforeExit", value);
|
||||
},
|
||||
},
|
||||
"Electron.alwaysShowMenuBar": { // not supported on macOS
|
||||
"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.store.set("autoHideMenuBar", !value);
|
||||
global.mainWindow!.autoHideMenuBar = !value;
|
||||
global.mainWindow!.setMenuBarVisibility(value);
|
||||
},
|
||||
},
|
||||
"Electron.showTrayIcon": { // not supported on macOS
|
||||
"Electron.showTrayIcon": {
|
||||
// not supported on macOS
|
||||
async read(): Promise<any> {
|
||||
return tray.hasTray();
|
||||
},
|
||||
@@ -63,15 +65,15 @@ export const Settings: Record<string, Setting> = {
|
||||
} else {
|
||||
tray.destroy();
|
||||
}
|
||||
global.store.set('minimizeToTray', value);
|
||||
global.store.set("minimizeToTray", value);
|
||||
},
|
||||
},
|
||||
"Electron.enableHardwareAcceleration": {
|
||||
async read(): Promise<any> {
|
||||
return !global.store.get('disableHardwareAcceleration', false);
|
||||
return !global.store.get("disableHardwareAcceleration", false);
|
||||
},
|
||||
async write(value: any): Promise<void> {
|
||||
global.store.set('disableHardwareAcceleration', !value);
|
||||
global.store.set("disableHardwareAcceleration", !value);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+10
-10
@@ -23,29 +23,29 @@ function runUpdateExe(args: string[]): Promise<void> {
|
||||
// Note that there's an Update.exe in the app-x.x.x directory and one in the parent
|
||||
// directory: we need to run the one in the parent directory, because it discovers
|
||||
// information about the app by inspecting the directory it's run from.
|
||||
const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe');
|
||||
const updateExe = path.resolve(path.dirname(process.execPath), "..", "Update.exe");
|
||||
console.log(`Spawning '${updateExe}' with args '${args}'`);
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve) => {
|
||||
spawn(updateExe, args, {
|
||||
detached: true,
|
||||
}).on('close', resolve);
|
||||
}).on("close", resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function checkSquirrelHooks(): boolean {
|
||||
if (process.platform !== 'win32') return false;
|
||||
if (process.platform !== "win32") return false;
|
||||
const cmd = process.argv[1];
|
||||
const target = path.basename(process.execPath);
|
||||
if (cmd === '--squirrel-install') {
|
||||
runUpdateExe(['--createShortcut=' + target]).then(() => app.quit());
|
||||
if (cmd === "--squirrel-install") {
|
||||
runUpdateExe(["--createShortcut=" + target]).then(() => app.quit());
|
||||
return true;
|
||||
} else if (cmd === '--squirrel-updated') {
|
||||
} else if (cmd === "--squirrel-updated") {
|
||||
app.quit();
|
||||
return true;
|
||||
} else if (cmd === '--squirrel-uninstall') {
|
||||
runUpdateExe(['--removeShortcut=' + target]).then(() => app.quit());
|
||||
} else if (cmd === "--squirrel-uninstall") {
|
||||
runUpdateExe(["--removeShortcut=" + target]).then(() => app.quit());
|
||||
return true;
|
||||
} else if (cmd === '--squirrel-obsolete') {
|
||||
} else if (cmd === "--squirrel-obsolete") {
|
||||
app.quit();
|
||||
return true;
|
||||
}
|
||||
|
||||
+12
-12
@@ -25,7 +25,7 @@ import { _t } from "./language-helper";
|
||||
let trayIcon: Tray | null = null;
|
||||
|
||||
export function hasTray(): boolean {
|
||||
return (trayIcon !== null);
|
||||
return trayIcon !== null;
|
||||
}
|
||||
|
||||
export function destroy(): void {
|
||||
@@ -52,17 +52,17 @@ interface IConfig {
|
||||
|
||||
export function create(config: IConfig): void {
|
||||
// no trays on darwin
|
||||
if (process.platform === 'darwin' || trayIcon) return;
|
||||
if (process.platform === "darwin" || trayIcon) return;
|
||||
const defaultIcon = nativeImage.createFromPath(config.icon_path);
|
||||
|
||||
trayIcon = new Tray(defaultIcon);
|
||||
trayIcon.setToolTip(config.brand);
|
||||
initApplicationMenu();
|
||||
trayIcon.on('click', toggleWin);
|
||||
trayIcon.on("click", toggleWin);
|
||||
|
||||
let lastFavicon: string | null = null;
|
||||
global.mainWindow?.webContents.on('page-favicon-updated', async function(ev, favicons) {
|
||||
if (!favicons || favicons.length <= 0 || !favicons[0].startsWith('data:')) {
|
||||
global.mainWindow?.webContents.on("page-favicon-updated", async function (ev, favicons) {
|
||||
if (!favicons || favicons.length <= 0 || !favicons[0].startsWith("data:")) {
|
||||
if (lastFavicon !== null) {
|
||||
global.mainWindow?.setIcon(defaultIcon);
|
||||
trayIcon?.setImage(defaultIcon);
|
||||
@@ -78,9 +78,9 @@ export function create(config: IConfig): void {
|
||||
let newFavicon = nativeImage.createFromDataURL(favicons[0]);
|
||||
|
||||
// Windows likes ico's too much.
|
||||
if (process.platform === 'win32') {
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
const icoPath = path.join(app.getPath('temp'), 'win32_element_icon.ico');
|
||||
const icoPath = path.join(app.getPath("temp"), "win32_element_icon.ico");
|
||||
fs.writeFileSync(icoPath, await pngToIco(newFavicon.toPNG()));
|
||||
newFavicon = nativeImage.createFromPath(icoPath);
|
||||
} catch (e) {
|
||||
@@ -92,7 +92,7 @@ export function create(config: IConfig): void {
|
||||
global.mainWindow?.setIcon(newFavicon);
|
||||
});
|
||||
|
||||
global.mainWindow?.webContents.on('page-title-updated', function(ev, title) {
|
||||
global.mainWindow?.webContents.on("page-title-updated", function (ev, title) {
|
||||
trayIcon?.setToolTip(title);
|
||||
});
|
||||
}
|
||||
@@ -104,13 +104,13 @@ export function initApplicationMenu(): void {
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: _t('Show/Hide'),
|
||||
label: _t("Show/Hide"),
|
||||
click: toggleWin,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: _t('Quit'),
|
||||
click: function(): void {
|
||||
label: _t("Quit"),
|
||||
click: function (): void {
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
|
||||
+30
-27
@@ -37,33 +37,33 @@ function pollForUpdates(): void {
|
||||
autoUpdater.checkForUpdates();
|
||||
} else {
|
||||
console.log("Skipping update check as download already present");
|
||||
global.mainWindow?.webContents.send('update-downloaded', latestUpdateDownloaded);
|
||||
global.mainWindow?.webContents.send("update-downloaded", latestUpdateDownloaded);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Couldn\'t check for update', e);
|
||||
console.log("Couldn't check for update", e);
|
||||
}
|
||||
}
|
||||
|
||||
export function start(updateBaseUrl: string): void {
|
||||
if (updateBaseUrl.slice(-1) !== '/') {
|
||||
updateBaseUrl = updateBaseUrl + '/';
|
||||
if (updateBaseUrl.slice(-1) !== "/") {
|
||||
updateBaseUrl = updateBaseUrl + "/";
|
||||
}
|
||||
try {
|
||||
let url: string;
|
||||
let serverType: "json" | undefined;
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
if (process.platform === "darwin") {
|
||||
// On macOS it takes a JSON file with a map between versions and their URLs
|
||||
url = `${updateBaseUrl}macos/releases.json`;
|
||||
serverType = "json";
|
||||
} else if (process.platform === 'win32') {
|
||||
} else if (process.platform === "win32") {
|
||||
// On windows it takes a base path and looks for files under that path.
|
||||
url = `${updateBaseUrl}win32/${process.arch}/`;
|
||||
} else {
|
||||
// Squirrel / electron only supports auto-update on these two platforms.
|
||||
// I'm not even going to try to guess which feed style they'd use if they
|
||||
// implemented it on Linux, or if it would be different again.
|
||||
console.log('Auto update not supported on this platform');
|
||||
console.log("Auto update not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,15 +82,15 @@ export function start(updateBaseUrl: string): void {
|
||||
}
|
||||
} catch (err) {
|
||||
// will fail if running in debug mode
|
||||
console.log('Couldn\'t enable update checking', err);
|
||||
console.log("Couldn't enable update checking", err);
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.on('install_update', installUpdate);
|
||||
ipcMain.on('check_updates', pollForUpdates);
|
||||
ipcMain.on("install_update", installUpdate);
|
||||
ipcMain.on("check_updates", pollForUpdates);
|
||||
|
||||
function ipcChannelSendUpdateStatus(status: boolean | string): void {
|
||||
global.mainWindow?.webContents.send('check_updates', status);
|
||||
global.mainWindow?.webContents.send("check_updates", status);
|
||||
}
|
||||
|
||||
interface ICachedUpdate {
|
||||
@@ -102,23 +102,26 @@ interface ICachedUpdate {
|
||||
|
||||
// cache the latest update which has been downloaded as electron offers no api to read it
|
||||
let latestUpdateDownloaded: ICachedUpdate;
|
||||
autoUpdater.on('update-available', function() {
|
||||
ipcChannelSendUpdateStatus(true);
|
||||
}).on('update-not-available', function() {
|
||||
if (latestUpdateDownloaded) {
|
||||
// the only time we will get `update-not-available` if `latestUpdateDownloaded` is already set
|
||||
// is if the user used the Manual Update check and there is no update newer than the one we
|
||||
// have downloaded, so show it to them as the latest again.
|
||||
global.mainWindow?.webContents.send('update-downloaded', latestUpdateDownloaded);
|
||||
} else {
|
||||
ipcChannelSendUpdateStatus(false);
|
||||
}
|
||||
}).on('error', function(error) {
|
||||
ipcChannelSendUpdateStatus(error.message);
|
||||
});
|
||||
autoUpdater
|
||||
.on("update-available", function () {
|
||||
ipcChannelSendUpdateStatus(true);
|
||||
})
|
||||
.on("update-not-available", function () {
|
||||
if (latestUpdateDownloaded) {
|
||||
// the only time we will get `update-not-available` if `latestUpdateDownloaded` is already set
|
||||
// is if the user used the Manual Update check and there is no update newer than the one we
|
||||
// have downloaded, so show it to them as the latest again.
|
||||
global.mainWindow?.webContents.send("update-downloaded", latestUpdateDownloaded);
|
||||
} else {
|
||||
ipcChannelSendUpdateStatus(false);
|
||||
}
|
||||
})
|
||||
.on("error", function (error) {
|
||||
ipcChannelSendUpdateStatus(error.message);
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (ev, releaseNotes, releaseName, releaseDate, updateURL) => {
|
||||
autoUpdater.on("update-downloaded", (ev, releaseNotes, releaseName, releaseDate, updateURL) => {
|
||||
// forward to renderer
|
||||
latestUpdateDownloaded = { releaseNotes, releaseName, releaseDate, updateURL };
|
||||
global.mainWindow?.webContents.send('update-downloaded', latestUpdateDownloaded);
|
||||
global.mainWindow?.webContents.send("update-downloaded", latestUpdateDownloaded);
|
||||
});
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ export async function randomArray(size: number): Promise<string> {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(buf.toString("base64").replace(/=+$/g, ''));
|
||||
resolve(buf.toString("base64").replace(/=+$/g, ""));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+112
-101
@@ -14,125 +14,133 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { app, shell, Menu, MenuItem, MenuItemConstructorOptions } from 'electron';
|
||||
import { app, shell, Menu, MenuItem, MenuItemConstructorOptions } from "electron";
|
||||
|
||||
import { _t } from './language-helper';
|
||||
import { _t } from "./language-helper";
|
||||
|
||||
const isMac = process.platform === 'darwin';
|
||||
const isMac = process.platform === "darwin";
|
||||
|
||||
export function buildMenuTemplate(): Menu {
|
||||
// Menu template from http://electron.atom.io/docs/api/menu/, edited
|
||||
const template: Array<(MenuItemConstructorOptions) | (MenuItem)> = [
|
||||
const template: Array<MenuItemConstructorOptions | MenuItem> = [
|
||||
{
|
||||
label: _t('Edit'),
|
||||
accelerator: 'e',
|
||||
label: _t("Edit"),
|
||||
accelerator: "e",
|
||||
submenu: [
|
||||
{
|
||||
role: 'undo',
|
||||
label: _t('Undo'),
|
||||
role: "undo",
|
||||
label: _t("Undo"),
|
||||
},
|
||||
{
|
||||
role: 'redo',
|
||||
label: _t('Redo'),
|
||||
role: "redo",
|
||||
label: _t("Redo"),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ type: "separator" },
|
||||
{
|
||||
role: 'cut',
|
||||
label: _t('Cut'),
|
||||
role: "cut",
|
||||
label: _t("Cut"),
|
||||
},
|
||||
{
|
||||
role: 'copy',
|
||||
label: _t('Copy'),
|
||||
role: "copy",
|
||||
label: _t("Copy"),
|
||||
},
|
||||
{
|
||||
role: 'paste',
|
||||
label: _t('Paste'),
|
||||
role: "paste",
|
||||
label: _t("Paste"),
|
||||
},
|
||||
{
|
||||
role: 'pasteAndMatchStyle',
|
||||
label: _t('Paste and Match Style'),
|
||||
role: "pasteAndMatchStyle",
|
||||
label: _t("Paste and Match Style"),
|
||||
},
|
||||
{
|
||||
role: 'delete',
|
||||
label: _t('Delete'),
|
||||
role: "delete",
|
||||
label: _t("Delete"),
|
||||
},
|
||||
{
|
||||
role: 'selectAll',
|
||||
label: _t('Select All'),
|
||||
role: "selectAll",
|
||||
label: _t("Select All"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: _t('View'),
|
||||
accelerator: 'V',
|
||||
label: _t("View"),
|
||||
accelerator: "V",
|
||||
submenu: [
|
||||
{ type: 'separator' },
|
||||
{ type: "separator" },
|
||||
{
|
||||
role: 'resetZoom',
|
||||
accelerator: 'CmdOrCtrl+Num0',
|
||||
role: "resetZoom",
|
||||
accelerator: "CmdOrCtrl+Num0",
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
role: 'zoomIn',
|
||||
accelerator: 'CmdOrCtrl+NumAdd',
|
||||
role: "zoomIn",
|
||||
accelerator: "CmdOrCtrl+NumAdd",
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
role: 'zoomOut',
|
||||
accelerator: 'CmdOrCtrl+NumSub',
|
||||
role: "zoomOut",
|
||||
accelerator: "CmdOrCtrl+NumSub",
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
role: 'resetZoom',
|
||||
label: _t('Actual Size'),
|
||||
role: "resetZoom",
|
||||
label: _t("Actual Size"),
|
||||
},
|
||||
{
|
||||
role: 'zoomIn',
|
||||
label: _t('Zoom In'),
|
||||
role: "zoomIn",
|
||||
label: _t("Zoom In"),
|
||||
},
|
||||
{
|
||||
role: 'zoomOut',
|
||||
label: _t('Zoom Out'),
|
||||
role: "zoomOut",
|
||||
label: _t("Zoom Out"),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ type: "separator" },
|
||||
// in macOS the Preferences menu item goes in the first menu
|
||||
...(!isMac ? [{
|
||||
label: _t('Preferences'),
|
||||
click(): void { global.mainWindow?.webContents.send('preferences'); },
|
||||
}] : []),
|
||||
...(!isMac
|
||||
? [
|
||||
{
|
||||
label: _t("Preferences"),
|
||||
click(): void {
|
||||
global.mainWindow?.webContents.send("preferences");
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
role: 'togglefullscreen',
|
||||
label: _t('Toggle Full Screen'),
|
||||
role: "togglefullscreen",
|
||||
label: _t("Toggle Full Screen"),
|
||||
},
|
||||
{
|
||||
role: 'toggleDevTools',
|
||||
label: _t('Toggle Developer Tools'),
|
||||
role: "toggleDevTools",
|
||||
label: _t("Toggle Developer Tools"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: _t('Window'),
|
||||
accelerator: 'w',
|
||||
role: 'window',
|
||||
label: _t("Window"),
|
||||
accelerator: "w",
|
||||
role: "window",
|
||||
submenu: [
|
||||
{
|
||||
role: 'minimize',
|
||||
label: _t('Minimize'),
|
||||
role: "minimize",
|
||||
label: _t("Minimize"),
|
||||
},
|
||||
{
|
||||
role: 'close',
|
||||
label: _t('Close'),
|
||||
role: "close",
|
||||
label: _t("Close"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: _t('Help'),
|
||||
accelerator: 'h',
|
||||
role: 'help',
|
||||
label: _t("Help"),
|
||||
accelerator: "h",
|
||||
role: "help",
|
||||
submenu: [
|
||||
{
|
||||
label: _t('Element Help'),
|
||||
click(): void { shell.openExternal('https://element.io/help'); },
|
||||
label: _t("Element Help"),
|
||||
click(): void {
|
||||
shell.openExternal("https://element.io/help");
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -142,92 +150,95 @@ export function buildMenuTemplate(): Menu {
|
||||
if (isMac) {
|
||||
template.unshift({
|
||||
// first macOS menu is the name of the app
|
||||
role: 'appMenu',
|
||||
role: "appMenu",
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{
|
||||
role: 'about',
|
||||
label: _t('About') + ' ' + app.name,
|
||||
role: "about",
|
||||
label: _t("About") + " " + app.name,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: _t('Preferences') + '…',
|
||||
accelerator: 'Command+,', // Mac-only accelerator
|
||||
click(): void { global.mainWindow?.webContents.send('preferences'); },
|
||||
label: _t("Preferences") + "…",
|
||||
accelerator: "Command+,", // Mac-only accelerator
|
||||
click(): void {
|
||||
global.mainWindow?.webContents.send("preferences");
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ type: "separator" },
|
||||
{
|
||||
role: 'services',
|
||||
label: _t('Services'),
|
||||
role: "services",
|
||||
label: _t("Services"),
|
||||
submenu: [],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ type: "separator" },
|
||||
{
|
||||
role: 'hide',
|
||||
label: _t('Hide'),
|
||||
role: "hide",
|
||||
label: _t("Hide"),
|
||||
},
|
||||
{
|
||||
role: 'hideOthers',
|
||||
label: _t('Hide Others'),
|
||||
role: "hideOthers",
|
||||
label: _t("Hide Others"),
|
||||
},
|
||||
{
|
||||
role: 'unhide',
|
||||
label: _t('Unhide'),
|
||||
role: "unhide",
|
||||
label: _t("Unhide"),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ type: "separator" },
|
||||
{
|
||||
role: 'quit',
|
||||
label: _t('Quit'),
|
||||
role: "quit",
|
||||
label: _t("Quit"),
|
||||
},
|
||||
],
|
||||
});
|
||||
// Edit menu.
|
||||
// This has a 'speech' section on macOS
|
||||
(template[1].submenu as MenuItemConstructorOptions[]).push(
|
||||
{ type: 'separator' },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: _t('Speech'),
|
||||
label: _t("Speech"),
|
||||
submenu: [
|
||||
{
|
||||
role: 'startSpeaking',
|
||||
label: _t('Start Speaking'),
|
||||
role: "startSpeaking",
|
||||
label: _t("Start Speaking"),
|
||||
},
|
||||
{
|
||||
role: 'stopSpeaking',
|
||||
label: _t('Stop Speaking'),
|
||||
role: "stopSpeaking",
|
||||
label: _t("Stop Speaking"),
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Window menu.
|
||||
// This also has specific functionality on macOS
|
||||
template[3].submenu = [
|
||||
{
|
||||
label: _t('Close'),
|
||||
accelerator: 'CmdOrCtrl+W',
|
||||
role: 'close',
|
||||
label: _t("Close"),
|
||||
accelerator: "CmdOrCtrl+W",
|
||||
role: "close",
|
||||
},
|
||||
{
|
||||
label: _t('Minimize'),
|
||||
accelerator: 'CmdOrCtrl+M',
|
||||
role: 'minimize',
|
||||
label: _t("Minimize"),
|
||||
accelerator: "CmdOrCtrl+M",
|
||||
role: "minimize",
|
||||
},
|
||||
{
|
||||
label: _t('Zoom'),
|
||||
role: 'zoom',
|
||||
label: _t("Zoom"),
|
||||
role: "zoom",
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: _t('Bring All to Front'),
|
||||
role: 'front',
|
||||
label: _t("Bring All to Front"),
|
||||
role: "front",
|
||||
},
|
||||
];
|
||||
} else {
|
||||
template.unshift({
|
||||
label: _t('File'),
|
||||
accelerator: 'f',
|
||||
label: _t("File"),
|
||||
accelerator: "f",
|
||||
submenu: [
|
||||
// For some reason, 'about' does not seem to work on windows.
|
||||
/*{
|
||||
@@ -235,8 +246,8 @@ export function buildMenuTemplate(): Menu {
|
||||
label: _t('About'),
|
||||
},*/
|
||||
{
|
||||
role: 'quit',
|
||||
label: _t('Quit'),
|
||||
role: "quit",
|
||||
label: _t("Quit"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
+133
-119
@@ -28,22 +28,18 @@ import {
|
||||
DownloadItem,
|
||||
MenuItemConstructorOptions,
|
||||
IpcMainEvent,
|
||||
} from 'electron';
|
||||
import url from 'url';
|
||||
import fs from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
import { pipeline } from 'stream';
|
||||
import path from 'path';
|
||||
} from "electron";
|
||||
import url from "url";
|
||||
import fs from "fs";
|
||||
import fetch from "node-fetch";
|
||||
import { pipeline } from "stream";
|
||||
import path from "path";
|
||||
|
||||
import { _t } from './language-helper';
|
||||
import { _t } from "./language-helper";
|
||||
|
||||
const MAILTO_PREFIX = "mailto:";
|
||||
|
||||
const PERMITTED_URL_SCHEMES: string[] = [
|
||||
'http:',
|
||||
'https:',
|
||||
MAILTO_PREFIX,
|
||||
];
|
||||
const PERMITTED_URL_SCHEMES: string[] = ["http:", "https:", MAILTO_PREFIX];
|
||||
|
||||
function safeOpenURL(target: string): void {
|
||||
// openExternal passes the target to open/start/xdg-open,
|
||||
@@ -70,7 +66,7 @@ function onWindowOrNavigate(ev: Event, target: string): void {
|
||||
}
|
||||
|
||||
function writeNativeImage(filePath: string, img: NativeImage): Promise<void> {
|
||||
switch (filePath.split('.').pop()?.toLowerCase()) {
|
||||
switch (filePath.split(".").pop()?.toLowerCase()) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
return fs.promises.writeFile(filePath, img.toJPEG(100));
|
||||
@@ -85,7 +81,7 @@ function writeNativeImage(filePath: string, img: NativeImage): Promise<void> {
|
||||
function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: WebContents): void {
|
||||
let url = params.linkURL || params.srcURL;
|
||||
|
||||
if (url.startsWith('vector://vector/webapp')) {
|
||||
if (url.startsWith("vector://vector/webapp")) {
|
||||
// Avoid showing a context menu for app icons
|
||||
if (params.hasImageContents) return;
|
||||
// Rewrite URL so that it can be used outside of the app
|
||||
@@ -94,82 +90,90 @@ function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: We
|
||||
|
||||
const popupMenu = new Menu();
|
||||
// No point trying to open blob: URLs in an external browser: it ain't gonna work.
|
||||
if (!url.startsWith('blob:')) {
|
||||
popupMenu.append(new MenuItem({
|
||||
label: url,
|
||||
click(): void {
|
||||
safeOpenURL(url);
|
||||
},
|
||||
}));
|
||||
if (!url.startsWith("blob:")) {
|
||||
popupMenu.append(
|
||||
new MenuItem({
|
||||
label: url,
|
||||
click(): void {
|
||||
safeOpenURL(url);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (params.hasImageContents) {
|
||||
popupMenu.append(new MenuItem({
|
||||
label: _t('Copy image'),
|
||||
accelerator: 'c',
|
||||
click(): void {
|
||||
webContents.copyImageAt(params.x, params.y);
|
||||
},
|
||||
}));
|
||||
popupMenu.append(
|
||||
new MenuItem({
|
||||
label: _t("Copy image"),
|
||||
accelerator: "c",
|
||||
click(): void {
|
||||
webContents.copyImageAt(params.x, params.y);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// No point offering to copy a blob: URL either
|
||||
if (!url.startsWith('blob:')) {
|
||||
if (!url.startsWith("blob:")) {
|
||||
// Special-case e-mail URLs to strip the `mailto:` like modern browsers do
|
||||
if (url.startsWith(MAILTO_PREFIX)) {
|
||||
popupMenu.append(new MenuItem({
|
||||
label: _t('Copy email address'),
|
||||
accelerator: 'a',
|
||||
click(): void {
|
||||
clipboard.writeText(url.substr(MAILTO_PREFIX.length));
|
||||
},
|
||||
}));
|
||||
popupMenu.append(
|
||||
new MenuItem({
|
||||
label: _t("Copy email address"),
|
||||
accelerator: "a",
|
||||
click(): void {
|
||||
clipboard.writeText(url.substr(MAILTO_PREFIX.length));
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
popupMenu.append(new MenuItem({
|
||||
label: params.hasImageContents
|
||||
? _t('Copy image address')
|
||||
: _t('Copy link address'),
|
||||
accelerator: 'a',
|
||||
click(): void {
|
||||
clipboard.writeText(url);
|
||||
},
|
||||
}));
|
||||
popupMenu.append(
|
||||
new MenuItem({
|
||||
label: params.hasImageContents ? _t("Copy image address") : _t("Copy link address"),
|
||||
accelerator: "a",
|
||||
click(): void {
|
||||
clipboard.writeText(url);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: We cannot easily save a blob from the main process as
|
||||
// only the renderer can resolve them so don't give the user an option to.
|
||||
if (params.hasImageContents && !url.startsWith('blob:')) {
|
||||
popupMenu.append(new MenuItem({
|
||||
label: _t('Save image as...'),
|
||||
accelerator: 's',
|
||||
async click(): Promise<void> {
|
||||
const targetFileName = params.suggestedFilename || params.altText || "image.png";
|
||||
const { filePath } = await dialog.showSaveDialog({
|
||||
defaultPath: targetFileName,
|
||||
});
|
||||
|
||||
if (!filePath) return; // user cancelled dialog
|
||||
|
||||
try {
|
||||
if (url.startsWith("data:")) {
|
||||
await writeNativeImage(filePath, nativeImage.createFromDataURL(url));
|
||||
} else {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error(`unexpected response ${resp.statusText}`);
|
||||
if (!resp.body) throw new Error(`unexpected response has no body ${resp.statusText}`);
|
||||
pipeline(resp.body, fs.createWriteStream(filePath));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
dialog.showMessageBox({
|
||||
type: "error",
|
||||
title: _t("Failed to save image"),
|
||||
message: _t("The image failed to save"),
|
||||
if (params.hasImageContents && !url.startsWith("blob:")) {
|
||||
popupMenu.append(
|
||||
new MenuItem({
|
||||
label: _t("Save image as..."),
|
||||
accelerator: "s",
|
||||
async click(): Promise<void> {
|
||||
const targetFileName = params.suggestedFilename || params.altText || "image.png";
|
||||
const { filePath } = await dialog.showSaveDialog({
|
||||
defaultPath: targetFileName,
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
if (!filePath) return; // user cancelled dialog
|
||||
|
||||
try {
|
||||
if (url.startsWith("data:")) {
|
||||
await writeNativeImage(filePath, nativeImage.createFromDataURL(url));
|
||||
} else {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error(`unexpected response ${resp.statusText}`);
|
||||
if (!resp.body) throw new Error(`unexpected response has no body ${resp.statusText}`);
|
||||
pipeline(resp.body, fs.createWriteStream(filePath));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
dialog.showMessageBox({
|
||||
type: "error",
|
||||
title: _t("Failed to save image"),
|
||||
message: _t("The image failed to save"),
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// popup() requires an options object even for no options
|
||||
@@ -181,7 +185,7 @@ function cutCopyPasteSelectContextMenus(params: ContextMenuParams): MenuItemCons
|
||||
const options: MenuItemConstructorOptions[] = [];
|
||||
|
||||
if (params.misspelledWord) {
|
||||
params.dictionarySuggestions.forEach(word => {
|
||||
params.dictionarySuggestions.forEach((word) => {
|
||||
options.push({
|
||||
label: word,
|
||||
click: (menuItem, browserWindow) => {
|
||||
@@ -189,42 +193,52 @@ function cutCopyPasteSelectContextMenus(params: ContextMenuParams): MenuItemCons
|
||||
},
|
||||
});
|
||||
});
|
||||
options.push({
|
||||
type: 'separator',
|
||||
}, {
|
||||
label: _t('Add to dictionary'),
|
||||
click: (menuItem, browserWindow) => {
|
||||
browserWindow?.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord);
|
||||
options.push(
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
}, {
|
||||
type: 'separator',
|
||||
});
|
||||
{
|
||||
label: _t("Add to dictionary"),
|
||||
click: (menuItem, browserWindow) => {
|
||||
browserWindow?.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
options.push({
|
||||
role: 'cut',
|
||||
label: _t('Cut'),
|
||||
accelerator: 't',
|
||||
enabled: params.editFlags.canCut,
|
||||
}, {
|
||||
role: 'copy',
|
||||
label: _t('Copy'),
|
||||
accelerator: 'c',
|
||||
enabled: params.editFlags.canCopy,
|
||||
}, {
|
||||
role: 'paste',
|
||||
label: _t('Paste'),
|
||||
accelerator: 'p',
|
||||
enabled: params.editFlags.canPaste,
|
||||
}, {
|
||||
role: 'pasteAndMatchStyle',
|
||||
enabled: params.editFlags.canPaste,
|
||||
}, {
|
||||
role: 'selectAll',
|
||||
label: _t("Select All"),
|
||||
accelerator: 'a',
|
||||
enabled: params.editFlags.canSelectAll,
|
||||
});
|
||||
options.push(
|
||||
{
|
||||
role: "cut",
|
||||
label: _t("Cut"),
|
||||
accelerator: "t",
|
||||
enabled: params.editFlags.canCut,
|
||||
},
|
||||
{
|
||||
role: "copy",
|
||||
label: _t("Copy"),
|
||||
accelerator: "c",
|
||||
enabled: params.editFlags.canCopy,
|
||||
},
|
||||
{
|
||||
role: "paste",
|
||||
label: _t("Paste"),
|
||||
accelerator: "p",
|
||||
enabled: params.editFlags.canPaste,
|
||||
},
|
||||
{
|
||||
role: "pasteAndMatchStyle",
|
||||
enabled: params.editFlags.canPaste,
|
||||
},
|
||||
{
|
||||
role: "selectAll",
|
||||
label: _t("Select All"),
|
||||
accelerator: "a",
|
||||
enabled: params.editFlags.canSelectAll,
|
||||
},
|
||||
);
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -239,9 +253,9 @@ function onSelectedContextMenu(ev: Event, params: ContextMenuParams): void {
|
||||
|
||||
function onEditableContextMenu(ev: Event, params: ContextMenuParams): void {
|
||||
const items: MenuItemConstructorOptions[] = [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo', enabled: params.editFlags.canRedo },
|
||||
{ type: 'separator' },
|
||||
{ role: "undo" },
|
||||
{ role: "redo", enabled: params.editFlags.canRedo },
|
||||
{ type: "separator" },
|
||||
...cutCopyPasteSelectContextMenus(params),
|
||||
];
|
||||
|
||||
@@ -254,7 +268,7 @@ function onEditableContextMenu(ev: Event, params: ContextMenuParams): void {
|
||||
|
||||
let userDownloadIndex = 0;
|
||||
const userDownloadMap = new Map<number, string>(); // Map from id to path
|
||||
ipcMain.on('userDownloadAction', function(ev: IpcMainEvent, { id, open = false }) {
|
||||
ipcMain.on("userDownloadAction", function (ev: IpcMainEvent, { id, open = false }) {
|
||||
const path = userDownloadMap.get(id);
|
||||
if (open && path) {
|
||||
shell.openPath(path);
|
||||
@@ -268,12 +282,12 @@ export default (webContents: WebContents): void => {
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
webContents.on('will-navigate', (ev: Event, target: string): void => {
|
||||
webContents.on("will-navigate", (ev: Event, target: string): void => {
|
||||
if (target.startsWith("vector://")) return;
|
||||
return onWindowOrNavigate(ev, target);
|
||||
});
|
||||
|
||||
webContents.on('context-menu', function(ev: Event, params: ContextMenuParams): void {
|
||||
webContents.on("context-menu", function (ev: Event, params: ContextMenuParams): void {
|
||||
if (params.linkURL || params.srcURL) {
|
||||
onLinkContextMenu(ev, params, webContents);
|
||||
} else if (params.selectionText) {
|
||||
@@ -283,13 +297,13 @@ export default (webContents: WebContents): void => {
|
||||
}
|
||||
});
|
||||
|
||||
webContents.session.on('will-download', (event: Event, item: DownloadItem): void => {
|
||||
item.once('done', (event, state) => {
|
||||
if (state === 'completed') {
|
||||
webContents.session.on("will-download", (event: Event, item: DownloadItem): void => {
|
||||
item.once("done", (event, state) => {
|
||||
if (state === "completed") {
|
||||
const savePath = item.getSavePath();
|
||||
const id = userDownloadIndex++;
|
||||
userDownloadMap.set(id, savePath);
|
||||
webContents.send('userDownloadCompleted', {
|
||||
webContents.send("userDownloadCompleted", {
|
||||
id,
|
||||
name: path.basename(savePath),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user