Merge branch 'fix-double-at' into 'main'

MentionPlugin: fix double-@ mentions

Closes #1530

See merge request soapbox-pub/soapbox!2757
environments/review-main-yi2y9f/deployments/4052
Alex Gleason 2023-09-25 20:29:58 +00:00
commit 29974c33e7
12 zmienionych plików z 35 dodań i 84 usunięć

Wyświetl plik

@ -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<LexicalEditor, IComposeEditor>(({
/>
<HistoryPlugin />
<HashtagPlugin />
<MentionPlugin />
<AutosuggestPlugin composeId={composeId} suggestionsHidden={suggestionsHidden} setSuggestionsHidden={setSuggestionsHidden} />
<AutoLinkPlugin matchers={LINK_MATCHERS} />
<StatePlugin composeId={composeId} handleSubmit={handleSubmit} />

Wyświetl plik

@ -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';

Wyświetl plik

@ -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,

Wyświetl plik

@ -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 };
}

Wyświetl plik

@ -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';

Wyświetl plik

@ -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<MentionNode>(
getEntityMatch,
MentionNode,
createMentionNode,
);
return null;
};
export default MentionPlugin;

Wyświetl plik

@ -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 */

Wyświetl plik

@ -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';

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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';

Wyświetl plik

@ -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;

Wyświetl plik

@ -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 => {