diff --git a/.gitignore b/.gitignore index f24a721..7bd751c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ lerna-debug.log* *.tsbuildinfo src/userplugins + +ExtensionCache/ +settings/ diff --git a/src/api/MessagePopover.ts b/src/api/MessagePopover.ts new file mode 100644 index 0000000..85dff9c --- /dev/null +++ b/src/api/MessagePopover.ts @@ -0,0 +1,69 @@ +/* + * 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 . +*/ + +import Logger from "@utils/Logger"; +import { Channel, Message } from "discord-types/general"; +import type { MouseEventHandler } from "react"; + +const logger = new Logger("MessagePopover"); + +export interface ButtonItem { + key?: string, + label: string, + icon: React.ComponentType, + message: Message, + channel: Channel, + onClick?: MouseEventHandler, + onContextMenu?: MouseEventHandler; +} + +export type getButtonItem = (message: Message) => ButtonItem | null; + +export const buttons = new Map(); + +export function addButton( + identifier: string, + item: getButtonItem, +) { + buttons.set(identifier, item); +} + +export function removeButton(identifier: string) { + buttons.delete(identifier); +} + +export function _buildPopoverElements( + msg: Message, + makeButton: (item: ButtonItem) => React.ComponentType +) { + const items = [] as React.ComponentType[]; + + for (const [identifier, getItem] of buttons.entries()) { + try { + const item = getItem(msg); + if (item) { + item.key ??= identifier; + items.push(makeButton(item)); + } + } catch (err) { + logger.error(`[${identifier}]`, err); + } + } + + return items; +} diff --git a/src/api/index.ts b/src/api/index.ts index 98fc6a4..b74da6e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -21,6 +21,7 @@ import * as $Commands from "./Commands"; import * as $DataStore from "./DataStore"; import * as $MessageAccessories from "./MessageAccessories"; import * as $MessageEventsAPI from "./MessageEvents"; +import * as $MessagePopover from "./MessagePopover"; import * as $Notices from "./Notices"; import * as $ServerList from "./ServerList"; @@ -59,6 +60,10 @@ const DataStore = $DataStore; * An API allowing you to add custom components as message accessories */ const MessageAccessories = $MessageAccessories; +/** + * An API allowing you to add custom buttons in the message popover + */ +const MessagePopover = $MessagePopover; /** * An API allowing you to add badges to user profiles */ @@ -68,4 +73,4 @@ const Badges = $Badges; */ const ServerList = $ServerList; -export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, Notices, ServerList }; +export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList }; diff --git a/src/plugins/apiMessagePopover.ts b/src/plugins/apiMessagePopover.ts new file mode 100644 index 0000000..95814e0 --- /dev/null +++ b/src/plugins/apiMessagePopover.ts @@ -0,0 +1,33 @@ +/* + * 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 . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "MessagePopoverAPI", + description: "API to add buttons to message popovers.", + authors: [Devs.KingFish], + patches: [{ + find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", + replacement: { + match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,90}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/, + replace: "$1...Vencord.Api.MessagePopover._buildPopoverElements($2,$4),$3" + } + }], +}); diff --git a/src/plugins/HideAttachments.tsx b/src/plugins/hideAttachments.tsx similarity index 83% rename from src/plugins/HideAttachments.tsx rename to src/plugins/hideAttachments.tsx index 2c1a0d4..944da65 100644 --- a/src/plugins/HideAttachments.tsx +++ b/src/plugins/hideAttachments.tsx @@ -17,11 +17,10 @@ */ import { get, set } from "@api/DataStore"; +import { addButton, removeButton } from "@api/MessagePopover"; import { Devs } from "@utils/constants"; -import Logger from "@utils/Logger"; import definePlugin from "@utils/types"; import { ChannelStore, FluxDispatcher } from "@webpack/common"; -import { Message } from "discord-types/general"; let style: HTMLStyleElement; @@ -49,13 +48,7 @@ export default definePlugin({ name: "HideAttachments", description: "Hide attachments and Embeds for individual messages via hover button", authors: [Devs.Ven], - patches: [{ - find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", - replacement: { - match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,40}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/, - replace: "$1Vencord.Plugins.plugins.HideAttachments.renderButton($2, $4),$3" - } - }], + dependencies: ["MessagePopoverAPI"], async start() { style = document.createElement("style"); @@ -64,11 +57,26 @@ export default definePlugin({ await getHiddenMessages(); await this.buildCss(); + + addButton("HideAttachments", msg => { + if (!msg.attachments.length && !msg.embeds.length) return null; + + const isHidden = hiddenMessages.has(msg.id); + + return { + label: isHidden ? "Show Attachments" : "Hide Attachments", + icon: isHidden ? ImageVisible : ImageInvisible, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: () => this.toggleHide(msg.id) + }; + }); }, stop() { style.remove(); hiddenMessages.clear(); + removeButton("HideAttachments"); }, async buildCss() { @@ -86,26 +94,6 @@ export default definePlugin({ `; }, - renderButton(msg: Message, makeItem: (data: any) => React.ComponentType) { - try { - if (!msg.attachments.length && !msg.embeds.length) return null; - - const isHidden = hiddenMessages.has(msg.id); - - return makeItem({ - key: "HideAttachments", - label: isHidden ? "Show Attachments" : "Hide Attachments", - icon: isHidden ? ImageVisible : ImageInvisible, - message: msg, - channel: ChannelStore.getChannel(msg.channel_id), - onClick: () => this.toggleHide(msg.id) - }); - } catch (err) { - new Logger("HideAttachments").error(err); - return null; - } - }, - async toggleHide(id: string) { const ids = await getHiddenMessages(); if (!ids.delete(id)) diff --git a/src/plugins/quickMention.tsx b/src/plugins/quickMention.tsx index 1c0a6c6..6e00dd0 100644 --- a/src/plugins/quickMention.tsx +++ b/src/plugins/quickMention.tsx @@ -16,9 +16,11 @@ * along with this program. If not, see . */ +import { addButton, removeButton } from "@api/MessagePopover"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { findLazy } from "@webpack"; +import { ChannelStore } from "@webpack/common"; const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT); @@ -26,29 +28,22 @@ export default definePlugin({ name: "QuickMention", authors: [Devs.kemo], description: "Adds a quick mention button to the message actions bar", - - patches: [ - { - find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", - replacement: { - match: /(null,)(.{1,3}&&!.{1,3}\?(.{1,3})\(\{key:"reply",label:.{1,10}\.Messages\.MESSAGE_ACTION_REPLY,icon:.{1,10},channel:(.+?),message:(.+?),onClick:.+?\}\))/, - replace: (m, post, og, functionName, channelVar, messageVar) => { - - const functionSig = - `${functionName}({ - key: "QuickMention", - label: "Mention", - icon: Vencord.Plugins.plugins.QuickMention.Icon, - channel: ${channelVar}, - message: ${messageVar}, - onClick: ()=> Vencord.Plugins.plugins.QuickMention.onClick(${messageVar}) - })`; - - return `${post}${functionSig},${og}`; - } - } - } - ], + dependencies: ["MessagePopoverAPI"], + + start() { + addButton("QuickMention", msg => { + return { + label: "Quick Mention", + icon: this.Icon, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: () => ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: `<@${msg.author.id}> ` }) + }; + }); + }, + stop() { + removeButton("QuickMention"); + }, Icon: () => ( ), - - onClick: (message: any) => ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: `<@${message.author.id}> ` }) }); diff --git a/src/plugins/viewRaw.tsx b/src/plugins/viewRaw.tsx new file mode 100644 index 0000000..c49180b --- /dev/null +++ b/src/plugins/viewRaw.tsx @@ -0,0 +1,147 @@ +/* + * 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 . +*/ + +import { addButton, removeButton } from "@api/MessagePopover"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Flex } from "@components/Flex"; +import { Devs } from "@utils/constants"; +import { copyWithToast } from "@utils/misc"; +import { closeModal, ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import definePlugin from "@utils/types"; +import { Button, ChannelStore, Forms, Margins, Parser } from "@webpack/common"; +import { Message } from "discord-types/general"; + + +const CopyIcon = () => { + return ; +}; + +function sortObject(obj: T): T { + return Object.fromEntries(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))) as T; +} + +function cleanMessage(msg: Message) { + const clone = sortObject(JSON.parse(JSON.stringify(msg))); + for (const key in clone.author) { + switch (key) { + case "id": + case "username": + case "usernameNormalized": + case "discriminator": + case "avatar": + case "bot": + case "system": + case "publicFlags": + break; + default: + // phone number, email, etc + delete clone.author[key]; + } + } + + // message logger added properties + const cloneAny = clone as any; + delete cloneAny.editHistory; + delete cloneAny.deleted; + cloneAny.attachments?.forEach(a => delete a.deleted); + + return clone; +} + +function CodeBlock(props: { content: string, lang: string; }) { + return ( + // make text selectable +
+ {Parser.defaultRules.codeBlock.react(props, null, {})} +
+ ); +} + +function openViewRawModal(msg: Message) { + msg = cleanMessage(msg); + const msgJson = JSON.stringify(msg, null, 4); + + const key = openModal(props => ( + + + + View Raw + closeModal(key)} /> + + + + + + + + {!!msg.content && ( + <> + Content + + + + )} + + Message Data + + + + + )); +} + +export default definePlugin({ + name: "ViewRaw", + description: "Copy and view the raw content/data of any message.", + authors: [Devs.KingFish, Devs.Ven], + dependencies: ["MessagePopoverAPI"], + + start() { + addButton("ViewRaw", msg => { + return { + label: "View Raw (Left Click) / Copy Raw (Right Click)", + icon: CopyIcon, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: () => openViewRawModal(msg), + onContextMenu: e => { + e.preventDefault(); + e.stopPropagation(); + copyWithToast(msg.content); + } + }; + }); + }, + + stop() { + removeButton("CopyRawMessage"); + } +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 3766c74..eead2a3 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -156,5 +156,9 @@ export const Devs = Object.freeze({ Luna: { name: "Luny", id: 821472922140803112n + }, + KingFish: { + name: "King Fish", + id: 499400512559382538n } }); diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index 4ae3fd5..d9164a0 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { React } from "@webpack/common"; +import { Clipboard, React, Toasts } from "@webpack/common"; /** * Makes a lazy function. On first call, the value is computed. @@ -175,5 +175,25 @@ export function suppressErrors(name: string, func: F, thisOb */ export function makeCodeblock(text: string, language?: string) { const chars = "```"; - return `${chars}${language || ""}\n${text}\n${chars}`; + return `${chars}${language || ""}\n${text.replaceAll("```", "\\`\\`\\`")}\n${chars}`; +} + +export function copyWithToast(text: string, toastMessage = "Copied to clipboard!") { + if (Clipboard.SUPPORTS_COPY) { + Clipboard.copy(text); + } else { + toastMessage = "Your browser does not support copying to clipboard"; + } + Toasts.show({ + message: toastMessage, + id: Toasts.genId(), + type: Toasts.Type.SUCCESS + }); +} + +/** + * Check if obj is a true object: of type "object" and not null or array + */ +export function isObject(obj: unknown): obj is object { + return typeof obj === "object" && obj !== null && !Array.isArray(obj); }