kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'emoji-node' into 'main'
lexical: restore EmojiNode, fix Android backspace! Closes #1529 See merge request soapbox-pub/soapbox!2758environments/review-main-yi2y9f/deployments/4056
commit
33c775709b
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>({
|
||||
|
|
|
@ -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);
|
||||
|
|
Ładowanie…
Reference in New Issue