Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(RichTextEditor/InputBar): file structure [WPB-12089] #18568

Merged
merged 3 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading