From 6483b3a3d96181c9dca87abb095f94291146d161 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Mon, 15 Jan 2024 18:52:46 +0100 Subject: [PATCH] port popout logic from discord desktop --- .eslintrc.json | 8 +- src/main/constants.ts | 2 + src/main/utils/ipcWrappers.ts | 10 +- src/main/utils/makeLinksOpenExternally.ts | 81 +++++++++------- src/main/utils/popout.ts | 112 ++++++++++++++++++++++ 5 files changed, 172 insertions(+), 41 deletions(-) create mode 100644 src/main/utils/popout.ts diff --git a/.eslintrc.json b/.eslintrc.json index 4287749..fb0e152 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,7 +22,13 @@ "eqeqeq": ["error", "always", { "null": "ignore" }], "spaced-comment": ["error", "always", { "markers": ["!"] }], "yoda": "error", - "prefer-destructuring": ["error", { "object": true, "array": false }], + "prefer-destructuring": [ + "error", + { + "VariableDeclarator": { "array": false, "object": true }, + "AssignmentExpression": { "array": false, "object": false } + } + ], "operator-assignment": ["error", "always"], "no-useless-computed-key": "error", "no-unneeded-ternary": ["error", { "defaultAssignment": false }], diff --git a/src/main/constants.ts b/src/main/constants.ts index fb873ee..9b1a7e9 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -42,6 +42,8 @@ export const MIN_HEIGHT = 500; export const DEFAULT_WIDTH = 1280; export const DEFAULT_HEIGHT = 720; +export const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"]; + const UserAgents = { darwin: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", diff --git a/src/main/utils/ipcWrappers.ts b/src/main/utils/ipcWrappers.ts index b99298c..3ceb908 100644 --- a/src/main/utils/ipcWrappers.ts +++ b/src/main/utils/ipcWrappers.ts @@ -5,20 +5,14 @@ */ import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron"; +import { DISCORD_HOSTNAMES } from "main/constants"; import { IpcEvents } from "shared/IpcEvents"; export function validateSender(frame: WebFrameMain) { const { hostname, protocol } = new URL(frame.url); if (protocol === "file:") return; - switch (hostname) { - case "discord.com": - case "ptb.discord.com": - case "canary.discord.com": - break; - default: - throw new Error("ipc: Disallowed host " + hostname); - } + if (!DISCORD_HOSTNAMES.includes(hostname)) throw new Error("ipc: Disallowed host " + hostname); } export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) { diff --git a/src/main/utils/makeLinksOpenExternally.ts b/src/main/utils/makeLinksOpenExternally.ts index c58bad1..12f2c3d 100644 --- a/src/main/utils/makeLinksOpenExternally.ts +++ b/src/main/utils/makeLinksOpenExternally.ts @@ -5,50 +5,67 @@ */ import { BrowserWindow, shell } from "electron"; +import { DISCORD_HOSTNAMES } from "main/constants"; import { Settings } from "../settings"; +import { createOrFocusPopup, setupPopout } from "./popout"; import { execSteamURL, isDeckGameMode, steamOpenURL } from "./steamOS"; -const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"]; +export function handleExternalUrl(url: string, protocol?: string): { action: "deny" | "allow" } { + if (protocol == null) { + try { + protocol = new URL(url).protocol; + } catch { + return { action: "deny" }; + } + } + + switch (protocol) { + case "http:": + case "https:": + if (Settings.store.openLinksWithElectron) { + return { action: "allow" }; + } + // eslint-disable-next-line no-fallthrough + case "mailto:": + case "spotify:": + if (isDeckGameMode) { + steamOpenURL(url); + } else { + shell.openExternal(url); + } + break; + case "steam:": + if (isDeckGameMode) { + execSteamURL(url); + } else { + shell.openExternal(url); + } + break; + } + + return { action: "deny" }; +} + export function makeLinksOpenExternally(win: BrowserWindow) { - win.webContents.setWindowOpenHandler(({ url, frameName }) => { + win.webContents.setWindowOpenHandler(({ url, frameName, features }) => { try { var { protocol, hostname, pathname } = new URL(url); } catch { return { action: "deny" }; } - if ( - url === "about:blank" || - (pathname === "/popout" && DISCORD_HOSTNAMES.includes(hostname)) || - (frameName === "authorize" && DISCORD_HOSTNAMES.includes(hostname)) - ) - return { action: "allow" }; - - switch (protocol) { - case "http:": - case "https:": - if (Settings.store.openLinksWithElectron) { - return { action: "allow" }; - } - // eslint-disable-next-line no-fallthrough - case "mailto:": - case "spotify:": - if (isDeckGameMode) { - steamOpenURL(url); - } else { - shell.openExternal(url); - } - break; - case "steam:": - if (isDeckGameMode) { - execSteamURL(url); - } else { - shell.openExternal(url); - } - break; + if (frameName.startsWith("DISCORD_") && pathname === "/popout" && DISCORD_HOSTNAMES.includes(hostname)) { + return createOrFocusPopup(frameName, features); } - return { action: "deny" }; + if (url === "about:blank" || (frameName === "authorize" && DISCORD_HOSTNAMES.includes(hostname))) + return { action: "allow" }; + + return handleExternalUrl(url, protocol); + }); + + win.webContents.on("did-create-window", (win, { frameName }) => { + if (frameName.startsWith("DISCORD_")) setupPopout(win, frameName); }); } diff --git a/src/main/utils/popout.ts b/src/main/utils/popout.ts new file mode 100644 index 0000000..754fdb0 --- /dev/null +++ b/src/main/utils/popout.ts @@ -0,0 +1,112 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { BrowserWindow, BrowserWindowConstructorOptions } from "electron"; + +import { handleExternalUrl } from "./makeLinksOpenExternally"; + +const ALLOWED_FEATURES = new Set([ + "width", + "height", + "left", + "top", + "resizable", + "movable", + "alwaysOnTop", + "frame", + "transparent", + "hasShadow", + "closable", + "skipTaskbar", + "backgroundColor", + "menubar", + "toolbar", + "location", + "directories", + "titleBarStyle" +]); + +const MIN_POPOUT_WIDTH = 320; +const MIN_POPOUT_HEIGHT = 180; +const DEFAULT_POPOUT_OPTIONS: BrowserWindowConstructorOptions = { + title: "Discord Popout", + backgroundColor: "#2f3136", + minWidth: MIN_POPOUT_WIDTH, + minHeight: MIN_POPOUT_HEIGHT, + frame: process.platform === "linux", + titleBarStyle: process.platform === "darwin" ? "hidden" : undefined, + trafficLightPosition: + process.platform === "darwin" + ? { + x: 10, + y: 3 + } + : undefined, + webPreferences: { + nodeIntegration: false, + contextIsolation: true + } +}; + +const PopoutWindows = new Map(); + +function focusWindow(window: BrowserWindow) { + window.setAlwaysOnTop(true); + window.focus(); + window.setAlwaysOnTop(false); +} + +function parseFeatureValue(feature: string) { + if (feature === "yes") return true; + if (feature === "no") return false; + + const n = Number(feature); + if (!isNaN(n)) return n; + + return feature; +} + +function parseWindowFeatures(features: string) { + const keyValuesParsed = features.split(","); + + return keyValuesParsed.reduce((features, feature) => { + const [key, value] = feature.split("="); + if (ALLOWED_FEATURES.has(key)) features[key] = parseFeatureValue(value); + + return features; + }, {}); +} + +export function createOrFocusPopup(key: string, features: string) { + const existingWindow = PopoutWindows.get(key); + if (existingWindow) { + focusWindow(existingWindow); + return { action: "deny" }; + } + + return { + action: "allow", + overrideBrowserWindowOptions: { + ...DEFAULT_POPOUT_OPTIONS, + ...parseWindowFeatures(features) + } + }; +} + +export function setupPopout(win: BrowserWindow, key: string) { + PopoutWindows.set(key, win); + + /* win.webContents.on("will-navigate", (evt, url) => { + // maybe prevent if not origin match + })*/ + + win.webContents.setWindowOpenHandler(({ url }) => handleExternalUrl(url)); + + win.once("closed", () => { + win.removeAllListeners(); + PopoutWindows.delete(key); + }); +}