mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(app): use +:shortcode: for reactions, no space before emoji (#3001)
This commit is contained in:
@@ -326,7 +326,7 @@ export const LexicalChannelTextareaContent = observer(
|
|||||||
}, [value, segmentManagerRef]);
|
}, [value, segmentManagerRef]);
|
||||||
const handleEmojiSelect = useCallback(
|
const handleEmojiSelect = useCallback(
|
||||||
(emoji: FlatEmoji, shiftKey?: boolean): boolean => {
|
(emoji: FlatEmoji, shiftKey?: boolean): boolean => {
|
||||||
const didInsert = insertComposerEmoji(handleRef.current, emoji);
|
const didInsert = insertComposerEmoji(handleRef.current, emoji, {reactionShorthand: true});
|
||||||
if (didInsert && !shiftKey) {
|
if (didInsert && !shiftKey) {
|
||||||
ExpressionPickerCommands.close();
|
ExpressionPickerCommands.close();
|
||||||
PopoutCommands.close(`expression-picker-${channel.id}`);
|
PopoutCommands.close(`expression-picker-${channel.id}`);
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ import {
|
|||||||
} from '@app/features/lexical/composer/nodes/ComposerStandardEmojiNode';
|
} from '@app/features/lexical/composer/nodes/ComposerStandardEmojiNode';
|
||||||
import {$isSyntaxMarkerNode} from '@app/features/lexical/composer/nodes/SyntaxMarkerNode';
|
import {$isSyntaxMarkerNode} from '@app/features/lexical/composer/nodes/SyntaxMarkerNode';
|
||||||
import {findTypedEmojiShortcode, type TypedEmojiMatch} from '@app/features/messaging/utils/markdown/TypedEmojiMatch';
|
import {findTypedEmojiShortcode, type TypedEmojiMatch} from '@app/features/messaging/utils/markdown/TypedEmojiMatch';
|
||||||
|
import {isReactionShorthandText} from '@app/features/messaging/utils/ReactionShorthandUtils';
|
||||||
import type {ResolvedTypedEmoji} from '@app/features/messaging/utils/TypedEmojiShortcodeUtils';
|
import type {ResolvedTypedEmoji} from '@app/features/messaging/utils/TypedEmojiShortcodeUtils';
|
||||||
import {type LexicalEditor, TextNode} from 'lexical';
|
import {$getRoot, type LexicalEditor, TextNode} from 'lexical';
|
||||||
|
|
||||||
export type ComposerEmojiResolver = (shortcodeName: string) => ResolvedTypedEmoji | null;
|
export type ComposerEmojiResolver = (shortcodeName: string) => ResolvedTypedEmoji | null;
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ function isEscapedAt(text: string, index: number): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function $convertEmojiShortcode(node: TextNode, resolve: ComposerEmojiResolver): void {
|
export function $convertEmojiShortcode(node: TextNode, resolve: ComposerEmojiResolver): void {
|
||||||
if ($isSyntaxMarkerNode(node) || node.hasFormat('code')) {
|
if ($isSyntaxMarkerNode(node) || node.hasFormat('code') || isReactionShorthandText($getRoot().getTextContent())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parent = node.getParent();
|
const parent = node.getParent();
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import * as EmojiImageUtils from '@app/features/expressions/utils/EmojiUtils';
|
|||||||
import {getSkinTonedSurrogate} from '@app/features/expressions/utils/SkinToneUtils';
|
import {getSkinTonedSurrogate} from '@app/features/expressions/utils/SkinToneUtils';
|
||||||
import type {ComposerHandle, ComposerSelectionRange} from '@app/features/lexical/composer/ComposerHandle';
|
import type {ComposerHandle, ComposerSelectionRange} from '@app/features/lexical/composer/ComposerHandle';
|
||||||
import type {ComposerInsertPayload, ComposerInsertSpacing} from '@app/features/lexical/composer/composerOffsets';
|
import type {ComposerInsertPayload, ComposerInsertSpacing} from '@app/features/lexical/composer/composerOffsets';
|
||||||
|
import {
|
||||||
|
getReactionShortcodeName,
|
||||||
|
isReactionShorthandPrefix,
|
||||||
|
} from '@app/features/messaging/utils/ReactionShorthandUtils';
|
||||||
import {type MentionSegment, TextareaSegmentManager} from '@app/features/messaging/utils/TextareaSegmentManager';
|
import {type MentionSegment, TextareaSegmentManager} from '@app/features/messaging/utils/TextareaSegmentManager';
|
||||||
|
|
||||||
export interface ComposerReplacementPlan {
|
export interface ComposerReplacementPlan {
|
||||||
@@ -24,6 +28,10 @@ export interface ComposerReplacementLimit {
|
|||||||
onExceedMaxLength?: () => void;
|
onExceedMaxLength?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ComposerEmojiInsertOptions extends ComposerReplacementLimit {
|
||||||
|
reactionShorthand?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface ComposerPayloadSegment {
|
interface ComposerPayloadSegment {
|
||||||
type: MentionSegment['type'];
|
type: MentionSegment['type'];
|
||||||
id: string;
|
id: string;
|
||||||
@@ -162,23 +170,27 @@ export function applyComposerReplacement(
|
|||||||
export function insertComposerEmoji(
|
export function insertComposerEmoji(
|
||||||
handle: ComposerHandle | null,
|
handle: ComposerHandle | null,
|
||||||
emoji: FlatEmoji,
|
emoji: FlatEmoji,
|
||||||
limit: ComposerReplacementLimit = {},
|
options: ComposerEmojiInsertOptions = {},
|
||||||
): boolean {
|
): boolean {
|
||||||
if (handle == null) {
|
if (handle == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const display = handle.getDisplayValue();
|
const display = handle.getDisplayValue();
|
||||||
const selection = normalizeSelection(display, handle.getSelection());
|
const selection = normalizeSelection(display, handle.getSelection());
|
||||||
const charBefore = selection.start > 0 ? display[selection.start - 1] : '';
|
|
||||||
const charAfter = selection.end < display.length ? display[selection.end] : '';
|
const charAfter = selection.end < display.length ? display[selection.end] : '';
|
||||||
|
const payload: ComposerInsertPayload =
|
||||||
|
options.reactionShorthand === true &&
|
||||||
|
isReactionShorthandPrefix(display.slice(0, selection.start), display.slice(selection.end))
|
||||||
|
? {kind: 'text', text: `:${getReactionShortcodeName(emoji)}:`}
|
||||||
|
: createComposerEmojiPayload(emoji);
|
||||||
return applyComposerReplacement(
|
return applyComposerReplacement(
|
||||||
handle,
|
handle,
|
||||||
selection,
|
selection,
|
||||||
createComposerEmojiPayload(emoji),
|
payload,
|
||||||
{
|
{
|
||||||
leading: charBefore !== '' && !/\s/.test(charBefore),
|
leading: false,
|
||||||
trailing: charAfter === '' || !/\s/.test(charAfter),
|
trailing: charAfter === '' || !/\s/.test(charAfter),
|
||||||
},
|
},
|
||||||
limit,
|
options,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ import type {GuildMember} from '@app/features/member/models/GuildMember';
|
|||||||
import GuildMembers from '@app/features/member/state/GuildMembers';
|
import GuildMembers from '@app/features/member/state/GuildMembers';
|
||||||
import type {SearchContext} from '@app/features/member/state/MemberSearch';
|
import type {SearchContext} from '@app/features/member/state/MemberSearch';
|
||||||
import * as HighlightCommands from '@app/features/messaging/commands/HighlightCommands';
|
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 Messages from '@app/features/messaging/state/MessagingMessages';
|
||||||
import {
|
import {
|
||||||
filterAutocompleteMediaOptions,
|
filterAutocompleteMediaOptions,
|
||||||
@@ -77,8 +75,7 @@ import {
|
|||||||
SPECIAL_MENTIONS,
|
SPECIAL_MENTIONS,
|
||||||
} from '@app/features/messaging/utils/AutocompleteOptionBuilders';
|
} from '@app/features/messaging/utils/AutocompleteOptionBuilders';
|
||||||
import {isAutocompleteTriggerAllowed, type TriggerType} from '@app/features/messaging/utils/AutocompleteTriggerPolicy';
|
import {isAutocompleteTriggerAllowed, type TriggerType} from '@app/features/messaging/utils/AutocompleteTriggerPolicy';
|
||||||
import {toReactionEmoji} from '@app/features/messaging/utils/MessageReactionUtils';
|
import {getReactionShortcodeName} from '@app/features/messaging/utils/ReactionShorthandUtils';
|
||||||
import {getReactionShorthandTargetId} from '@app/features/messaging/utils/ReactionShorthandUtils';
|
|
||||||
import {
|
import {
|
||||||
type AutocompleteTrigger,
|
type AutocompleteTrigger,
|
||||||
detectAutocompleteTrigger,
|
detectAutocompleteTrigger,
|
||||||
@@ -706,14 +703,13 @@ export function useLexicalAutocomplete({
|
|||||||
const caret = currentTextUpToCursor.length;
|
const caret = currentTextUpToCursor.length;
|
||||||
const matchStart = getComposerAutocompleteReplacementStart(currentTextUpToCursor, trigger.type, trigger.match);
|
const matchStart = getComposerAutocompleteReplacementStart(currentTextUpToCursor, trigger.type, trigger.match);
|
||||||
if (trigger.type === 'emojiReaction' && isEmoji(option)) {
|
if (trigger.type === 'emojiReaction' && isEmoji(option)) {
|
||||||
if (channel != null) {
|
applyComposerReplacement(
|
||||||
const targetId = getReactionShorthandTargetId(channel.id);
|
handle,
|
||||||
if (targetId !== null) {
|
{start: matchStart, end: caret},
|
||||||
ReactionCommands.addReaction(i18n, channel.id, targetId, toReactionEmoji(option.emoji));
|
{kind: 'text', text: `+:${getReactionShortcodeName(option.emoji)}:`},
|
||||||
MessageCommands.stopReply(channel.id);
|
{trailing: true},
|
||||||
}
|
{maxWireLength: maxActualLength, onExceedMaxLength},
|
||||||
}
|
);
|
||||||
handle.clear();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isCommand(option)) {
|
if (isCommand(option)) {
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ export const useTextareaSubmit = ({
|
|||||||
const replaceCommand = ReplaceCommandUtils.parseReplaceCommand(actualContent);
|
const replaceCommand = ReplaceCommandUtils.parseReplaceCommand(actualContent);
|
||||||
const reactionShorthand =
|
const reactionShorthand =
|
||||||
editingMessage === null && uploadAttachmentsLength === 0 && !hasPendingSticker
|
editingMessage === null && uploadAttachmentsLength === 0 && !hasPendingSticker
|
||||||
? parseReactionShorthand(resolvedContent)
|
? parseReactionShorthand(actualContent, Channels.getChannel(channelId) ?? null, guildId, i18n)
|
||||||
: null;
|
: null;
|
||||||
const reactionTargetId = reactionShorthand === null ? null : getReactionShorthandTargetId(channelId);
|
const reactionTargetId = reactionShorthand === null ? null : getReactionShorthandTargetId(channelId);
|
||||||
if (reactionShorthand !== null && reactionTargetId !== null) {
|
if (reactionShorthand !== null && reactionTargetId !== null) {
|
||||||
|
|||||||
@@ -1,34 +1,54 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import type {Channel} from '@app/features/channel/models/Channel';
|
||||||
|
import type {FlatEmoji} from '@app/features/emoji/types/EmojiTypes';
|
||||||
|
import {getSkinTonedSurrogate} from '@app/features/expressions/utils/SkinToneUtils';
|
||||||
import UnicodeEmojis from '@app/features/expressions/utils/UnicodeEmojis';
|
import UnicodeEmojis from '@app/features/expressions/utils/UnicodeEmojis';
|
||||||
import MessageReply from '@app/features/messaging/state/MessageReply';
|
import MessageReply from '@app/features/messaging/state/MessageReply';
|
||||||
import Messages from '@app/features/messaging/state/MessagingMessages';
|
import Messages from '@app/features/messaging/state/MessagingMessages';
|
||||||
import {type ReactionEmoji, toReactionEmoji} from '@app/features/messaging/utils/MessageReactionUtils';
|
import type {ReactionEmoji} from '@app/features/messaging/utils/MessageReactionUtils';
|
||||||
|
import {resolveTypedEmojiToken} from '@app/features/messaging/utils/TypedEmojiShortcodeUtils';
|
||||||
|
import type {I18n} from '@lingui/core';
|
||||||
|
|
||||||
const REACTION_SHORTHAND_PATTERN = /^\+(\S+)$/u;
|
const REACTION_SHORTHAND_PATTERN = /^\s*\+:([^\s:]+(?:::skin-tone-[1-5])?):\s*$/u;
|
||||||
const CUSTOM_EMOJI_MARKDOWN_PATTERN = /^<(a)?:([a-zA-Z0-9_+-]{2,}):(\d+)>$/;
|
const REACTION_SHORTHAND_PREFIX_PATTERN = /^\s*\+$/;
|
||||||
const SHORTCODE_PATTERN = /^:([^\s:]+):$/;
|
|
||||||
|
|
||||||
export function parseReactionShorthand(content: string): ReactionEmoji | null {
|
export function isReactionShorthandText(text: string): boolean {
|
||||||
const match = REACTION_SHORTHAND_PATTERN.exec(content.trim());
|
return REACTION_SHORTHAND_PATTERN.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isReactionShorthandPrefix(textBefore: string, textAfter: string): boolean {
|
||||||
|
return REACTION_SHORTHAND_PREFIX_PATTERN.test(textBefore) && textAfter.trim() === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReactionShortcodeName(emoji: FlatEmoji): string {
|
||||||
|
if (emoji.id) {
|
||||||
|
return emoji.name;
|
||||||
|
}
|
||||||
|
const name = UnicodeEmojis.nameForSurrogate(getSkinTonedSurrogate(emoji), false);
|
||||||
|
return name === '' ? emoji.name : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseReactionShorthand(
|
||||||
|
content: string,
|
||||||
|
channel: Channel | null,
|
||||||
|
guildId: string | null,
|
||||||
|
i18n: I18n,
|
||||||
|
): ReactionEmoji | null {
|
||||||
|
const match = REACTION_SHORTHAND_PATTERN.exec(content);
|
||||||
if (match === null) {
|
if (match === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const token = match[1];
|
const shortcodeName = match[1];
|
||||||
const customMatch = CUSTOM_EMOJI_MARKDOWN_PATTERN.exec(token);
|
const unicodeEmoji = UnicodeEmojis.findEmojiByShortcodeName(shortcodeName);
|
||||||
if (customMatch !== null) {
|
if (unicodeEmoji !== null) {
|
||||||
return {id: customMatch[3], name: customMatch[2], animated: customMatch[1] === 'a'};
|
return {name: unicodeEmoji.surrogates};
|
||||||
}
|
}
|
||||||
const shortcodeMatch = SHORTCODE_PATTERN.exec(token);
|
const resolved = resolveTypedEmojiToken(shortcodeName, channel, guildId, i18n);
|
||||||
if (shortcodeMatch !== null) {
|
if (resolved === null || resolved.kind !== 'custom') {
|
||||||
const emoji = UnicodeEmojis.findEmojiByShortcodeName(shortcodeMatch[1]);
|
|
||||||
return emoji === null ? null : toReactionEmoji(emoji);
|
|
||||||
}
|
|
||||||
const name = UnicodeEmojis.nameForSurrogate(token, false);
|
|
||||||
if (name === '') {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {name: UnicodeEmojis.surrogateForName(name, token)};
|
return {id: resolved.emojiId, name: shortcodeName.replace(/~\d+$/, ''), animated: resolved.animated};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getReactionShorthandTargetId(channelId: string): string | null {
|
export function getReactionShorthandTargetId(channelId: string): string | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user