export function AddComment()

in translate/src/modules/comments/components/AddComment.tsx [79:301]


export function AddComment({
  contactPerson,
  initFocus,
  onAddComment,
  resetContactPerson,
  user: { gravatarURLSmall, username },
}: Props): React.ReactElement<'div'> {
  const [mentionTarget, setMentionTarget] = useState<Range | null>(null);
  const [mentionIndex, setMentionIndex] = useState(0);
  const [mentionSearch, setMentionSearch] = useState('');
  const [requireUsers, setRequireUsers] = useState(false);
  const banner = useUserBanner();

  const { initMentions, mentionUsers } = useContext(MentionUsers);
  const [slateKey, resetValue] = useReducer((key) => key + 1, 0);
  const initialValue = useMemo<Paragraph[]>(
    () => [{ type: 'paragraph', children: [{ text: '' }] }],
    [slateKey],
  );

  const editor = useMemo(() => withMentions(withReact(createEditor())), []);
  const placeFocus = useCallback(() => {
    ReactEditor.focus(editor);
    Transforms.select(editor, Editor.end(editor, []));
  }, []);
  const insertMention = useCallback(
    ({ name, url }: { name: string; url?: string }) => {
      const mention: Mention = {
        type: 'mention',
        character: name,
        url,
        children: [{ text: name }],
      };
      Transforms.insertNodes(editor, mention);
      Transforms.select(editor, Editor.end(editor, []));
      Transforms.insertText(editor, ' ');
    },
    [editor],
  );

  useEffect(initMentions, []);

  // Insert project manager as mention when 'Request context / Report issue' button used
  // and then clear the value from state
  useEffect(() => {
    if (contactPerson) {
      insertMention({ name: contactPerson });
      setRequireUsers(true);
      resetContactPerson?.();
      placeFocus();
    }
  }, [contactPerson, resetContactPerson]);

  // Set focus on Editor
  useEffect(() => {
    if (initFocus && !isEditable(document.activeElement)) {
      placeFocus();
    }
  }, [initFocus]);

  const suggestedUsers = mentionUsers
    .filter(
      (user) =>
        user.username?.toLowerCase().includes(mentionSearch.toLowerCase()) ||
        user.name.toLowerCase().includes(mentionSearch.toLowerCase()),
    )
    .slice(0, 5);

  const handleEditableKeyDown = (event: React.KeyboardEvent) => {
    if (mentionTarget) {
      switch (event.key) {
        case 'ArrowDown': {
          event.preventDefault();
          const prevIndex =
            mentionIndex >= suggestedUsers.length - 1 ? 0 : mentionIndex + 1;
          setMentionIndex(prevIndex);
          break;
        }
        case 'ArrowUp': {
          event.preventDefault();
          const nextIndex =
            mentionIndex <= 0 ? suggestedUsers.length - 1 : mentionIndex - 1;
          setMentionIndex(nextIndex);
          break;
        }
        case 'Tab':
        case 'Enter':
          event.preventDefault();
          Transforms.select(editor, mentionTarget);
          insertMention(suggestedUsers[mentionIndex]);
          setMentionTarget(null);
          placeFocus();
          break;
        case 'Escape':
          event.preventDefault();
          setMentionTarget(null);
          break;
      }
    } else if (event.key === 'Enter') {
      event.preventDefault();
      if (event.shiftKey) {
        /*
         * This allows for the new lines to render while adding comments.
         * To avoid an issue with the cursor placement and an error when
         * navigating with arrows that occurs in Firefox '\n' can't be
         * the last character so the BOM was added
         */
        editor.insertText('\n\uFEFF');
      } else {
        submitComment();
      }
    }
  };

  const handleSelectMention = (user: MentionUser) => {
    if (mentionTarget) {
      Transforms.select(editor, mentionTarget);
      insertMention(user);
      setMentionTarget(null);
    }
  };

  const handleEditorChange = () => {
    const { selection } = editor;

    if (selection && Range.isCollapsed(selection)) {
      const [start] = Range.edges(selection);
      const wordBefore = Editor.before(editor, start, { unit: 'word' });
      const before = wordBefore && Editor.before(editor, wordBefore);
      const beforeRange = before && Editor.range(editor, before, start);
      const beforeText = beforeRange && Editor.string(editor, beforeRange);
      // Unicode property escapes allow for matching non-ASCII characters
      const beforeMatch =
        beforeText && beforeText.match(/^@((\p{L}|\p{N}|\p{P})+)$/u);
      const after = Editor.after(editor, start);
      const afterRange = Editor.range(editor, start, after);
      const afterText = Editor.string(editor, afterRange);
      const afterMatch = afterText.match(/^(\s|$)/);

      if (beforeMatch && afterMatch) {
        setMentionTarget(beforeRange);
        setMentionSearch(beforeMatch[1]);
        setMentionIndex(0);
        return;
      }
    }

    setMentionTarget(null);
  };

  const submitComment = () => {
    if (
      Node.string(editor).trim() !== '' &&
      (!requireUsers || mentionUsers.length > 0)
    ) {
      const comment = editor.children
        .map((node) => serialize(node, mentionUsers))
        .join('');
      onAddComment(comment);

      Transforms.select(editor, {
        anchor: { path: [0, 0], offset: 0 },
        focus: { path: [0, 0], offset: 0 },
      });
      resetValue();
    }
  };

  return (
    <div className='comment add-comment'>
      <UserAvatar
        username={username}
        imageUrl={gravatarURLSmall}
        userBanner={banner}
      />
      <div className='container'>
        <Slate
          editor={editor}
          key={slateKey}
          onChange={handleEditorChange}
          value={initialValue}
        >
          <Localized
            id='comments-AddComment--input'
            attrs={{ placeholder: true }}
          >
            <Editable
              className='comment-editor'
              name='comment'
              dir='auto'
              placeholder={`Write a comment…`}
              renderElement={RenderElement}
              onKeyDown={handleEditableKeyDown}
            />
          </Localized>
          {mentionTarget && suggestedUsers.length > 0 && (
            <MentionList
              editor={editor}
              index={mentionIndex}
              onSelect={handleSelectMention}
              suggestedUsers={suggestedUsers}
              target={mentionTarget}
            />
          )}
        </Slate>
        <Localized
          id='comments-AddComment--submit-button'
          attrs={{ title: true }}
          elems={{ glyph: <i className='fas fa-paper-plane' /> }}
        >
          <button
            className='submit-button'
            disabled={requireUsers && mentionUsers.length === 0}
            title='Submit comment'
            onClick={submitComment}
          >
            {'<glyph></glyph>'}
          </button>
        </Localized>
      </div>
    </div>
  );
}