diff --git a/app/soapbox/features/compose/editor/index.tsx b/app/soapbox/features/compose/editor/index.tsx index 89ba043fe..761442016 100644 --- a/app/soapbox/features/compose/editor/index.tsx +++ b/app/soapbox/features/compose/editor/index.tsx @@ -48,6 +48,8 @@ const ComposeEditor = React.forwardRef(({ composeId, condensed, onF const dispatch = useAppDispatch(); const features = useFeatures(); + const [suggestionsHidden, setSuggestionsHidden] = useState(true); + const initialConfig: InitialConfigType = useMemo(function() { return { namespace: 'ComposeForm', @@ -138,7 +140,7 @@ const ComposeEditor = React.forwardRef(({ composeId, condensed, onF /> - + {features.richText && } {features.richText && floatingAnchorElem && ( <> diff --git a/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx b/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx index d4c91e63d..ab95f26a9 100644 --- a/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx +++ b/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx @@ -13,14 +13,19 @@ import { useBasicTypeaheadTriggerMatch, } from '@lexical/react/LexicalTypeaheadMenuPlugin'; import { useLexicalTextEntity } from '@lexical/react/useLexicalTextEntity'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import clsx from 'clsx'; +import React, { useCallback, useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; +import { fetchComposeSuggestions } from 'soapbox/actions/compose'; +import { useAppDispatch, useCompose } from 'soapbox/hooks'; + +import AutosuggestAccount from '../../components/autosuggest-account'; import { $createMentionNode, MentionNode } from '../nodes/mention-node'; import { TypeaheadMenuPlugin } from './typeahead-menu-plugin'; -import type { TextNode } from 'lexical'; +import type { RangeSelection, TextNode } from 'lexical'; const REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i'); @@ -44,32 +49,7 @@ const TRIGGERS = ['@'].join(''); // Chars we expect to see in a mention (non-space, non-punctuation). const VALID_CHARS = '[^' + TRIGGERS + PUNC + '\\s]'; -// Non-standard series of chars. Each series must be preceded and followed by -// a valid char. -// const VALID_JOINS = -// '(?:' + -// '\\.[ |$]|' + // E.g. "r. " in "Mr. Smith" -// ' |' + // E.g. " " in "Josh Duck" -// '[' + -// PUNC + -// ']|' + // E.g. "-' in "Salier-Hellendag" -// ')'; - -// const LENGTH_LIMIT = 75; - -const AtSignMentionsRegex = REGEX; /* new RegExp( - '(^|\\s|\\()(' + - '[' + - TRIGGERS + - ']' + - '((?:' + - VALID_CHARS + - VALID_JOINS + - '){0,' + - LENGTH_LIMIT + - '})' + - ')$', -); */ +const AtSignMentionsRegex = REGEX; // 50 is the longest alias length limit. const ALIAS_LENGTH_LIMIT = 50; @@ -88,9 +68,6 @@ const AtSignMentionsRegexAliasRegex = new RegExp( ')$', ); -// At most, 5 suggestions are shown in the popup. -const SUGGESTION_LIST_LENGTH_LIMIT = 5; - const mentionsCache = new Map(); const dummyMentionsData = ['Test']; @@ -199,45 +176,20 @@ class MentionTypeaheadOption extends TypeaheadOption { } -const MentionsTypeaheadMenuItem = ({ - index, - isSelected, - onClick, - onMouseEnter, - option, -}: { - index: number - isSelected: boolean - onClick: () => void - onMouseEnter: () => void - option: MentionTypeaheadOption -}) => { - let className = 'item'; - if (isSelected) { - className += ' selected'; - } - return ( -
  • - {option.picture} - {option.name} -
  • - ); -}; +export const MentionPlugin: React.FC<{ + composeId: string + suggestionsHidden: boolean + setSuggestionsHidden: (value: boolean) => void +}> = ({ + composeId, suggestionsHidden, setSuggestionsHidden, +}): JSX.Element | null => { + const { suggestions } = useCompose(composeId); + const dispatch = useAppDispatch(); -export const MentionPlugin = (): JSX.Element | null => { const [editor] = useLexicalComposerContext(); const [queryString, setQueryString] = useState(null); + const [selectedSuggestion] = useState(0); const results = useMentionLookupService(queryString); @@ -245,34 +197,28 @@ export const MentionPlugin = (): JSX.Element | null => { minLength: 0, }); - const options = useMemo( - () => - results - .map( - (result) => - new MentionTypeaheadOption(result, ), - ) - .slice(0, SUGGESTION_LIST_LENGTH_LIMIT), - [results], - ); + const options = [new MentionTypeaheadOption('', )]; - const onSelectOption = useCallback( - ( - selectedOption: MentionTypeaheadOption, - nodeToReplace: TextNode | null, - closeMenu: () => void, - ) => { - editor.update(() => { - const mentionNode = $createMentionNode(selectedOption.name); - if (nodeToReplace) { - nodeToReplace.replace(mentionNode); - } - mentionNode.select(); - closeMenu(); + const onSelectOption = useCallback(() => { }, [editor]); + + const onSelectSuggestion: React.MouseEventHandler = (e) => { + e.preventDefault(); + + const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index') as any); + + editor.update(() => { + + dispatch((_, getState) => { + const state = editor.getEditorState(); + const node = (state._selection as RangeSelection)?.anchor?.getNode(); + + const content = getState().accounts.get(suggestion)!.acct; + + node.setTextContent(`@${content} `); + node.select(); }); - }, - [editor], - ); + }); + }; const checkForMentionMatch = useCallback( (text: string) => { @@ -300,6 +246,8 @@ export const MentionPlugin = (): JSX.Element | null => { return null; } + dispatch(fetchComposeSuggestions(composeId, matchArr[0])); + const mentionLength = matchArr[3].length + 1; const startOffset = matchArr.index + matchArr[1].length; const endOffset = startOffset + mentionLength; @@ -315,6 +263,31 @@ export const MentionPlugin = (): JSX.Element | null => { createMentionNode, ); + const renderSuggestion = (suggestion: string, i: number) => { + const inner = ; + const key = suggestion; + + return ( +
    + {inner} +
    + ); + }; + + useEffect(() => { + if (suggestions && suggestions.size > 0) setSuggestionsHidden(false); + }, [suggestions]); + return ( onQueryChange={setQueryString} @@ -327,24 +300,15 @@ export const MentionPlugin = (): JSX.Element | null => { ) => anchorElementRef.current && results.length ? ReactDOM.createPortal( -
    -
      - {options.map((option, i: number) => ( - { - setHighlightedIndex(i); - selectOptionAndCleanUp(option); - }} - onMouseEnter={() => { - setHighlightedIndex(i); - }} - key={option.key} - option={option} - /> - ))} -
    +
    + {suggestions.map(renderSuggestion)}
    , anchorElementRef.current, )