Skip to content

Commit

Permalink
refactor(RichTextEditor/InputBar): file structure (#18568)
Browse files Browse the repository at this point in the history
  • Loading branch information
olafsulich authored Jan 9, 2025
1 parent 5050f8e commit b669a84
Show file tree
Hide file tree
Showing 56 changed files with 252 additions and 164 deletions.
10 changes: 5 additions & 5 deletions src/script/components/InputBar/InputBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ import {checkFileSharingPermission} from 'Components/Conversation/utils/checkFil
import {EmojiPicker} from 'Components/EmojiPicker/EmojiPicker';
import {PrimaryModal} from 'Components/Modals/PrimaryModal';
import {showWarningModal} from 'Components/Modals/utils/showWarningModal';
import {RichTextContent, RichTextEditor} from 'Components/RichTextEditor';
import {SendMessageButton} from 'Components/RichTextEditor/components/SendMessageButton';
import {ConversationRepository} from 'src/script/conversation/ConversationRepository';
import {useUserPropertyValue} from 'src/script/hooks/useUserProperty';
import {PropertiesRepository} from 'src/script/properties/PropertiesRepository';
Expand All @@ -47,8 +45,10 @@ import {formatLocale, TIME_IN_MILLIS} from 'Util/TimeUtil';
import {getFileExtension} from 'Util/util';

import {ControlButtons} from './components/InputBarControls/ControlButtons';
import {PastedFileControls} from './components/PastedFileControls';
import {ReplyBar} from './components/ReplyBar';
import {PastedFileControls} from './components/PastedFileControls/PastedFileControls';
import {ReplyBar} from './components/ReplyBar/ReplyBar';
import {RichTextContent, RichTextEditor} from './components/RichTextEditor';
import {SendMessageButton} from './components/RichTextEditor/components/SendMessageButton';
import {TypingIndicator} from './components/TypingIndicator/TypingIndicator';
import {useEmojiPicker} from './hooks/useEmojiPicker/useEmojiPicker';
import {useFilePaste} from './hooks/useFilePaste/useFilePaste';
Expand Down Expand Up @@ -127,7 +127,7 @@ export const InputBar = ({
'isSelfUserRemoved',
'is1to1',
]);
const {isOutgoingRequest, isIncomingRequest} = useKoSubscribableChildren(connection, [
const {isOutgoingRequest, isIncomingRequest} = useKoSubscribableChildren(connection!, [
'isOutgoingRequest',
'isIncomingRequest',
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums';
import * as Icon from 'Components/Icon';
import {t} from 'Util/LocalizerUtil';

import {Config} from '../../../Config';
import {Config} from '../../../../Config';

interface PastedFileControlsProps {
pastedFile: File;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {useKoSubscribableChildren} from 'Util/ComponentUtil';
import {t} from 'Util/LocalizerUtil';
import {renderMessage} from 'Util/messageRenderer';

import {ContentMessage} from '../../../entity/message/ContentMessage';
import {ContentMessage} from '../../../../entity/message/ContentMessage';

interface ReplyBarProps {
replyMessageEntity: ContentMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,123 +19,49 @@

import {ReactElement, useRef} from 'react';

import {CodeHighlightNode, CodeNode} from '@lexical/code';
import {LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {$convertToMarkdownString} from '@lexical/markdown';
import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin';
import {InitialConfigType, LexicalComposer} from '@lexical/react/LexicalComposer';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {EditorRefPlugin} from '@lexical/react/LexicalEditorRefPlugin';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';
import {ListPlugin} from '@lexical/react/LexicalListPlugin';
import {MarkdownShortcutPlugin} from '@lexical/react/LexicalMarkdownShortcutPlugin';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import cx from 'classnames';
import {LexicalEditor, EditorState, $nodesOfType} from 'lexical';
import {LexicalEditor, EditorState} from 'lexical';

import {DraftState} from 'Components/InputBar/util/DraftStateUtil';
import {ContentMessage} from 'src/script/entity/message/ContentMessage';
import {User} from 'src/script/entity/User';
import {getLogger} from 'Util/Logger';

import {FormatToolbar} from './components/FormatToolbar/FormatToolbar';
import {EmojiNode} from './nodes/EmojiNode';
import {MentionNode} from './nodes/MentionNode';
import {AutoFocusPlugin} from './plugins/AutoFocusPlugin';
import {Placeholder} from './components/Placeholder/Placeholder';
import {editorConfig} from './editorConfig';
import {AutoFocusPlugin} from './plugins/AutoFocusPlugin/AutoFocusPlugin';
import {BlockquotePlugin} from './plugins/BlockquotePlugin/BlockquotePlugin';
import {CodeHighlightPlugin} from './plugins/CodeHighlightPlugin/CodeHighlightPlugin';
import {DraftStatePlugin} from './plugins/DraftStatePlugin';
import {DraftStatePlugin} from './plugins/DraftStatePlugin/DraftStatePlugin';
import {EditedMessagePlugin} from './plugins/EditedMessagePlugin/EditedMessagePlugin';
import {EmojiPickerPlugin} from './plugins/EmojiPickerPlugin';
import {GlobalEventsPlugin} from './plugins/GlobalEventsPlugin';
import {HistoryPlugin} from './plugins/HistoryPlugin';
import {GlobalEventsPlugin} from './plugins/GlobalEventsPlugin/GlobalEventsPlugin';
import {HistoryPlugin} from './plugins/HistoryPlugin/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 {SendPlugin} from './plugins/SendPlugin/SendPlugin';
import {markdownTransformers} from './utils/markdownTransformers';
import {parseMentions} from './utils/parseMentions';

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

const theme = {
ltr: 'ltr',
rtl: 'rtl',
placeholder: 'editor-placeholder',
paragraph: 'editor-paragraph',
mentions: {
'@': `at-mentions`, // use the trigger name as the key
'@Focused': 'focused-mentions', // add the "Focused" suffix to style the focused mention
},
text: {
bold: 'editor-bold',
italic: 'editor-italic',
underline: 'editor-underline',
strikethrough: 'editor-strikethrough',
code: 'editor-inline-code',
},
quote: 'editor-quote',
list: {
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',
h2: 'editor-heading editor-heading--2',
h3: 'editor-heading editor-heading--3',
},
code: 'editor-code',
codeHighlight: {
atrule: 'editor-tokenAtrule',
attr: 'editor-tokenAttr',
boolean: 'editor-tokenBoolean',
builtin: 'editor-tokenBuiltin',
cdata: 'editor-tokenCdata',
char: 'editor-tokenChar',
class: 'editor-tokenClass',
'class-name': 'editor-tokenClassName',
comment: 'editor-tokenComment',
constant: 'editor-tokenConstant',
deleted: 'editor-tokenDeleted',
doctype: 'editor-tokenDoctype',
entity: 'editor-tokenEntity',
function: 'editor-tokenFunction',
important: 'editor-tokenImportant',
inserted: 'editor-tokenInserted',
keyword: 'editor-tokenKeyword',
namespace: 'editor-tokenNamespace',
number: 'editor-tokenNumber',
operator: 'editor-tokenOperator',
prolog: 'editor-tokenProlog',
property: 'editor-tokenProperty',
punctuation: 'editor-tokenPunctuation',
regex: 'editor-tokenRegex',
selector: 'editor-tokenSelector',
string: 'editor-tokenString',
symbol: 'editor-tokenSymbol',
tag: 'editor-tokenTag',
url: 'editor-tokenUrl',
variable: 'editor-tokenVariable',
},
};
import {MentionEntity} from '../../../../message/MentionEntity';

export type RichTextContent = {
text: string;
mentions?: MentionEntity[];
};

const logger = getLogger('LexicalInput');

interface RichTextEditorProps {
placeholder: string;
replaceEmojis?: boolean;
Expand All @@ -156,53 +82,6 @@ interface RichTextEditorProps {
onSetup?: (editor: LexicalEditor) => void;
}

const createMentionEntity = (user: Pick<User, 'id' | 'name' | 'domain'>, mentionPosition: number): MentionEntity => {
const userName = user.name();
const mentionLength = userName.length + 1;

return new MentionEntity(mentionPosition, mentionLength, user.id, user.domain);
};

const parseMentions = (editor: LexicalEditor, textValue: string, mentions: User[]) => {
const editorMentions = editor.getEditorState().read(() =>
$nodesOfType(MentionNode)
// The nodes given by lexical are not sorted by their position in the text. Instead they are sorted according to the moment they were inserted into the global text.
// We need to manually sort the nodes by their position before parsing the mentions in the entire text
.sort((m1, m2) => (m1.isBefore(m2) ? -1 : 1))
.map(node => node.getValue()),
);
let position = -1;

return editorMentions.flatMap(mention => {
const mentionPosition = textValue.indexOf(`@${mention}`, position + 1);
const mentionOption = mentions.find(user => user.name() === mention);

position = mentionPosition;
return mentionOption ? [createMentionEntity(mentionOption, mentionPosition)] : [];
});
};

const editorConfig: InitialConfigType = {
namespace: 'WireLexicalEditor',
theme,
onError(error: unknown) {
logger.error(error);
},
nodes: [
MentionNode,
EmojiNode,
ListItemNode,
ListNode,
HeadingNode,
HorizontalRuleNode,
QuoteNode,
CodeNode,
CodeHighlightNode,
LinkNode,
QuoteNode,
],
};

export const RichTextEditor = ({
placeholder,
children,
Expand Down Expand Up @@ -303,16 +182,3 @@ export const RichTextEditor = ({
</LexicalComposer>
);
};

function Placeholder({text, hasLocalEphemeralTimer}: {text: string; hasLocalEphemeralTimer: boolean}) {
return (
<div
className={cx('editor-placeholder', {
'conversation-input-bar-text--accent': hasLocalEphemeralTimer,
})}
data-uie-name="input-placeholder"
>
{text}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Wire
* Copyright (C) 2025 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/.
*
*/

import cx from 'classnames';

export const Placeholder = ({text, hasLocalEphemeralTimer}: {text: string; hasLocalEphemeralTimer: boolean}) => {
return (
<div
className={cx('editor-placeholder', {
'conversation-input-bar-text--accent': hasLocalEphemeralTimer,
})}
data-uie-name="input-placeholder"
>
{text}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Wire
* Copyright (C) 2025 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/.
*
*/

import {CodeHighlightNode, CodeNode} from '@lexical/code';
import {LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {InitialConfigType} from '@lexical/react/LexicalComposer';
import {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';

import {getLogger} from 'Util/Logger';

import {EmojiNode} from './nodes/EmojiNode';
import {MentionNode} from './nodes/MentionNode';
import {theme} from './theme';

const logger = getLogger('LexicalInput');

export const editorConfig: InitialConfigType = {
namespace: 'WireLexicalEditor',
theme,
onError(error: unknown) {
logger.error(error);
},
nodes: [
MentionNode,
EmojiNode,
ListItemNode,
ListNode,
HeadingNode,
HorizontalRuleNode,
QuoteNode,
CodeNode,
CodeHighlightNode,
LinkNode,
QuoteNode,
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {$convertFromMarkdownString} from '@lexical/markdown';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$getRoot, $setSelection} from 'lexical';

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

import {getMentionMarkdownTransformer} from './getMentionMarkdownTransformer/getMentionMarkdownTransformer';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import {TextMatchTransformer} from '@lexical/markdown';

import {$createMentionNode, $isMentionNode, MentionNode} from 'Components/RichTextEditor/nodes/MentionNode';
import {$createMentionNode, $isMentionNode, MentionNode} from '../../../nodes/MentionNode';

// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import {ContentMessage} from 'src/script/entity/message/ContentMessage';

import {Text} from '../../../../../entity/message/Text';
import {Text} from '../../../../../../../entity/message/Text';
import {$createMentionNode, MentionNode} from '../../../nodes/MentionNode';
import {createNodes} from '../../../utils/generateNodes';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import {$createParagraphNode, $createTextNode} from 'lexical';

import {$createMentionNode} from 'Components/RichTextEditor/nodes/MentionNode';
import {$createMentionNode} from 'Components/InputBar/components/RichTextEditor/nodes/MentionNode';
import {ContentMessage} from 'src/script/entity/message/ContentMessage';
import {Text} from 'src/script/entity/message/Text';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ import {forwardRef, ForwardRefRenderFunction} from 'react';

import cx from 'classnames';

import {EmojiOption} from 'Components/RichTextEditor/plugins/EmojiPickerPlugin/EmojiPickerPlugin';

import {itemStyle, symbolStyle, nameStyle} from './EmojiItem.styles';

import {EmojiOption} from '../EmojiPickerPlugin/EmojiPickerPlugin';

interface EmojiItemProps {
emoji: EmojiOption;
onClick: () => void;
Expand Down
Loading

0 comments on commit b669a84

Please sign in to comment.