MessageClickActions: Use dropdowns instead of boolean toggles for better customization of actions (#618)
* fix(MessageClickActions): use reactions constant to construct PUT request for emoji click action * feat(messageClickActions): toast if we dont have permission to react to the msg * feat(messageClickActions): explicit action configuration via dropdown selectors you can now pick actions to fire when we pick up events * fix(MessageClickActions): call pins correctly and toggle reactions/pins instead of just enabling them and silently failing otherwise * feat(messageClickActions): add configurable click timeout for triple clicks * feat(messageClickActions): quoter things * fix(messageClickActions): clean up quoteMessage a little i guess * refactor(MessageClickActions): make quoteMessage stateless and a little more readable * t * use pin actions * thororen uwahhhhh * refactor(messageClickActions): addresses prisms comments (i cba to write a commit msg) * feat(messageClickActions): dont use selection in quote action by default also: - add reply, edit options to triple click action - add disable system dms boolean - fix operator precedence bug in DM check --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
parent
0c2f5cf7d5
commit
a04f8131d3
2 changed files with 287 additions and 54 deletions
|
|
@ -20,14 +20,12 @@ import { isPluginEnabled } from "@api/PluginManager";
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import NoReplyMentionPlugin from "@plugins/noReplyMention";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { copyWithToast, insertTextIntoChatInputBox } from "@utils/discord";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Channel, Message } from "@vencord/discord-types";
|
||||
import { ApplicationIntegrationType, MessageFlags } from "@vencord/discord-types/enums";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { AuthenticationStore, FluxDispatcher, MessageTypeSets, PermissionsBits, PermissionStore, RestAPI, WindowStore } from "@webpack/common";
|
||||
|
||||
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
||||
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
||||
import { AuthenticationStore, Constants, EditStore, FluxDispatcher, MessageActions, MessageTypeSets, PermissionsBits, PermissionStore, PinActions, RestAPI, Toasts, WindowStore } from "@webpack/common";
|
||||
|
||||
let isDeletePressed = false;
|
||||
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
||||
|
|
@ -37,60 +35,231 @@ const focusChanged = () => !WindowStore.isFocused() && (isDeletePressed = false)
|
|||
let doubleClickTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let pendingDoubleClickAction: (() => void) | null = null;
|
||||
|
||||
enum ClickAction {
|
||||
NONE = "none",
|
||||
DELETE = "delete",
|
||||
EDIT = "edit",
|
||||
REPLY = "reply",
|
||||
COPY_CONTENT = "copy_content",
|
||||
COPY_LINK = "copy_link",
|
||||
COPY_MESSAGE_ID = "copy_message_id",
|
||||
COPY_USER_ID = "copy_user_id",
|
||||
QUOTE = "quote",
|
||||
REACT = "react",
|
||||
PIN = "pin"
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
enableDeleteOnClick: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable delete on click while holding backspace",
|
||||
default: true
|
||||
backspaceClickAction: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Action when holding Backspace and clicking a message",
|
||||
options: [
|
||||
{ label: "Delete Message", value: ClickAction.DELETE, default: true },
|
||||
{ label: "Copy Content", value: ClickAction.COPY_CONTENT },
|
||||
{ label: "Copy Link", value: ClickAction.COPY_LINK },
|
||||
{ label: "Copy Message ID", value: ClickAction.COPY_MESSAGE_ID },
|
||||
{ label: "Copy User ID", value: ClickAction.COPY_USER_ID },
|
||||
{ label: "None (Disabled)", value: ClickAction.NONE }
|
||||
]
|
||||
},
|
||||
enableDoubleClickToEdit: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable double click to edit",
|
||||
default: true
|
||||
doubleClickAction: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Action on double-click (on your messages)",
|
||||
options: [
|
||||
{ label: "Edit Message", value: ClickAction.EDIT, default: true },
|
||||
{ label: "Quote", value: ClickAction.QUOTE },
|
||||
{ label: "Copy Content", value: ClickAction.COPY_CONTENT },
|
||||
{ label: "Copy Link", value: ClickAction.COPY_LINK },
|
||||
{ label: "Copy Message ID", value: ClickAction.COPY_MESSAGE_ID },
|
||||
{ label: "Copy User ID", value: ClickAction.COPY_USER_ID },
|
||||
{ label: "React", value: ClickAction.REACT },
|
||||
{ label: "Pin Message", value: ClickAction.PIN },
|
||||
{ label: "None (Disabled)", value: ClickAction.NONE }
|
||||
]
|
||||
},
|
||||
enableDoubleClickToReply: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable double click to reply",
|
||||
default: true
|
||||
doubleClickOthersAction: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Action on double-click (on others' messages)",
|
||||
options: [
|
||||
{ label: "Reply", value: ClickAction.REPLY, default: true },
|
||||
{ label: "Quote", value: ClickAction.QUOTE },
|
||||
{ label: "Copy Content", value: ClickAction.COPY_CONTENT },
|
||||
{ label: "Copy Link", value: ClickAction.COPY_LINK },
|
||||
{ label: "Copy Message ID", value: ClickAction.COPY_MESSAGE_ID },
|
||||
{ label: "Copy User ID", value: ClickAction.COPY_USER_ID },
|
||||
{ label: "React", value: ClickAction.REACT },
|
||||
{ label: "Pin Message", value: ClickAction.PIN },
|
||||
{ label: "None (Disabled)", value: ClickAction.NONE }
|
||||
]
|
||||
},
|
||||
enableTripleClickToReact: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable triple click to react with an emoji",
|
||||
default: false
|
||||
tripleClickAction: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Action on triple-click",
|
||||
options: [
|
||||
{ label: "React", value: ClickAction.REACT, default: true },
|
||||
{ label: "Edit Message", value: ClickAction.EDIT },
|
||||
{ label: "Reply", value: ClickAction.REPLY },
|
||||
{ label: "Quote", value: ClickAction.QUOTE },
|
||||
{ label: "Copy Content", value: ClickAction.COPY_CONTENT },
|
||||
{ label: "Copy Link", value: ClickAction.COPY_LINK },
|
||||
{ label: "Copy Message ID", value: ClickAction.COPY_MESSAGE_ID },
|
||||
{ label: "Copy User ID", value: ClickAction.COPY_USER_ID },
|
||||
{ label: "Pin Message", value: ClickAction.PIN },
|
||||
{ label: "None (Disabled)", value: ClickAction.NONE }
|
||||
]
|
||||
},
|
||||
reactEmoji: {
|
||||
type: OptionType.STRING,
|
||||
description: "Emoji to react with (e.g. 💀 or pepe:123456789)",
|
||||
description: "Emoji to use for react actions (e.g. 💀 or pepe:123456789)",
|
||||
default: "💀"
|
||||
},
|
||||
requireModifier: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Only do double click actions when shift/ctrl is held",
|
||||
description: "Only perform click actions when shift or ctrl is held",
|
||||
default: false
|
||||
},
|
||||
disableInDms: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Disable all click actions in direct messages",
|
||||
default: false
|
||||
},
|
||||
disableInSystemDms: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Disable all click actions in system DMs (e.g. Clyde, Discord)",
|
||||
default: true
|
||||
},
|
||||
clickTimeout: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Timeout in milliseconds to distinguish double/triple clicks",
|
||||
default: 300
|
||||
},
|
||||
quoteWithReply: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "When quoting, also reply to the message",
|
||||
default: true
|
||||
},
|
||||
useSelectionForQuote: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "When quoting, use selected text if available (otherwise quotes full message)",
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
async function react(channelId: string, messageId: string, emoji: string) {
|
||||
function showWarning(message: string) {
|
||||
Toasts.show({
|
||||
message,
|
||||
type: Toasts.Type.FAILURE,
|
||||
id: Toasts.genId(),
|
||||
options: {
|
||||
duration: 3000
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function executeCommonAction(action: ClickAction, channel: Channel, msg: Message): boolean {
|
||||
switch (action) {
|
||||
case ClickAction.COPY_CONTENT:
|
||||
copyWithToast(msg.content || "", "Message content copied!");
|
||||
return true;
|
||||
case ClickAction.COPY_LINK:
|
||||
copyWithToast(`https://discord.com/channels/${channel.guild_id ?? "@me"}/${channel.id}/${msg.id}`, "Message link copied!");
|
||||
return true;
|
||||
case ClickAction.COPY_MESSAGE_ID:
|
||||
copyWithToast(msg.id, "Message ID copied!");
|
||||
return true;
|
||||
case ClickAction.COPY_USER_ID:
|
||||
copyWithToast(msg.author.id, "User ID copied!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isMessageReplyable(msg: Message) {
|
||||
return MessageTypeSets.REPLYABLE.has(msg.type) && !msg.hasFlag(MessageFlags.EPHEMERAL);
|
||||
}
|
||||
|
||||
async function toggleReaction(channelId: string, messageId: string, emoji: string, channel: Channel, msg: Message) {
|
||||
const trimmed = emoji.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
if (!PermissionStore.can(PermissionsBits.ADD_REACTIONS, channel) || !PermissionStore.can(PermissionsBits.READ_MESSAGE_HISTORY, channel)) {
|
||||
showWarning("Cannot react: Missing permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
const customMatch = trimmed.match(/^:?([\w-]+):(\d+)$/);
|
||||
const emojiParam = customMatch
|
||||
? `${customMatch[1]}:${customMatch[2]}`
|
||||
: encodeURIComponent(trimmed);
|
||||
: trimmed;
|
||||
|
||||
const hasReacted = msg.reactions?.some(r => {
|
||||
const reactionEmoji = r.emoji.id
|
||||
? `${r.emoji.name}:${r.emoji.id}`
|
||||
: r.emoji.name;
|
||||
return r.me && reactionEmoji === emojiParam;
|
||||
});
|
||||
|
||||
try {
|
||||
await RestAPI.put({
|
||||
url: `/channels/${channelId}/messages/${messageId}/reactions/${emojiParam}/@me`
|
||||
});
|
||||
if (hasReacted) {
|
||||
await RestAPI.del({
|
||||
url: Constants.Endpoints.REACTION(channelId, messageId, emojiParam, "@me")
|
||||
});
|
||||
} else {
|
||||
await RestAPI.put({
|
||||
url: Constants.Endpoints.REACTION(channelId, messageId, emojiParam, "@me")
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
new Logger("MessageClickActions").error("Failed to add reaction:", e);
|
||||
new Logger("MessageClickActions").error("Failed to toggle reaction:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function togglePin(channel: Channel, msg: Message) {
|
||||
if (!PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel)) {
|
||||
showWarning("Cannot pin: Missing permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.pinned) {
|
||||
PinActions.unpinMessage(channel, msg.id);
|
||||
} else {
|
||||
PinActions.pinMessage(channel, msg.id);
|
||||
}
|
||||
}
|
||||
|
||||
function quoteMessage(channel: Channel, msg: Message) {
|
||||
if (!isMessageReplyable(msg)) {
|
||||
showWarning("Cannot quote this message type");
|
||||
return;
|
||||
}
|
||||
|
||||
let { content } = msg;
|
||||
if (settings.store.useSelectionForQuote) {
|
||||
const selection = window.getSelection()?.toString().trim();
|
||||
if (selection && msg.content?.includes(selection)) {
|
||||
content = selection;
|
||||
}
|
||||
}
|
||||
if (!content) return;
|
||||
|
||||
const quoteText = content.split("\n").map(line => `> ${line}`).join("\n") + "\n";
|
||||
|
||||
insertTextIntoChatInputBox(quoteText);
|
||||
|
||||
if (settings.store.quoteWithReply) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "CREATE_PENDING_REPLY",
|
||||
channel,
|
||||
message: msg,
|
||||
shouldMention: false,
|
||||
showMentionToggle: !channel.isPrivate()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageClickActions",
|
||||
description: "Hold Backspace and click to delete, double click to edit/reply, triple click to react",
|
||||
description: "Customize message click actions - choose what happens when you click, double-click, or hold backspace",
|
||||
authors: [Devs.Ven, EquicordDevs.keyages],
|
||||
|
||||
settings,
|
||||
|
|
@ -117,23 +286,35 @@ export default definePlugin({
|
|||
const myId = AuthenticationStore.getId();
|
||||
const isMe = msg.author.id === myId;
|
||||
const isSelfInvokedUserApp = msg.interactionMetadata?.authorizing_integration_owners[ApplicationIntegrationType.USER_INSTALL] === myId;
|
||||
const isDM = channel.isDM();
|
||||
const isSystemDM = channel.isSystemDM();
|
||||
|
||||
if ((settings.store.disableInDms && isDM) || (settings.store.disableInSystemDms && isSystemDM)) return;
|
||||
|
||||
if (isDeletePressed) {
|
||||
if (!settings.store.enableDeleteOnClick) return;
|
||||
if (!(isMe || PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel) || isSelfInvokedUserApp)) return;
|
||||
const action = settings.store.backspaceClickAction;
|
||||
if (action === ClickAction.NONE) return;
|
||||
|
||||
if (msg.deleted) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_DELETE",
|
||||
channelId: channel.id,
|
||||
id: msg.id,
|
||||
mlDeleted: true
|
||||
});
|
||||
} else {
|
||||
MessageActions.deleteMessage(channel.id, msg.id);
|
||||
if (action === ClickAction.DELETE) {
|
||||
if (!(isMe || PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel) || isSelfInvokedUserApp)) {
|
||||
showWarning("Cannot delete: Missing permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.deleted) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_DELETE",
|
||||
channelId: channel.id,
|
||||
id: msg.id,
|
||||
mlDeleted: true
|
||||
});
|
||||
} else {
|
||||
MessageActions.deleteMessage(channel.id, msg.id);
|
||||
}
|
||||
} else if (executeCommonAction(action, channel, msg)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.detail === 3) {
|
||||
|
|
@ -143,25 +324,66 @@ export default definePlugin({
|
|||
pendingDoubleClickAction = null;
|
||||
}
|
||||
|
||||
if (settings.store.enableTripleClickToReact) {
|
||||
react(channel.id, msg.id, settings.store.reactEmoji);
|
||||
const action = settings.store.tripleClickAction;
|
||||
if (action === ClickAction.NONE) return;
|
||||
|
||||
if (action === ClickAction.REACT) {
|
||||
toggleReaction(channel.id, msg.id, settings.store.reactEmoji, channel, msg);
|
||||
} else if (action === ClickAction.EDIT) {
|
||||
if (!isMe) return;
|
||||
if (EditStore.isEditing(channel.id, msg.id) || msg.state !== "SENT") return;
|
||||
MessageActions.startEditMessage(channel.id, msg.id, msg.content);
|
||||
} else if (action === ClickAction.REPLY) {
|
||||
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) {
|
||||
showWarning("Cannot reply: Missing permissions");
|
||||
return;
|
||||
}
|
||||
if (!isMessageReplyable(msg)) {
|
||||
showWarning("Cannot reply to this message type");
|
||||
return;
|
||||
}
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
type: "CREATE_PENDING_REPLY",
|
||||
channel,
|
||||
message: msg,
|
||||
shouldMention: !isMe,
|
||||
showMentionToggle: !channel.isPrivate()
|
||||
});
|
||||
} else if (executeCommonAction(action, channel, msg)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else if (action === ClickAction.QUOTE) {
|
||||
quoteMessage(channel, msg);
|
||||
} else if (action === ClickAction.PIN) {
|
||||
togglePin(channel, msg);
|
||||
}
|
||||
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;
|
||||
const action = isMe ? settings.store.doubleClickAction : settings.store.doubleClickOthersAction;
|
||||
if (action === ClickAction.NONE) return;
|
||||
|
||||
if (action === ClickAction.EDIT) {
|
||||
if (!isMe) return;
|
||||
if (EditStore.isEditing(channel.id, msg.id) || msg.state !== "SENT") return;
|
||||
MessageActions.startEditMessage(channel.id, msg.id, msg.content);
|
||||
} else {
|
||||
if (!settings.store.enableDoubleClickToReply) return;
|
||||
if (!MessageTypeSets.REPLYABLE.has(msg.type) || msg.hasFlag(MessageFlags.EPHEMERAL)) return;
|
||||
} else if (action === ClickAction.REPLY) {
|
||||
if (isMe) return;
|
||||
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) {
|
||||
showWarning("Cannot reply: Missing permissions");
|
||||
return;
|
||||
}
|
||||
if (!isMessageReplyable(msg)) {
|
||||
showWarning("Cannot reply to this message type");
|
||||
return;
|
||||
}
|
||||
|
||||
const isShiftPress = event.shiftKey && !settings.store.requireModifier;
|
||||
const shouldMention = isPluginEnabled(NoReplyMentionPlugin.name)
|
||||
|
|
@ -173,12 +395,21 @@ export default definePlugin({
|
|||
channel,
|
||||
message: msg,
|
||||
shouldMention,
|
||||
showMentionToggle: channel.guild_id !== null
|
||||
showMentionToggle: !channel.isPrivate()
|
||||
});
|
||||
} else if (executeCommonAction(action, channel, msg)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else if (action === ClickAction.QUOTE) {
|
||||
quoteMessage(channel, msg);
|
||||
} else if (action === ClickAction.REACT) {
|
||||
toggleReaction(channel.id, msg.id, settings.store.reactEmoji, channel, msg);
|
||||
} else if (action === ClickAction.PIN) {
|
||||
togglePin(channel, msg);
|
||||
}
|
||||
};
|
||||
|
||||
if (settings.store.enableTripleClickToReact) {
|
||||
if (settings.store.tripleClickAction !== ClickAction.NONE) {
|
||||
if (doubleClickTimeout) {
|
||||
clearTimeout(doubleClickTimeout);
|
||||
}
|
||||
|
|
@ -187,7 +418,7 @@ export default definePlugin({
|
|||
pendingDoubleClickAction?.();
|
||||
pendingDoubleClickAction = null;
|
||||
doubleClickTimeout = null;
|
||||
}, 300);
|
||||
}, settings.store.clickTimeout);
|
||||
event.preventDefault();
|
||||
} else {
|
||||
executeDoubleClick();
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export let RTCConnectionStore: t.RTCConnectionStore;
|
|||
export let SoundboardStore: t.SoundboardStore;
|
||||
export let PopoutWindowStore: t.PopoutWindowStore;
|
||||
export let ApplicationCommandIndexStore: GenericStore;
|
||||
export let EditStore: GenericStore;
|
||||
|
||||
/**
|
||||
* @see jsdoc of {@link t.useStateFromStores}
|
||||
|
|
@ -132,6 +133,7 @@ waitForStore("RTCConnectionStore", m => RTCConnectionStore = m);
|
|||
waitForStore("SoundboardStore", m => SoundboardStore = m);
|
||||
waitForStore("PopoutWindowStore", m => PopoutWindowStore = m);
|
||||
waitForStore("ApplicationCommandIndexStore", m => ApplicationCommandIndexStore = m);
|
||||
waitForStore("EditStore", m => EditStore = m);
|
||||
waitForStore("ThemeStore", m => {
|
||||
ThemeStore = m;
|
||||
// Importing this directly causes all webpack commons to be imported, which can easily cause circular dependencies.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue