diff --git a/src/plugins/messageClickActions/index.ts b/src/plugins/messageClickActions/index.ts index b9eb718d1..63d1ae2ce 100644 --- a/src/plugins/messageClickActions/index.ts +++ b/src/plugins/messageClickActions/index.ts @@ -19,11 +19,11 @@ import { isPluginEnabled } from "@api/PluginManager"; import { definePluginSettings } from "@api/Settings"; import NoReplyMentionPlugin from "@plugins/noReplyMention"; -import { Devs } from "@utils/constants"; +import { Devs, EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { ApplicationIntegrationType, MessageFlags } from "@vencord/discord-types/enums"; import { findByPropsLazy } from "@webpack"; -import { AuthenticationStore, FluxDispatcher, MessageTypeSets, PermissionsBits, PermissionStore, WindowStore } from "@webpack/common"; +import { AuthenticationStore, FluxDispatcher, MessageTypeSets, PermissionsBits, PermissionStore, RestAPI, WindowStore } from "@webpack/common"; const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage"); const EditStore = findByPropsLazy("isEditing", "isEditingAny"); @@ -33,6 +33,9 @@ const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false); const focusChanged = () => !WindowStore.isFocused() && (isDeletePressed = false); +let doubleClickTimeout: ReturnType | null = null; +let pendingDoubleClickAction: (() => void) | null = null; + const settings = definePluginSettings({ enableDeleteOnClick: { type: OptionType.BOOLEAN, @@ -49,6 +52,16 @@ const settings = definePluginSettings({ description: "Enable double click to reply", default: true }, + enableTripleClickToReact: { + type: OptionType.BOOLEAN, + description: "Enable triple click to react with an emoji", + default: false + }, + reactEmoji: { + type: OptionType.STRING, + description: "Emoji to react with (e.g. 💀 or pepe:123456789)", + default: "💀" + }, requireModifier: { type: OptionType.BOOLEAN, description: "Only do double click actions when shift/ctrl is held", @@ -56,10 +69,28 @@ const settings = definePluginSettings({ } }); +async function react(channelId: string, messageId: string, emoji: string) { + const trimmed = emoji.trim(); + if (!trimmed) return; + + const customMatch = trimmed.match(/^:?([\w-]+):(\d+)$/); + const emojiParam = customMatch + ? `${customMatch[1]}:${customMatch[2]}` + : encodeURIComponent(trimmed); + + try { + await RestAPI.put({ + url: `/channels/${channelId}/messages/${messageId}/reactions/${emojiParam}/@me` + }); + } catch (e) { + console.error("[MessageClickActions] Failed to add reaction:", e); + } +} + export default definePlugin({ name: "MessageClickActions", - description: "Hold Backspace and click to delete, double click to edit/reply", - authors: [Devs.Ven], + description: "Hold Backspace and click to delete, double click to edit/reply, triple click to react", + authors: [Devs.Ven, EquicordDevs.keircn], settings, @@ -79,20 +110,50 @@ export default definePlugin({ const myId = AuthenticationStore.getId(); const isMe = msg.author.id === myId; const isSelfInvokedUserApp = msg.interactionMetadata?.authorizing_integration_owners[ApplicationIntegrationType.USER_INSTALL] === myId; - if (!isDeletePressed) { - if (event.detail < 2) return; - if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return; - if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return; - if (msg.deleted === true) return; + if (isDeletePressed) { + if (!settings.store.enableDeleteOnClick) return; + if (!(isMe || PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel) || isSelfInvokedUserApp)) return; + + if (msg.deleted) { + FluxDispatcher.dispatch({ + type: "MESSAGE_DELETE", + channelId: channel.id, + id: msg.id, + mlDeleted: true + }); + } else { + MessageActions.deleteMessage(channel.id, msg.id); + } + event.preventDefault(); + return; + } + + if (event.detail === 3) { + if (doubleClickTimeout) { + clearTimeout(doubleClickTimeout); + doubleClickTimeout = null; + pendingDoubleClickAction = null; + } + + if (settings.store.enableTripleClickToReact) { + react(channel.id, msg.id, settings.store.reactEmoji); + event.preventDefault(); + } + return; + } + + if (event.detail !== 2) return; + if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return; + if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return; + if (msg.deleted === true) return; + + const executeDoubleClick = () => { if (isMe) { if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id) || msg.state !== "SENT") return; - MessageActions.startEditMessage(channel.id, msg.id, msg.content); - event.preventDefault(); } else { if (!settings.store.enableDoubleClickToReply) return; - if (!MessageTypeSets.REPLYABLE.has(msg.type) || msg.hasFlag(MessageFlags.EPHEMERAL)) return; const isShiftPress = event.shiftKey && !settings.store.requireModifier; @@ -108,17 +169,21 @@ export default definePlugin({ showMentionToggle: channel.guild_id !== null }); } - } else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel) || isSelfInvokedUserApp)) { - if (msg.deleted) { - FluxDispatcher.dispatch({ - type: "MESSAGE_DELETE", - channelId: channel.id, - id: msg.id, - mlDeleted: true - }); - } else { - MessageActions.deleteMessage(channel.id, msg.id); + }; + + if (settings.store.enableTripleClickToReact) { + if (doubleClickTimeout) { + clearTimeout(doubleClickTimeout); } + pendingDoubleClickAction = executeDoubleClick; + doubleClickTimeout = setTimeout(() => { + pendingDoubleClickAction?.(); + pendingDoubleClickAction = null; + doubleClickTimeout = null; + }, 300); + event.preventDefault(); + } else { + executeDoubleClick(); event.preventDefault(); } }, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 8da6e4e3e..68cea8ddd 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -615,11 +615,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({ }, jamesbt365: { name: "jamesbt365", - id: 158567567487795200n, + id: 158567567487795200n }, samsam: { name: "samsam", - id: 400482410279469056n, + id: 400482410279469056n }, Cootshk: { name: "Cootshk", @@ -1031,23 +1031,23 @@ export const EquicordDevs = Object.freeze({ }, cassie: { name: "cassie", - id: 280411966126948353n, + id: 280411966126948353n }, mochienya: { name: "mochie", - id: 1043599230247374869n, + id: 1043599230247374869n }, okiso: { name: "okiso", - id: 274178934143451137n, + id: 274178934143451137n }, port22exposed: { name: "port", - id: 1318383159645311009n, + id: 1318383159645311009n }, PhoenixAceVFX: { name: "PhoenixAceVFX", - id: 1016895892055396484n, + id: 1016895892055396484n }, TheArmagan: { name: "TheArmagan", @@ -1099,27 +1099,27 @@ export const EquicordDevs = Object.freeze({ }, Reycko: { name: "Reycko", - id: 1123725368004726794n, + id: 1123725368004726794n }, Campfire: { name: "Campfire", - id: 376414446840578081n, + id: 376414446840578081n }, Cootshk: { name: "Cootshk", - id: 921605971577548820n, + id: 921605971577548820n }, sliwka: { name: "sliwka", - id: 1165286199628419129n, + id: 1165286199628419129n }, bbgaming25k: { name: "bbgaming25k", - id: 851222385528274964n, + id: 851222385528274964n }, davidkra230: { name: "davidkra230", - id: 652699312631054356n, + id: 652699312631054356n }, GroupXyz: { name: "GroupXyz", @@ -1143,7 +1143,7 @@ export const EquicordDevs = Object.freeze({ }, Etorix: { name: "Etorix", - id: 94597845868355584n, + id: 94597845868355584n }, Johannes7k75: { name: "Johannes7k75", @@ -1171,11 +1171,11 @@ export const EquicordDevs = Object.freeze({ }, qouesm: { name: "qouesm", - id: 130388483494641664n, + id: 130388483494641664n }, CallMeGii: { name: "gii", - id: 156481332652802048n, + id: 156481332652802048n }, mmeta: { name: "mmeta", @@ -1221,6 +1221,10 @@ export const EquicordDevs = Object.freeze({ name: "Benjii", id: 463702169443368970n }, + keircn: { + name: "Key", + id: 1230319937155760131n + }, } satisfies Record); // iife so #__PURE__ works correctly