2022-11-17 00:30:23 +00:00
/ *
* Vencord , a modification for Discord ' s desktop app
* Copyright ( c ) 2022 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 < https : / / www.gnu.org / licenses / > .
* /
2023-04-08 01:51:37 +00:00
import { definePluginSettings } from "@api/settings" ;
2022-11-28 12:37:55 +00:00
import { Devs } from "@utils/constants" ;
2023-04-08 01:51:37 +00:00
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
}
} ) ;
2022-11-17 00:30:23 +00:00
export default definePlugin ( {
name : "WebContextMenus" ,
2023-04-08 01:51:37 +00:00
description : "Re-adds context menus missing in the web version of Discord: Images, ChatInputBar, Links, 'Copy Link', 'Open Link', 'Copy Image', 'Save Image'" ,
2022-11-17 00:30:23 +00:00
authors : [ Devs . Ven ] ,
2023-03-11 13:18:32 +00:00
enabledByDefault : true ,
2022-11-17 00:30:23 +00:00
2023-04-08 01:51:37 +00:00
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?"
2022-11-17 00:30:23 +00:00
}
2023-04-08 01:51:37 +00:00
} ,
{
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 ) {
2023-04-08 21:05:38 +00:00
// Clipboard only supports image/png, jpeg and similar won't work. Thus, we need to convert it to png
// via canvas first
const img = new Image ( ) ;
img . onload = ( ) = > {
const canvas = document . createElement ( "canvas" ) ;
canvas . width = img . naturalWidth ;
canvas . height = img . naturalHeight ;
canvas . getContext ( "2d" ) ! . drawImage ( img , 0 , 0 ) ;
canvas . toBlob ( data = > {
navigator . clipboard . write ( [
new ClipboardItem ( {
"image/png" : data !
} )
] ) ;
} , "image/png" ) ;
} ;
img . crossOrigin = "anonymous" ;
img . src = url ;
2023-04-08 01:51:37 +00:00
} ,
async saveImage ( url : string ) {
const data = await fetchImage ( url ) ;
if ( ! data ) return ;
2023-04-08 20:57:34 +00:00
const name = new URL ( url ) . pathname . split ( "/" ) . pop ( ) ! ;
2023-04-08 01:51:37 +00:00
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
} )
) ;
}
2022-11-17 00:30:23 +00:00
} ) ;