diff --git a/fluxer_app/src/features/lexical/composer/useLexicalAutocomplete.ts b/fluxer_app/src/features/lexical/composer/useLexicalAutocomplete.ts index 18bdb4fc8..20434234f 100644 --- a/fluxer_app/src/features/lexical/composer/useLexicalAutocomplete.ts +++ b/fluxer_app/src/features/lexical/composer/useLexicalAutocomplete.ts @@ -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(); diff --git a/fluxer_app/src/features/messaging/hooks/useTextareaSubmit.ts b/fluxer_app/src/features/messaging/hooks/useTextareaSubmit.ts index f9a83023b..dc69421b9 100644 --- a/fluxer_app/src/features/messaging/hooks/useTextareaSubmit.ts +++ b/fluxer_app/src/features/messaging/hooks/useTextareaSubmit.ts @@ -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, diff --git a/fluxer_app/src/features/messaging/utils/ReactionShorthandUtils.ts b/fluxer_app/src/features/messaging/utils/ReactionShorthandUtils.ts new file mode 100644 index 000000000..27fa9f629 --- /dev/null +++ b/fluxer_app/src/features/messaging/utils/ReactionShorthandUtils.ts @@ -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; +}