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-09-09 10:46:02 +02:00
import {
app ,
BrowserWindow ,
Menu ,
autoUpdater ,
dialog ,
type Input ,
type Event ,
session ,
protocol ,
desktopCapturer ,
} from "electron" ;
2023-03-16 10:31:06 +00:00
import * as Sentry from "@sentry/electron/main" ;
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 { URL , fileURLToPath } from "node:url" ;
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" ;
import { setupMediaAuth } from "./media-auth.js" ;
2026-07-14 15:42:51 +03:00
import { type RendererRecovery , setupRendererRecovery } from "./renderer-recovery.js" ;
2026-03-31 10:22:27 +02:00
import { getBuildConfig } from "./build-config.js" ;
import { getAsarPath } from "./asar.js" ;
import { getIconPath } from "./icon.js" ;
2026-06-17 11:19:37 +01:00
import { getArgs } from "./args.js" ;
2026-06-17 20:29:45 +01:00
import { type ConfigOptions , loadConfig } from "./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
2026-03-31 10:22:27 +02:00
const buildConfig = getBuildConfig ();
2025-05-22 11:40:28 +01:00
const protocolHandler = new ProtocolHandler ( buildConfig . protocol );
2026-06-17 11:19:37 +01:00
const args = getArgs ( protocolHandler );
2025-05-22 11:40:28 +01:00
2026-06-17 11:19:37 +01:00
app . setPath ( "userData" , args . userDataPath );
2019-12-06 18:17:34 +00:00
2026-07-14 15:42:51 +03:00
// Renderer crash auto-recovery for the main window (element-web#32222). Held at module scope so the
// dock `activate` / `second-instance` relaunch handlers can route a crashed renderer through the same
// capped recovery rather than reloading inline (which would re-arm an already-given-up crash loop).
let rendererRecovery : RendererRecovery | undefined ;
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 > {
2026-06-17 20:29:45 +01:00
const config = await loadConfig ( args . localConfigPath );
const { dsn , environment } = config . sentry || {};
2023-03-16 10:31:06 +00:00
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 ,
});
}
}
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
}
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 ();
}
// 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
2026-06-17 11:19:37 +01:00
const store = Store . initialize ( args . storageMode ); // must be called before any async actions
2025-04-29 11:40:06 +01:00
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 ;
2026-06-17 20:29:45 +01:00
let config : ConfigOptions ;
2023-03-16 10:31:06 +00:00
2019-12-10 17:40:17 +00:00
try {
2023-03-16 10:31:06 +00:00
asarPath = await getAsarPath ();
2026-06-17 20:29:45 +01:00
config = await loadConfig ( args . localConfigPath );
2019-12-10 17:40:17 +00:00
} 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 ;
}
2026-06-17 11:19:37 +01:00
if ( args . 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 ,
});
});
2026-06-17 11:19:37 +01:00
if ( ! args . update ) {
2025-03-17 11:54:07 +00:00
console . log ( "Auto update disabled via command line flag" );
2026-06-17 20:29:45 +01:00
} else if ( config . update_base_url ) {
void updater . start ( config . 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 },
2026-03-31 10:22:27 +02:00
icon : await getIconPath (),
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 );
}
2026-06-17 11:19:37 +01:00
// do this after we know we are the primary instance of the app
const hasDeeplink = protocolHandler . initialise ( args );
if ( ! hasDeeplink ) {
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
2026-03-31 10:22:27 +02:00
if ( store . get ( "minimizeToTray" )) await tray . create ();
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
2026-06-17 11:19:37 +01:00
if ( ! args . 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 exitShortcutPressed =
2025-12-11 17:57:18 +05:30
input . type === "keyDown" && exitShortcuts . some (( shortcutFn ) => shortcutFn ( input , process . platform ));
2026-02-09 19:55:38 +05:30
// We only care about the exit shortcuts here
if ( ! exitShortcutPressed || ! global . mainWindow ) return ;
2025-04-29 11:40:06 +01:00
2025-12-11 17:52:41 +05:30
// Prevent the default behaviour
event . preventDefault ();
2026-02-09 19:55:38 +05:30
// Let's ask the user if they really want to exit the app
2025-12-11 17:52:41 +05:30
const shouldWarnBeforeExit = store . get ( "warnBeforeExit" , true );
2026-02-09 19:55:38 +05:30
if ( shouldWarnBeforeExit ) {
2025-04-29 11:40:06 +01:00
const shouldCancelCloseRequest =
dialog . showMessageBoxSync ( global . mainWindow , {
type : "question" ,
buttons : [
_t ( "action|cancel" ),
_t ( "action|close_brand" , {
2026-06-17 20:29:45 +01:00
brand : config.brand ,
2025-04-29 11:40:06 +01:00
}),
],
message : _t ( "confirm_quit" ),
defaultId : 1 ,
cancelId : 0 ,
}) === 0 ;
2026-02-09 19:55:38 +05:30
if ( shouldCancelCloseRequest ) return ;
}
// Exit the app
app . exit ();
});
global . mainWindow . on ( "closed" , () => {
global . mainWindow = null ;
});
global . mainWindow . on ( "close" , async ( e ) => {
// If we are not quitting and have a tray icon then minimize to tray
if ( ! global . appQuitting && ( tray . hasTray () || process . platform === "darwin" )) {
// On Mac, closing the window just hides it
// (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 . setFullScreen ( false );
} else {
global . mainWindow ? . hide ();
2021-05-04 21:55:28 -05:00
}
2026-02-09 19:55:38 +05:30
return false ;
2021-03-25 14:50:33 +00:00
}
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
2026-07-14 15:42:51 +03:00
// Auto-recover from an upstream renderer/GPU-process crash (white screen, element-web#32222). This
// is a MITIGATION of an upstream Electron/Chromium defect, not a root-cause fix — without it a dead
// renderer stays a permanent blank window the user can only escape by killing the whole app.
rendererRecovery = setupRendererRecovery ( global . mainWindow );
2025-09-09 10:46:02 +02:00
session . defaultSession . setDisplayMediaRequestHandler (
( _ , callback ) => {
if ( process . env . XDG_SESSION_TYPE === "wayland" ) {
// On Wayland, calling getSources() opens the xdg-desktop-portal picker.
// The user can only select a single source there, so Electron will return an array with exactly one entry.
desktopCapturer
. getSources ({ types : [ "screen" , "window" ] })
. then (( sources ) => {
2026-07-10 10:47:41 +01:00
// oxlint-disable-next-line promise/no-callback-in-promise
2025-09-09 10:46:02 +02:00
callback ({ video : sources [ 0 ] });
})
. catch (( err ) => {
// If the user cancels the dialog an error occurs "Failed to get sources"
console . error ( "Wayland: failed to get user-selected source:" , err );
2026-07-10 10:47:41 +01:00
// oxlint-disable-next-line promise/no-callback-in-promise
2025-09-09 10:46:02 +02:00
callback ({ video : { id : "" , name : "" } }); // The promise does not return if no dummy is passed here as source
});
} else {
global . mainWindow ? . webContents . send ( "openDesktopCapturerSourcePicker" );
}
setDisplayMediaCallback ( callback );
},
{ useSystemPicker : true },
); // Use Mac OS 15+ native picker
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" , () => {
2026-07-14 15:42:51 +03:00
// If the renderer crashed while the window was hidden (element-web#32222), reload it before showing
// so the user sees the UI rather than the white screen. Routed through the capped recovery (rather
// than an inline reload) so a relaunch can't re-arm a crash loop we've already given up on; it is a
// no-op when the renderer is healthy.
rendererRecovery ? . recoverIfCrashed ();
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 ) {
2026-07-14 15:42:51 +03:00
// If the renderer crashed (element-web#32222), reload before surfacing the window so the user is
// brought to a working UI rather than a white screen. Routed through the capped recovery so a
// relaunch can't re-arm a crash loop we've already given up on; a no-op for a healthy window.
rendererRecovery ? . recoverIfCrashed ();
2022-07-01 20:17:40 +01:00
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 );