Fix handling of deeplinks on Element Desktop (#33827)
* Fix desktop registering protocol handler wrong Was previously registering with too many args and was also only handling deeplinks if the app was already open, on a cold start they would be blindly ignored. Tests aplenty * Rename field * Move protocolHandler initialisation to after mainWindow is navigating * Add comment * Avoid double call to loadURL
This commit is contained in:
@@ -0,0 +1,323 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, describe, it, beforeEach, vi } from "vitest";
|
||||||
|
import { fs as memfs, vol } from "memfs";
|
||||||
|
import { app } from "electron";
|
||||||
|
|
||||||
|
import { type Args, getArgs, getArgsForProtocolRegistration } from "./args.js";
|
||||||
|
import type ProtocolHandler from "./protocol.js";
|
||||||
|
|
||||||
|
vi.mock("node:fs", () => ({ default: memfs }));
|
||||||
|
vi.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getPath: vi.fn().mockImplementation((dirName) => {
|
||||||
|
if (dirName === "userData") return "/Users/name/Library/Application Support/Element";
|
||||||
|
if (dirName === "appData") return "/Users/name/Library/Application Support";
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}),
|
||||||
|
getName: vi.fn().mockReturnValue("Element"),
|
||||||
|
exit: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset the state of the in-memory fs
|
||||||
|
vol.reset();
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getArgsForProtocolRegistration", () => {
|
||||||
|
it("should return an empty array for default args", () => {
|
||||||
|
expect(
|
||||||
|
getArgsForProtocolRegistration({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: [],
|
||||||
|
}),
|
||||||
|
).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle standard args", () => {
|
||||||
|
expect(
|
||||||
|
getArgsForProtocolRegistration({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Custom",
|
||||||
|
localConfigPath: "/root/config.json",
|
||||||
|
devtools: true,
|
||||||
|
update: false,
|
||||||
|
hidden: false,
|
||||||
|
positional: [],
|
||||||
|
}),
|
||||||
|
).toStrictEqual([
|
||||||
|
"--no-update",
|
||||||
|
"--config",
|
||||||
|
"/root/config.json",
|
||||||
|
"--profile-dir",
|
||||||
|
"/Users/name/Library/Application Support/Custom",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore hidden=true", () => {
|
||||||
|
expect(
|
||||||
|
getArgsForProtocolRegistration({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: true,
|
||||||
|
positional: [],
|
||||||
|
}),
|
||||||
|
).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore positional args", () => {
|
||||||
|
expect(
|
||||||
|
getArgsForProtocolRegistration({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["element://foobar"],
|
||||||
|
}),
|
||||||
|
).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getArgs", () => {
|
||||||
|
function run(...args: string[]): Args {
|
||||||
|
vi.spyOn(process, "argv", "get").mockReturnValue(["/path/to/app", ...args]);
|
||||||
|
const mockProtocolHandler = {
|
||||||
|
getProfileFromDeeplink: vi.fn(),
|
||||||
|
} as unknown as ProtocolHandler;
|
||||||
|
return getArgs(mockProtocolHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should handle '--help'", () => {
|
||||||
|
const args = run("--help");
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
});
|
||||||
|
expect(app.exit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle no command line args", () => {
|
||||||
|
const args = run();
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
});
|
||||||
|
expect(app.exit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle '--hidden'", () => {
|
||||||
|
const args = run("--hidden");
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: true,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle '--no-update'", () => {
|
||||||
|
const args = run("--no-update");
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: false,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("storageMode", () => {
|
||||||
|
it("should handle valid '--storage-mode'", () => {
|
||||||
|
const args = run("--storage-mode=force-plaintext");
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
storageMode: "force-plaintext",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore invalid '--storage-mode'", () => {
|
||||||
|
const args = run("--storage-mode=magic");
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
storageMode: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("userDataPath", () => {
|
||||||
|
it("should handle deeplinks", () => {
|
||||||
|
vi.spyOn(process, "argv", "get").mockReturnValue([
|
||||||
|
"/path/to/app",
|
||||||
|
"protocol:/#state=foo:element-desktop-ssoid:XXYYZZ&code=bar",
|
||||||
|
]);
|
||||||
|
const mockProtocolHandler = {
|
||||||
|
getProfileFromDeeplink: vi.fn().mockReturnValue("/path/to/deeplinked-profile"),
|
||||||
|
} as unknown as ProtocolHandler;
|
||||||
|
const args = getArgs(mockProtocolHandler);
|
||||||
|
|
||||||
|
expect(mockProtocolHandler.getProfileFromDeeplink).toHaveBeenCalledWith(process.argv);
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/path/to/deeplinked-profile",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: [...process.argv],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle '--profile-dir'", () => {
|
||||||
|
const args = run("--profile-dir", "/path/to/profile");
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/path/to/profile",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle '--profile'", () => {
|
||||||
|
const args = run("--profile", "work");
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element-work",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle ELEMENT_PROFILE_DIR", () => {
|
||||||
|
vi.spyOn(process, "env", "get").mockReturnValue({
|
||||||
|
ELEMENT_PROFILE_DIR: "/mnt/foo/profile",
|
||||||
|
});
|
||||||
|
const args = run();
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/mnt/foo/profile",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer deeplink over ELEMENT_PROFILE_DIR", () => {
|
||||||
|
vi.spyOn(process, "argv", "get").mockReturnValue(["/path/to/app", "protocol:/#state=foo&code=bar"]);
|
||||||
|
vi.spyOn(process, "env", "get").mockReturnValue({
|
||||||
|
ELEMENT_PROFILE_DIR: "/mnt/foo/profile",
|
||||||
|
});
|
||||||
|
const mockProtocolHandler = {
|
||||||
|
getProfileFromDeeplink: vi.fn().mockReturnValue("/path/to/deeplinked-profile"),
|
||||||
|
} as unknown as ProtocolHandler;
|
||||||
|
const args = getArgs(mockProtocolHandler);
|
||||||
|
|
||||||
|
expect(mockProtocolHandler.getProfileFromDeeplink).toHaveBeenCalledWith(process.argv);
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/path/to/deeplinked-profile",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: [...process.argv],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should combine ELEMENT_PROFILE_DIR with '--profile'", () => {
|
||||||
|
vi.spyOn(process, "env", "get").mockReturnValue({
|
||||||
|
ELEMENT_PROFILE_DIR: "/mnt/foo/profile",
|
||||||
|
});
|
||||||
|
const args = run("--profile", "play");
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/mnt/foo/profile-play",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle old Riot data dirs", () => {
|
||||||
|
vol.fromJSON({
|
||||||
|
"/Users/name/Library/Application Support/Riot/IndexedDB": "This is a real IDB. I promise.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const args = run();
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Riot",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("localConfigPath", () => {
|
||||||
|
it("should handle '--config'", () => {
|
||||||
|
const args = run("--config", "/path/to/config.json");
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
localConfigPath: "/path/to/config.json",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle ELEMENT_DESKTOP_CONFIG_JSON", () => {
|
||||||
|
vi.spyOn(process, "env", "get").mockReturnValue({
|
||||||
|
ELEMENT_DESKTOP_CONFIG_JSON: "/path/for/config.json",
|
||||||
|
});
|
||||||
|
const args = run();
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
localConfigPath: "/path/for/config.json",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer arg over env", () => {
|
||||||
|
vi.spyOn(process, "env", "get").mockReturnValue({
|
||||||
|
ELEMENT_DESKTOP_CONFIG_JSON: "/path/for/config.json",
|
||||||
|
});
|
||||||
|
const args = run("--config", "/path/to/config.json");
|
||||||
|
expect(args).toEqual({
|
||||||
|
userDataPath: "/Users/name/Library/Application Support/Element",
|
||||||
|
devtools: false,
|
||||||
|
update: true,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/path/to/app"],
|
||||||
|
localConfigPath: "/path/to/config.json",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import minimist, { type ParsedArgs } from "minimist";
|
||||||
|
import { app } from "electron";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { Mode } from "./store.js";
|
||||||
|
import type ProtocolHandler from "./protocol.js";
|
||||||
|
|
||||||
|
const defaultUserDataDir = app.getPath("userData");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the command line arguments to include in the protocol registration,
|
||||||
|
* some parameters, e.g. '--hidden' are omitted as it'd cause the app to not be focused.
|
||||||
|
* Excludes all positional arguments as those are only relevant once, e.g. for OIDC auth callbacks.
|
||||||
|
* Includes unknown parameters as they are sometimes handled by Electron, e.g. `--proxy`.
|
||||||
|
* @param parsedArgs - the args the application was started with
|
||||||
|
*/
|
||||||
|
export function getArgsForProtocolRegistration(parsedArgs: Args): string[] {
|
||||||
|
const args: string[] = [];
|
||||||
|
|
||||||
|
if (!parsedArgs.update) {
|
||||||
|
args.push("--no-update");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedArgs.localConfigPath) {
|
||||||
|
args.push("--config", parsedArgs.localConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedArgs.userDataPath != defaultUserDataDir) {
|
||||||
|
args.push("--profile-dir", parsedArgs.userDataPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element Desktop launch args, returned by {@link getArgs}
|
||||||
|
*/
|
||||||
|
export interface Args {
|
||||||
|
/**
|
||||||
|
* Path to user data, root of all persistent data for this profile.
|
||||||
|
*/
|
||||||
|
userDataPath: string;
|
||||||
|
/**
|
||||||
|
* Path to local override config.json file.
|
||||||
|
*/
|
||||||
|
localConfigPath?: string;
|
||||||
|
/**
|
||||||
|
* The store {@link Mode} to use.
|
||||||
|
*/
|
||||||
|
storageMode?: Mode;
|
||||||
|
/**
|
||||||
|
* Whether to install devtools.
|
||||||
|
*/
|
||||||
|
devtools: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to start the auto-updater.
|
||||||
|
*/
|
||||||
|
update: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to start the app hidden.
|
||||||
|
*/
|
||||||
|
hidden: boolean;
|
||||||
|
/**
|
||||||
|
* Additional positional arguments found.
|
||||||
|
*/
|
||||||
|
positional: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
function isRealUserDataDir(d: string): boolean {
|
||||||
|
return fs.existsSync(path.join(d, "IndexedDB"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserDataPath(argv: ParsedArgs, protocolHandler: ProtocolHandler): string {
|
||||||
|
// check if we are passed a profile in the SSO callback url
|
||||||
|
const userDataPathInProtocol = protocolHandler.getProfileFromDeeplink(argv["_"]);
|
||||||
|
if (userDataPathInProtocol) {
|
||||||
|
return userDataPathInProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argv["profile-dir"]) {
|
||||||
|
return argv["profile-dir"];
|
||||||
|
}
|
||||||
|
|
||||||
|
let newUserDataPath = process.env.ELEMENT_PROFILE_DIR ?? defaultUserDataDir;
|
||||||
|
if (argv["profile"]) {
|
||||||
|
newUserDataPath += "-" + argv["profile"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUserDataPathExists = isRealUserDataDir(newUserDataPath);
|
||||||
|
let oldUserDataPath = path.join(app.getPath("appData"), app.getName().replace("Element", "Riot"));
|
||||||
|
if (argv["profile"]) {
|
||||||
|
oldUserDataPath += "-" + argv["profile"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldUserDataPathExists = isRealUserDataDir(oldUserDataPath);
|
||||||
|
console.log(`${newUserDataPath} exists: ${newUserDataPathExists ? "yes" : "no"}`);
|
||||||
|
console.log(`${oldUserDataPath} exists: ${oldUserDataPathExists ? "yes" : "no"}`);
|
||||||
|
|
||||||
|
if (!newUserDataPathExists && oldUserDataPathExists) {
|
||||||
|
console.log(`Using legacy user data path: ${oldUserDataPath}`);
|
||||||
|
return oldUserDataPath;
|
||||||
|
}
|
||||||
|
return newUserDataPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses command line arguments and handles the `--help` flag.
|
||||||
|
* If `--help` is present, it prints usage information and exits the application.
|
||||||
|
* Must be called before Electron's userData is set.
|
||||||
|
*/
|
||||||
|
export function getArgs(protocolHandler: ProtocolHandler): Args {
|
||||||
|
const argv = minimist(process.argv, {
|
||||||
|
alias: { help: "h" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (argv["help"]) {
|
||||||
|
console.log("Options:");
|
||||||
|
console.log(" --profile-dir {path}: Path to where to store the profile.");
|
||||||
|
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.`,
|
||||||
|
);
|
||||||
|
console.log(" --devtools: Install and use react-devtools and react-perf.");
|
||||||
|
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 '${defaultUserDataDir}'`,
|
||||||
|
);
|
||||||
|
console.log(" --no-update: Disable automatic updating.");
|
||||||
|
console.log(" --hidden: Start the application hidden in the system tray.");
|
||||||
|
console.log(" --help: Displays this help message.");
|
||||||
|
console.log("And more such as --proxy, see: https://electronjs.org/docs/api/command-line-switches");
|
||||||
|
app.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
let storageMode: Mode | undefined;
|
||||||
|
if ([Mode.Encrypted, Mode.ForcePlaintext, Mode.AllowPlaintext].includes(argv["storage-mode"])) {
|
||||||
|
storageMode = argv["storage-mode"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userDataPath: getUserDataPath(argv, protocolHandler),
|
||||||
|
localConfigPath: argv["config"] ?? process.env.ELEMENT_DESKTOP_CONFIG_JSON,
|
||||||
|
storageMode,
|
||||||
|
devtools: argv["devtools"] || false,
|
||||||
|
// Minimist parses `--no-`-prefixed arguments as booleans with value `false` rather than verbatim.
|
||||||
|
update: argv["update"] ?? true,
|
||||||
|
hidden: argv["hidden"] || false,
|
||||||
|
positional: argv["_"],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -26,9 +26,7 @@ import {
|
|||||||
import * as Sentry from "@sentry/electron/main";
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import path, { dirname } from "node:path";
|
import path, { dirname } from "node:path";
|
||||||
import windowStateKeeper from "electron-window-state";
|
import windowStateKeeper from "electron-window-state";
|
||||||
import fs from "node:fs";
|
|
||||||
import { URL, fileURLToPath } from "node:url";
|
import { URL, fileURLToPath } from "node:url";
|
||||||
import minimist from "minimist";
|
|
||||||
|
|
||||||
import "./ipc.js";
|
import "./ipc.js";
|
||||||
import "./seshat.js";
|
import "./seshat.js";
|
||||||
@@ -48,77 +46,18 @@ import { setupMediaAuth } from "./media-auth.js";
|
|||||||
import { getBuildConfig } from "./build-config.js";
|
import { getBuildConfig } from "./build-config.js";
|
||||||
import { getAsarPath } from "./asar.js";
|
import { getAsarPath } from "./asar.js";
|
||||||
import { getIconPath } from "./icon.js";
|
import { getIconPath } from "./icon.js";
|
||||||
|
import { getArgs } from "./args.js";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const argv = minimist(process.argv, {
|
|
||||||
alias: { help: "h" },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (argv["help"]) {
|
|
||||||
console.log("Options:");
|
|
||||||
console.log(" --profile-dir {path}: Path to where to store the profile.");
|
|
||||||
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.`,
|
|
||||||
);
|
|
||||||
console.log(" --devtools: Install and use react-devtools and react-perf.");
|
|
||||||
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")}'`,
|
|
||||||
);
|
|
||||||
console.log(" --no-update: Disable automatic updating.");
|
|
||||||
console.log(" --hidden: Start the application hidden in the system tray.");
|
|
||||||
console.log(" --help: Displays this help message.");
|
|
||||||
console.log("And more such as --proxy, see: https://electronjs.org/docs/api/command-line-switches");
|
|
||||||
app.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const LocalConfigLocation = process.env.ELEMENT_DESKTOP_CONFIG_JSON ?? argv["config"];
|
|
||||||
const LocalConfigFilename = "config.json";
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
function isRealUserDataDir(d: string): boolean {
|
|
||||||
return fs.existsSync(path.join(d, "IndexedDB"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildConfig = getBuildConfig();
|
const buildConfig = getBuildConfig();
|
||||||
const protocolHandler = new ProtocolHandler(buildConfig.protocol);
|
const protocolHandler = new ProtocolHandler(buildConfig.protocol);
|
||||||
|
const args = getArgs(protocolHandler);
|
||||||
|
|
||||||
// check if we are passed a profile in the SSO callback url
|
const LocalConfigLocation = args.localConfigPath;
|
||||||
let userDataPath: string;
|
const LocalConfigFilename = "config.json";
|
||||||
|
|
||||||
const userDataPathInProtocol = protocolHandler.getProfileFromDeeplink(argv["_"]);
|
app.setPath("userData", args.userDataPath);
|
||||||
if (userDataPathInProtocol) {
|
|
||||||
userDataPath = userDataPathInProtocol;
|
|
||||||
} else if (argv["profile-dir"]) {
|
|
||||||
userDataPath = argv["profile-dir"];
|
|
||||||
} else {
|
|
||||||
let newUserDataPath = process.env.ELEMENT_PROFILE_DIR ?? app.getPath("userData");
|
|
||||||
if (argv["profile"]) {
|
|
||||||
newUserDataPath += "-" + argv["profile"];
|
|
||||||
}
|
|
||||||
const newUserDataPathExists = isRealUserDataDir(newUserDataPath);
|
|
||||||
let oldUserDataPath = path.join(app.getPath("appData"), app.getName().replace("Element", "Riot"));
|
|
||||||
if (argv["profile"]) {
|
|
||||||
oldUserDataPath += "-" + argv["profile"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldUserDataPathExists = isRealUserDataDir(oldUserDataPath);
|
|
||||||
console.log(newUserDataPath + " exists: " + (newUserDataPathExists ? "yes" : "no"));
|
|
||||||
console.log(oldUserDataPath + " exists: " + (oldUserDataPathExists ? "yes" : "no"));
|
|
||||||
if (!newUserDataPathExists && oldUserDataPathExists) {
|
|
||||||
console.log("Using legacy user data path: " + oldUserDataPath);
|
|
||||||
userDataPath = oldUserDataPath;
|
|
||||||
} else {
|
|
||||||
userDataPath = newUserDataPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
app.setPath("userData", userDataPath);
|
|
||||||
|
|
||||||
const homeserverProps = ["default_is_url", "default_hs_url", "default_server_name", "default_server_config"] as const;
|
const homeserverProps = ["default_is_url", "default_hs_url", "default_server_name", "default_server_config"] as const;
|
||||||
|
|
||||||
@@ -251,9 +190,6 @@ if (!gotLock) {
|
|||||||
app.exit();
|
app.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// do this after we know we are the primary instance of the app
|
|
||||||
protocolHandler.initialise(userDataPath);
|
|
||||||
|
|
||||||
// Register the scheme the app is served from as 'standard'
|
// Register the scheme the app is served from as 'standard'
|
||||||
// which allows things like relative URLs and IndexedDB to
|
// which allows things like relative URLs and IndexedDB to
|
||||||
// work.
|
// work.
|
||||||
@@ -284,7 +220,7 @@ app.enableSandbox();
|
|||||||
// We disable media controls here. We do this because calls use audio and video elements and they sometimes capture the media keys. See https://github.com/vector-im/element-web/issues/15704
|
// We disable media controls here. We do this because calls use audio and video elements and they sometimes capture the media keys. See https://github.com/vector-im/element-web/issues/15704
|
||||||
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");
|
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");
|
||||||
|
|
||||||
const store = Store.initialize(argv["storage-mode"]); // must be called before any async actions
|
const store = Store.initialize(args.storageMode); // must be called before any async actions
|
||||||
|
|
||||||
// Disable hardware acceleration if the setting has been set.
|
// Disable hardware acceleration if the setting has been set.
|
||||||
if (store.get("disableHardwareAcceleration")) {
|
if (store.get("disableHardwareAcceleration")) {
|
||||||
@@ -310,7 +246,7 @@ app.on("ready", async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (argv["devtools"]) {
|
if (args.devtools) {
|
||||||
try {
|
try {
|
||||||
const { installExtension, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer");
|
const { installExtension, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer");
|
||||||
installExtension(REACT_DEVELOPER_TOOLS)
|
installExtension(REACT_DEVELOPER_TOOLS)
|
||||||
@@ -373,8 +309,7 @@ app.on("ready", async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Minimist parses `--no-`-prefixed arguments as booleans with value `false` rather than verbatim.
|
if (!args.update) {
|
||||||
if (argv["update"] === false) {
|
|
||||||
console.log("Auto update disabled via command line flag");
|
console.log("Auto update disabled via command line flag");
|
||||||
} else if (global.vectorConfig["update_base_url"]) {
|
} else if (global.vectorConfig["update_base_url"]) {
|
||||||
void updater.start(global.vectorConfig["update_base_url"]);
|
void updater.start(global.vectorConfig["update_base_url"]);
|
||||||
@@ -430,7 +365,11 @@ app.on("ready", async () => {
|
|||||||
app.exit(1);
|
app.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void global.mainWindow.loadURL("vector://vector/webapp/");
|
// 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/");
|
||||||
|
}
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
setupMacosTitleBar(global.mainWindow);
|
setupMacosTitleBar(global.mainWindow);
|
||||||
@@ -447,7 +386,7 @@ app.on("ready", async () => {
|
|||||||
if (!global.mainWindow) return;
|
if (!global.mainWindow) return;
|
||||||
mainWindowState.manage(global.mainWindow);
|
mainWindowState.manage(global.mainWindow);
|
||||||
|
|
||||||
if (!argv["hidden"]) {
|
if (!args.hidden) {
|
||||||
global.mainWindow.show();
|
global.mainWindow.show();
|
||||||
} else {
|
} else {
|
||||||
// hide here explicitly because window manage above sometimes shows it
|
// hide here explicitly because window manage above sometimes shows it
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { expect, describe, it, beforeEach, vi } from "vitest";
|
import { expect, describe, it, beforeEach, vi } from "vitest";
|
||||||
import { fs as memfs, vol } from "memfs";
|
import { fs as memfs, vol } from "memfs";
|
||||||
|
import EventEmitter from "node:events";
|
||||||
|
import { app } from "electron";
|
||||||
|
|
||||||
import ProtocolHandler from "./protocol.js";
|
import ProtocolHandler from "./protocol.js";
|
||||||
|
|
||||||
@@ -15,34 +17,45 @@ const TEST_SESSION_ID = "test_session_id";
|
|||||||
const USER_DATA_DIR = "/Users/name/Library/Application Support/Element";
|
const USER_DATA_DIR = "/Users/name/Library/Application Support/Element";
|
||||||
|
|
||||||
vi.mock("node:fs", () => ({ default: memfs }));
|
vi.mock("node:fs", () => ({ default: memfs }));
|
||||||
vi.mock("electron", () => ({
|
vi.mock("electron", () => {
|
||||||
app: {
|
const emitter = new EventEmitter();
|
||||||
getPath: vi.fn().mockReturnValue("/Users/name/Library/Application Support/Element"),
|
|
||||||
on: vi.fn(),
|
return {
|
||||||
},
|
app: {
|
||||||
ipcMain: {
|
isPackaged: true,
|
||||||
handle: vi.fn(),
|
getPath: vi.fn().mockReturnValue("/Users/name/Library/Application Support/Element"),
|
||||||
},
|
getAppPath: vi.fn().mockReturnValue("/bin/element-desktop"),
|
||||||
}));
|
setAsDefaultProtocolClient: vi.fn(),
|
||||||
|
on: emitter.on.bind(emitter),
|
||||||
|
emit: emitter.emit.bind(emitter),
|
||||||
|
removeAllListeners: emitter.removeAllListeners.bind(emitter),
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
handle: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset the state of the in-memory fs
|
// Reset the state of the in-memory fs
|
||||||
vol.reset();
|
vol.reset();
|
||||||
|
// Clear the event emitter
|
||||||
|
app.removeAllListeners();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ProtocolHandler", () => {
|
describe("ProtocolHandler", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vol.fromJSON(
|
||||||
|
{
|
||||||
|
"./sso-sessions.json": JSON.stringify({ [TEST_SESSION_ID]: USER_DATA_DIR }),
|
||||||
|
},
|
||||||
|
USER_DATA_DIR,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe("getProfileFromDeeplink", () => {
|
describe("getProfileFromDeeplink", () => {
|
||||||
const handler = new ProtocolHandler(TEST_PROTOCOL);
|
const handler = new ProtocolHandler(TEST_PROTOCOL);
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vol.fromJSON(
|
|
||||||
{
|
|
||||||
"./sso-sessions.json": JSON.stringify({ [TEST_SESSION_ID]: USER_DATA_DIR }),
|
|
||||||
},
|
|
||||||
USER_DATA_DIR,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle legacy SSO URIs", () => {
|
it("should handle legacy SSO URIs", () => {
|
||||||
expect(
|
expect(
|
||||||
handler.getProfileFromDeeplink([
|
handler.getProfileFromDeeplink([
|
||||||
@@ -84,4 +97,77 @@ describe("ProtocolHandler", () => {
|
|||||||
expect(handler.getProfileFromDeeplink(["Element.app", `test.unrelated:/vector/webapp/`])).toBeUndefined();
|
expect(handler.getProfileFromDeeplink(["Element.app", `test.unrelated:/vector/webapp/`])).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(["darwin", "linux", "win32"] as const)("should handle deeplink on %s", (platform) => {
|
||||||
|
vi.spyOn(process, "platform", "get").mockReturnValue(platform);
|
||||||
|
vi.stubGlobal("mainWindow", {
|
||||||
|
loadURL: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handler = new ProtocolHandler(TEST_PROTOCOL);
|
||||||
|
expect(handler).toBeTruthy();
|
||||||
|
|
||||||
|
const incomingUri = "test.proto:/#/room/#matrix:matrix.org";
|
||||||
|
const expectedUri = "vector://vector/webapp/#/room/#matrix:matrix.org";
|
||||||
|
|
||||||
|
if (platform === "darwin") {
|
||||||
|
app.emit("open-url", new Event("test"), incomingUri);
|
||||||
|
} else {
|
||||||
|
app.emit("second-instance", new Event("test"), ["/path/to/app", incomingUri]);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(global.mainWindow!.loadURL).toHaveBeenCalledWith(expectedUri);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should safely deal with wrong protocol deeplinks", () => {
|
||||||
|
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
||||||
|
vi.stubGlobal("mainWindow", {
|
||||||
|
loadURL: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handler = new ProtocolHandler(TEST_PROTOCOL);
|
||||||
|
expect(handler).toBeTruthy();
|
||||||
|
|
||||||
|
app.emit("open-url", new Event("test"), "random.proto:/#/room/#matrix:matrix.org");
|
||||||
|
|
||||||
|
expect(global.mainWindow!.loadURL).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("initialise", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(process, "execPath", "get").mockReturnValue("/bin/element-desktop");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set as default protocol client", () => {
|
||||||
|
const handler = new ProtocolHandler(TEST_PROTOCOL);
|
||||||
|
handler.initialise({
|
||||||
|
userDataPath: USER_DATA_DIR,
|
||||||
|
devtools: false,
|
||||||
|
update: false,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/bin/element-desktop"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const args = ["--no-update"];
|
||||||
|
expect(app.setAsDefaultProtocolClient).toHaveBeenCalledWith(TEST_PROTOCOL, "/bin/element-desktop", args);
|
||||||
|
expect(app.setAsDefaultProtocolClient).toHaveBeenCalledWith("element", "/bin/element-desktop", args);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle deeplink", () => {
|
||||||
|
vi.stubGlobal("mainWindow", {
|
||||||
|
loadURL: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handler = new ProtocolHandler(TEST_PROTOCOL);
|
||||||
|
handler.initialise({
|
||||||
|
userDataPath: "/data",
|
||||||
|
devtools: false,
|
||||||
|
update: false,
|
||||||
|
hidden: false,
|
||||||
|
positional: ["/bin/element-desktop", "test.proto:/#/room/#matrix:matrix.org"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(global.mainWindow!.loadURL).toHaveBeenCalledWith("vector://vector/webapp/#/room/#matrix:matrix.org");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import path from "node:path";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
|
import { type Args, getArgsForProtocolRegistration } from "./args.js";
|
||||||
|
|
||||||
const LEGACY_PROTOCOL = "element";
|
const LEGACY_PROTOCOL = "element";
|
||||||
const SEARCH_PARAM = "element-desktop-ssoid";
|
const SEARCH_PARAM = "element-desktop-ssoid";
|
||||||
const STORE_FILE_NAME = "sso-sessions.json";
|
const STORE_FILE_NAME = "sso-sessions.json";
|
||||||
@@ -24,32 +26,19 @@ export default class ProtocolHandler {
|
|||||||
private readonly sessionId: string;
|
private readonly sessionId: string;
|
||||||
|
|
||||||
public constructor(private readonly protocol: string) {
|
public constructor(private readonly protocol: string) {
|
||||||
// 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(this.protocol, process.execPath, args);
|
|
||||||
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, 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(this.protocol, process.execPath, [app.getAppPath(), ...args]);
|
|
||||||
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, process.execPath, [app.getAppPath(), ...args]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
// Protocol handler for macos
|
// Protocol handler for macos
|
||||||
app.on("open-url", (ev, url) => {
|
app.on("open-url", (ev, url) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.processUrl(url);
|
this.handleDeeplink(url);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Protocol handler for win32/Linux
|
// Protocol handler for win32/Linux
|
||||||
app.on("second-instance", (ev, commandLine) => {
|
app.on("second-instance", (ev, commandLine) => {
|
||||||
const url = commandLine[commandLine.length - 1];
|
const url = commandLine.at(-1);
|
||||||
if (!url.startsWith(`${this.protocol}:/`) && !url.startsWith(`${LEGACY_PROTOCOL}://`)) return;
|
if (url && this.checkArgIsUrl(url)) {
|
||||||
this.processUrl(url);
|
this.handleDeeplink(url);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +48,23 @@ export default class ProtocolHandler {
|
|||||||
ipcMain.handle("getProtocol", this.onGetProtocol);
|
ipcMain.handle("getProtocol", this.onGetProtocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private checkArgIsUrl = (arg: string): boolean => {
|
||||||
|
return arg.startsWith(`${this.protocol}:/`) || arg.startsWith(`${LEGACY_PROTOCOL}://`);
|
||||||
|
};
|
||||||
|
|
||||||
|
private setAsDefaultProtocolClient(parsedArgs: Args): void {
|
||||||
|
const args = getArgsForProtocolRegistration(parsedArgs);
|
||||||
|
if (app.isPackaged) {
|
||||||
|
app.setAsDefaultProtocolClient(this.protocol, process.execPath, args);
|
||||||
|
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, 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(this.protocol, process.execPath, [app.getAppPath(), ...args]);
|
||||||
|
app.setAsDefaultProtocolClient(LEGACY_PROTOCOL, process.execPath, [app.getAppPath(), ...args]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly onGetProtocol = (): { protocol: string; sessionId: string } => {
|
private readonly onGetProtocol = (): { protocol: string; sessionId: string } => {
|
||||||
return {
|
return {
|
||||||
protocol: this.protocol,
|
protocol: this.protocol,
|
||||||
@@ -66,8 +72,8 @@ export default class ProtocolHandler {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
private processUrl(url: string): void {
|
private handleDeeplink(url: string): boolean {
|
||||||
if (!global.mainWindow) return;
|
if (!global.mainWindow) return false;
|
||||||
|
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
// sanity check: we only register for the one protocol, so we shouldn't
|
// sanity check: we only register for the one protocol, so we shouldn't
|
||||||
@@ -75,7 +81,7 @@ export default class ProtocolHandler {
|
|||||||
// with the Element app.
|
// with the Element app.
|
||||||
if (parsed.protocol !== `${this.protocol}:` && parsed.protocol !== `${LEGACY_PROTOCOL}:`) {
|
if (parsed.protocol !== `${this.protocol}:` && parsed.protocol !== `${LEGACY_PROTOCOL}:`) {
|
||||||
console.log("Ignoring unexpected protocol: ", parsed.protocol);
|
console.log("Ignoring unexpected protocol: ", parsed.protocol);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlToLoad = new URL("vector://vector/webapp/");
|
const urlToLoad = new URL("vector://vector/webapp/");
|
||||||
@@ -90,6 +96,7 @@ export default class ProtocolHandler {
|
|||||||
|
|
||||||
console.log("Opening URL: ", urlToLoad.href);
|
console.log("Opening URL: ", urlToLoad.href);
|
||||||
void global.mainWindow.loadURL(urlToLoad.href);
|
void global.mainWindow.loadURL(urlToLoad.href);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readStore(): Record<string, string> {
|
private readStore(): Record<string, string> {
|
||||||
@@ -107,16 +114,30 @@ export default class ProtocolHandler {
|
|||||||
fs.writeFileSync(storePath, JSON.stringify(this.store));
|
fs.writeFileSync(storePath, JSON.stringify(this.store));
|
||||||
}
|
}
|
||||||
|
|
||||||
public initialise(userDataPath: string): void {
|
/**
|
||||||
|
* Initialises the ProtocolHandler
|
||||||
|
* Registers the app as the default protocol client for deeplink handling.
|
||||||
|
* Handles any deeplink passed in via args on app start.
|
||||||
|
* Must be called after mainWindow is set up and any initial navigation is fired.
|
||||||
|
* @returns whether a deeplink was present in args and navigated to.
|
||||||
|
*/
|
||||||
|
public initialise(args: Args): boolean {
|
||||||
|
this.setAsDefaultProtocolClient(args);
|
||||||
|
|
||||||
|
const url = args.positional.find(this.checkArgIsUrl);
|
||||||
|
const hasDeeplink = url ? this.handleDeeplink(url) : false;
|
||||||
|
|
||||||
for (const key in this.store) {
|
for (const key in this.store) {
|
||||||
// ensure each instance only has one (the latest) session ID to prevent the file growing unbounded
|
// ensure each instance only has one (the latest) session ID to prevent the file growing unbounded
|
||||||
if (this.store[key] === userDataPath) {
|
if (this.store[key] === args.userDataPath) {
|
||||||
delete this.store[key];
|
delete this.store[key];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.store[this.sessionId] = userDataPath;
|
this.store[this.sessionId] = args.userDataPath;
|
||||||
this.writeStore();
|
this.writeStore();
|
||||||
|
|
||||||
|
return hasDeeplink;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getProfileFromDeeplink(args: string[]): string | undefined {
|
public getProfileFromDeeplink(args: string[]): string | undefined {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class SafeStorageWriter extends StorageWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const enum Mode {
|
export const enum Mode {
|
||||||
Encrypted = "encrypted", // default
|
Encrypted = "encrypted", // default
|
||||||
AllowPlaintext = "allow-plaintext",
|
AllowPlaintext = "allow-plaintext",
|
||||||
ForcePlaintext = "force-plaintext",
|
ForcePlaintext = "force-plaintext",
|
||||||
|
|||||||
Reference in New Issue
Block a user