From eaf1af75bd9df5fa70df2a4011d21c1e609519fb Mon Sep 17 00:00:00 2001 From: V Date: Sat, 8 Apr 2023 03:51:37 +0200 Subject: [PATCH] WebContextMenus: Port more menus (#818) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> --- src/plugins/moreUserTags.ts | 3 +- src/plugins/webContextMenus.web.ts | 218 ++++++++++++++++++++++++++--- src/utils/settingsSync.ts | 13 +- src/utils/web.ts | 30 ++++ 4 files changed, 232 insertions(+), 32 deletions(-) create mode 100644 src/utils/web.ts diff --git a/src/plugins/moreUserTags.ts b/src/plugins/moreUserTags.ts index c8420ff30..e45e30a43 100644 --- a/src/plugins/moreUserTags.ts +++ b/src/plugins/moreUserTags.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { definePluginSettings, migratePluginSettings } from "@api/settings"; +import { definePluginSettings } from "@api/settings"; import { Devs } from "@utils/constants"; import { proxyLazy } from "@utils/proxyLazy.js"; import definePlugin, { OptionType } from "@utils/types"; @@ -115,7 +115,6 @@ const settings = definePluginSettings({ ])) }); -migratePluginSettings("MoreUserTags", "Webhook Tags"); export default definePlugin({ name: "MoreUserTags", description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)", diff --git a/src/plugins/webContextMenus.web.ts b/src/plugins/webContextMenus.web.ts index 56990e1ba..e1c9725dc 100644 --- a/src/plugins/webContextMenus.web.ts +++ b/src/plugins/webContextMenus.web.ts @@ -16,31 +16,211 @@ * along with this program. If not, see . */ +import { definePluginSettings } from "@api/settings"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; +import { saveFile } from "@utils/web"; +import { findByProps, findLazy } from "@webpack"; +import { Clipboard } from "@webpack/common"; + +async function fetchImage(url: string) { + const res = await fetch(url); + if (res.status !== 200) return; + + return await res.blob(); +} + +const MiniDispatcher = findLazy(m => m.emitter?._events?.INSERT_TEXT); + +const settings = definePluginSettings({ + // This needs to be all in one setting because to enable any of these, we need to make Discord use their desktop context + // menu handler instead of the web one, which breaks the other menus that aren't enabled + addBack: { + type: OptionType.BOOLEAN, + description: "Add back the Discord context menus for images, links and the chat input bar", + // Web slate menu has proper spellcheck suggestions and image context menu is also pretty good, + // so disable this by default. Vencord Desktop just doesn't, so enable by default + default: IS_VENCORD_DESKTOP, + restartNeeded: true + } +}); export default definePlugin({ name: "WebContextMenus", - description: "Re-adds some of context menu items missing on the web version of Discord, namely Copy/Open Link", + description: "Re-adds context menus missing in the web version of Discord: Images, ChatInputBar, Links, 'Copy Link', 'Open Link', 'Copy Image', 'Save Image'", authors: [Devs.Ven], enabledByDefault: true, - patches: [{ - // There is literally no reason for Discord to make this Desktop only. - // The only thing broken is copy, but they already have a different copy function - // with web support???? - find: "open-native-link", - replacement: [ - { - // if (isNative || null == - match: /if\(!\w\..{1,3}\|\|null==/, - replace: "if(null==" - }, - // Fix silly Discord calling the non web support copy - { - match: /\w\.default\.copy/, - replace: "Vencord.Webpack.Common.Clipboard.copy" + settings, + + start() { + if (settings.store.addBack) { + const ctxMenuCallbacks = findByProps("contextMenuCallbackNative"); + window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb); + window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative); + this.changedListeners = true; + } + }, + + stop() { + if (this.changedListeners) { + const ctxMenuCallbacks = findByProps("contextMenuCallbackNative"); + window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative); + window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb); + } + }, + + patches: [ + // Add back Copy & Open Link + { + // There is literally no reason for Discord to make this Desktop only. + // The only thing broken is copy, but they already have a different copy function + // with web support???? + find: "open-native-link", + replacement: [ + { + // if (IS_DESKTOP || null == ...) + match: /if\(!\i\.\i\|\|null==/, + replace: "if(null==" + }, + // Fix silly Discord calling the non web support copy + { + match: /\w\.default\.copy/, + replace: "Vencord.Webpack.Common.Clipboard.copy" + } + ] + }, + + // Add back Copy & Save Image + { + find: 'id:"copy-image"', + replacement: [ + { + // if (!IS_WEB || null == + match: /if\(!\i\.\i\|\|null==/, + replace: "if(null==" + }, + { + match: /return\s*?\[\i\.default\.canCopyImage\(\)/, + replace: "return [true" + }, + { + match: /(?<=COPY_IMAGE_MENU_ITEM,)action:/, + replace: "action:()=>$self.copyImage(arguments[0]),oldAction:" + }, + { + match: /(?<=SAVE_IMAGE_MENU_ITEM,)action:/, + replace: "action:()=>$self.saveImage(arguments[0]),oldAction:" + }, + ] + }, + + // Add back image context menu + { + find: 'navId:"image-context"', + predicate: () => settings.store.addBack, + replacement: { + // return IS_DESKTOP ? React.createElement(Menu, ...) + match: /return \i\.\i\?(?=\(0,\i\.jsxs?\)\(\i\.Menu)/, + replace: "return true?" } - ] - }] + }, + + // Add back link context menu + { + find: '"interactionUsernameProfile"', + predicate: () => settings.store.addBack, + replacement: { + match: /if\("A"===\i\.tagName&&""!==\i\.textContent\)/, + replace: "if(false)" + } + }, + + // Add back slate / text input context menu + { + find: '"slate-toolbar"', + predicate: () => settings.store.addBack, + replacement: { + match: /(?<=\.handleContextMenu=.+?"bottom";)\i\.\i\?/, + replace: "true?" + } + }, + { + find: 'navId:"textarea-context"', + predicate: () => settings.store.addBack, + replacement: [ + { + // desktopOnlyEntries = makeEntries(), spellcheckChildren = desktopOnlyEntries[0], languageChildren = desktopOnlyEntries[1] + match: /\i=.{0,30}text:\i,target:\i,onHeightUpdate:\i\}\),2\),(\i)=\i\[0\],(\i)=\i\[1\]/, + // set spellcheckChildren & languageChildren to empty arrays, so just in case patch 3 fails, we don't + // reference undefined variables + replace: "$1=[],$2=[]", + }, + { + // if (!IS_DESKTOP) return + match: /(?<=showApplicationCommandSuggestions;)if\(!\i\.\i\)/, + replace: "if(false)" + }, + { + // do not add menu items for entries removed in patch 1. Using a lookbehind for group 1 is slow, + // so just capture and add back + match: /("submit-button".+?)(\(0,\i\.jsx\)\(\i\.MenuGroup,\{children:\i\}\),){2}/, + replace: "$1" + }, + { + // Change calls to DiscordNative.clipboard to us instead + match: /\b\i\.default\.(copy|cut|paste)/g, + replace: "$self.$1" + } + ] + } + + // TODO: Maybe add spellcheck for VencordDesktop + ], + + async copyImage(url: string) { + const data = await fetchImage(url); + if (!data) return; + + await navigator.clipboard.write([ + new ClipboardItem({ + [data.type]: data + }) + ]); + }, + + async saveImage(url: string) { + const data = await fetchImage(url); + if (!data) return; + + const name = url.split("/").pop()!; + const file = new File([data], name, { type: data.type }); + + saveFile(file); + }, + + copy() { + const selection = document.getSelection(); + if (!selection) return; + + Clipboard.copy(selection.toString()); + }, + + cut() { + this.copy(); + MiniDispatcher.dispatch("INSERT_TEXT", { rawText: "" }); + }, + + async paste() { + const text = await navigator.clipboard.readText(); + + const data = new DataTransfer(); + data.setData("text/plain", text); + + document.dispatchEvent( + new ClipboardEvent("paste", { + clipboardData: data + }) + ); + } }); diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index d59787d5f..61fd37ea7 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -24,6 +24,7 @@ import { deflateSync, inflateSync } from "fflate"; import { getCloudAuth, getCloudUrl } from "./cloud"; import IpcEvents from "./IpcEvents"; import Logger from "./Logger"; +import { saveFile } from "./web"; export async function importSettings(data: string) { try { @@ -54,17 +55,7 @@ export async function downloadSettingsBackup() { if (IS_DISCORD_DESKTOP) { DiscordNative.fileManager.saveWithDialog(data, filename); } else { - const file = new File([data], filename, { type: "application/json" }); - const a = document.createElement("a"); - a.href = URL.createObjectURL(file); - a.download = filename; - - document.body.appendChild(a); - a.click(); - setImmediate(() => { - URL.revokeObjectURL(a.href); - document.body.removeChild(a); - }); + saveFile(new File([data], filename, { type: "application/json" })); } } diff --git a/src/utils/web.ts b/src/utils/web.ts new file mode 100644 index 000000000..9cfe71847 --- /dev/null +++ b/src/utils/web.ts @@ -0,0 +1,30 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +export function saveFile(file: File) { + const a = document.createElement("a"); + a.href = URL.createObjectURL(file); + a.download = file.name; + + document.body.appendChild(a); + a.click(); + setImmediate(() => { + URL.revokeObjectURL(a.href); + document.body.removeChild(a); + }); +}