Merge pull request #219 from vector-im/t3chguy/ts/d
This commit is contained in:
26
src/@types/global.d.ts
vendored
Normal file
26
src/@types/global.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
Copyright 2021 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 { BrowserWindow } from "electron";
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
mainWindow: BrowserWindow;
|
||||
appQuitting: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,32 +20,29 @@ 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.
|
||||
const checkSquirrelHooks = require('./squirrelhooks');
|
||||
if (checkSquirrelHooks()) return;
|
||||
import { checkSquirrelHooks } from "./squirrelhooks";
|
||||
if (checkSquirrelHooks()) process.exit(1);
|
||||
|
||||
const argv = require('minimist')(process.argv, {
|
||||
alias: { help: "h" },
|
||||
});
|
||||
|
||||
const {
|
||||
app, ipcMain, powerSaveBlocker, BrowserWindow, Menu, autoUpdater, protocol, dialog,
|
||||
} = require('electron');
|
||||
const AutoLaunch = require('auto-launch');
|
||||
const path = require('path');
|
||||
import { app, ipcMain, powerSaveBlocker, 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 fs, { promises as afs } from "fs";
|
||||
import crypto from "crypto";
|
||||
import { URL } from "url";
|
||||
|
||||
const tray = require('./tray');
|
||||
const buildMenuTemplate = require('./vectormenu');
|
||||
const webContentsHandler = require('./webcontents-handler');
|
||||
const updater = require('./updater');
|
||||
const { getProfileFromDeeplink, protocolInit, recordSSOSession } = require('./protocol');
|
||||
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 { _t, AppLocalization } from './language-helper';
|
||||
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
const Store = require('electron-store');
|
||||
|
||||
const fs = require('fs');
|
||||
const afs = fs.promises;
|
||||
|
||||
const crypto = require('crypto');
|
||||
let keytar;
|
||||
try {
|
||||
keytar = require('keytar');
|
||||
@@ -57,8 +54,6 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
const { _t, AppLocalization } = require('./language-helper');
|
||||
|
||||
let seshatSupported = false;
|
||||
let Seshat;
|
||||
let SeshatRecovery;
|
||||
@@ -259,7 +254,13 @@ async function moveAutoLauncher() {
|
||||
}
|
||||
|
||||
const eventStorePath = path.join(app.getPath('userData'), 'EventStore');
|
||||
const store = new Store({ name: "electron-config" });
|
||||
const store = new Store<{
|
||||
warnBeforeExit?: boolean;
|
||||
minimizeToTray?: boolean;
|
||||
spellCheckerEnabled?: boolean;
|
||||
autoHideMenuBar?: boolean;
|
||||
locale?: string | string[];
|
||||
}>({ name: "electron-config" });
|
||||
|
||||
let eventIndex = null;
|
||||
|
||||
@@ -1016,7 +1017,7 @@ function beforeQuit() {
|
||||
}
|
||||
|
||||
app.on('before-quit', beforeQuit);
|
||||
app.on('before-quit-for-update', beforeQuit);
|
||||
autoUpdater.on('before-quit-for-update', beforeQuit);
|
||||
|
||||
app.on('second-instance', (ev, commandLine, workingDirectory) => {
|
||||
// If other instance launched with --hidden then skip showing window
|
||||
@@ -14,15 +14,23 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const counterpart = require('counterpart');
|
||||
import counterpart from "counterpart";
|
||||
import type Store from 'electron-store';
|
||||
|
||||
const DEFAULT_LOCALE = "en";
|
||||
|
||||
function _td(text) {
|
||||
export function _td(text: string): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
function _t(text, variables = {}) {
|
||||
type SubstitutionValue = number | string;
|
||||
|
||||
interface IVariables {
|
||||
[key: string]: SubstitutionValue;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export function _t(text: string, variables: IVariables = {}): string {
|
||||
const args = Object.assign({ interpolate: false }, variables);
|
||||
|
||||
const { count } = args;
|
||||
@@ -55,11 +63,17 @@ function _t(text, variables = {}) {
|
||||
return translated;
|
||||
}
|
||||
|
||||
class AppLocalization {
|
||||
constructor({ store, components = [] }) {
|
||||
// TODO: Should be static field, but that doesn't parse without Babel
|
||||
this.STORE_KEY = "locale";
|
||||
type Component = () => void;
|
||||
|
||||
type TypedStore = Store<{ locale?: string | string[] }>;
|
||||
|
||||
export class AppLocalization {
|
||||
private static readonly STORE_KEY = "locale";
|
||||
|
||||
private readonly store: TypedStore;
|
||||
private readonly localizedComponents: Set<Component>;
|
||||
|
||||
constructor({ store, components = [] }: { store: TypedStore, components: Component[] }) {
|
||||
counterpart.registerTranslations("en", this.fetchTranslationJson("en_EN"));
|
||||
counterpart.setFallbackLocale('en');
|
||||
counterpart.setSeparator('|');
|
||||
@@ -69,15 +83,15 @@ class AppLocalization {
|
||||
}
|
||||
|
||||
this.store = store;
|
||||
if (this.store.has(this.STORE_KEY)) {
|
||||
const locales = this.store.get(this.STORE_KEY);
|
||||
if (this.store.has(AppLocalization.STORE_KEY)) {
|
||||
const locales = this.store.get(AppLocalization.STORE_KEY);
|
||||
this.setAppLocale(locales);
|
||||
}
|
||||
|
||||
this.resetLocalizedUI();
|
||||
}
|
||||
|
||||
fetchTranslationJson(locale) {
|
||||
public fetchTranslationJson(locale: string): Record<string, string> {
|
||||
try {
|
||||
console.log("Fetching translation json for locale: " + locale);
|
||||
return require(`./i18n/strings/${locale}.json`);
|
||||
@@ -87,11 +101,7 @@ class AppLocalization {
|
||||
}
|
||||
}
|
||||
|
||||
get languageTranslationJson() {
|
||||
return this.translationJsonMap.get(this.language);
|
||||
}
|
||||
|
||||
setAppLocale(locales) {
|
||||
public setAppLocale(locales: string | string[]): void {
|
||||
console.log(`Changing application language to ${locales}`);
|
||||
|
||||
if (!Array.isArray(locales)) {
|
||||
@@ -105,13 +115,14 @@ class AppLocalization {
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore - this looks like a bug but is out of scope for this conversion
|
||||
counterpart.setLocale(locales);
|
||||
this.store.set(this.STORE_KEY, locales);
|
||||
this.store.set(AppLocalization.STORE_KEY, locales);
|
||||
|
||||
this.resetLocalizedUI();
|
||||
}
|
||||
|
||||
resetLocalizedUI() {
|
||||
public resetLocalizedUI(): void {
|
||||
console.log("Resetting the UI components after locale change");
|
||||
this.localizedComponents.forEach(componentSetup => {
|
||||
if (typeof componentSetup === "function") {
|
||||
@@ -120,9 +131,3 @@ class AppLocalization {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AppLocalization,
|
||||
_t,
|
||||
_td,
|
||||
};
|
||||
102
src/protocol.js
102
src/protocol.js
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
const { app } = require("electron");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const PROTOCOL = "element://";
|
||||
const SEARCH_PARAM = "element-desktop-ssoid";
|
||||
const STORE_FILE_NAME = "sso-sessions.json";
|
||||
|
||||
// we getPath userData before electron-main changes it, so this is the default value
|
||||
const storePath = path.join(app.getPath("userData"), STORE_FILE_NAME);
|
||||
|
||||
const processUrl = (url) => {
|
||||
if (!global.mainWindow) return;
|
||||
console.log("Handling link: ", url);
|
||||
global.mainWindow.loadURL(url.replace(PROTOCOL, "vector://"));
|
||||
};
|
||||
|
||||
const readStore = () => {
|
||||
try {
|
||||
const s = fs.readFileSync(storePath, { encoding: "utf8" });
|
||||
const o = JSON.parse(s);
|
||||
return typeof o === "object" ? o : {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const writeStore = (data) => {
|
||||
fs.writeFileSync(storePath, JSON.stringify(data));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
recordSSOSession: (sessionID) => {
|
||||
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
|
||||
if (store[key] === userDataPath) {
|
||||
delete store[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
store[sessionID] = userDataPath;
|
||||
writeStore(store);
|
||||
},
|
||||
getProfileFromDeeplink: (args) => {
|
||||
// check if we are passed a profile in the SSO callback url
|
||||
const deeplinkUrl = args.find(arg => arg.startsWith('element://'));
|
||||
if (deeplinkUrl && deeplinkUrl.includes(SEARCH_PARAM)) {
|
||||
const parsedUrl = new URL(deeplinkUrl);
|
||||
if (parsedUrl.protocol === 'element:') {
|
||||
const ssoID = parsedUrl.searchParams.get(SEARCH_PARAM);
|
||||
const store = readStore();
|
||||
console.log("Forwarding to profile: ", store[ssoID]);
|
||||
return store[ssoID];
|
||||
}
|
||||
}
|
||||
},
|
||||
protocolInit: () => {
|
||||
// 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");
|
||||
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
|
||||
// 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]);
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
// Protocol handler for macos
|
||||
app.on('open-url', function(ev, url) {
|
||||
ev.preventDefault();
|
||||
processUrl(url);
|
||||
});
|
||||
} else {
|
||||
// Protocol handler for win32/Linux
|
||||
app.on('second-instance', (ev, commandLine) => {
|
||||
const url = commandLine[commandLine.length - 1];
|
||||
if (!url.startsWith(PROTOCOL)) return;
|
||||
processUrl(url);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
103
src/protocol.ts
Normal file
103
src/protocol.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 } from "electron";
|
||||
import { URL } from "url";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
const PROTOCOL = "element://";
|
||||
const SEARCH_PARAM = "element-desktop-ssoid";
|
||||
const STORE_FILE_NAME = "sso-sessions.json";
|
||||
|
||||
// we getPath userData before electron-main changes it, so this is the default value
|
||||
const storePath = path.join(app.getPath("userData"), STORE_FILE_NAME);
|
||||
|
||||
function processUrl(url: string): void {
|
||||
if (!global.mainWindow) return;
|
||||
console.log("Handling link: ", url);
|
||||
global.mainWindow.loadURL(url.replace(PROTOCOL, "vector://"));
|
||||
}
|
||||
|
||||
function readStore(): object {
|
||||
try {
|
||||
const s = fs.readFileSync(storePath, { encoding: "utf8" });
|
||||
const o = JSON.parse(s);
|
||||
return typeof o === "object" ? o : {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(data: object): void {
|
||||
fs.writeFileSync(storePath, JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function recordSSOSession(sessionID: string): void {
|
||||
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
|
||||
if (store[key] === userDataPath) {
|
||||
delete store[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
store[sessionID] = userDataPath;
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
export function getProfileFromDeeplink(args): string | undefined {
|
||||
// check if we are passed a profile in the SSO callback url
|
||||
const deeplinkUrl = args.find(arg => arg.startsWith('element://'));
|
||||
if (deeplinkUrl && deeplinkUrl.includes(SEARCH_PARAM)) {
|
||||
const parsedUrl = new URL(deeplinkUrl);
|
||||
if (parsedUrl.protocol === 'element:') {
|
||||
const ssoID = parsedUrl.searchParams.get(SEARCH_PARAM);
|
||||
const store = readStore();
|
||||
console.log("Forwarding to profile: ", store[ssoID]);
|
||||
return store[ssoID];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
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
|
||||
// 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]);
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
// Protocol handler for macos
|
||||
app.on('open-url', function(ev, url) {
|
||||
ev.preventDefault();
|
||||
processUrl(url);
|
||||
});
|
||||
} else {
|
||||
// Protocol handler for win32/Linux
|
||||
app.on('second-instance', (ev, commandLine) => {
|
||||
const url = commandLine[commandLine.length - 1];
|
||||
if (!url.startsWith(PROTOCOL)) return;
|
||||
processUrl(url);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const spawn = require('child_process').spawn;
|
||||
const { app } = require('electron');
|
||||
const fsProm = require('fs').promises;
|
||||
import path from "path";
|
||||
import { spawn } from "child_process";
|
||||
import { app } from "electron";
|
||||
import { promises as fsProm } from "fs";
|
||||
|
||||
function runUpdateExe(args) {
|
||||
function runUpdateExe(args: string[]): Promise<void> {
|
||||
// Invokes Squirrel's Update.exe which will do things for us like create shortcuts
|
||||
// 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
|
||||
@@ -33,7 +33,7 @@ function runUpdateExe(args) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkSquirrelHooks() {
|
||||
export function checkSquirrelHooks(): boolean {
|
||||
if (process.platform !== 'win32') return false;
|
||||
|
||||
const cmd = process.argv[1];
|
||||
@@ -82,5 +82,3 @@ function checkSquirrelHooks() {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = checkSquirrelHooks;
|
||||
@@ -15,26 +15,26 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const { app, Tray, Menu, nativeImage } = require('electron');
|
||||
const pngToIco = require('png-to-ico');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { _t } = require('./language-helper');
|
||||
import { app, Tray, Menu, nativeImage } from "electron";
|
||||
import pngToIco from "png-to-ico";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { _t } from "./language-helper";
|
||||
|
||||
let trayIcon = null;
|
||||
let trayIcon: Tray = null;
|
||||
|
||||
exports.hasTray = function hasTray() {
|
||||
export function hasTray(): boolean {
|
||||
return (trayIcon !== null);
|
||||
};
|
||||
}
|
||||
|
||||
exports.destroy = function() {
|
||||
export function destroy(): void {
|
||||
if (trayIcon) {
|
||||
trayIcon.destroy();
|
||||
trayIcon = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const toggleWin = function() {
|
||||
function toggleWin(): void {
|
||||
if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized()) {
|
||||
global.mainWindow.hide();
|
||||
} else {
|
||||
@@ -42,9 +42,14 @@ const toggleWin = function() {
|
||||
if (!global.mainWindow.isVisible()) global.mainWindow.show();
|
||||
global.mainWindow.focus();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
exports.create = function(config) {
|
||||
interface IConfig {
|
||||
icon_path: string;
|
||||
brand: string;
|
||||
}
|
||||
|
||||
export function create(config: IConfig): void {
|
||||
// no trays on darwin
|
||||
if (process.platform === 'darwin' || trayIcon) return;
|
||||
const defaultIcon = nativeImage.createFromPath(config.icon_path);
|
||||
@@ -89,9 +94,9 @@ exports.create = function(config) {
|
||||
global.mainWindow.webContents.on('page-title-updated', function(ev, title) {
|
||||
trayIcon.setToolTip(title);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function initApplicationMenu() {
|
||||
export function initApplicationMenu(): void {
|
||||
if (!trayIcon) {
|
||||
return;
|
||||
}
|
||||
@@ -112,5 +117,3 @@ function initApplicationMenu() {
|
||||
|
||||
trayIcon.setContextMenu(contextMenu);
|
||||
}
|
||||
|
||||
exports.initApplicationMenu = initApplicationMenu;
|
||||
@@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const { app, autoUpdater, ipcMain } = require('electron');
|
||||
import { app, autoUpdater, ipcMain } from "electron";
|
||||
|
||||
const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000;
|
||||
const INITIAL_UPDATE_DELAY_MS = 30 * 1000;
|
||||
|
||||
function installUpdate() {
|
||||
function installUpdate(): void {
|
||||
// for some reason, quitAndInstall does not fire the
|
||||
// before-quit event, so we need to set the flag here.
|
||||
global.appQuitting = true;
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
|
||||
function pollForUpdates() {
|
||||
function pollForUpdates(): void {
|
||||
try {
|
||||
autoUpdater.checkForUpdates();
|
||||
} catch (e) {
|
||||
@@ -34,8 +34,7 @@ function pollForUpdates() {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {};
|
||||
module.exports.start = function startAutoUpdate(updateBaseUrl) {
|
||||
export function start(updateBaseUrl: string): void {
|
||||
if (updateBaseUrl.slice(-1) !== '/') {
|
||||
updateBaseUrl = updateBaseUrl + '/';
|
||||
}
|
||||
@@ -80,18 +79,25 @@ module.exports.start = function startAutoUpdate(updateBaseUrl) {
|
||||
// will fail if running in debug mode
|
||||
console.log('Couldn\'t enable update checking', err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ipcMain.on('install_update', installUpdate);
|
||||
ipcMain.on('check_updates', pollForUpdates);
|
||||
|
||||
function ipcChannelSendUpdateStatus(status) {
|
||||
function ipcChannelSendUpdateStatus(status: boolean | string): void {
|
||||
if (!global.mainWindow) return;
|
||||
global.mainWindow.webContents.send('check_updates', status);
|
||||
}
|
||||
|
||||
interface ICachedUpdate {
|
||||
releaseNotes: string;
|
||||
releaseName: string;
|
||||
releaseDate: Date;
|
||||
updateURL : string;
|
||||
}
|
||||
|
||||
// cache the latest update which has been downloaded as electron offers no api to read it
|
||||
let latestUpdateDownloaded;
|
||||
let latestUpdateDownloaded: ICachedUpdate;
|
||||
autoUpdater.on('update-available', function() {
|
||||
ipcChannelSendUpdateStatus(true);
|
||||
}).on('update-not-available', function() {
|
||||
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const { app, shell, Menu } = require('electron');
|
||||
const { _t } = require('./language-helper');
|
||||
import { app, shell, Menu, MenuItem, MenuItemConstructorOptions } from 'electron';
|
||||
import { _t } from './language-helper';
|
||||
|
||||
const isMac = process.platform === 'darwin';
|
||||
|
||||
function buildMenuTemplate() {
|
||||
export function buildMenuTemplate(): Menu {
|
||||
// Menu template from http://electron.atom.io/docs/api/menu/, edited
|
||||
const template = [
|
||||
const template: Array<(MenuItemConstructorOptions) | (MenuItem)> = [
|
||||
{
|
||||
label: _t('Edit'),
|
||||
accelerator: 'e',
|
||||
@@ -48,7 +48,7 @@ function buildMenuTemplate() {
|
||||
label: _t('Paste'),
|
||||
},
|
||||
{
|
||||
role: 'pasteandmatchstyle',
|
||||
role: 'pasteAndMatchStyle',
|
||||
label: _t('Paste and Match Style'),
|
||||
},
|
||||
{
|
||||
@@ -56,7 +56,7 @@ function buildMenuTemplate() {
|
||||
label: _t('Delete'),
|
||||
},
|
||||
{
|
||||
role: 'selectall',
|
||||
role: 'selectAll',
|
||||
label: _t('Select All'),
|
||||
},
|
||||
],
|
||||
@@ -67,30 +67,30 @@ function buildMenuTemplate() {
|
||||
submenu: [
|
||||
{ type: 'separator' },
|
||||
{
|
||||
role: 'resetzoom',
|
||||
role: 'resetZoom',
|
||||
accelerator: 'CmdOrCtrl+Num0',
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
role: 'zoomin',
|
||||
role: 'zoomIn',
|
||||
accelerator: 'CmdOrCtrl+NumAdd',
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
role: 'zoomout',
|
||||
role: 'zoomOut',
|
||||
accelerator: 'CmdOrCtrl+NumSub',
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
role: 'resetzoom',
|
||||
role: 'resetZoom',
|
||||
label: _t('Actual Size'),
|
||||
},
|
||||
{
|
||||
role: 'zoomin',
|
||||
role: 'zoomIn',
|
||||
label: _t('Zoom In'),
|
||||
},
|
||||
{
|
||||
role: 'zoomout',
|
||||
role: 'zoomOut',
|
||||
label: _t('Zoom Out'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
@@ -104,7 +104,7 @@ function buildMenuTemplate() {
|
||||
label: _t('Toggle Full Screen'),
|
||||
},
|
||||
{
|
||||
role: 'toggledevtools',
|
||||
role: 'toggleDevTools',
|
||||
label: _t('Toggle Developer Tools'),
|
||||
},
|
||||
],
|
||||
@@ -166,7 +166,7 @@ function buildMenuTemplate() {
|
||||
label: _t('Hide'),
|
||||
},
|
||||
{
|
||||
role: 'hideothers',
|
||||
role: 'hideOthers',
|
||||
label: _t('Hide Others'),
|
||||
},
|
||||
{
|
||||
@@ -182,17 +182,17 @@ function buildMenuTemplate() {
|
||||
});
|
||||
// Edit menu.
|
||||
// This has a 'speech' section on macOS
|
||||
template[1].submenu.push(
|
||||
(template[1].submenu as MenuItemConstructorOptions[]).push(
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: _t('Speech'),
|
||||
submenu: [
|
||||
{
|
||||
role: 'startspeaking',
|
||||
role: 'startSpeaking',
|
||||
label: _t('Start Speaking'),
|
||||
},
|
||||
{
|
||||
role: 'stopspeaking',
|
||||
role: 'stopSpeaking',
|
||||
label: _t('Stop Speaking'),
|
||||
},
|
||||
],
|
||||
@@ -243,6 +243,3 @@ function buildMenuTemplate() {
|
||||
|
||||
return Menu.buildFromTemplate(template);
|
||||
}
|
||||
|
||||
module.exports = buildMenuTemplate;
|
||||
|
||||
@@ -1,19 +1,49 @@
|
||||
const { clipboard, nativeImage, Menu, MenuItem, shell, dialog, ipcMain } = require('electron');
|
||||
const url = require('url');
|
||||
const fs = require('fs');
|
||||
const request = require('request');
|
||||
const path = require('path');
|
||||
const { _t } = require('./language-helper');
|
||||
/*
|
||||
Copyright 2021 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 {
|
||||
clipboard,
|
||||
nativeImage,
|
||||
Menu,
|
||||
MenuItem,
|
||||
shell,
|
||||
dialog,
|
||||
ipcMain,
|
||||
NativeImage,
|
||||
WebContents,
|
||||
ContextMenuParams,
|
||||
DownloadItem,
|
||||
MenuItemConstructorOptions,
|
||||
IpcMainEvent,
|
||||
} from 'electron';
|
||||
import url from 'url';
|
||||
import fs from 'fs';
|
||||
import request from 'request';
|
||||
import path from 'path';
|
||||
import { _t } from './language-helper';
|
||||
|
||||
const MAILTO_PREFIX = "mailto:";
|
||||
|
||||
const PERMITTED_URL_SCHEMES = [
|
||||
const PERMITTED_URL_SCHEMES: string[] = [
|
||||
'http:',
|
||||
'https:',
|
||||
MAILTO_PREFIX,
|
||||
];
|
||||
|
||||
function safeOpenURL(target) {
|
||||
function safeOpenURL(target: string): void {
|
||||
// openExternal passes the target to open/start/xdg-open,
|
||||
// so put fairly stringent limits on what can be opened
|
||||
// (for instance, open /bin/sh does indeed open a terminal
|
||||
@@ -28,7 +58,7 @@ function safeOpenURL(target) {
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowOrNavigate(ev, target) {
|
||||
function onWindowOrNavigate(ev: Event, target: string): void {
|
||||
// always prevent the default: if something goes wrong,
|
||||
// we don't want to end up opening it in the electron
|
||||
// app, as we could end up opening any sort of random
|
||||
@@ -37,7 +67,7 @@ function onWindowOrNavigate(ev, target) {
|
||||
safeOpenURL(target);
|
||||
}
|
||||
|
||||
function writeNativeImage(filePath, img) {
|
||||
function writeNativeImage(filePath: string, img: NativeImage): Promise<void> {
|
||||
switch (filePath.split('.').pop().toLowerCase()) {
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
@@ -50,7 +80,7 @@ function writeNativeImage(filePath, img) {
|
||||
}
|
||||
}
|
||||
|
||||
function onLinkContextMenu(ev, params) {
|
||||
function onLinkContextMenu(ev: Event, params: ContextMenuParams, webContents: WebContents): void {
|
||||
let url = params.linkURL || params.srcURL;
|
||||
|
||||
if (url.startsWith('vector://vector/webapp')) {
|
||||
@@ -76,7 +106,7 @@ function onLinkContextMenu(ev, params) {
|
||||
label: _t('Copy image'),
|
||||
accelerator: 'c',
|
||||
click() {
|
||||
ev.sender.copyImageAt(params.x, params.y);
|
||||
webContents.copyImageAt(params.x, params.y);
|
||||
},
|
||||
}));
|
||||
}
|
||||
@@ -140,8 +170,8 @@ function onLinkContextMenu(ev, params) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function _CutCopyPasteSelectContextMenus(params) {
|
||||
const options = [];
|
||||
function CutCopyPasteSelectContextMenus(params: ContextMenuParams): MenuItemConstructorOptions[] {
|
||||
const options: MenuItemConstructorOptions[] = [];
|
||||
|
||||
if (params.misspelledWord) {
|
||||
params.dictionarySuggestions.forEach(word => {
|
||||
@@ -180,10 +210,10 @@ function _CutCopyPasteSelectContextMenus(params) {
|
||||
accelerator: 'p',
|
||||
enabled: params.editFlags.canPaste,
|
||||
}, {
|
||||
role: 'pasteandmatchstyle',
|
||||
role: 'pasteAndMatchStyle',
|
||||
enabled: params.editFlags.canPaste,
|
||||
}, {
|
||||
role: 'selectall',
|
||||
role: 'selectAll',
|
||||
label: _t("Select All"),
|
||||
accelerator: 'a',
|
||||
enabled: params.editFlags.canSelectAll,
|
||||
@@ -192,7 +222,7 @@ function _CutCopyPasteSelectContextMenus(params) {
|
||||
}
|
||||
|
||||
function onSelectedContextMenu(ev, params) {
|
||||
const items = _CutCopyPasteSelectContextMenus(params);
|
||||
const items = CutCopyPasteSelectContextMenus(params);
|
||||
const popupMenu = Menu.buildFromTemplate(items);
|
||||
|
||||
// popup() requires an options object even for no options
|
||||
@@ -200,12 +230,13 @@ function onSelectedContextMenu(ev, params) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function onEditableContextMenu(ev, params) {
|
||||
const items = [
|
||||
function onEditableContextMenu(ev: Event, params: ContextMenuParams) {
|
||||
const items: MenuItemConstructorOptions[] = [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo', enabled: params.editFlags.canRedo },
|
||||
{ type: 'separator' },
|
||||
].concat(_CutCopyPasteSelectContextMenus(params));
|
||||
...CutCopyPasteSelectContextMenus(params),
|
||||
];
|
||||
|
||||
const popupMenu = Menu.buildFromTemplate(items);
|
||||
|
||||
@@ -214,20 +245,20 @@ function onEditableContextMenu(ev, params) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
ipcMain.on('userDownloadOpen', function(ev, { path }) {
|
||||
ipcMain.on('userDownloadOpen', function(ev: IpcMainEvent, { path }) {
|
||||
shell.openPath(path);
|
||||
});
|
||||
|
||||
module.exports = (webContents) => {
|
||||
export default (webContents: WebContents): void => {
|
||||
webContents.on('new-window', onWindowOrNavigate);
|
||||
webContents.on('will-navigate', (ev, target) => {
|
||||
webContents.on('will-navigate', (ev: Event, target: string): void => {
|
||||
if (target.startsWith("vector://")) return;
|
||||
return onWindowOrNavigate(ev, target);
|
||||
});
|
||||
|
||||
webContents.on('context-menu', function(ev, params) {
|
||||
webContents.on('context-menu', function(ev: Event, params: ContextMenuParams): void {
|
||||
if (params.linkURL || params.srcURL) {
|
||||
onLinkContextMenu(ev, params);
|
||||
onLinkContextMenu(ev, params, webContents);
|
||||
} else if (params.selectionText) {
|
||||
onSelectedContextMenu(ev, params);
|
||||
} else if (params.isEditable) {
|
||||
@@ -235,7 +266,7 @@ module.exports = (webContents) => {
|
||||
}
|
||||
});
|
||||
|
||||
webContents.session.on('will-download', (event, item) => {
|
||||
webContents.session.on('will-download', (event: Event, item: DownloadItem): void => {
|
||||
item.once('done', (event, state) => {
|
||||
if (state === 'completed') {
|
||||
const savePath = item.getSavePath();
|
||||
Reference in New Issue
Block a user