Merge branch 'emoji-node' into 'main'

lexical: restore EmojiNode, fix Android backspace!

Closes #1529

See merge request soapbox-pub/soapbox!2758
environments/review-main-yi2y9f/deployments/4056
Alex Gleason 2023-09-25 21:38:16 +00:00
commit 33c775709b
9 zmienionych plików z 62 dodań i 71 usunięć

Wyświetl plik

@ -13,7 +13,7 @@ import toast from 'soapbox/toast';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures, parseVersion } from 'soapbox/utils/features';
import { useEmoji } from './emojis';
import { chooseEmoji } from './emojis';
import { importFetchedAccounts } from './importer';
import { uploadFile, updateMedia } from './media';
import { openModal, closeModal } from './modals';
@ -631,7 +631,7 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
startPosition = position - 1;
dispatch(useEmoji(suggestion));
dispatch(chooseEmoji(suggestion));
} else if (typeof suggestion === 'string' && suggestion[0] === '#') {
completion = suggestion;
startPosition = position - 1;

Wyświetl plik

@ -3,12 +3,12 @@ import { saveSettings } from './settings';
import type { Emoji } from 'soapbox/features/emoji';
import type { AppDispatch } from 'soapbox/store';
const EMOJI_USE = 'EMOJI_USE';
const EMOJI_CHOOSE = 'EMOJI_CHOOSE';
const useEmoji = (emoji: Emoji) =>
const chooseEmoji = (emoji: Emoji) =>
(dispatch: AppDispatch) => {
dispatch({
type: EMOJI_USE,
type: EMOJI_CHOOSE,
emoji,
});
@ -16,6 +16,6 @@ const useEmoji = (emoji: Emoji) =>
};
export {
EMOJI_USE,
useEmoji,
EMOJI_CHOOSE,
chooseEmoji,
};

Wyświetl plik

@ -1,5 +1,5 @@
import clsx from 'clsx';
import { CLEAR_EDITOR_COMMAND, type LexicalEditor } from 'lexical';
import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor } from 'lexical';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link, useHistory } from 'react-router-dom';
@ -11,7 +11,6 @@ import {
clearComposeSuggestions,
fetchComposeSuggestions,
selectComposeSuggestion,
insertEmojiCompose,
uploadCompose,
} from 'soapbox/actions/compose';
import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input';
@ -28,6 +27,7 @@ import ReplyIndicatorContainer from '../containers/reply-indicator-container';
import ScheduleFormContainer from '../containers/schedule-form-container';
import UploadButtonContainer from '../containers/upload-button-container';
import WarningContainer from '../containers/warning-container';
import { $createEmojiNode } from '../editor/nodes/emoji-node';
import { countableText } from '../util/counter';
import MarkdownButton from './markdown-button';
@ -46,8 +46,6 @@ import Warning from './warning';
import type { Emoji } from 'soapbox/features/emoji';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What\'s on your mind?' },
pollPlaceholder: { id: 'compose_form.poll_placeholder', defaultMessage: 'Add a poll topic…' },
@ -181,10 +179,12 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
};
const handleEmojiPick = (data: Emoji) => {
const position = autosuggestTextareaRef.current!.textarea!.selectionStart;
const needsSpace = !!data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
const editor = editorRef.current;
if (!editor) return;
dispatch(insertEmojiCompose(id, position, data, needsSpace));
editor.update(() => {
editor.getEditorState()._selection?.insertNodes([$createEmojiNode(data), new TextNode(' ')]);
});
};
const onPaste = (files: FileList) => {

Wyświetl plik

@ -49,6 +49,7 @@ interface IComposeEditor {
}
const theme: InitialConfigType['theme'] = {
emoji: 'select-none',
hashtag: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue',
mention: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue',
link: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue',

Wyświetl plik

@ -1,10 +1,10 @@
import { $applyNodeReplacement, DecoratorNode } from 'lexical';
import React from 'react';
import { Emoji } from 'soapbox/components/ui';
import { Emoji as Component } from 'soapbox/components/ui';
import { isNativeEmoji, type Emoji } from 'soapbox/features/emoji';
import type {
DOMExportOutput,
EditorConfig,
LexicalNode,
NodeKey,
@ -13,29 +13,26 @@ import type {
} from 'lexical';
type SerializedEmojiNode = Spread<{
name: string
src: string
data: Emoji
type: 'emoji'
version: 1
}, SerializedLexicalNode>;
class EmojiNode extends DecoratorNode<JSX.Element> {
__name: string;
__src: string;
__emoji: Emoji;
static getType(): 'emoji' {
return 'emoji';
}
static clone(node: EmojiNode): EmojiNode {
return new EmojiNode(node.__name, node.__src, node.__key);
return new EmojiNode(node.__emoji, node.__key);
}
constructor(name: string, src: string, key?: NodeKey) {
constructor(emoji: Emoji, key?: NodeKey) {
super(key);
this.__name = name;
this.__src = src;
this.__emoji = emoji;
}
createDOM(config: EditorConfig): HTMLElement {
@ -52,24 +49,13 @@ class EmojiNode extends DecoratorNode<JSX.Element> {
return false;
}
exportDOM(): DOMExportOutput {
const element = document.createElement('img');
element.setAttribute('src', this.__src);
element.setAttribute('alt', this.__name);
element.classList.add('h-4', 'w-4');
return { element };
}
static importJSON(serializedNode: SerializedEmojiNode): EmojiNode {
const { name, src } = serializedNode;
const node = $createEmojiNode(name, src);
return node;
static importJSON({ data }: SerializedEmojiNode): EmojiNode {
return $createEmojiNode(data);
}
exportJSON(): SerializedEmojiNode {
return {
name: this.__name,
src: this.__src,
data: this.__emoji,
type: 'emoji',
version: 1,
};
@ -84,18 +70,29 @@ class EmojiNode extends DecoratorNode<JSX.Element> {
}
getTextContent(): string {
return this.__name;
const emoji = this.__emoji;
if (isNativeEmoji(emoji)) {
return emoji.native;
} else {
return emoji.colons;
}
}
decorate(): JSX.Element {
return (
<Emoji src={this.__src} alt={this.__name} className='emojione h-4 w-4' />
);
const emoji = this.__emoji;
if (isNativeEmoji(emoji)) {
return <Component emoji={emoji.native} alt={emoji.colons} className='emojione h-4 w-4' />;
} else {
return <Component src={emoji.imageUrl} alt={emoji.colons} className='emojione h-4 w-4' />;
}
}
}
const $createEmojiNode = (name = '', src: string): EmojiNode => $applyNodeReplacement(new EmojiNode(name, src));
function $createEmojiNode(emoji: Emoji): EmojiNode {
const node = new EmojiNode(emoji);
return $applyNodeReplacement(node);
}
const $isEmojiNode = (
node: LexicalNode | null | undefined,

Wyświetl plik

@ -18,6 +18,7 @@ import {
KEY_ESCAPE_COMMAND,
KEY_TAB_COMMAND,
LexicalEditor,
LexicalNode,
RangeSelection,
TextNode,
} from 'lexical';
@ -32,14 +33,14 @@ import React, {
import ReactDOM from 'react-dom';
import { clearComposeSuggestions, fetchComposeSuggestions } from 'soapbox/actions/compose';
import { useEmoji } from 'soapbox/actions/emojis';
import { chooseEmoji } from 'soapbox/actions/emojis';
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
import { isNativeEmoji } from 'soapbox/features/emoji';
import { useAppDispatch, useCompose } from 'soapbox/hooks';
import { selectAccount } from 'soapbox/selectors';
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
import AutosuggestAccount from '../../components/autosuggest-account';
import { $createEmojiNode } from '../nodes/emoji-node';
import { $createMentionNode } from '../nodes/mention-node';
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
@ -309,25 +310,25 @@ const AutosuggestPlugin = ({
/** Offset for the beginning of the matched text, including the token. */
const offset = leadOffset - 1;
/** Replace the matched text with the given node. */
function replaceMatch(replaceWith: LexicalNode) {
const result = (node as TextNode).splitText(offset, offset + matchingString.length);
const textNode = result[1] ?? result[0];
const replacedNode = textNode.replace(replaceWith);
replacedNode.insertAfter(new TextNode(' '));
replacedNode.selectNext();
}
if (typeof suggestion === 'object') {
if (!suggestion.id) return;
dispatch(useEmoji(suggestion)); // eslint-disable-line react-hooks/rules-of-hooks
if (isNativeEmoji(suggestion)) {
node.spliceText(offset, matchingString.length, `${suggestion.native} `, true);
} else {
node.spliceText(offset, matchingString.length, `${suggestion.colons} `, true);
}
dispatch(chooseEmoji(suggestion));
replaceMatch($createEmojiNode(suggestion));
} else if (suggestion[0] === '#') {
node.setTextContent(`${suggestion} `);
node.select();
} else {
const acct = selectAccount(getState(), suggestion)!.acct;
const result = (node as TextNode).splitText(offset, offset + matchingString.length);
const textNode = result[1] ?? result[0];
const mentionNode = textNode.replace($createMentionNode(`@${acct}`));
mentionNode.insertAfter(new TextNode(' '));
mentionNode.selectNext();
replaceMatch($createMentionNode(`@${acct}`));
}
dispatch(clearComposeSuggestions(composeId));
@ -341,11 +342,6 @@ const AutosuggestPlugin = ({
if (!node) return null;
if (['hashtag'].includes(node.getType())) {
const matchingString = node.getTextContent();
return { leadOffset: 0, matchingString };
}
if (node.getType() === 'text') {
const [leadOffset, matchingString] = textAtCursorMatchesToken(
node.getTextContent(),

Wyświetl plik

@ -3,9 +3,9 @@ import React, { useEffect, useState, useLayoutEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { createSelector } from 'reselect';
import { useEmoji } from 'soapbox/actions/emojis';
import { chooseEmoji } from 'soapbox/actions/emojis';
import { changeSetting } from 'soapbox/actions/settings';
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useTheme } from 'soapbox/hooks';
import { RootState } from 'soapbox/store';
import { buildCustomEmojis } from '../../emoji';
@ -130,10 +130,8 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
}) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const settings = useSettings();
const title = intl.formatMessage(messages.emoji);
const userTheme = settings.get('themeMode');
const theme = (userTheme === 'dark' || userTheme === 'light') ? userTheme : 'auto';
const theme = useTheme();
const customEmojis = useAppSelector((state) => getCustomEmojis(state));
const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state));
@ -162,7 +160,7 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
} as CustomEmoji;
}
dispatch(useEmoji(pickedEmoji)); // eslint-disable-line react-hooks/rules-of-hooks
dispatch(chooseEmoji(pickedEmoji));
if (onPickEmoji) {
onPickEmoji(pickedEmoji);

Wyświetl plik

@ -18,7 +18,6 @@ const EmojiPickerDropdownContainer = (
) => {
const intl = useIntl();
const title = intl.formatMessage(messages.emoji);
const [visible, setVisible] = useState(false);
const { x, y, strategy, refs, update } = useFloating<HTMLButtonElement>({

Wyświetl plik

@ -3,7 +3,7 @@ import { AnyAction } from 'redux';
import { ME_FETCH_SUCCESS } from 'soapbox/actions/me';
import { EMOJI_USE } from '../actions/emojis';
import { EMOJI_CHOOSE } from '../actions/emojis';
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
import { SEARCH_FILTER_SET } from '../actions/search';
import {
@ -40,7 +40,7 @@ export default function settings(state: State = ImmutableMap<string, any>({ save
return state
.setIn(action.path, action.value)
.set('saved', false);
case EMOJI_USE:
case EMOJI_CHOOSE:
return updateFrequentEmojis(state, action.emoji);
case SETTING_SAVE:
return state.set('saved', true);