@ -16,31 +16,211 @@
* along with this program . If not , see < https : / / www.gnu.org / licenses / > .
* along with this program . If not , see < https : / / www.gnu.org / licenses / > .
* /
* /
import { definePluginSettings } from "@api/settings" ;
import { Devs } from "@utils/constants" ;
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 ( {
export default definePlugin ( {
name : "WebContextMenus" ,
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 ] ,
authors : [ Devs . Ven ] ,
enabledByDefault : true ,
enabledByDefault : true ,
patches : [ {
settings ,
// 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
start() {
// with web support????
if ( settings . store . addBack ) {
find : "open-native-link" ,
const ctxMenuCallbacks = findByProps ( "contextMenuCallbackNative" ) ;
replacement : [
window . removeEventListener ( "contextmenu" , ctxMenuCallbacks . contextMenuCallbackWeb ) ;
{
window . addEventListener ( "contextmenu" , ctxMenuCallbacks . contextMenuCallbackNative ) ;
// if (isNative || null ==
this . changedListeners = true ;
match : /if\(!\w\..{1,3}\|\|null==/ ,
}
replace : "if(null=="
} ,
} ,
// Fix silly Discord calling the non web support copy
stop() {
{
if ( this . changedListeners ) {
match : /\w\.default\.copy/ ,
const ctxMenuCallbacks = findByProps ( "contextMenuCallbackNative" ) ;
replace : "Vencord.Webpack.Common.Clipboard.copy"
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
} )
) ;
}
} ) ;
} ) ;