Plugin
QuickReply
Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds
1
import { isPluginEnabled } from "@api/PluginManager";2
import { definePluginSettings } from "@api/Settings";3
import NoBlockedMessagesPlugin from "@plugins/noBlockedMessages";4
import NoReplyMentionPlugin from "@plugins/noReplyMention";5
import { Devs, IS_MAC } from "@utils/constants";6
import definePlugin, { OptionType } from "@utils/types";7
import { Message } from "@vencord/discord-types";8
import { MessageFlags } from "@vencord/discord-types/enums";9
import { ChannelStore, ComponentDispatch, FluxDispatcher as Dispatcher, MessageActions, MessageStore, MessageTypeSets, PermissionsBits, PermissionStore, RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common";10
11
let currentlyReplyingId: string | null = null;12
let currentlyEditingId: string | null = null;13
14
const enum MentionOptions {15
DISABLED,16
ENABLED,17
NO_REPLY_MENTION_PLUGIN18
}19
20
const settings = definePluginSettings({21
shouldMention: {22
type: OptionType.SELECT,23
description: "Ping reply by default",24
options: [25
{26
label: "Follow NoReplyMention plugin (if enabled)",27
value: MentionOptions.NO_REPLY_MENTION_PLUGIN,28
default: true29
},30
{ label: "Enabled", value: MentionOptions.ENABLED },31
{ label: "Disabled", value: MentionOptions.DISABLED },32
]33
},34
ignoreBlockedAndIgnored: {35
type: OptionType.BOOLEAN,36
description: "Ignore messages by blocked/ignored users when navigating",37
default: true38
}39
});40
41
export default definePlugin({42
name: "QuickReply",43
authors: [Devs.fawn, Devs.Ven, Devs.pylix],44
description: "Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds",45
tags: ["Chat", "Shortcuts"],46
settings,47
48
start() {49
document.addEventListener("keydown", onKeydown);50
},51
52
stop() {53
document.removeEventListener("keydown", onKeydown);54
},55
56
flux: {57
DELETE_PENDING_REPLY() {58
currentlyReplyingId = null;59
},60
MESSAGE_END_EDIT() {61
currentlyEditingId = null;62
},63
CHANNEL_SELECT() {64
currentlyReplyingId = null;65
currentlyEditingId = null;66
},67
MESSAGE_START_EDIT: onStartEdit,68
CREATE_PENDING_REPLY: onCreatePendingReply69
}70
});71
72
function onStartEdit({ messageId, _isQuickEdit }: any) {73
if (_isQuickEdit) return;74
currentlyEditingId = messageId;75
}76
77
function onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) {78
if (_isQuickReply) return;79
80
currentlyReplyingId = message.id;81
}82
83
const isCtrl = (e: KeyboardEvent) => IS_MAC ? e.metaKey : e.ctrlKey;84
const isAltOrMeta = (e: KeyboardEvent) => e.altKey || (!IS_MAC && e.metaKey);85
86
function onKeydown(e: KeyboardEvent) {87
const isUp = e.key === "ArrowUp";88
if (!isUp && e.key !== "ArrowDown") return;89
if (!isCtrl(e) || isAltOrMeta(e)) return;90
91
e.preventDefault();92
93
if (e.shiftKey)94
nextEdit(isUp);95
else96
nextReply(isUp);97
}98
99
function jumpIfOffScreen(channelId: string, messageId: string) {100
const element = document.getElementById("message-content-" + messageId);101
if (!element) return;102
103
const vh = Math.max(document.documentElement.clientHeight, window.innerHeight);104
const rect = element.getBoundingClientRect();105
const isOffscreen = rect.bottom < 150 || rect.top - vh >= -150;106
107
if (isOffscreen) {108
MessageActions.jumpToMessage({109
channelId,110
messageId,111
flash: false,112
jumpType: "INSTANT"113
});114
}115
}116
117
function getNextMessage(isUp: boolean, isReply: boolean) {118
let messages: Message[] = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;119
120
const meId = UserStore.getCurrentUser().id;121
const hasNoBlockedMessages = isPluginEnabled(NoBlockedMessagesPlugin.name);122
123
messages = messages.filter(m => {124
if (m.deleted) return false;125
if (!isReply && m.author.id !== meId) return false; class="ts-cmt">// editing only own messages126
if (!MessageTypeSets.REPLYABLE.has(m.type) || m.hasFlag(MessageFlags.EPHEMERAL)) return false;127
if (settings.store.ignoreBlockedAndIgnored && RelationshipStore.isBlockedOrIgnored(m.author.id)) return false;128
if (hasNoBlockedMessages && NoBlockedMessagesPlugin.shouldIgnoreMessage(m)) return false;129
130
return true;131
});132
133
const findNextNonDeleted = (id: string | null) => {134
if (id === null) return messages[messages.length - 1];135
136
const idx = messages.findIndex(m => m.id === id);137
if (idx === -1) return messages[messages.length - 1];138
139
const i = isUp ? idx - 1 : idx + 1;140
return messages[i] ?? null;141
};142
143
if (isReply) {144
const msg = findNextNonDeleted(currentlyReplyingId);145
currentlyReplyingId = msg?.id ?? null;146
return msg;147
} else {148
const msg = findNextNonDeleted(currentlyEditingId);149
currentlyEditingId = msg?.id ?? null;150
return msg;151
}152
}153
154
function shouldMention(message: Message) {155
switch (settings.store.shouldMention) {156
case MentionOptions.NO_REPLY_MENTION_PLUGIN:157
if (!isPluginEnabled(NoReplyMentionPlugin.name)) return true;158
return NoReplyMentionPlugin.shouldMention(message, false);159
case MentionOptions.DISABLED:160
return false;161
default:162
return true;163
}164
}165
166
// handle next/prev reply167
function nextReply(isUp: boolean) {168
const currChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());169
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;170
171
const message = getNextMessage(isUp, true);172
173
if (!message) {174
return void Dispatcher.dispatch({175
type: "DELETE_PENDING_REPLY",176
channelId: SelectedChannelStore.getChannelId(),177
});178
}179
180
const channel = ChannelStore.getChannel(message.channel_id);181
const meId = UserStore.getCurrentUser().id;182
183
Dispatcher.dispatch({184
type: "CREATE_PENDING_REPLY",185
channel,186
message,187
shouldMention: shouldMention(message),188
showMentionToggle: !channel.isPrivate() && message.author.id !== meId,189
_isQuickReply: true190
});191
192
ComponentDispatch.dispatchToLastSubscribed("TEXTAREA_FOCUS");193
jumpIfOffScreen(channel.id, message.id);194
}195
196
// handle next/prev edit197
function nextEdit(isUp: boolean) {198
const currChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());199
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;200
const message = getNextMessage(isUp, false);201
202
if (!message) {203
return Dispatcher.dispatch({204
type: "MESSAGE_END_EDIT",205
channelId: SelectedChannelStore.getChannelId()206
});207
}208
209
Dispatcher.dispatch({210
type: "MESSAGE_START_EDIT",211
channelId: message.channel_id,212
messageId: message.id,213
content: message.content,214
_isQuickEdit: true215
});216
217
jumpIfOffScreen(message.channel_id, message.id);218
}219