diff --git a/src/features/compose/editor/index.tsx b/src/features/compose/editor/index.tsx index 293922e46..c5bcfb48e 100644 --- a/src/features/compose/editor/index.tsx +++ b/src/features/compose/editor/index.tsx @@ -23,7 +23,6 @@ import { useAppDispatch } from 'soapbox/hooks'; import { useNodes } from './nodes'; import AutosuggestPlugin from './plugins/autosuggest-plugin'; import FocusPlugin from './plugins/focus-plugin'; -import MentionPlugin from './plugins/mention-plugin'; import RefPlugin from './plugins/ref-plugin'; import StatePlugin from './plugins/state-plugin'; @@ -162,7 +161,6 @@ const ComposeEditor = React.forwardRef(({ /> - diff --git a/src/features/compose/editor/nodes/index.ts b/src/features/compose/editor/nodes/index.ts index de1102a76..7c7695259 100644 --- a/src/features/compose/editor/nodes/index.ts +++ b/src/features/compose/editor/nodes/index.ts @@ -1,7 +1,7 @@ /** * This source code is derived from code from Meta Platforms, Inc. * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. + * LICENSE file in the /src/features/compose/editor directory. */ import { HashtagNode } from '@lexical/hashtag'; diff --git a/src/features/compose/editor/nodes/mention-node.ts b/src/features/compose/editor/nodes/mention-node.ts index a559eda40..343ea4af5 100644 --- a/src/features/compose/editor/nodes/mention-node.ts +++ b/src/features/compose/editor/nodes/mention-node.ts @@ -1,7 +1,7 @@ /** * This source code is derived from code from Meta Platforms, Inc. * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. + * LICENSE file in the /src/features/compose/editor directory. */ import { addClassNamesToElement } from '@lexical/utils'; @@ -60,7 +60,11 @@ class MentionNode extends TextNode { } -const $createMentionNode = (text = ''): MentionNode => $applyNodeReplacement(new MentionNode(text)); +function $createMentionNode(text: string): MentionNode { + const node = new MentionNode(text); + node.setMode('segmented').toggleDirectionless(); + return $applyNodeReplacement(node); +} const $isMentionNode = ( node: LexicalNode | null | undefined, diff --git a/src/features/compose/editor/plugins/autosuggest-plugin.tsx b/src/features/compose/editor/plugins/autosuggest-plugin.tsx index 6bd831c5a..f13fcaf55 100644 --- a/src/features/compose/editor/plugins/autosuggest-plugin.tsx +++ b/src/features/compose/editor/plugins/autosuggest-plugin.tsx @@ -1,7 +1,7 @@ /** * This source code is derived from code from Meta Platforms, Inc. * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. + * LICENSE file in the /src/features/compose/editor directory. */ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; @@ -19,6 +19,7 @@ import { KEY_TAB_COMMAND, LexicalEditor, RangeSelection, + TextNode, } from 'lexical'; import React, { MutableRefObject, @@ -39,6 +40,7 @@ import { selectAccount } from 'soapbox/selectors'; import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions'; import AutosuggestAccount from '../../components/autosuggest-account'; +import { $createMentionNode } from '../nodes/mention-node'; import type { AutoSuggestion } from 'soapbox/components/autosuggest-input'; @@ -303,27 +305,29 @@ const AutosuggestPlugin = ({ dispatch((dispatch, getState) => { const state = editor.getEditorState(); const node = (state._selection as RangeSelection)?.anchor?.getNode(); + const { leadOffset, matchingString } = resolution!.match; + /** Offset for the beginning of the matched text, including the token. */ + const offset = leadOffset - 1; if (typeof suggestion === 'object') { if (!suggestion.id) return; - dispatch(useEmoji(suggestion)); // eslint-disable-line react-hooks/rules-of-hooks - const { leadOffset, matchingString } = resolution!.match; - if (isNativeEmoji(suggestion)) { - node.spliceText(leadOffset - 1, matchingString.length, `${suggestion.native} `, true); + node.spliceText(offset, matchingString.length, `${suggestion.native} `, true); } else { - node.spliceText(leadOffset - 1, matchingString.length, `${suggestion.colons} `, true); + node.spliceText(offset, matchingString.length, `${suggestion.colons} `, true); } } else if (suggestion[0] === '#') { node.setTextContent(`${suggestion} `); node.select(); } else { - const content = selectAccount(getState(), suggestion)!.acct; - - node.setTextContent(`@${content} `); - node.select(); + 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(); } dispatch(clearComposeSuggestions(composeId)); @@ -337,13 +341,18 @@ const AutosuggestPlugin = ({ if (!node) return null; - if (['mention', 'hashtag'].includes(node.getType())) { + if (['hashtag'].includes(node.getType())) { const matchingString = node.getTextContent(); return { leadOffset: 0, matchingString }; } if (node.getType() === 'text') { - const [leadOffset, matchingString] = textAtCursorMatchesToken(node.getTextContent(), (state._selection as RangeSelection)?.anchor?.offset, [':']); + const [leadOffset, matchingString] = textAtCursorMatchesToken( + node.getTextContent(), + (state._selection as RangeSelection)?.anchor?.offset, + [':', '@'], + ); + if (!leadOffset || !matchingString) return null; return { leadOffset, matchingString }; } diff --git a/src/features/compose/editor/plugins/link-plugin.tsx b/src/features/compose/editor/plugins/link-plugin.tsx index 07f24f447..175f3184f 100644 --- a/src/features/compose/editor/plugins/link-plugin.tsx +++ b/src/features/compose/editor/plugins/link-plugin.tsx @@ -1,7 +1,7 @@ /** * This source code is derived from code from Meta Platforms, Inc. * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. + * LICENSE file in the /src/features/compose/editor directory. */ import { LinkPlugin as LexicalLinkPlugin } from '@lexical/react/LexicalLinkPlugin'; diff --git a/src/features/compose/editor/plugins/mention-plugin.tsx b/src/features/compose/editor/plugins/mention-plugin.tsx deleted file mode 100644 index 0465a6a75..000000000 --- a/src/features/compose/editor/plugins/mention-plugin.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * This source code is derived from code from Meta Platforms, Inc. - * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. - */ - -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { useLexicalTextEntity } from '@lexical/react/useLexicalTextEntity'; -import { useCallback, useEffect } from 'react'; - -import { $createMentionNode, MentionNode } from '../nodes/mention-node'; - -import type { TextNode } from 'lexical'; - -const MENTION_REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i'); - -const getMentionMatch = (text: string) => { - const matchArr = MENTION_REGEX.exec(text); - - if (!matchArr) return null; - return matchArr; -}; - -const MentionPlugin = (): JSX.Element | null => { - const [editor] = useLexicalComposerContext(); - - useEffect(() => { - if (!editor.hasNodes([MentionNode])) { - throw new Error('MentionPlugin: MentionNode not registered on editor'); - } - }, [editor]); - - const createMentionNode = useCallback((textNode: TextNode): MentionNode => { - return $createMentionNode(textNode.getTextContent()); - }, []); - - const getEntityMatch = useCallback((text: string) => { - const matchArr = getMentionMatch(text); - - if (!matchArr) return null; - - const mentionLength = matchArr[3].length + 1; - const startOffset = matchArr.index + matchArr[1].length; - const endOffset = startOffset + mentionLength; - return { - end: endOffset, - start: startOffset, - }; - }, []); - - useLexicalTextEntity( - getEntityMatch, - MentionNode, - createMentionNode, - ); - - return null; -}; - -export default MentionPlugin; diff --git a/src/features/compose/editor/utils/get-dom-range-rect.ts b/src/features/compose/editor/utils/get-dom-range-rect.ts index 4f8d9eb0d..fe6d10ad0 100644 --- a/src/features/compose/editor/utils/get-dom-range-rect.ts +++ b/src/features/compose/editor/utils/get-dom-range-rect.ts @@ -1,7 +1,7 @@ /** * This source code is derived from code from Meta Platforms, Inc. * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. + * LICENSE file in the /src/features/compose/editor directory. */ /* eslint-disable eqeqeq */ diff --git a/src/features/compose/editor/utils/get-selected-node.ts b/src/features/compose/editor/utils/get-selected-node.ts index 992eafa0a..2f093b983 100644 --- a/src/features/compose/editor/utils/get-selected-node.ts +++ b/src/features/compose/editor/utils/get-selected-node.ts @@ -1,7 +1,7 @@ /** * This source code is derived from code from Meta Platforms, Inc. * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. + * LICENSE file in the /src/features/compose/editor directory. */ import { $isAtNodeEnd } from '@lexical/selection'; diff --git a/src/features/compose/editor/utils/point.ts b/src/features/compose/editor/utils/point.ts index f8de0f168..38e825b18 100644 --- a/src/features/compose/editor/utils/point.ts +++ b/src/features/compose/editor/utils/point.ts @@ -1,7 +1,7 @@ /** * This source code is derived from code from Meta Platforms, Inc. * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. + * LICENSE file in the /src/features/compose/editor directory. */ class Point { diff --git a/src/features/compose/editor/utils/rect.ts b/src/features/compose/editor/utils/rect.ts index 901f30411..9a23d85e5 100644 --- a/src/features/compose/editor/utils/rect.ts +++ b/src/features/compose/editor/utils/rect.ts @@ -2,7 +2,7 @@ /** * This source code is derived from code from Meta Platforms, Inc. * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. + * LICENSE file in the /src/features/compose/editor directory. */ import { isPoint, Point } from './point'; diff --git a/src/features/compose/editor/utils/set-floating-elem-position.ts b/src/features/compose/editor/utils/set-floating-elem-position.ts index e9271e3dd..371b383cc 100644 --- a/src/features/compose/editor/utils/set-floating-elem-position.ts +++ b/src/features/compose/editor/utils/set-floating-elem-position.ts @@ -1,7 +1,7 @@ /** * This source code is derived from code from Meta Platforms, Inc. * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. + * LICENSE file in the /src/features/compose/editor directory. */ const VERTICAL_GAP = 10; diff --git a/src/features/compose/editor/utils/url.ts b/src/features/compose/editor/utils/url.ts index f10af4e5c..412a77a2d 100644 --- a/src/features/compose/editor/utils/url.ts +++ b/src/features/compose/editor/utils/url.ts @@ -1,7 +1,7 @@ /** * This source code is derived from code from Meta Platforms, Inc. * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /app/soapbox/features/compose/editor directory. + * LICENSE file in the /src/features/compose/editor directory. */ export const sanitizeUrl = (url: string): string => {