From 7ff9d2ed3bdf316fc7379d7865830b08c2d33605 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Sep 2023 11:41:06 -0500 Subject: [PATCH 1/7] MentionPlugin: do a little refactoring --- .../compose/editor/plugins/mention-plugin.tsx | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/features/compose/editor/plugins/mention-plugin.tsx b/src/features/compose/editor/plugins/mention-plugin.tsx index 0465a6a75..09278d7f7 100644 --- a/src/features/compose/editor/plugins/mention-plugin.tsx +++ b/src/features/compose/editor/plugins/mention-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'; @@ -14,13 +14,6 @@ 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(); @@ -30,28 +23,25 @@ const MentionPlugin = (): JSX.Element | null => { } }, [editor]); - const createMentionNode = useCallback((textNode: TextNode): MentionNode => { + const createNode = useCallback((textNode: TextNode): MentionNode => { return $createMentionNode(textNode.getTextContent()); }, []); - const getEntityMatch = useCallback((text: string) => { - const matchArr = getMentionMatch(text); + const getMatch = useCallback((text: string) => { + const match = MENTION_REGEX.exec(text); + if (!match) return null; - if (!matchArr) return null; + const length = match[3].length + 1; + const start = match.index + match[1].length; + const end = start + length; - const mentionLength = matchArr[3].length + 1; - const startOffset = matchArr.index + matchArr[1].length; - const endOffset = startOffset + mentionLength; - return { - end: endOffset, - start: startOffset, - }; + return { start, end }; }, []); useLexicalTextEntity( - getEntityMatch, + getMatch, MentionNode, - createMentionNode, + createNode, ); return null; From 5449ea33261f2e563321f3b61fbb9fa408cd67e4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Sep 2023 12:25:40 -0500 Subject: [PATCH 2/7] MentionPlugin: simplify regex and getMatch --- src/features/compose/editor/plugins/mention-plugin.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/compose/editor/plugins/mention-plugin.tsx b/src/features/compose/editor/plugins/mention-plugin.tsx index 09278d7f7..51c7b98b6 100644 --- a/src/features/compose/editor/plugins/mention-plugin.tsx +++ b/src/features/compose/editor/plugins/mention-plugin.tsx @@ -12,7 +12,7 @@ import { $createMentionNode, MentionNode } from '../nodes/mention-node'; import type { TextNode } from 'lexical'; -const MENTION_REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i'); +const MENTION_REGEX = /(?:^|\s)@(?:[a-z\d_-]+(?:@[^@\s]+)?)/i; const MentionPlugin = (): JSX.Element | null => { const [editor] = useLexicalComposerContext(); @@ -31,8 +31,8 @@ const MentionPlugin = (): JSX.Element | null => { const match = MENTION_REGEX.exec(text); if (!match) return null; - const length = match[3].length + 1; - const start = match.index + match[1].length; + const length = match[0].length; + const start = match.index; const end = start + length; return { start, end }; From f628cd73b0b828832aa0cd99bf9cabb917f360c0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Sep 2023 13:30:52 -0500 Subject: [PATCH 3/7] MentionPlugin: use a simpler regex, fix double-@ mentions Fixes https://gitlab.com/soapbox-pub/soapbox/-/issues/1530 --- src/features/compose/editor/plugins/mention-plugin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/compose/editor/plugins/mention-plugin.tsx b/src/features/compose/editor/plugins/mention-plugin.tsx index 51c7b98b6..4a9df94d0 100644 --- a/src/features/compose/editor/plugins/mention-plugin.tsx +++ b/src/features/compose/editor/plugins/mention-plugin.tsx @@ -12,7 +12,7 @@ import { $createMentionNode, MentionNode } from '../nodes/mention-node'; import type { TextNode } from 'lexical'; -const MENTION_REGEX = /(?:^|\s)@(?:[a-z\d_-]+(?:@[^@\s]+)?)/i; +const MENTION_REGEX = /(?:^|\s)@[^\s]+/i; const MentionPlugin = (): JSX.Element | null => { const [editor] = useLexicalComposerContext(); From 71bb8cc73e204478ad7cb5dc3eb4f38ddb8e2e7d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Sep 2023 13:32:12 -0500 Subject: [PATCH 4/7] lexical: fix license path in lexical files --- src/features/compose/editor/nodes/index.ts | 2 +- src/features/compose/editor/nodes/mention-node.ts | 2 +- src/features/compose/editor/plugins/autosuggest-plugin.tsx | 2 +- src/features/compose/editor/plugins/link-plugin.tsx | 2 +- src/features/compose/editor/utils/get-dom-range-rect.ts | 2 +- src/features/compose/editor/utils/get-selected-node.ts | 2 +- src/features/compose/editor/utils/point.ts | 2 +- src/features/compose/editor/utils/rect.ts | 2 +- src/features/compose/editor/utils/set-floating-elem-position.ts | 2 +- src/features/compose/editor/utils/url.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) 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..30dcb9b89 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'; diff --git a/src/features/compose/editor/plugins/autosuggest-plugin.tsx b/src/features/compose/editor/plugins/autosuggest-plugin.tsx index 6bd831c5a..1770b983e 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'; 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/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 => { From e88b952a6f512f9e7a9c798f6c5601e216a3e4bf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Sep 2023 14:49:00 -0500 Subject: [PATCH 5/7] lexical: remove MentionPlugin, only insert mention when selected from autosuggest --- src/features/compose/editor/index.tsx | 2 - .../compose/editor/nodes/mention-node.ts | 6 ++- .../editor/plugins/autosuggest-plugin.tsx | 31 ++++++++---- .../compose/editor/plugins/mention-plugin.tsx | 50 ------------------- 4 files changed, 25 insertions(+), 64 deletions(-) delete mode 100644 src/features/compose/editor/plugins/mention-plugin.tsx 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/mention-node.ts b/src/features/compose/editor/nodes/mention-node.ts index 30dcb9b89..abc6909e6 100644 --- a/src/features/compose/editor/nodes/mention-node.ts +++ b/src/features/compose/editor/nodes/mention-node.ts @@ -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('token').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 1770b983e..51fcc47a7 100644 --- a/src/features/compose/editor/plugins/autosuggest-plugin.tsx +++ b/src/features/compose/editor/plugins/autosuggest-plugin.tsx @@ -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/mention-plugin.tsx b/src/features/compose/editor/plugins/mention-plugin.tsx deleted file mode 100644 index 4a9df94d0..000000000 --- a/src/features/compose/editor/plugins/mention-plugin.tsx +++ /dev/null @@ -1,50 +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 /src/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 = /(?:^|\s)@[^\s]+/i; - -const MentionPlugin = (): JSX.Element | null => { - const [editor] = useLexicalComposerContext(); - - useEffect(() => { - if (!editor.hasNodes([MentionNode])) { - throw new Error('MentionPlugin: MentionNode not registered on editor'); - } - }, [editor]); - - const createNode = useCallback((textNode: TextNode): MentionNode => { - return $createMentionNode(textNode.getTextContent()); - }, []); - - const getMatch = useCallback((text: string) => { - const match = MENTION_REGEX.exec(text); - if (!match) return null; - - const length = match[0].length; - const start = match.index; - const end = start + length; - - return { start, end }; - }, []); - - useLexicalTextEntity( - getMatch, - MentionNode, - createNode, - ); - - return null; -}; - -export default MentionPlugin; From ab22422aa7db2c27d323cadb169ac280e7aa6a5e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Sep 2023 15:12:43 -0500 Subject: [PATCH 6/7] MentionNode: use 'segmented' to fix Android problems --- src/features/compose/editor/nodes/mention-node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/compose/editor/nodes/mention-node.ts b/src/features/compose/editor/nodes/mention-node.ts index abc6909e6..343ea4af5 100644 --- a/src/features/compose/editor/nodes/mention-node.ts +++ b/src/features/compose/editor/nodes/mention-node.ts @@ -62,7 +62,7 @@ class MentionNode extends TextNode { function $createMentionNode(text: string): MentionNode { const node = new MentionNode(text); - node.setMode('token').toggleDirectionless(); + node.setMode('segmented').toggleDirectionless(); return $applyNodeReplacement(node); } From aea2653b19acc75312978d42b6f726713deca91e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 25 Sep 2023 15:18:46 -0500 Subject: [PATCH 7/7] AutosuggestPlugin: remove extraneous `?` --- src/features/compose/editor/plugins/autosuggest-plugin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/compose/editor/plugins/autosuggest-plugin.tsx b/src/features/compose/editor/plugins/autosuggest-plugin.tsx index 51fcc47a7..f13fcaf55 100644 --- a/src/features/compose/editor/plugins/autosuggest-plugin.tsx +++ b/src/features/compose/editor/plugins/autosuggest-plugin.tsx @@ -325,7 +325,7 @@ const AutosuggestPlugin = ({ 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}`)); + const mentionNode = textNode.replace($createMentionNode(`@${acct}`)); mentionNode.insertAfter(new TextNode(' ')); mentionNode.selectNext(); }