feat(app): make +emoji react on send and target replies (#2998)

This commit is contained in:
Hampus
2026-09-28 00:08:40 +02:00
committed by GitHub
parent 153dad11e1
commit 463c03fb6d
3 changed files with 66 additions and 4 deletions
@@ -57,6 +57,7 @@ import type {GuildMember} from '@app/features/member/models/GuildMember';
import GuildMembers from '@app/features/member/state/GuildMembers';
import type {SearchContext} from '@app/features/member/state/MemberSearch';
import * as HighlightCommands from '@app/features/messaging/commands/HighlightCommands';
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
import * as ReactionCommands from '@app/features/messaging/commands/ReactionCommands';
import Messages from '@app/features/messaging/state/MessagingMessages';
import {
@@ -77,6 +78,7 @@ import {
} from '@app/features/messaging/utils/AutocompleteOptionBuilders';
import {isAutocompleteTriggerAllowed, type TriggerType} from '@app/features/messaging/utils/AutocompleteTriggerPolicy';
import {toReactionEmoji} from '@app/features/messaging/utils/MessageReactionUtils';
import {getReactionShorthandTargetId} from '@app/features/messaging/utils/ReactionShorthandUtils';
import {
type AutocompleteTrigger,
detectAutocompleteTrigger,
@@ -705,10 +707,10 @@ export function useLexicalAutocomplete({
const matchStart = getComposerAutocompleteReplacementStart(currentTextUpToCursor, trigger.type, trigger.match);
if (trigger.type === 'emojiReaction' && isEmoji(option)) {
if (channel != null) {
const messages = Messages.getMessages(channel.id).toArray();
const mostRecent = messages[messages.length - 1];
if (mostRecent != null) {
ReactionCommands.addReaction(i18n, channel.id, mostRecent.id, toReactionEmoji(option.emoji));
const targetId = getReactionShorthandTargetId(channel.id);
if (targetId !== null) {
ReactionCommands.addReaction(i18n, channel.id, targetId, toReactionEmoji(option.emoji));
MessageCommands.stopReply(channel.id);
}
}
handle.clear();
@@ -20,6 +20,7 @@ import GuildMembers from '@app/features/member/state/GuildMembers';
import MemberSidebar from '@app/features/member/state/MemberSidebar';
import * as DraftCommands from '@app/features/messaging/commands/DraftCommands';
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
import * as ReactionCommands from '@app/features/messaging/commands/ReactionCommands';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import type {MentionConfirmationInfo, MentionType} from '@app/features/messaging/state/MentionConfirmationStateMachine';
import Messages from '@app/features/messaging/state/MessagingMessages';
@@ -29,6 +30,10 @@ import {
isAttachmentOnlyMessage,
} from '@app/features/messaging/utils/MessageEditContentUtils';
import {canSubmitMessage, hasVisibleMessageContent} from '@app/features/messaging/utils/MessageRequestUtils';
import {
getReactionShorthandTargetId,
parseReactionShorthand,
} from '@app/features/messaging/utils/ReactionShorthandUtils';
import * as ReplaceCommandUtils from '@app/features/messaging/utils/ReplaceCommandUtils';
import {resolveTypedEmojiShortcodes} from '@app/features/messaging/utils/TypedEmojiShortcodeUtils';
import Permission from '@app/features/permissions/state/Permission';
@@ -458,6 +463,20 @@ export const useTextareaSubmit = ({
parsedCommand = lexicalCommand.command;
}
const replaceCommand = ReplaceCommandUtils.parseReplaceCommand(actualContent);
const reactionShorthand =
editingMessage === null && uploadAttachmentsLength === 0 && !hasPendingSticker
? parseReactionShorthand(resolvedContent)
: null;
const reactionTargetId = reactionShorthand === null ? null : getReactionShorthandTargetId(channelId);
if (reactionShorthand !== null && reactionTargetId !== null) {
ReactionCommands.addReaction(i18n, channelId, reactionTargetId, reactionShorthand);
setValue('');
clearSegments();
DraftCommands.deleteDraft(channelId);
TypingUtils.clear(channelId);
MessageCommands.stopReply(channelId);
return;
}
if (
shouldBlockSubmissionForSlowmode(
isSlowmodeActive,
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import UnicodeEmojis from '@app/features/expressions/utils/UnicodeEmojis';
import MessageReply from '@app/features/messaging/state/MessageReply';
import Messages from '@app/features/messaging/state/MessagingMessages';
import {type ReactionEmoji, toReactionEmoji} from '@app/features/messaging/utils/MessageReactionUtils';
const REACTION_SHORTHAND_PATTERN = /^\+(\S+)$/u;
const CUSTOM_EMOJI_MARKDOWN_PATTERN = /^<(a)?:([a-zA-Z0-9_+-]{2,}):(\d+)>$/;
const SHORTCODE_PATTERN = /^:([^\s:]+):$/;
export function parseReactionShorthand(content: string): ReactionEmoji | null {
const match = REACTION_SHORTHAND_PATTERN.exec(content.trim());
if (match === null) {
return null;
}
const token = match[1];
const customMatch = CUSTOM_EMOJI_MARKDOWN_PATTERN.exec(token);
if (customMatch !== null) {
return {id: customMatch[3], name: customMatch[2], animated: customMatch[1] === 'a'};
}
const shortcodeMatch = SHORTCODE_PATTERN.exec(token);
if (shortcodeMatch !== null) {
const emoji = UnicodeEmojis.findEmojiByShortcodeName(shortcodeMatch[1]);
return emoji === null ? null : toReactionEmoji(emoji);
}
const name = UnicodeEmojis.nameForSurrogate(token, false);
if (name === '') {
return null;
}
return {name: UnicodeEmojis.surrogateForName(name, token)};
}
export function getReactionShorthandTargetId(channelId: string): string | null {
const reply = MessageReply.getReplyingMessage(channelId);
if (reply !== null) {
return reply.messageId;
}
const messages = Messages.getMessages(channelId).toArray();
return messages[messages.length - 1]?.id ?? null;
}