Lexical: Hashtag, emoji autocompletion
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
46ffd053bb
commit
9c49fc3d8a
10 changed files with 184 additions and 143 deletions
|
@ -6,6 +6,7 @@ import { defineMessages, IntlShape } from 'react-intl';
|
||||||
import api from 'soapbox/api';
|
import api from 'soapbox/api';
|
||||||
import { isNativeEmoji } from 'soapbox/features/emoji';
|
import { isNativeEmoji } from 'soapbox/features/emoji';
|
||||||
import emojiSearch from 'soapbox/features/emoji/search';
|
import emojiSearch from 'soapbox/features/emoji/search';
|
||||||
|
import { normalizeTag } from 'soapbox/normalizers';
|
||||||
import { tagHistory } from 'soapbox/settings';
|
import { tagHistory } from 'soapbox/settings';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
|
@ -29,7 +30,7 @@ import type { History } from 'soapbox/types/history';
|
||||||
|
|
||||||
const { CancelToken, isCancel } = axios;
|
const { CancelToken, isCancel } = axios;
|
||||||
|
|
||||||
let cancelFetchComposeSuggestionsAccounts: Canceler;
|
let cancelFetchComposeSuggestions: Canceler;
|
||||||
|
|
||||||
const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
||||||
const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
||||||
|
@ -500,8 +501,8 @@ const setGroupTimelineVisible = (composeId: string, groupTimelineVisible: boolea
|
||||||
});
|
});
|
||||||
|
|
||||||
const clearComposeSuggestions = (composeId: string) => {
|
const clearComposeSuggestions = (composeId: string) => {
|
||||||
if (cancelFetchComposeSuggestionsAccounts) {
|
if (cancelFetchComposeSuggestions) {
|
||||||
cancelFetchComposeSuggestionsAccounts();
|
cancelFetchComposeSuggestions();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_SUGGESTIONS_CLEAR,
|
type: COMPOSE_SUGGESTIONS_CLEAR,
|
||||||
|
@ -510,12 +511,12 @@ const clearComposeSuggestions = (composeId: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, token) => {
|
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, token) => {
|
||||||
if (cancelFetchComposeSuggestionsAccounts) {
|
if (cancelFetchComposeSuggestions) {
|
||||||
cancelFetchComposeSuggestionsAccounts(composeId);
|
cancelFetchComposeSuggestions(composeId);
|
||||||
}
|
}
|
||||||
api(getState).get('/api/v1/accounts/search', {
|
api(getState).get('/api/v1/accounts/search', {
|
||||||
cancelToken: new CancelToken(cancel => {
|
cancelToken: new CancelToken(cancel => {
|
||||||
cancelFetchComposeSuggestionsAccounts = cancel;
|
cancelFetchComposeSuggestions = cancel;
|
||||||
}),
|
}),
|
||||||
params: {
|
params: {
|
||||||
q: token.slice(1),
|
q: token.slice(1),
|
||||||
|
@ -540,10 +541,37 @@ const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => Ro
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
||||||
const state = getState();
|
if (cancelFetchComposeSuggestions) {
|
||||||
const currentTrends = state.trends.items;
|
cancelFetchComposeSuggestions(composeId);
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(updateSuggestionTags(composeId, token, currentTrends));
|
const state = getState();
|
||||||
|
|
||||||
|
const instance = state.instance;
|
||||||
|
const { trends } = getFeatures(instance);
|
||||||
|
|
||||||
|
if (trends) {
|
||||||
|
const currentTrends = state.trends.items;
|
||||||
|
|
||||||
|
return dispatch(updateSuggestionTags(composeId, token, currentTrends));
|
||||||
|
}
|
||||||
|
|
||||||
|
api(getState).get('/api/v2/search', {
|
||||||
|
cancelToken: new CancelToken(cancel => {
|
||||||
|
cancelFetchComposeSuggestions = cancel;
|
||||||
|
}),
|
||||||
|
params: {
|
||||||
|
q: token.slice(1),
|
||||||
|
limit: 4,
|
||||||
|
type: 'hashtags',
|
||||||
|
},
|
||||||
|
}).then(response => {
|
||||||
|
dispatch(updateSuggestionTags(composeId, token, response.data?.hashtags.map(normalizeTag)));
|
||||||
|
}).catch(error => {
|
||||||
|
if (!isCancel(error)) {
|
||||||
|
toast.showAlertForError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchComposeSuggestions = (composeId: string, token: string) =>
|
const fetchComposeSuggestions = (composeId: string, token: string) =>
|
||||||
|
@ -602,11 +630,11 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSuggestionTags = (composeId: string, token: string, currentTrends: ImmutableList<Tag>) => ({
|
const updateSuggestionTags = (composeId: string, token: string, tags: ImmutableList<Tag>) => ({
|
||||||
type: COMPOSE_SUGGESTION_TAGS_UPDATE,
|
type: COMPOSE_SUGGESTION_TAGS_UPDATE,
|
||||||
id: composeId,
|
id: composeId,
|
||||||
token,
|
token,
|
||||||
currentTrends,
|
tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateTagHistory = (composeId: string, tags: string[]) => ({
|
const updateTagHistory = (composeId: string, tags: string[]) => ({
|
||||||
|
|
|
@ -5,16 +5,21 @@ import { joinPublicPath } from 'soapbox/utils/static';
|
||||||
|
|
||||||
interface IEmoji extends React.ImgHTMLAttributes<HTMLImageElement> {
|
interface IEmoji extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
/** Unicode emoji character. */
|
/** Unicode emoji character. */
|
||||||
emoji: string
|
emoji?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A single emoji image. */
|
/** A single emoji image. */
|
||||||
const Emoji: React.FC<IEmoji> = (props): JSX.Element | null => {
|
const Emoji: React.FC<IEmoji> = (props): JSX.Element | null => {
|
||||||
const { emoji, alt, src, ...rest } = props;
|
const { emoji, alt, src, ...rest } = props;
|
||||||
const codepoints = toCodePoints(removeVS16s(emoji));
|
|
||||||
const filename = codepoints.join('-');
|
|
||||||
|
|
||||||
if (!filename) return null;
|
let filename;
|
||||||
|
|
||||||
|
if (emoji) {
|
||||||
|
const codepoints = toCodePoints(removeVS16s(emoji));
|
||||||
|
filename = codepoints.join('-');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filename && !src) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
|
|
|
@ -29,7 +29,6 @@ import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||||
import nodes from './nodes';
|
import nodes from './nodes';
|
||||||
import { AutosuggestPlugin } from './plugins/autosuggest-plugin';
|
import { AutosuggestPlugin } from './plugins/autosuggest-plugin';
|
||||||
import DraggableBlockPlugin from './plugins/draggable-block-plugin';
|
import DraggableBlockPlugin from './plugins/draggable-block-plugin';
|
||||||
import { EmojiPlugin } from './plugins/emoji-plugin';
|
|
||||||
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
|
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
|
||||||
import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin';
|
import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin';
|
||||||
import { MentionPlugin } from './plugins/mention-plugin';
|
import { MentionPlugin } from './plugins/mention-plugin';
|
||||||
|
@ -143,7 +142,6 @@ const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onF
|
||||||
/>
|
/>
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<HashtagPlugin />
|
<HashtagPlugin />
|
||||||
<EmojiPlugin />
|
|
||||||
<MentionPlugin />
|
<MentionPlugin />
|
||||||
<AutosuggestPlugin composeId={composeId} suggestionsHidden={suggestionsHidden} setSuggestionsHidden={setSuggestionsHidden} />
|
<AutosuggestPlugin composeId={composeId} suggestionsHidden={suggestionsHidden} setSuggestionsHidden={setSuggestionsHidden} />
|
||||||
{features.richText && <LinkPlugin />}
|
{features.richText && <LinkPlugin />}
|
||||||
|
|
|
@ -1,54 +1,74 @@
|
||||||
/**
|
import { $applyNodeReplacement, DecoratorNode } from 'lexical';
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
import React from 'react';
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { addClassNamesToElement } from '@lexical/utils';
|
import { Emoji } from 'soapbox/components/ui';
|
||||||
import { $applyNodeReplacement, TextNode } from 'lexical';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
DOMExportOutput,
|
||||||
EditorConfig,
|
EditorConfig,
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
NodeKey,
|
NodeKey,
|
||||||
SerializedTextNode,
|
SerializedLexicalNode,
|
||||||
|
Spread,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
|
||||||
class EmojiNode extends TextNode {
|
type SerializedEmojiNode = Spread<{
|
||||||
|
name: string
|
||||||
|
src: string
|
||||||
|
type: 'emoji'
|
||||||
|
version: 1
|
||||||
|
}, SerializedLexicalNode>;
|
||||||
|
|
||||||
static getType(): string {
|
class EmojiNode extends DecoratorNode<JSX.Element> {
|
||||||
|
|
||||||
|
__name: string;
|
||||||
|
__src: string;
|
||||||
|
|
||||||
|
static getType(): 'emoji' {
|
||||||
return 'emoji';
|
return 'emoji';
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: EmojiNode): EmojiNode {
|
static clone(node: EmojiNode): EmojiNode {
|
||||||
return new EmojiNode(node.__text, node.__key);
|
return new EmojiNode(node.__name, node.__key);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(text: string, key?: NodeKey) {
|
constructor(name: string, src: string, key?: NodeKey) {
|
||||||
super(text, key);
|
super(key);
|
||||||
|
this.__name = name;
|
||||||
|
this.__src = src;
|
||||||
}
|
}
|
||||||
|
|
||||||
createDOM(config: EditorConfig): HTMLElement {
|
createDOM(config: EditorConfig): HTMLElement {
|
||||||
const element = super.createDOM(config);
|
const span = document.createElement('span');
|
||||||
addClassNamesToElement(element, config.theme.emoji);
|
const theme = config.theme;
|
||||||
return element;
|
const className = theme.emoji;
|
||||||
|
if (className !== undefined) {
|
||||||
|
span.className = className;
|
||||||
|
}
|
||||||
|
return span;
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedTextNode): EmojiNode {
|
exportDOM(): DOMExportOutput {
|
||||||
const node = $createEmojiNode(serializedNode.text);
|
const element = document.createElement('img');
|
||||||
node.setFormat(serializedNode.format);
|
element.setAttribute('src', this.__src);
|
||||||
node.setDetail(serializedNode.detail);
|
element.setAttribute('alt', this.__name);
|
||||||
node.setMode(serializedNode.mode);
|
element.classList.add('h-4', 'w-4');
|
||||||
node.setStyle(serializedNode.style);
|
return { element };
|
||||||
|
}
|
||||||
|
|
||||||
|
static importJSON(serializedNode: SerializedEmojiNode): EmojiNode {
|
||||||
|
const { name, src } =
|
||||||
|
serializedNode;
|
||||||
|
const node = $createEmojiNode(name, src);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportJSON(): SerializedTextNode {
|
exportJSON(): SerializedEmojiNode {
|
||||||
return {
|
return {
|
||||||
...super.exportJSON(),
|
name: this.__name,
|
||||||
|
src: this.__src,
|
||||||
type: 'emoji',
|
type: 'emoji',
|
||||||
|
version: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,9 +80,15 @@ class EmojiNode extends TextNode {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decorate(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Emoji src={this.__src} alt={this.__name} className='emojione h-4 w-4' />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const $createEmojiNode = (text = ''): EmojiNode => $applyNodeReplacement(new EmojiNode(text).setMode('token'));
|
const $createEmojiNode = (name = '', src: string): EmojiNode => $applyNodeReplacement(new EmojiNode(name, src));
|
||||||
|
|
||||||
const $isEmojiNode = (
|
const $isEmojiNode = (
|
||||||
node: LexicalNode | null | undefined,
|
node: LexicalNode | null | undefined,
|
||||||
|
|
|
@ -26,18 +26,16 @@ import React, {
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import { fetchComposeSuggestions } from 'soapbox/actions/compose';
|
import { fetchComposeSuggestions } from 'soapbox/actions/compose';
|
||||||
|
import { useEmoji } from 'soapbox/actions/emojis';
|
||||||
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
|
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
|
||||||
|
import { isNativeEmoji } from 'soapbox/features/emoji';
|
||||||
import { useAppDispatch, useCompose } from 'soapbox/hooks';
|
import { useAppDispatch, useCompose } from 'soapbox/hooks';
|
||||||
|
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||||
|
|
||||||
import AutosuggestAccount from '../../components/autosuggest-account';
|
import AutosuggestAccount from '../../components/autosuggest-account';
|
||||||
|
|
||||||
import { MENTION_REGEX } from './mention-plugin';
|
|
||||||
|
|
||||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||||
|
|
||||||
|
|
||||||
const EMOJI_REGEX = new RegExp('(^|$|(?:^|\\s))([:])([a-z\\d_-]+([:]?))', 'i');
|
|
||||||
|
|
||||||
export type QueryMatch = {
|
export type QueryMatch = {
|
||||||
leadOffset: number
|
leadOffset: number
|
||||||
matchingString: string
|
matchingString: string
|
||||||
|
@ -75,15 +73,6 @@ function tryToPositionRange(leadOffset: number, range: Range): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQueryTextForSearch(editor: LexicalEditor): string | null {
|
|
||||||
const state = editor.getEditorState();
|
|
||||||
const node = (state._selection as RangeSelection)?.anchor?.getNode();
|
|
||||||
|
|
||||||
if (node && (node.getType() === 'mention' || node.getType() === 'text')) return node.getTextContent();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSelectionOnEntityBoundary(
|
function isSelectionOnEntityBoundary(
|
||||||
editor: LexicalEditor,
|
editor: LexicalEditor,
|
||||||
offset: number,
|
offset: number,
|
||||||
|
@ -309,34 +298,60 @@ export function AutosuggestPlugin({
|
||||||
const onSelectSuggestion: React.MouseEventHandler<HTMLDivElement> = (e) => {
|
const onSelectSuggestion: React.MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index') as any);
|
const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index') as any) as AutoSuggestion;
|
||||||
|
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
|
|
||||||
dispatch((_, getState) => {
|
dispatch((dispatch, getState) => {
|
||||||
const state = editor.getEditorState();
|
const state = editor.getEditorState();
|
||||||
const node = (state._selection as RangeSelection)?.anchor?.getNode();
|
const node = (state._selection as RangeSelection)?.anchor?.getNode();
|
||||||
|
|
||||||
const content = getState().accounts.get(suggestion)!.acct;
|
if (typeof suggestion === 'object' && suggestion.id) {
|
||||||
|
dispatch(useEmoji(suggestion)); // eslint-disable-line react-hooks/rules-of-hooks
|
||||||
|
|
||||||
node.setTextContent(`@${content} `);
|
const { leadOffset, matchingString } = resolution!.match;
|
||||||
node.select();
|
|
||||||
|
if (isNativeEmoji(suggestion)) {
|
||||||
|
node.spliceText(leadOffset - 1, matchingString.length, `${suggestion.native} `, true);
|
||||||
|
} else {
|
||||||
|
// const completion = suggestion.colons;
|
||||||
|
// node.replace($createEmojiNode(completion, suggestion.imageUrl));
|
||||||
|
}
|
||||||
|
} else if ((suggestion as string)[0] === '#') {
|
||||||
|
node.setTextContent(`${suggestion} `);
|
||||||
|
node.select();
|
||||||
|
} else {
|
||||||
|
const content = getState().accounts.get(suggestion)!.acct;
|
||||||
|
|
||||||
|
node.setTextContent(`@${content} `);
|
||||||
|
node.select();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkForMatch = useCallback((text: string) => {
|
const getQueryTextForSearch = (editor: LexicalEditor) => {
|
||||||
const matchArr = MENTION_REGEX.exec(text) || EMOJI_REGEX.exec(text);
|
const state = editor.getEditorState();
|
||||||
|
const node = (state._selection as RangeSelection)?.anchor?.getNode();
|
||||||
|
|
||||||
if (!matchArr) return null;
|
if (!node) return null;
|
||||||
|
|
||||||
dispatch(fetchComposeSuggestions(composeId, matchArr[0]?.trim()));
|
if (['mention', 'hashtag'].includes(node.getType())) {
|
||||||
|
const matchingString = node.getTextContent();
|
||||||
|
|
||||||
return {
|
return { leadOffset: 0, matchingString };
|
||||||
leadOffset: matchArr.index,
|
}
|
||||||
matchingString: matchArr[0],
|
|
||||||
};
|
if (node.getType() === 'text') {
|
||||||
}, []);
|
const [leadOffset, matchingString] = textAtCursorMatchesToken(node.getTextContent(), (state._selection as RangeSelection)?.anchor?.offset, [':']);
|
||||||
|
|
||||||
|
if (!leadOffset || !matchingString) return null;
|
||||||
|
|
||||||
|
return { leadOffset, matchingString };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const renderSuggestion = (suggestion: AutoSuggestion, i: number) => {
|
const renderSuggestion = (suggestion: AutoSuggestion, i: number) => {
|
||||||
let inner;
|
let inner;
|
||||||
|
@ -345,6 +360,9 @@ export function AutosuggestPlugin({
|
||||||
if (typeof suggestion === 'object') {
|
if (typeof suggestion === 'object') {
|
||||||
inner = <AutosuggestEmoji emoji={suggestion} />;
|
inner = <AutosuggestEmoji emoji={suggestion} />;
|
||||||
key = suggestion.id;
|
key = suggestion.id;
|
||||||
|
} else if (suggestion[0] === '#') {
|
||||||
|
inner = suggestion;
|
||||||
|
key = suggestion;
|
||||||
} else {
|
} else {
|
||||||
inner = <AutosuggestAccount id={suggestion} />;
|
inner = <AutosuggestAccount id={suggestion} />;
|
||||||
key = suggestion;
|
key = suggestion;
|
||||||
|
@ -382,17 +400,16 @@ export function AutosuggestPlugin({
|
||||||
const updateListener = () => {
|
const updateListener = () => {
|
||||||
editor.getEditorState().read(() => {
|
editor.getEditorState().read(() => {
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
const text = getQueryTextForSearch(editor);
|
const match = getQueryTextForSearch(editor);
|
||||||
|
|
||||||
if (!text) {
|
if (!match) {
|
||||||
closeTypeahead();
|
closeTypeahead();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = checkForMatch(text);
|
dispatch(fetchComposeSuggestions(composeId, match.matchingString.trim()));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
match !== null &&
|
|
||||||
!isSelectionOnEntityBoundary(editor, match.leadOffset)
|
!isSelectionOnEntityBoundary(editor, match.leadOffset)
|
||||||
) {
|
) {
|
||||||
const isRangePositioned = tryToPositionRange(match.leadOffset, range);
|
const isRangePositioned = tryToPositionRange(match.leadOffset, range);
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
||||||
import { useLexicalTextEntity } from '@lexical/react/useLexicalTextEntity';
|
|
||||||
import { useCallback, useEffect } from 'react';
|
|
||||||
|
|
||||||
import { $createEmojiNode, EmojiNode } from '../nodes/emoji-node';
|
|
||||||
|
|
||||||
|
|
||||||
import type { TextNode } from 'lexical';
|
|
||||||
|
|
||||||
const REGEX = new RegExp('ggfafsdasdf(^|$|(?:^|\\s))([:])([a-z\\d_-]+([:]))', 'i');
|
|
||||||
|
|
||||||
export const getEmojiMatch = (text: string) => {
|
|
||||||
const matchArr = REGEX.exec(text);
|
|
||||||
|
|
||||||
if (!matchArr) return null;
|
|
||||||
return matchArr;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmojiPlugin = (): JSX.Element | null => {
|
|
||||||
const [editor] = useLexicalComposerContext();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor.hasNodes([EmojiNode])) {
|
|
||||||
throw new Error('EmojiPlugin: EmojiNode not registered on editor');
|
|
||||||
}
|
|
||||||
}, [editor]);
|
|
||||||
|
|
||||||
const createEmojiNode = useCallback((textNode: TextNode): EmojiNode => {
|
|
||||||
return $createEmojiNode(textNode.getTextContent());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getEntityMatch = useCallback((text: string) => {
|
|
||||||
const matchArr = getEmojiMatch(text);
|
|
||||||
|
|
||||||
if (!matchArr) return null;
|
|
||||||
|
|
||||||
const emojiLength = matchArr[3].length + 1;
|
|
||||||
const startOffset = matchArr.index + matchArr[1].length;
|
|
||||||
const endOffset = startOffset + emojiLength;
|
|
||||||
return {
|
|
||||||
end: endOffset,
|
|
||||||
start: startOffset,
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useLexicalTextEntity<EmojiNode>(
|
|
||||||
getEntityMatch,
|
|
||||||
EmojiNode,
|
|
||||||
createEmojiNode,
|
|
||||||
);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
|
@ -183,11 +183,11 @@ const insertSuggestion = (compose: Compose, position: number, token: string, com
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSuggestionTags = (compose: Compose, token: string, currentTrends: ImmutableList<Tag>) => {
|
const updateSuggestionTags = (compose: Compose, token: string, tags: ImmutableList<Tag>) => {
|
||||||
const prefix = token.slice(1);
|
const prefix = token.slice(1);
|
||||||
|
|
||||||
return compose.merge({
|
return compose.merge({
|
||||||
suggestions: ImmutableList(currentTrends
|
suggestions: ImmutableList(tags
|
||||||
.filter((tag) => tag.get('name').toLowerCase().startsWith(prefix.toLowerCase()))
|
.filter((tag) => tag.get('name').toLowerCase().startsWith(prefix.toLowerCase()))
|
||||||
.slice(0, 4)
|
.slice(0, 4)
|
||||||
.map((tag) => '#' + tag.name)),
|
.map((tag) => '#' + tag.name)),
|
||||||
|
@ -412,7 +412,7 @@ export default function compose(state = initialState, action: AnyAction) {
|
||||||
case COMPOSE_SUGGESTION_SELECT:
|
case COMPOSE_SUGGESTION_SELECT:
|
||||||
return updateCompose(state, action.id, compose => insertSuggestion(compose, action.position, action.token, action.completion, action.path));
|
return updateCompose(state, action.id, compose => insertSuggestion(compose, action.position, action.token, action.completion, action.path));
|
||||||
case COMPOSE_SUGGESTION_TAGS_UPDATE:
|
case COMPOSE_SUGGESTION_TAGS_UPDATE:
|
||||||
return updateCompose(state, action.id, compose => updateSuggestionTags(compose, action.token, action.currentTrends));
|
return updateCompose(state, action.id, compose => updateSuggestionTags(compose, action.token, action.tags));
|
||||||
case COMPOSE_TAG_HISTORY_UPDATE:
|
case COMPOSE_TAG_HISTORY_UPDATE:
|
||||||
return updateCompose(state, action.id, compose => compose.set('tagHistory', ImmutableList(fromJS(action.tags)) as ImmutableList<string>));
|
return updateCompose(state, action.id, compose => compose.set('tagHistory', ImmutableList(fromJS(action.tags)) as ImmutableList<string>));
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
|
|
|
@ -32,4 +32,4 @@ const textAtCursorMatchesToken = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { textAtCursorMatchesToken };
|
export { textAtCursorMatchesToken };
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
"@lexical/react": "^0.9.0",
|
"@lexical/react": "^0.9.0",
|
||||||
"@lexical/rich-text": "^0.9.0",
|
"@lexical/rich-text": "^0.9.0",
|
||||||
"@lexical/selection": "^0.9.0",
|
"@lexical/selection": "^0.9.0",
|
||||||
"@lexical/utils": "^0.9.0",
|
"@lexical/utils": "^0.9.1",
|
||||||
"@metamask/providers": "^10.0.0",
|
"@metamask/providers": "^10.0.0",
|
||||||
"@popperjs/core": "^2.11.5",
|
"@popperjs/core": "^2.11.5",
|
||||||
"@reach/combobox": "^0.18.0",
|
"@reach/combobox": "^0.18.0",
|
||||||
|
|
30
yarn.lock
30
yarn.lock
|
@ -2440,6 +2440,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils" "0.9.0"
|
"@lexical/utils" "0.9.0"
|
||||||
|
|
||||||
|
"@lexical/list@0.9.1":
|
||||||
|
version "0.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.9.1.tgz#18eba1e28d818a53661b7381f32b4461f024981e"
|
||||||
|
integrity sha512-z3wJfDjStesqjvamUgMez+CVvfGrmVqyGgMnCkYBrlfSZ8zlHvz8sJ/6iZkTh7TXIEkCTQpusCrkjUeYccmTxQ==
|
||||||
|
dependencies:
|
||||||
|
"@lexical/utils" "0.9.1"
|
||||||
|
|
||||||
"@lexical/mark@0.9.0":
|
"@lexical/mark@0.9.0":
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.9.0.tgz#040d3e8d3e2f46160bd4e5b1e6a5489df0c6aa46"
|
resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.9.0.tgz#040d3e8d3e2f46160bd4e5b1e6a5489df0c6aa46"
|
||||||
|
@ -2508,6 +2515,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.9.0.tgz#2c11085f94435c1c71344654a1f2b9a0c3549a22"
|
resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.9.0.tgz#2c11085f94435c1c71344654a1f2b9a0c3549a22"
|
||||||
integrity sha512-jPLeaqNujER1t61OWmSuUMS010A6Nz49efO8rEJFjMbUK7GB9IDsSDgvE4WnT/yBrXLAickjSiXgUq51LQTYYw==
|
integrity sha512-jPLeaqNujER1t61OWmSuUMS010A6Nz49efO8rEJFjMbUK7GB9IDsSDgvE4WnT/yBrXLAickjSiXgUq51LQTYYw==
|
||||||
|
|
||||||
|
"@lexical/selection@0.9.1":
|
||||||
|
version "0.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.9.1.tgz#6d809d9b8c3673992ba6fc6ccd6af5f402f57ba0"
|
||||||
|
integrity sha512-4eZcH8d4Kq/GswobVNIkM5fKk8YXPNFLCOHIyi0FmHymW/Ku7HLz2+mcOR6EQmk1Ce/4VJ4Oq+d1XlZw4PPTNA==
|
||||||
|
|
||||||
"@lexical/table@0.9.0":
|
"@lexical/table@0.9.0":
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.9.0.tgz#8f4e0d797c15141e26667cc54d7b640c90f5a9b8"
|
resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.9.0.tgz#8f4e0d797c15141e26667cc54d7b640c90f5a9b8"
|
||||||
|
@ -2515,12 +2527,19 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils" "0.9.0"
|
"@lexical/utils" "0.9.0"
|
||||||
|
|
||||||
|
"@lexical/table@0.9.1":
|
||||||
|
version "0.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.9.1.tgz#06978da97425e46399d48b5d75d19b389c5b6a38"
|
||||||
|
integrity sha512-BPKmpToQRxv87OtA6W22XwfOBtZf+vG95jp/bj2ecJAW5BVMjHf5ViI3vjXf3Z701giVh1OVXNct8EPkcfvg3w==
|
||||||
|
dependencies:
|
||||||
|
"@lexical/utils" "0.9.1"
|
||||||
|
|
||||||
"@lexical/text@0.9.0":
|
"@lexical/text@0.9.0":
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.9.0.tgz#ee90b42d8558123c917f561cfbfb804dac25e0a4"
|
resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.9.0.tgz#ee90b42d8558123c917f561cfbfb804dac25e0a4"
|
||||||
integrity sha512-+OHoB1Qb2SajGTEm9K4I3hDtCmeLr0Bs62psra6qYE/lil0Y8brrEXB5egppDpm6mKtiMzGrkBDMpcsKw/kiWA==
|
integrity sha512-+OHoB1Qb2SajGTEm9K4I3hDtCmeLr0Bs62psra6qYE/lil0Y8brrEXB5egppDpm6mKtiMzGrkBDMpcsKw/kiWA==
|
||||||
|
|
||||||
"@lexical/utils@0.9.0", "@lexical/utils@^0.9.0":
|
"@lexical/utils@0.9.0":
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.9.0.tgz#923e79af94566844442bc8699aba89f7d1cf5779"
|
resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.9.0.tgz#923e79af94566844442bc8699aba89f7d1cf5779"
|
||||||
integrity sha512-s4BrBKrd7VHexLSYdSKrRAVmuxmfVV49MYx/khILGnNQKYS2O8PuERCXi+IRi8Ac5ASELO6d28Y2my8NMM1CYA==
|
integrity sha512-s4BrBKrd7VHexLSYdSKrRAVmuxmfVV49MYx/khILGnNQKYS2O8PuERCXi+IRi8Ac5ASELO6d28Y2my8NMM1CYA==
|
||||||
|
@ -2529,6 +2548,15 @@
|
||||||
"@lexical/selection" "0.9.0"
|
"@lexical/selection" "0.9.0"
|
||||||
"@lexical/table" "0.9.0"
|
"@lexical/table" "0.9.0"
|
||||||
|
|
||||||
|
"@lexical/utils@0.9.1", "@lexical/utils@^0.9.1":
|
||||||
|
version "0.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.9.1.tgz#5fd58468da890e7ed5260c502f66fd19ec64e9b3"
|
||||||
|
integrity sha512-cszNQufH6UZxOVew7DTELM63wjj8qOiGiOFklMmM6QmkirHb2fK4LxeLjaKiwP5DkA0A1NDNz8yuuMXqA/mWPQ==
|
||||||
|
dependencies:
|
||||||
|
"@lexical/list" "0.9.1"
|
||||||
|
"@lexical/selection" "0.9.1"
|
||||||
|
"@lexical/table" "0.9.1"
|
||||||
|
|
||||||
"@lexical/yjs@0.9.0":
|
"@lexical/yjs@0.9.0":
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.9.0.tgz#88d90c346102bf46f5ee2ef6a6c763bef1f12bf6"
|
resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.9.0.tgz#88d90c346102bf46f5ee2ef6a6c763bef1f12bf6"
|
||||||
|
|
Loading…
Reference in a new issue