Skip to content

Commit

Permalink
fix: rich text editor [WPB-12089] (#18520)
Browse files Browse the repository at this point in the history
* feat(RichTextEditor): replace markdown transformers with custom implementation

* fix(EditedMessagePlugin): editting a message with mentions included spacing

* fix: lists

* chore: remove unused code

* fix: reorder lists

* fix: heading inserting

* fix: heading margin

* fix: restore avatar only on text value + fix styles

* feat: update list styles to use lower-alpha and lower-roman

* fix: lint

* fix: adjust emoji picker position calculations

* feat: update lexical to v0.21.0
  • Loading branch information
olafsulich authored Dec 18, 2024
1 parent 92bd1c6 commit aec9c85
Show file tree
Hide file tree
Showing 18 changed files with 582 additions and 244 deletions.
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"@datadog/browser-logs": "5.33.0",
"@datadog/browser-rum": "5.33.0",
"@emotion/react": "11.11.4",
"@lexical/code": "0.20.2",
"@lexical/history": "0.20.2",
"@lexical/list": "0.20.2",
"@lexical/markdown": "0.20.2",
"@lexical/react": "0.20.2",
"@lexical/rich-text": "0.20.2",
"@lexical/code": "0.21.0",
"@lexical/history": "0.21.0",
"@lexical/list": "0.21.0",
"@lexical/markdown": "0.21.0",
"@lexical/react": "0.21.0",
"@lexical/rich-text": "0.21.0",
"@mediapipe/tasks-vision": "0.10.20",
"@wireapp/avs": "10.0.4",
"@wireapp/avs-debugger": "0.0.7",
Expand All @@ -35,7 +35,7 @@
"kalium-backup": "./TEMP-crossplatform-backup",
"keyboardjs": "2.7.0",
"knockout": "3.5.1",
"lexical": "0.20.2",
"lexical": "0.21.0",
"libsodium-wrappers": "0.7.15",
"linkify-it": "5.0.0",
"long": "5.2.3",
Expand Down
24 changes: 15 additions & 9 deletions src/script/components/InputBar/InputBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ export const InputBar = ({

const enableSending = textValue.length > 0;

const showAvatar = messageFormatButtonsEnabled || !!textValue.length;
const showAvatar = !!textValue.length;

return (
<div ref={wrapperRef}>
Expand Down Expand Up @@ -627,14 +627,20 @@ export const InputBar = ({
onSend={handleSendMessage}
onBlur={() => isTypingRef.current && conversationRepository.sendTypingStop(conversation)}
>
<ul
className={cx('controls-right buttons-group input-bar-buttons', {
'controls-right-shrinked': textValue.length !== 0,
})}
>
<ControlButtons {...controlButtonsProps} showGiphyButton={showGiphyButton} />
<SendMessageButton disabled={!enableSending} onSend={handleSendMessage} />
</ul>
<div className="input-bar-buttons">
<ul
className={cx('controls-right buttons-group input-bar-buttons__list', {
'controls-right-shrinked': textValue.length !== 0,
})}
>
<ControlButtons {...controlButtonsProps} showGiphyButton={showGiphyButton} />
</ul>
<SendMessageButton
disabled={!enableSending}
onSend={handleSendMessage}
className="input-bar-buttons__send"
/>
</div>
</RichTextEditor>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ interface EmojiPickerParams {
onEmojiPicked: (emoji: string) => void;
}

const TRIGGER_WIDTH = 40;
const TRIGGER_HEIGHT = 32;
const Y_OFFSET = 8;

export const useEmojiPicker = ({wrapperRef, onEmojiPicked}: EmojiPickerParams) => {
const [open, setOpen] = useState(false);

Expand All @@ -43,7 +47,7 @@ export const useEmojiPicker = ({wrapperRef, onEmojiPicked}: EmojiPickerParams) =
const handleToggle = (event: MouseEvent<HTMLButtonElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
// eslint-disable-next-line id-length
emojiPickerPosition.current = {x: rect.x, y: rect.y};
emojiPickerPosition.current = {x: rect.x + TRIGGER_WIDTH, y: rect.y - TRIGGER_HEIGHT - Y_OFFSET};
setOpen(prev => !prev);
};

Expand Down
27 changes: 20 additions & 7 deletions src/script/components/RichTextEditor/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {ReactElement, useRef} from 'react';
import {CodeHighlightNode, CodeNode} from '@lexical/code';
import {LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {$convertToMarkdownString, TRANSFORMERS} from '@lexical/markdown';
import {$convertToMarkdownString} from '@lexical/markdown';
import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin';
import {InitialConfigType, LexicalComposer} from '@lexical/react/LexicalComposer';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
Expand Down Expand Up @@ -52,9 +52,12 @@ import {EmojiPickerPlugin} from './plugins/EmojiPickerPlugin';
import {GlobalEventsPlugin} from './plugins/GlobalEventsPlugin';
import {HistoryPlugin} from './plugins/HistoryPlugin';
import {findAndTransformEmoji, ReplaceEmojiPlugin} from './plugins/InlineEmojiReplacementPlugin';
import {ListItemTabIndentationPlugin} from './plugins/ListIndentationPlugin/ListIndentationPlugin';
import {ListMaxIndentLevelPlugin} from './plugins/ListMaxIndentLevelPlugin/ListMaxIndentLevelPlugin';
import {MentionsPlugin} from './plugins/MentionsPlugin';
import {ReplaceCarriageReturnPlugin} from './plugins/ReplaceCarriageReturnPlugin/ReplaceCarriageReturnPlugin';
import {SendPlugin} from './plugins/SendPlugin';
import {markdownTransformers} from './utils/markdownTransformers';

import {MentionEntity} from '../../message/MentionEntity';

Expand All @@ -75,8 +78,13 @@ const theme = {
code: 'editor-code',
},
list: {
ul: 'editor-list editor-list--unordered',
ol: 'editor-list editor-list--ordered',
ul: 'editor-list editor-list-unordered',
ol: 'editor-list editor-list-ordered',
listitem: 'editor-list__item',
nested: {
listitem: 'editor-list__item--nested',
},
olDepth: ['editor-list-ordered--1', 'editor-list-ordered--2', 'editor-list-ordered--3'],
},
heading: {
h1: 'editor-heading editor-heading--1',
Expand Down Expand Up @@ -187,7 +195,7 @@ export const RichTextEditor = ({
return;
}

const markdown = $convertToMarkdownString(TRANSFORMERS);
const markdown = $convertToMarkdownString(markdownTransformers);

onUpdate({
text: replaceEmojis ? findAndTransformEmoji(markdown) : markdown,
Expand All @@ -210,14 +218,15 @@ export const RichTextEditor = ({
/>
<DraftStatePlugin loadDraftState={loadDraftState} />
<EditedMessagePlugin message={editedMessage} />

<ListItemTabIndentationPlugin />
<ListMaxIndentLevelPlugin maxDepth={3} />
<EmojiPickerPlugin openStateRef={emojiPickerOpen} />
<HistoryPlugin />
<ListPlugin />
{replaceEmojis && <ReplaceEmojiPlugin />}

<ReplaceCarriageReturnPlugin />
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
<MarkdownShortcutPlugin transformers={markdownTransformers} />

<RichTextPlugin
contentEditable={<ContentEditable className="conversation-input-bar-text" data-uie-name="input-message" />}
Expand All @@ -242,7 +251,11 @@ export const RichTextEditor = ({
/>
</div>
</div>
{showFormatToolbar && <FormatToolbar />}
{showFormatToolbar && (
<div className="input-bar-toolbar">
<FormatToolbar />
</div>
)}
{children}
</LexicalComposer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,4 @@ import {CSSObject} from '@emotion/react';
export const wrapperStyles: CSSObject = {
display: 'flex',
alignItems: 'center',
margin: '8px 0 8px auto',
gridArea: 'toolbar',
};
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,18 @@ export const FormatToolbar = () => {
active={activeFormats.includes('strikethrough')}
onClick={() => formatText('strikethrough')}
/>
<FormatButton
label={t('richTextUnorderedList')}
icon={BulletListIcon}
active={activeFormats.includes('unorderedList')}
onClick={() => toggleList('unordered')}
/>
<FormatButton
label={t('richTextOrderedList')}
icon={NumberedListIcon}
active={activeFormats.includes('orderedList')}
onClick={() => toggleList('ordered')}
/>
<FormatButton
label={t('richTextUnorderedList')}
icon={BulletListIcon}
active={activeFormats.includes('unorderedList')}
onClick={() => toggleList('unordered')}
/>
<FormatButton
label={t('richTextCode')}
icon={CodeIcon}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import {useEffect} from 'react';

import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$getSelection, $isRangeSelection, $createParagraphNode, createCommand} from 'lexical';
import {$createHeadingNode} from '@lexical/rich-text';
import {$setBlocksType} from '@lexical/selection';
import {$getSelection, createCommand, $isRangeSelection, $createParagraphNode} from 'lexical';

import {headingCommand} from './headingCommand';

Expand All @@ -46,20 +48,12 @@ export const useHeadingState = () => {
const anchorNode = selection.anchor.getNode();
const isHeading = isNodeHeading(anchorNode);

if (!isHeading) {
editor.dispatchCommand(INSERT_HEADING_COMMAND, {});
if (isHeading) {
$setBlocksType(selection, () => $createParagraphNode());
return;
}

const paragraphNode = $createParagraphNode();
const headingNode = anchorNode.getParent();

if (!headingNode) {
return;
}

headingNode.replace(paragraphNode);
paragraphNode.append(...headingNode.getChildren());
$setBlocksType(selection, () => $createHeadingNode('h1'));
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ import {t} from 'Util/LocalizerUtil';
interface SendMessageButtonProps {
disabled?: boolean;
onSend: () => void;
className?: string;
}

export const SendMessageButton = ({disabled, onSend}: SendMessageButtonProps) => {
export const SendMessageButton = ({disabled, onSend, className}: SendMessageButtonProps) => {
return (
<button
type="button"
className={cx('controls-right-button controls-right-button--send')}
className={cx('controls-right-button controls-right-button--send', className)}
disabled={disabled}
title={t('tooltipConversationSendMessage')}
aria-label={t('tooltipConversationSendMessage')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@

import {useEffect} from 'react';

import {$convertFromMarkdownString, TRANSFORMERS} from '@lexical/markdown';
import {$convertFromMarkdownString} from '@lexical/markdown';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$getRoot, $setSelection} from 'lexical';

import {markdownTransformers} from 'Components/RichTextEditor/utils/markdownTransformers';
import {ContentMessage} from 'src/script/entity/message/ContentMessage';

import {getMentionMarkdownTransformer} from './getMentionMarkdownTransformer/getMentionMarkdownTransformer';
import {getMentionNodesFromMessage} from './getMentionNodesFromMessage/getMentionNodesFromMessage';
import {wrapMentionsWithTags} from './wrapMentionsWithTags/wrapMentionsWithTags';

type Props = {
message?: ContentMessage;
Expand All @@ -51,14 +53,16 @@ export function EditedMessagePlugin({message}: Props): null {

const allowedMentions = mentionNodes.map(node => node.getTextContent());

const wrappedWithTags = wrapMentionsWithTags(messageContent, allowedMentions);

const mentionMarkdownTransformer = getMentionMarkdownTransformer(allowedMentions);

// Text comes from the message is in the raw markdown format, we need to convert it to the editor format (preview), display **bold** as bold, etc.
// The below function do that by getting the text, and transofrming it to the desired format.
// During the transformation, we have to tell the editor to transofrm mentions as well.
// We can't do that by diretcly updating the $root (e.g. $root.appent(...MentionNodes)), because this function will overwrite the result.
// One way of overcoming this issue is to use a custom transformer (quite a hacky way). Transformers are responisble for converting the text to the desired format (e.g. **bold** to bold).
$convertFromMarkdownString(messageContent, [...TRANSFORMERS, mentionMarkdownTransformer]);
$convertFromMarkdownString(wrappedWithTags, [mentionMarkdownTransformer, ...markdownTransformers]);

editor.focus();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,26 @@ import {$createMentionNode, $isMentionNode, MentionNode} from 'Components/RichTe

// Cutom transformer for handling mentions when converting markdown to editor format.
// Based on https://github.com/facebook/lexical/blob/main/packages/lexical-markdown/src/MarkdownTransformers.ts#L489
// It takes mentions from the markdown (e.g. <mention>@John Doe</mention>) and converts them to MentionNodes.
export const getMentionMarkdownTransformer = (allowedMentions: Array<string>): TextMatchTransformer => {
return {
dependencies: [MentionNode],
export: node => {
if (!$isMentionNode(node)) {
return null;
}
return `@${node.getTextContent()}`;
return `<mention>${node.getTextContent()}</mention>`;
},
importRegExp: /(@\w+)/,
regExp: /(@\w+)$/,
importRegExp: /<mention>([^<]+)<\/mention>/,
regExp: /<mention>([^<]+)<\/mention>/,
replace: (textNode, match) => {
const [mentionText] = match;
const mentionText = match[1];

if (!allowedMentions.includes(mentionText)) {
return;
}

const mentionNode = $createMentionNode('@', mentionText.slice(1));

textNode.replace(mentionNode);
},
trigger: ' ',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

/**
* Wraps mentions in a given text with a <mention> tag.
* It's useful cause it sets the mentions apart from the rest of the text.
* Thanks to that, we can differentiate them from the rest of the text, and render them as MentionNodes.
*/
export const wrapMentionsWithTags = (text: string, allMentions: string[]): string => {
if (!allMentions.length) {
return text;
}

const mentionRegex = new RegExp(`(${allMentions.join('|')})`, 'g');

return text.replace(mentionRegex, '<mention>$1</mention>');
};
Loading

0 comments on commit aec9c85

Please sign in to comment.