Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9e246ab
added proxy support
tim2zg Sep 23, 2025
170423d
Initial plan
Copilot Sep 23, 2025
277d022
Fix TypeScript error by adding type assertion to Store.get result
Copilot Sep 23, 2025
36dae2d
Merge pull request #1 from tim2zg/copilot/fix-17239290-f811-4d26-aaa4…
tim2zg Sep 23, 2025
023c534
Merge pull request #2 from element-hq/develop
tim2zg Sep 29, 2025
4498694
Merge branch 'element-hq:develop' into develop
tim2zg Feb 1, 2026
446fc81
feat(proxy): implement proxy authentication and configuration management
tim2zg Feb 1, 2026
e3f2e73
feat: implement native proxy settings window
tim2zg Feb 1, 2026
27ecd18
feat(proxy): enhance proxy configuration interface with additional fi…
tim2zg Feb 1, 2026
40f87f3
feat(proxy): enhance proxy configuration interface with additional fi…
tim2zg Feb 1, 2026
6d3d6b5
Merge branch 'feat/native-proxy-settings' of https://github.com/tim2z…
tim2zg Feb 2, 2026
c701014
Merge branch 'feat/native-proxy-settings' of https://github.com/tim2z…
tim2zg Feb 2, 2026
2d7bd30
Merge branch 'feat/native-proxy-settings' of https://github.com/tim2z…
tim2zg Feb 2, 2026
ec6b3c7
Merge branch 'feat/native-proxy-settings' of https://github.com/tim2z…
tim2zg Feb 2, 2026
80f721c
feat(proxy): remove unused proxy preload resource from copy script
tim2zg Feb 2, 2026
31b8770
Merge branch 'feat/native-proxy-settings' of https://github.com/tim2z…
tim2zg Feb 2, 2026
e630f29
feat(proxy): remove proxy window resources and add NetworkProxyModal …
tim2zg Feb 5, 2026
925bf9f
Merge branch 'develop' into feat/native-proxy-settings
tim2zg Feb 16, 2026
8146059
chore: Clean up obsolete proxy window files
tim2zg Mar 7, 2026
4dc80f7
Merge branch 'develop' into feat/native-proxy-settings
tim2zg Mar 14, 2026
7b28b23
feat: Improve system proxy reliability and fix password box styling
tim2zg Mar 14, 2026
5447bd7
refactor: Refactor proxy detection and use globalThis
tim2zg Mar 14, 2026
790f2b1
chore: Fix linting issues and redundant type assertions
tim2zg Mar 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scripts/copy-res.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,5 @@ INCLUDE_LANGS.forEach((file): void => {
if (watch) {
INCLUDE_LANGS.forEach((file) => watchLanguage(I18N_BASE_PATH + file, I18N_DEST));
}

// IPC resources
131 changes: 72 additions & 59 deletions src/electron-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import minimist from "minimist";

import "./ipc.js";
import "./seshat.js";
import "./settings.js";
import "./badge.js";
import * as tray from "./tray.js";
import Store from "./store.js";
Expand All @@ -46,13 +45,26 @@ import { setupMacosTitleBar } from "./macos-titlebar.js";
import { type Json, loadJsonFile } from "./utils.js";
import { setupMediaAuth } from "./media-auth.js";
import { readBuildConfig } from "./build-config.js";
import { getLastAppliedConfig } from "./proxy.js";
import { initProxy } from "./settings.js";

const __dirname = dirname(fileURLToPath(import.meta.url));

const argv = minimist(process.argv, {
alias: { help: "h" },
});

app.on("login", (event, _webContents, _request, authInfo, callback) => {
if (authInfo.isProxy) {
const proxyConfig = getLastAppliedConfig();
if (proxyConfig?.mode === "custom" && proxyConfig.username && proxyConfig.password) {
event.preventDefault();
callback(proxyConfig.username, proxyConfig.password);
console.log(`[proxy] Authenticating to ${authInfo.host}:${authInfo.port}`);
}
}
});

if (argv["help"]) {
console.log("Options:");
console.log(" --profile-dir {path}: Path to where to store the profile.");
Expand Down Expand Up @@ -170,7 +182,7 @@ function loadLocalConfigFile(): Json {

let loadConfigPromise: Promise<void> | undefined;
// Loads the config from asar, and applies a config.json from userData atop if one exists
// Writes config to `global.vectorConfig`. Idempotent, returns the same promise on subsequent calls.
// Writes config to `globalThis.vectorConfig`. Idempotent, returns the same promise on subsequent calls.
function loadConfig(): Promise<void> {
if (loadConfigPromise) return loadConfigPromise;

Expand All @@ -179,13 +191,13 @@ function loadConfig(): Promise<void> {

try {
console.log(`Loading app config: ${path.join(asarPath, LocalConfigFilename)}`);
global.vectorConfig = loadJsonFile(asarPath, LocalConfigFilename);
globalThis.vectorConfig = loadJsonFile(asarPath, LocalConfigFilename);
} catch {
// it would be nice to check the error code here and bail if the config
// is unparsable, but we get MODULE_NOT_FOUND in the case of a missing
// file or invalid json, so node is just very unhelpful.
// Continue with the defaults (ie. an empty config)
global.vectorConfig = {};
globalThis.vectorConfig = {};
}

try {
Expand All @@ -197,27 +209,27 @@ function loadConfig(): Promise<void> {
// defined, and panics as a result.
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)
globalThis.vectorConfig = Object.keys(globalThis.vectorConfig)
.filter((k) => !homeserverProps.includes(<any>k))
.reduce(
(obj, key) => {
obj[key] = global.vectorConfig[key];
obj[key] = globalThis.vectorConfig[key];
return obj;
},
{} as Omit<Partial<(typeof global)["vectorConfig"]>, keyof typeof homeserverProps>,
);
}

global.vectorConfig = Object.assign(global.vectorConfig, localConfig);
globalThis.vectorConfig = Object.assign(globalThis.vectorConfig, localConfig);
} catch (e) {
if (e instanceof SyntaxError) {
await app.whenReady();
void dialog.showMessageBox({
type: "error",
title: `Your ${global.vectorConfig.brand || "Element"} is misconfigured`,
title: `Your ${globalThis.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"}.`,
`Your custom ${globalThis.vectorConfig.brand || "Element"} configuration contains invalid JSON. ` +
`Please correct the problem and reopen ${globalThis.vectorConfig.brand || "Element"}.`,
detail: e.message || "",
});
}
Expand All @@ -226,8 +238,8 @@ function loadConfig(): Promise<void> {
}

// Tweak modules paths as they assume the root is at the same level as webapp, but for `vector://vector/webapp` it is not.
if (Array.isArray(global.vectorConfig.modules)) {
global.vectorConfig.modules = global.vectorConfig.modules.map((m) => {
if (Array.isArray(globalThis.vectorConfig.modules)) {
globalThis.vectorConfig.modules = globalThis.vectorConfig.modules.map((m) => {
if (m.startsWith("/")) {
return "/webapp" + m;
}
Expand All @@ -242,7 +254,7 @@ function loadConfig(): Promise<void> {
// 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 || {};
const { dsn, environment } = globalThis.vectorConfig.sentry || {};
if (dsn) {
console.log(`Enabling Sentry with dsn=${dsn} environment=${environment}`);
Sentry.init({
Expand All @@ -261,13 +273,13 @@ async function setupGlobals(): Promise<void> {

// Figure out the tray icon path & brand name
const iconFile = `icon.${process.platform === "win32" ? "ico" : "png"}`;
global.trayConfig = {
globalThis.trayConfig = {
icon_path: path.join(path.dirname(asarPath), "build", iconFile),
brand: global.vectorConfig.brand || "Element",
brand: globalThis.vectorConfig.brand || "Element",
};
}

global.appQuitting = false;
globalThis.appQuitting = false;

const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
(input, platform): boolean => platform !== "darwin" && input.alt && input.key.toUpperCase() === "F4",
Expand Down Expand Up @@ -342,6 +354,7 @@ if (store.get("disableHardwareAcceleration")) {

app.on("ready", async () => {
console.debug("Reached Electron ready state");
await initProxy();

let asarPath: string;

Expand Down Expand Up @@ -424,14 +437,14 @@ app.on("ready", async () => {
// 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");
} else if (global.vectorConfig["update_base_url"]) {
void updater.start(global.vectorConfig["update_base_url"]);
} else if (globalThis.vectorConfig["update_base_url"]) {
void updater.start(globalThis.vectorConfig["update_base_url"]);
} else {
console.log("No update_base_url is defined: auto update is disabled");
}

// Set up i18n before loading storage as we need translations for dialogs
global.appLocalization = new AppLocalization({
globalThis.appLocalization = new AppLocalization({
components: [(): void => tray.initApplicationMenu(), (): void => Menu.setApplicationMenu(buildMenuTemplate())],
store,
});
Expand All @@ -444,14 +457,14 @@ app.on("ready", async () => {

console.debug("Opening main window");
const preloadScript = path.normalize(`${__dirname}/preload.cjs`);
global.mainWindow = new BrowserWindow({
globalThis.mainWindow = new BrowserWindow({
// https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
backgroundColor: "#fff",

titleBarStyle: process.platform === "darwin" ? "hidden" : "default",
trafficLightPosition: { x: 9, y: 8 },

icon: global.trayConfig.icon_path,
icon: globalThis.trayConfig.icon_path,
show: false,
autoHideMenuBar: store.get("autoHideMenuBar"),

Expand All @@ -468,47 +481,47 @@ app.on("ready", async () => {
},
});

global.mainWindow.setContentProtection(store.get("enableContentProtection"));
globalThis.mainWindow.setContentProtection(store.get("enableContentProtection"));

try {
console.debug("Ensuring storage is ready");
if (!(await store.prepareSafeStorage(global.mainWindow.webContents.session))) return;
if (!(await store.prepareSafeStorage(globalThis.mainWindow.webContents.session))) return;
} catch (e) {
console.error(e);
app.exit(1);
}

void global.mainWindow.loadURL("vector://vector/webapp/");
globalThis.mainWindow.loadURL("vector://vector/webapp/");

if (process.platform === "darwin") {
setupMacosTitleBar(global.mainWindow);
setupMacosTitleBar(globalThis.mainWindow);
}

// Handle spellchecker
// For some reason spellCheckerEnabled isn't persisted, so we have to use the store here
global.mainWindow.webContents.session.setSpellCheckerEnabled(store.get("spellCheckerEnabled", true));
globalThis.mainWindow.webContents.session.setSpellCheckerEnabled(store.get("spellCheckerEnabled", true));

// Create trayIcon icon
if (store.get("minimizeToTray")) tray.create(global.trayConfig);
if (store.get("minimizeToTray")) tray.create(globalThis.trayConfig);

global.mainWindow.once("ready-to-show", () => {
if (!global.mainWindow) return;
mainWindowState.manage(global.mainWindow);
globalThis.mainWindow.once("ready-to-show", () => {
if (!globalThis.mainWindow) return;
mainWindowState.manage(globalThis.mainWindow);

if (!argv["hidden"]) {
global.mainWindow.show();
globalThis.mainWindow.show();
} else {
// hide here explicitly because window manage above sometimes shows it
global.mainWindow.hide();
globalThis.mainWindow.hide();
}
});

global.mainWindow.webContents.on("before-input-event", (event: Event, input: Input): void => {
globalThis.mainWindow.webContents.on("before-input-event", (event: Event, input: Input): void => {
const exitShortcutPressed =
input.type === "keyDown" && exitShortcuts.some((shortcutFn) => shortcutFn(input, process.platform));

// We only care about the exit shortcuts here
if (!exitShortcutPressed || !global.mainWindow) return;
if (!exitShortcutPressed || !globalThis.mainWindow) return;

// Prevent the default behaviour
event.preventDefault();
Expand All @@ -517,12 +530,12 @@ app.on("ready", async () => {
const shouldWarnBeforeExit = store.get("warnBeforeExit", true);
if (shouldWarnBeforeExit) {
const shouldCancelCloseRequest =
dialog.showMessageBoxSync(global.mainWindow, {
dialog.showMessageBoxSync(globalThis.mainWindow, {
type: "question",
buttons: [
_t("action|cancel"),
_t("action|close_brand", {
brand: global.vectorConfig.brand || "Element",
brand: globalThis.vectorConfig.brand || "Element",
}),
],
message: _t("confirm_quit"),
Expand All @@ -536,23 +549,23 @@ app.on("ready", async () => {
app.exit();
});

global.mainWindow.on("closed", () => {
global.mainWindow = null;
globalThis.mainWindow.on("closed", () => {
globalThis.mainWindow = null;
});
global.mainWindow.on("close", async (e) => {
globalThis.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 (!globalThis.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());
if (globalThis.mainWindow?.isFullScreen()) {
globalThis.mainWindow.once("leave-full-screen", () => globalThis.mainWindow?.hide());

global.mainWindow.setFullScreen(false);
globalThis.mainWindow.setFullScreen(false);
} else {
global.mainWindow?.hide();
globalThis.mainWindow?.hide();
}

return false;
Expand All @@ -561,16 +574,16 @@ app.on("ready", async () => {

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.webContents.goBack();
} else if (cmd === "browser-forward" && global.mainWindow?.webContents.canGoForward()) {
global.mainWindow.webContents.goForward();
globalThis.mainWindow.on("app-command", (e, cmd) => {
if (cmd === "browser-backward" && globalThis.mainWindow?.webContents.canGoBack()) {
globalThis.mainWindow.webContents.goBack();
} else if (cmd === "browser-forward" && globalThis.mainWindow?.webContents.canGoForward()) {
globalThis.mainWindow.webContents.goForward();
}
});
}

webContentsHandler(global.mainWindow.webContents);
webContentsHandler(globalThis.mainWindow.webContents);

session.defaultSession.setDisplayMediaRequestHandler(
(_, callback) => {
Expand All @@ -588,27 +601,27 @@ app.on("ready", async () => {
callback({ video: { id: "", name: "" } }); // The promise does not return if no dummy is passed here as source
});
} else {
global.mainWindow?.webContents.send("openDesktopCapturerSourcePicker");
globalThis.mainWindow?.webContents.send("openDesktopCapturerSourcePicker");
}
setDisplayMediaCallback(callback);
},
{ useSystemPicker: true },
); // Use Mac OS 15+ native picker

setupMediaAuth(global.mainWindow);
setupMediaAuth(globalThis.mainWindow);
});

app.on("window-all-closed", () => {
app.quit();
});

app.on("activate", () => {
global.mainWindow?.show();
globalThis.mainWindow?.show();
});

function beforeQuit(): void {
global.appQuitting = true;
global.mainWindow?.webContents.send("before-quit");
globalThis.appQuitting = true;
globalThis.mainWindow?.webContents.send("before-quit");
}

app.on("before-quit", beforeQuit);
Expand All @@ -619,10 +632,10 @@ app.on("second-instance", (ev, commandLine, workingDirectory) => {
if (commandLine.includes("--hidden")) return;

// Someone tried to run a second instance, we should focus our window.
if (global.mainWindow) {
if (!global.mainWindow.isVisible()) global.mainWindow.show();
if (global.mainWindow.isMinimized()) global.mainWindow.restore();
global.mainWindow.focus();
if (globalThis.mainWindow) {
if (!globalThis.mainWindow.isVisible()) globalThis.mainWindow.show();
if (globalThis.mainWindow.isMinimized()) globalThis.mainWindow.restore();
globalThis.mainWindow.focus();
}
});

Expand Down
17 changes: 17 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@
"save_image_as_error_description": "The image failed to save",
"save_image_as_error_title": "Failed to save image"
},
"proxy": {
"bypass": "No proxy for (comma separated)",
"cancel": "Cancel",
"close": "Close",
"host": "Proxy Host",
"mode_custom": "Manual configuration",
"mode_direct": "No proxy (direct connection)",
"mode_system": "Use system proxy settings",
"password": "Password",
"password_help": "Configuration is encrypted using the system's secure storage.",
"port": "Port",
"protocol": "Protocol",
"save": "Save",
"title": "Network Proxy",
"updates_warning": "Note: These settings may not apply to application updates.",
"username": "Username"
},
"store": {
"error": {
"backend_changed": "Clear data and reload?",
Expand Down
Loading
Loading