2019-12-06 18:17:34 +00:00
/*
2025-07-18 08:22:58 +01:00
Copyright 2018-2025 New Vector Ltd.
2024-09-06 17:56:18 +01:00
Copyright 2017-2019 Michael Telatynski <7t3chguy@gmail.com>
2019-12-06 18:17:34 +00:00
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
2025-01-17 11:44:49 +00:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-06 17:56:18 +01:00
Please see LICENSE files in the repository root for full details.
2019-12-06 18:17:34 +00:00
*/
2021-07-27 11:47:44 +01:00
// Squirrel on windows starts the app with various flags as hooks to tell us when we've been installed/uninstalled etc.
2024-10-30 18:06:03 +00:00
import "./squirrelhooks.js" ;
2025-05-22 11:40:28 +01:00
import { app , BrowserWindow , Menu , autoUpdater , dialog , type Input , type Event , session , protocol } from "electron" ;
2024-10-30 18:06:03 +00:00
// eslint-disable-next-line n/file-extension-in-import
2023-03-16 10:31:06 +00:00
import * as Sentry from "@sentry/electron/main" ;
2021-06-25 14:35:58 +01:00
import AutoLaunch from "auto-launch" ;
2024-10-30 18:06:03 +00:00
import path , { dirname } from "node:path" ;
2022-12-15 11:00:58 +00:00
import windowStateKeeper from "electron-window-state" ;
2024-10-30 18:06:03 +00:00
import fs , { promises as afs } from "node:fs" ;
import { URL , fileURLToPath } from "node:url" ;
2021-07-01 09:22:57 +01:00
import minimist from "minimist" ;
2019-12-06 18:17:34 +00:00
2024-10-30 18:06:03 +00:00
import "./ipc.js" ;
import "./seshat.js" ;
import "./settings.js" ;
2025-07-17 10:05:03 +01:00
import "./badge.js" ;
2024-10-30 18:06:03 +00:00
import * as tray from "./tray.js" ;
2025-04-29 11:40:06 +01:00
import Store from "./store.js" ;
2024-10-30 18:06:03 +00:00
import { buildMenuTemplate } from "./vectormenu.js" ;
import webContentsHandler from "./webcontents-handler.js" ;
import * as updater from "./updater.js" ;
2025-05-22 11:40:28 +01:00
import ProtocolHandler from "./protocol.js" ;
2024-10-30 18:06:03 +00:00
import { _t , AppLocalization } from "./language-helper.js" ;
import { setDisplayMediaCallback } from "./displayMediaCallback.js" ;
import { setupMacosTitleBar } from "./macos-titlebar.js" ;
2025-04-14 13:53:29 +01:00
import { type Json , loadJsonFile } from "./utils.js" ;
2024-10-30 18:06:03 +00:00
import { setupMediaAuth } from "./media-auth.js" ;
2025-05-22 11:40:28 +01:00
import { readBuildConfig } from "./build-config.js" ;
2024-10-30 18:06:03 +00:00
const __dirname = dirname ( fileURLToPath ( import . meta . url ));
2019-12-06 18:17:34 +00:00
2021-07-01 09:22:57 +01:00
const argv = minimist ( process . argv , {
alias : { help : "h" },
});
2019-12-06 18:17:34 +00:00
if ( argv [ "help" ]) {
console . log ( "Options:" );
console . log ( " --profile-dir {path}: Path to where to store the profile." );
2025-04-11 10:19:26 +02:00
console . log (
` --profile {name}: Name of alternate profile to use, allows for running multiple accounts. \ n` +
` Ignored if --profile-dir is specified. \ n` +
` The ELEMENT_PROFILE_DIR environment variable may be used to change the default profile path. \ n` +
` It is overridden by --profile-dir, but can be combined with --profile.` ,
);
2019-12-06 18:17:34 +00:00
console . log ( " --devtools: Install and use react-devtools and react-perf." );
2024-10-17 13:26:35 +01:00
console . log (
` --config: Path to the config.json file. May also be specified via the ELEMENT_DESKTOP_CONFIG_JSON environment variable. \ n` +
` Otherwise use the default user location ' ${ app . getPath ( "userData" ) } '` ,
);
2019-12-06 18:17:34 +00:00
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." );
2022-12-15 11:00:58 +00:00
console . log ( "And more such as --proxy, see:" + "https://electronjs.org/docs/api/command-line-switches" );
2019-12-06 18:17:34 +00:00
app . exit ();
}
2024-10-17 13:26:35 +01:00
const LocalConfigLocation = process . env . ELEMENT_DESKTOP_CONFIG_JSON ?? argv [ "config" ];
2025-03-31 16:09:57 +01:00
const LocalConfigFilename = "config.json" ;
2024-10-17 13:26:35 +01:00
2020-07-15 11:41:30 +01:00
// Electron creates the user data directory (with just an empty 'Dictionaries' directory...)
// 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.
2022-05-23 15:44:29 +01:00
function isRealUserDataDir ( d : string ) : boolean {
2022-12-15 11:00:58 +00:00
return fs . existsSync ( path . join ( d , "IndexedDB" ));
2020-07-15 11:41:30 +01:00
}
2025-05-22 11:40:28 +01:00
const buildConfig = readBuildConfig ();
const protocolHandler = new ProtocolHandler ( buildConfig . protocol );
2020-04-14 13:29:47 +01:00
// check if we are passed a profile in the SSO callback url
2022-05-23 15:44:29 +01:00
let userDataPath : string ;
2020-07-07 18:57:29 +01:00
2025-05-22 11:40:28 +01:00
const userDataPathInProtocol = protocolHandler . getProfileFromDeeplink ( argv [ "_" ]);
2020-04-14 13:29:47 +01:00
if ( userDataPathInProtocol ) {
2020-07-07 18:57:29 +01:00
userDataPath = userDataPathInProtocol ;
2022-12-15 11:00:58 +00:00
} else if ( argv [ "profile-dir" ]) {
userDataPath = argv [ "profile-dir" ];
2020-07-02 13:30:11 +01:00
} else {
2025-04-11 10:19:26 +02:00
let newUserDataPath = process . env . ELEMENT_PROFILE_DIR ?? app . getPath ( "userData" );
2022-12-15 11:00:58 +00:00
if ( argv [ "profile" ]) {
newUserDataPath += "-" + argv [ "profile" ];
2020-07-02 13:30:11 +01:00
}
const newUserDataPathExists = isRealUserDataDir ( newUserDataPath );
2022-12-15 11:00:58 +00:00
let oldUserDataPath = path . join ( app . getPath ( "appData" ), app . getName (). replace ( "Element" , "Riot" ));
if ( argv [ "profile" ]) {
oldUserDataPath += "-" + argv [ "profile" ];
2020-07-21 17:57:54 +01:00
}
2020-07-02 13:30:11 +01:00
const oldUserDataPathExists = isRealUserDataDir ( oldUserDataPath );
2022-12-15 11:00:58 +00:00
console . log ( newUserDataPath + " exists: " + ( newUserDataPathExists ? "yes" : "no" ));
console . log ( oldUserDataPath + " exists: " + ( oldUserDataPathExists ? "yes" : "no" ));
2020-07-02 13:30:11 +01:00
if ( ! newUserDataPathExists && oldUserDataPathExists ) {
console . log ( "Using legacy user data path: " + oldUserDataPath );
2020-07-07 18:57:29 +01:00
userDataPath = oldUserDataPath ;
} else {
userDataPath = newUserDataPath ;
2020-07-02 13:30:11 +01:00
}
2019-12-06 18:17:34 +00:00
}
2022-12-15 11:00:58 +00:00
app . setPath ( "userData" , userDataPath );
2019-12-06 18:17:34 +00:00
2022-05-23 15:44:29 +01:00
async function tryPaths ( name : string , root : string , rawPaths : string []) : Promise < string > {
2020-04-16 14:08:16 +02:00
// Make everything relative to root
2022-12-15 11:00:58 +00:00
const paths = rawPaths . map (( p ) => path . join ( root , p ));
2019-12-10 17:40:17 +00:00
for ( const p of paths ) {
try {
await afs . stat ( p );
2022-12-15 11:00:58 +00:00
return p + "/" ;
2024-10-16 15:59:12 +01:00
} catch {}
2019-12-10 17:40:17 +00:00
}
2020-04-16 14:08:16 +02:00
console . log ( `Couldn't find ${ name } files in any of: ` );
2019-12-10 17:40:17 +00:00
for ( const p of paths ) {
2022-12-15 11:00:58 +00:00
console . log ( "\t" + path . resolve ( p ));
2019-12-10 17:40:17 +00:00
}
2020-04-16 14:08:16 +02:00
throw new Error ( `Failed to find ${ name } files` );
2019-12-06 18:17:34 +00:00
}
2022-12-15 11:00:58 +00:00
const homeserverProps = [ "default_is_url" , "default_hs_url" , "default_server_name" , "default_server_config" ] as const ;
2022-11-30 13:51:54 +00:00
2023-03-16 10:31:06 +00:00
let asarPathPromise : Promise < string > | undefined ;
// Get the webapp resource file path, memoizes result
function getAsarPath () : Promise < string > {
if ( ! asarPathPromise ) {
asarPathPromise = tryPaths ( "webapp" , __dirname , [
// If run from the source checkout, this will be in the directory above
"../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" ,
// also try without the 'asar' suffix to allow symlinking in a directory
"../webapp" ,
// from a packaged application
"../../webapp" ,
]);
}
2020-04-16 14:08:16 +02:00
2023-03-16 10:31:06 +00:00
return asarPathPromise ;
}
2025-03-31 16:09:57 +01:00
function loadLocalConfigFile () : Json {
if ( LocalConfigLocation ) {
console . log ( "Loading local config: " + LocalConfigLocation );
return loadJsonFile ( LocalConfigLocation );
} else {
const configDir = app . getPath ( "userData" );
console . log ( `Loading local config: ${ path . join ( configDir , LocalConfigFilename ) } ` );
return loadJsonFile ( configDir , LocalConfigFilename );
}
}
2023-03-16 10:31:06 +00:00
// Loads the config from asar, and applies a config.json from userData atop if one exists
// Writes config to `global.vectorConfig`. Does nothing if `global.vectorConfig` is already set.
async function loadConfig () : Promise < void > {
if ( global . vectorConfig ) return ;
const asarPath = await getAsarPath ();
2019-12-06 18:17:34 +00:00
2019-12-10 17:40:17 +00:00
try {
2025-04-15 10:33:38 +01:00
console . log ( `Loading app config: ${ path . join ( asarPath , LocalConfigFilename ) } ` );
2025-03-31 16:09:57 +01:00
global . vectorConfig = loadJsonFile ( asarPath , LocalConfigFilename );
2024-10-16 15:59:12 +01:00
} catch {
2019-12-10 17:40:17 +00:00
// it would be nice to check the error code here and bail if the config
2020-11-14 21:54:07 +00:00
// is unparsable, but we get MODULE_NOT_FOUND in the case of a missing
2019-12-10 17:40:17 +00:00
// file or invalid json, so node is just very unhelpful.
// Continue with the defaults (ie. an empty config)
2022-07-01 20:17:40 +01:00
global . vectorConfig = {};
2019-12-06 18:17:34 +00:00
}
2019-12-10 17:40:17 +00:00
try {
// Load local config and use it to override values from the one baked with the build
2025-03-31 16:09:57 +01:00
const localConfig = loadLocalConfigFile ();
2019-12-10 17:40:17 +00:00
// 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.
2022-12-15 11:00:58 +00:00
if ( Object . keys ( localConfig ). find (( k ) => homeserverProps . includes (< any > k ))) {
2019-12-10 17:40:17 +00:00
// Rip out all the homeserver options from the vector config
2022-07-01 20:17:40 +01:00
global . vectorConfig = Object . keys ( global . vectorConfig )
2022-12-15 11:00:58 +00:00
. filter (( k ) => ! homeserverProps . includes (< any > k ))
2024-01-02 18:12:16 +00:00
. reduce (
( obj , key ) => {
obj [ key ] = global . vectorConfig [ key ];
return obj ;
},
{} as Omit < Partial <( typeof global )[ "vectorConfig" ] >, keyof typeof homeserverProps > ,
);
2019-12-10 17:40:17 +00:00
}
2022-07-01 20:17:40 +01:00
global . vectorConfig = Object . assign ( global . vectorConfig , localConfig );
2019-12-10 17:40:17 +00:00
} catch ( e ) {
2021-06-03 23:39:29 -05:00
if ( e instanceof SyntaxError ) {
2024-06-12 17:17:24 +01:00
void dialog . showMessageBox ({
2021-06-03 23:39:29 -05:00
type : "error" ,
2022-12-15 11:00:58 +00:00
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" } .` ,
2021-06-03 23:39:29 -05:00
detail : e.message || "" ,
});
}
2019-12-10 17:40:17 +00:00
// Could not load local config, this is expected in most cases.
}
2023-03-16 10:31:06 +00:00
}
// Configure Electron Sentry and crashReporter using sentry.dsn in config.json if one is present.
async function configureSentry () : Promise < void > {
await loadConfig ();
const { dsn , environment } = global . vectorConfig . sentry || {};
if ( dsn ) {
console . log ( `Enabling Sentry with dsn= ${ dsn } environment= ${ environment } ` );
Sentry . init ({
dsn ,
environment ,
// We don't actually use this IPC, but we do not want Sentry injecting preloads
ipcMode : Sentry.IPCMode.Classic ,
});
}
}
// Set up globals for Tray and AutoLaunch
async function setupGlobals () : Promise < void > {
const asarPath = await getAsarPath ();
await loadConfig ();
// we assume the resources path is in the same place as the asar
const resPath = await tryPaths ( "res" , path . dirname ( asarPath ), [
// If run from the source checkout
"res" ,
// if run from packaged application
"" ,
]);
2019-12-10 17:40:17 +00:00
// The tray icon
// It's important to call `path.join` so we don't end up with the packaged asar in the final path.
2024-10-16 17:21:49 +02:00
const iconFile = `element. ${ process . platform === "win32" ? "ico" : "png" } ` ;
2022-07-01 20:17:40 +01:00
global . trayConfig = {
2024-10-16 17:21:49 +02:00
icon_path : path.join ( resPath , "img" , iconFile ),
2022-12-15 11:00:58 +00:00
brand : global.vectorConfig.brand || "Element" ,
2019-12-10 17:40:17 +00:00
};
// launcher
2022-07-01 20:17:40 +01:00
global . launcher = new AutoLaunch ({
2022-12-15 11:00:58 +00:00
name : global.vectorConfig.brand || "Element" ,
2019-12-10 17:40:17 +00:00
isHidden : true ,
mac : {
useLaunchAgent : true ,
},
});
2019-12-06 18:17:34 +00:00
}
global . appQuitting = false ;
2022-05-23 15:44:29 +01:00
const exitShortcuts : Array < ( input : Input , platform : string ) => boolean > = [
2022-12-15 11:00:58 +00:00
( input , platform ) : boolean => platform !== "darwin" && input . alt && input . key . toUpperCase () === "F4" ,
( input , platform ) : boolean => platform !== "darwin" && input . control && input . key . toUpperCase () === "Q" ,
2023-09-05 17:09:47 +01:00
( input , platform ) : boolean =>
platform === "darwin" && input . meta && ! input . control && input . key . toUpperCase () === "Q" ,
2021-03-31 08:58:24 +01:00
];
2024-06-12 17:17:24 +01:00
void configureSentry ();
2023-03-16 10:31:06 +00:00
2019-12-06 18:17:34 +00:00
// handle uncaught errors otherwise it displays
// stack traces in popup dialogs, which is terrible (which
// it will do any time the auto update poke fails, and there's
// no other way to catch this error).
// Assuming we generally run from the console when developing,
// this is far preferable.
2022-12-15 11:00:58 +00:00
process . on ( "uncaughtException" , function ( error : Error ) : void {
console . log ( "Unhandled exception" , error );
2019-12-06 18:17:34 +00:00
});
2022-12-15 11:00:58 +00:00
app . commandLine . appendSwitch ( "--enable-usermedia-screen-capturing" );
if ( ! app . commandLine . hasSwitch ( "enable-features" )) {
app . commandLine . appendSwitch ( "enable-features" , "WebRTCPipeWireCapturer" );
2021-09-06 16:18:16 +02:00
}
2025-05-06 11:40:32 +01:00
// Workaround bug in electron 36:https://github.com/electron/electron/issues/46538
// Hopefully this will no longer be needed soon and can be removed
if ( process . platform === "linux" ) {
2025-05-06 11:47:45 +01:00
app . commandLine . appendSwitch ( "gtk-version" , "3" );
2025-05-06 11:40:32 +01:00
}
2019-12-06 18:17:34 +00:00
const gotLock = app . requestSingleInstanceLock ();
if ( ! gotLock ) {
2022-12-15 11:00:58 +00:00
console . log ( "Other instance detected: exiting" );
2019-12-06 18:17:34 +00:00
app . exit ();
}
2020-03-02 15:04:51 +00:00
// do this after we know we are the primary instance of the app
2025-05-22 11:40:28 +01:00
protocolHandler . initialise ( userDataPath );
2020-03-02 15:04:51 +00:00
2019-12-06 18:17:34 +00:00
// Register the scheme the app is served from as 'standard'
// which allows things like relative URLs and IndexedDB to
// work.
// Also mark it as secure (ie. accessing resources from this
// protocol and HTTPS won't trigger mixed content warnings).
2022-12-15 11:00:58 +00:00
protocol . registerSchemesAsPrivileged ([
{
scheme : "vector" ,
privileges : {
standard : true ,
secure : true ,
supportFetchAPI : true ,
},
2019-12-06 18:17:34 +00:00
},
2022-12-15 11:00:58 +00:00
]);
2019-12-06 18:17:34 +00:00
2020-05-20 16:16:57 -06:00
// 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.
//
// This also fixes an issue with window.open where if we only specified the sandbox
// on the main window we'd run into cryptic "ipc_renderer be broke" errors. Turns out
// it's trying to jump the sandbox and make some calls into electron, which it can't
// do when half of it is sandboxed. By turning on the sandbox for everything, the new
// window (no matter how temporary it may be) is also sandboxed, allowing for a clean
// transition into the user's browser.
app . enableSandbox ();
2021-04-03 14:10:11 +02:00
// 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
2022-12-15 11:00:58 +00:00
app . commandLine . appendSwitch ( "disable-features" , "HardwareMediaKeyHandling,MediaSessionService" );
2020-05-20 16:16:57 -06:00
2025-04-29 11:40:06 +01:00
const store = Store . initialize ( argv [ "storage-mode" ]); // must be called before any async actions
2022-05-20 13:26:16 +01:00
// Disable hardware acceleration if the setting has been set.
2025-04-29 11:40:06 +01:00
if ( store . get ( "disableHardwareAcceleration" )) {
2022-05-20 13:37:58 +01:00
console . log ( "Disabling hardware acceleration." );
2022-05-20 13:26:16 +01:00
app . disableHardwareAcceleration ();
}
2022-12-15 11:00:58 +00:00
app . on ( "ready" , async () => {
2025-04-29 11:40:06 +01:00
console . debug ( "Reached Electron ready state" );
2023-03-16 10:31:06 +00:00
let asarPath : string ;
2019-12-10 17:40:17 +00:00
try {
2023-03-16 10:31:06 +00:00
asarPath = await getAsarPath ();
2019-12-10 17:40:17 +00:00
await setupGlobals ();
} catch ( e ) {
console . log ( "App setup failed: exiting" , e );
process . exit ( 1 );
// process.exit doesn't cause node to stop running code immediately,
// so return (we could let the exception propagate but then we end up
// with node printing all sorts of stuff about unhandled exceptions
// when we want the actual error to be as obvious as possible).
return ;
}
2022-12-15 11:00:58 +00:00
if ( argv [ "devtools" ]) {
2019-12-06 18:17:34 +00:00
try {
2024-12-18 19:11:19 +00:00
const { installExtension , REACT_DEVELOPER_TOOLS } = await import ( "electron-devtools-installer" );
installExtension ( REACT_DEVELOPER_TOOLS )
. then (( ext ) => console . log ( `Added Extension: ${ ext . name } ` ))
2022-12-15 11:00:58 +00:00
. catch (( err : unknown ) => console . log ( "An error occurred: " , err ));
2019-12-06 18:17:34 +00:00
} catch ( e ) {
console . log ( e );
}
}
2022-12-15 11:00:58 +00:00
protocol . registerFileProtocol ( "vector" , ( request , callback ) => {
if ( request . method !== "GET" ) {
2021-05-27 14:39:26 +01:00
callback ({ error : - 322 }); // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h
2019-12-06 18:17:34 +00:00
return null ;
}
const parsedUrl = new URL ( request . url );
2022-12-15 11:00:58 +00:00
if ( parsedUrl . protocol !== "vector:" ) {
2021-05-27 14:39:26 +01:00
callback ({ error : - 302 }); // UNKNOWN_URL_SCHEME
2019-12-06 18:17:34 +00:00
return ;
}
2022-12-15 11:00:58 +00:00
if ( parsedUrl . host !== "vector" ) {
2021-05-27 14:39:26 +01:00
callback ({ error : - 105 }); // NAME_NOT_RESOLVED
2019-12-06 18:17:34 +00:00
return ;
}
2022-12-15 11:00:58 +00:00
const target = parsedUrl . pathname . split ( "/" );
2019-12-06 18:17:34 +00:00
// path starts with a '/'
2022-12-15 11:00:58 +00:00
if ( target [ 0 ] !== "" ) {
2021-05-27 14:39:26 +01:00
callback ({ error : - 6 }); // FILE_NOT_FOUND
2019-12-06 18:17:34 +00:00
return ;
}
2022-12-15 11:00:58 +00:00
if ( target [ target . length - 1 ] == "" ) {
target [ target . length - 1 ] = "index.html" ;
2019-12-06 18:17:34 +00:00
}
2022-05-23 15:44:29 +01:00
let baseDir : string ;
2022-12-15 11:00:58 +00:00
if ( target [ 1 ] === "webapp" ) {
2019-12-10 17:40:17 +00:00
baseDir = asarPath ;
2019-12-06 18:17:34 +00:00
} else {
2021-05-27 14:39:26 +01:00
callback ({ error : - 6 }); // FILE_NOT_FOUND
2019-12-06 18:17:34 +00:00
return ;
}
// Normalise the base dir and the target path separately, then make sure
// the target path isn't trying to back out beyond its root
baseDir = path . normalize ( baseDir );
const relTarget = path . normalize ( path . join (... target . slice ( 2 )));
2022-12-15 11:00:58 +00:00
if ( relTarget . startsWith ( ".." )) {
2021-05-27 14:39:26 +01:00
callback ({ error : - 6 }); // FILE_NOT_FOUND
2019-12-06 18:17:34 +00:00
return ;
}
const absTarget = path . join ( baseDir , relTarget );
callback ({
path : absTarget ,
});
});
2025-03-17 11:54:07 +00:00
// Minimist parses `--no-`-prefixed arguments as booleans with value `false` rather than verbatim.
if ( argv [ "update" ] === false ) {
console . log ( "Auto update disabled via command line flag" );
2022-12-15 11:00:58 +00:00
} else if ( global . vectorConfig [ "update_base_url" ]) {
console . log ( `Starting auto update with base URL: ${ global . vectorConfig [ "update_base_url" ] } ` );
2024-06-12 17:17:24 +01:00
void updater . start ( global . vectorConfig [ "update_base_url" ]);
2019-12-06 18:17:34 +00:00
} else {
2022-12-15 11:00:58 +00:00
console . log ( "No update_base_url is defined: auto update is disabled" );
2019-12-06 18:17:34 +00:00
}
2025-04-29 11:40:06 +01:00
// Set up i18n before loading storage as we need translations for dialogs
global . appLocalization = new AppLocalization ({
components : [() : void => tray . initApplicationMenu (), () : void => Menu . setApplicationMenu ( buildMenuTemplate ())],
store ,
});
2019-12-06 18:17:34 +00:00
// Load the previous window state with fallback to defaults
const mainWindowState = windowStateKeeper ({
defaultWidth : 1024 ,
defaultHeight : 768 ,
});
2025-04-29 11:40:06 +01:00
console . debug ( "Opening main window" );
2024-10-30 18:06:03 +00:00
const preloadScript = path . normalize ( ` ${ __dirname } /preload.cjs` );
2022-07-01 20:17:40 +01:00
global . mainWindow = new BrowserWindow ({
2020-02-21 11:17:18 +00:00
// https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
2022-12-15 11:00:58 +00:00
backgroundColor : "#fff" ,
2020-02-21 11:17:18 +00:00
2023-07-28 12:51:33 +01:00
titleBarStyle : process.platform === "darwin" ? "hidden" : "default" ,
trafficLightPosition : { x : 9 , y : 8 },
2024-10-16 17:21:49 +02:00
icon : global.trayConfig.icon_path ,
2019-12-06 18:17:34 +00:00
show : false ,
2025-04-29 11:40:06 +01:00
autoHideMenuBar : store.get ( "autoHideMenuBar" ),
2019-12-06 18:17:34 +00:00
x : mainWindowState.x ,
y : mainWindowState.y ,
width : mainWindowState.width ,
height : mainWindowState.height ,
webPreferences : {
preload : preloadScript ,
nodeIntegration : false ,
2020-05-20 16:16:57 -06:00
//sandbox: true, // We enable sandboxing from app.enableSandbox() above
2021-01-13 15:21:00 +00:00
contextIsolation : true ,
2021-12-10 15:55:35 +01:00
webgl : true ,
2019-12-06 18:17:34 +00:00
},
});
2025-06-04 16:01:03 +01:00
2025-06-10 08:55:51 +01:00
global . mainWindow . setContentProtection ( store . get ( "enableContentProtection" ));
2025-06-04 16:01:03 +01:00
try {
console . debug ( "Ensuring storage is ready" );
2025-06-04 16:23:59 +01:00
if ( ! ( await store . prepareSafeStorage ( global . mainWindow . webContents . session ))) return ;
2025-06-04 16:01:03 +01:00
} catch ( e ) {
console . error ( e );
app . exit ( 1 );
}
2024-06-12 17:17:24 +01:00
void global . mainWindow . loadURL ( "vector://vector/webapp/" );
2019-12-06 18:17:34 +00:00
2023-07-28 12:51:33 +01:00
if ( process . platform === "darwin" ) {
setupMacosTitleBar ( global . mainWindow );
}
2021-04-02 12:07:33 +02:00
// Handle spellchecker
2022-07-01 20:17:40 +01:00
// For some reason spellCheckerEnabled isn't persisted, so we have to use the store here
2025-04-29 11:40:06 +01:00
global . mainWindow . webContents . session . setSpellCheckerEnabled ( store . get ( "spellCheckerEnabled" , true ));
2021-04-02 12:07:33 +02:00
2019-12-06 18:17:34 +00:00
// Create trayIcon icon
2025-04-29 11:40:06 +01:00
if ( store . get ( "minimizeToTray" )) tray . create ( global . trayConfig );
2019-12-06 18:17:34 +00:00
2022-12-15 11:00:58 +00:00
global . mainWindow . once ( "ready-to-show" , () => {
2022-11-30 13:51:54 +00:00
if ( ! global . mainWindow ) return ;
2022-07-01 20:17:40 +01:00
mainWindowState . manage ( global . mainWindow );
2019-12-06 18:17:34 +00:00
2022-12-15 11:00:58 +00:00
if ( ! argv [ "hidden" ]) {
2022-07-01 20:17:40 +01:00
global . mainWindow . show ();
2019-12-06 18:17:34 +00:00
} else {
// hide here explicitly because window manage above sometimes shows it
2022-07-01 20:17:40 +01:00
global . mainWindow . hide ();
2019-12-06 18:17:34 +00:00
}
});
2025-04-29 11:40:06 +01:00
global . mainWindow . webContents . on ( "before-input-event" , ( event : Event , input : Input ) : void => {
const shouldWarnBeforeExit = store . get ( "warnBeforeExit" , true );
const exitShortcutPressed =
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 ( "action|cancel" ),
_t ( "action|close_brand" , {
brand : global.vectorConfig.brand || "Element" ,
}),
],
message : _t ( "confirm_quit" ),
defaultId : 1 ,
cancelId : 0 ,
}) === 0 ;
if ( shouldCancelCloseRequest ) {
event . preventDefault ();
}
}
});
2021-03-29 12:10:27 +01:00
2022-12-15 11:00:58 +00:00
global . mainWindow . on ( "closed" , () => {
2022-07-01 20:17:40 +01:00
global . mainWindow = null ;
2019-12-06 18:17:34 +00:00
});
2022-12-15 11:00:58 +00:00
global . mainWindow . on ( "close" , async ( e ) => {
2021-03-25 14:50:33 +00:00
// If we are not quitting and have a tray icon then minimize to tray
2022-12-15 11:00:58 +00:00
if ( ! global . appQuitting && ( tray . hasTray () || process . platform === "darwin" )) {
2021-03-25 14:50:33 +00:00
// On Mac, closing the window just hides it
// (this is generally how single-window Mac apps
// behave, eg. Mail.app)
e . preventDefault ();
2021-05-04 21:55:28 -05:00
2022-11-30 13:51:54 +00:00
if ( global . mainWindow ? . isFullScreen ()) {
2022-12-15 11:00:58 +00:00
global . mainWindow . once ( "leave-full-screen" , () => global . mainWindow ? . hide ());
2021-05-04 21:55:28 -05:00
2022-07-01 20:17:40 +01:00
global . mainWindow . setFullScreen ( false );
2021-05-04 21:55:28 -05:00
} else {
2022-11-30 13:51:54 +00:00
global . mainWindow ? . hide ();
2021-05-04 21:55:28 -05:00
}
2021-03-25 14:50:33 +00:00
return false ;
}
2019-12-06 18:17:34 +00:00
});
2022-12-15 11:00:58 +00:00
if ( process . platform === "win32" ) {
2019-12-06 18:17:34 +00:00
// Handle forward/backward mouse buttons in Windows
2022-12-15 11:00:58 +00:00
global . mainWindow . on ( "app-command" , ( e , cmd ) => {
if ( cmd === "browser-backward" && global . mainWindow ? . webContents . canGoBack ()) {
2022-07-01 20:17:40 +01:00
global . mainWindow . webContents . goBack ();
2022-12-15 11:00:58 +00:00
} else if ( cmd === "browser-forward" && global . mainWindow ? . webContents . canGoForward ()) {
2022-07-01 20:17:40 +01:00
global . mainWindow . webContents . goForward ();
2019-12-06 18:17:34 +00:00
}
});
}
2022-07-01 20:17:40 +01:00
webContentsHandler ( global . mainWindow . webContents );
2021-04-26 13:58:29 +01:00
2023-07-14 12:09:12 +02:00
session . defaultSession . setDisplayMediaRequestHandler (( _ , callback ) => {
global . mainWindow ? . webContents . send ( "openDesktopCapturerSourcePicker" );
setDisplayMediaCallback ( callback );
});
2024-07-10 07:41:27 -06:00
2024-08-07 09:44:18 +01:00
setupMediaAuth ( global . mainWindow );
2019-12-06 18:17:34 +00:00
});
2022-12-15 11:00:58 +00:00
app . on ( "window-all-closed" , () => {
2019-12-06 18:17:34 +00:00
app . quit ();
});
2022-12-15 11:00:58 +00:00
app . on ( "activate" , () => {
2022-11-30 13:51:54 +00:00
global . mainWindow ? . show ();
2019-12-06 18:17:34 +00:00
});
2022-05-23 15:44:29 +01:00
function beforeQuit () : void {
2019-12-06 18:17:34 +00:00
global . appQuitting = true ;
2022-12-15 11:00:58 +00:00
global . mainWindow ? . webContents . send ( "before-quit" );
2020-05-23 11:52:49 +01:00
}
2022-12-15 11:00:58 +00:00
app . on ( "before-quit" , beforeQuit );
autoUpdater . on ( "before-quit-for-update" , beforeQuit );
2019-12-06 18:17:34 +00:00
2022-12-15 11:00:58 +00:00
app . on ( "second-instance" , ( ev , commandLine , workingDirectory ) => {
2019-12-06 18:17:34 +00:00
// If other instance launched with --hidden then skip showing window
2022-12-15 11:00:58 +00:00
if ( commandLine . includes ( "--hidden" )) return ;
2019-12-06 18:17:34 +00:00
// Someone tried to run a second instance, we should focus our window.
2022-07-01 20:17:40 +01:00
if ( global . mainWindow ) {
if ( ! global . mainWindow . isVisible ()) global . mainWindow . show ();
if ( global . mainWindow . isMinimized ()) global . mainWindow . restore ();
global . mainWindow . focus ();
2019-12-06 18:17:34 +00:00
}
});
2025-04-22 12:59:39 -03:00
// This is required to make notification handlers work
// on Windows 8.1/10/11 (and is a noop on other platforms);
// It must also match the ID found in 'electron-builder'
// in order to get the title and icon to show up correctly.
// Ref: https://stackoverflow.com/a/77314604/3525780
2025-05-22 11:40:28 +01:00
app . setAppUserModelId ( buildConfig . appId );