Skip to content

Commit 5c9e250

Browse files
chore(real time composer): refactor messageboxes to use same base and sync feats with develop (#41374)
1 parent f47f973 commit 5c9e250

6 files changed

Lines changed: 474 additions & 361 deletions

File tree

apps/meteor/app/ui-message/client/messageBox/createRichTextComposerAPI.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import type { Options } from '@rocket.chat/message-parser';
22
import { escapeHTML } from '@rocket.chat/string-helpers';
3-
import { Accounts } from 'meteor/accounts-base';
43
import type { RefObject } from 'react';
54

65
import { createComposerAPICore, triggerEvent, type SetText } from './createComposerAPICore';
6+
import { limitQuoteChain } from './limitQuoteChain';
77
import { resolveComposerBox } from './messageStateHandler';
88
import { getSelectionRange, setSelectionRange } from './selectionRange';
99
import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI';
1010

1111
export const createRichTextComposerAPI = (
1212
input: HTMLDivElement,
13-
storageID: string,
13+
persistDraft: (value: string) => void,
14+
initialDraft: string,
15+
quoteChainLimit: number,
1416
parseOptions: Options,
1517
composerRef: RefObject<HTMLElement | null>,
1618
{ rid, tmid }: { rid: string; tmid?: string },
@@ -49,26 +51,19 @@ export const createRichTextComposerAPI = (
4951
!skipFocus && focus();
5052
};
5153

52-
input.addEventListener('input', (event: Event) => {
53-
resolveComposerBox(event, parseOptions);
54-
});
54+
const rerender = (event: Event): void => resolveComposerBox(event, parseOptions);
55+
56+
input.addEventListener('input', rerender);
5557

5658
const core = createComposerAPICore({
5759
input,
5860
composerRef,
5961
room: { rid, tmid },
60-
initialValue: Accounts.storageLocation.getItem(storageID) ?? '',
61-
save: () => {
62-
// Store the value entirely as HTML with the DOM structure intact
63-
if (input.innerHTML !== '<br>') {
64-
Accounts.storageLocation.setItem(storageID, input.innerHTML);
65-
return;
66-
}
67-
68-
Accounts.storageLocation.removeItem(storageID);
69-
},
62+
initialValue: initialDraft,
63+
save: () => persistDraft(input.innerText),
7064
setText,
7165
focus,
66+
prepareQuotedMessage: (message) => limitQuoteChain(message, quoteChainLimit),
7267
});
7368

7469
const { insertText } = core;
@@ -188,6 +183,10 @@ export const createRichTextComposerAPI = (
188183

189184
return {
190185
...core,
186+
release: () => {
187+
core.release();
188+
input.removeEventListener('input', rerender);
189+
},
191190
setText,
192191
wrapSelection,
193192
replaceText,

apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx

Lines changed: 37 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,50 @@
11
/* eslint-disable complexity */
22
import { isRoomFederated, isRoomNativeFederated, type IMessage, type ISubscription } from '@rocket.chat/core-typings';
33
import { useContentBoxSize, useStableCallback, useMediaQuery, useSafeRefCallback } from '@rocket.chat/fuselage-hooks';
4-
import {
5-
MessageComposerAction,
6-
MessageComposerToolbarActions,
7-
MessageComposer,
8-
MessageComposerToolbar,
9-
MessageComposerActionsDivider,
10-
MessageComposerToolbarSubmit,
11-
MessageComposerButton,
12-
MessageComposerInputExpandable,
13-
} from '@rocket.chat/ui-composer';
4+
import { MessageComposerInputExpandable } from '@rocket.chat/ui-composer';
145
import { useTranslation, useUserPreference, useLayout, useSetting } from '@rocket.chat/ui-contexts';
156
import { useMutation } from '@tanstack/react-query';
167
import type { MouseEvent, ClipboardEvent, ChangeEvent } from 'react';
178
import { memo, useRef, useReducer, useCallback, useSyncExternalStore } from 'react';
189

19-
import MessageBoxActionsToolbar from './MessageBoxActionsToolbar';
20-
import MessageBoxFormattingToolbar from './MessageBoxFormattingToolbar';
21-
import MessageBoxHint from './MessageBoxHint';
22-
import MessageBoxReplies from './MessageBoxReplies';
10+
import MessageBoxBase from './MessageBoxBase';
2311
import MessageComposerFiles from './MessageComposerFiles';
12+
import { emptySubscribe, getEmptyFalse, getEmptyArray, handleFormattingShortcut } from './messageBoxHelpers';
2413
import { handleSelectionWrapping } from './wrapSelection';
2514
import { emoji } from '../../../../../app/emoji/client';
2615
import { createComposerAPI } from '../../../../../app/ui-message/client/messageBox/createComposerAPI';
27-
import type { FormattingButton } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting';
2816
import { formattingButtons } from '../../../../../app/ui-message/client/messageBox/messageBoxFormatting';
29-
import { getImageExtensionFromMime } from '../../../../../lib/getImageExtensionFromMime';
3017
import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime';
3118
import { useIsFederationEnabled } from '../../../../hooks/useIsFederationEnabled';
32-
import type { ComposerAPI } from '../../../../lib/chats/ChatAPI';
3319
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
3420
import { keyCodes } from '../../../../lib/utils/keyCodes';
3521
import { Subscriptions } from '../../../../stores';
36-
import AudioMessageRecorder from '../../../composer/AudioMessageRecorder';
37-
import VideoMessageRecorder from '../../../composer/VideoMessageRecorder';
3822
import { useFileUpload } from '../../body/hooks/useFileUpload';
3923
import { useChat } from '../../contexts/ChatContext';
4024
import { useComposerPopupOptions } from '../../contexts/ComposerPopupContext';
4125
import { useRoom, useRoomSubscription } from '../../contexts/RoomContext';
42-
import ComposerBoxPopup from '../ComposerBoxPopup';
43-
import ComposerBoxPopupPreview from '../ComposerBoxPopupPreview';
44-
import ComposerUserActionIndicator from '../ComposerUserActionIndicator';
4526
import { useAutoGrow } from '../RoomComposer/hooks/useAutoGrow';
4627
import { useComposerBoxPopup } from '../hooks/useComposerBoxPopup';
4728
import { useEnablePopupPreview } from '../hooks/useEnablePopupPreview';
4829
import { useMessageComposerMergedRefs } from '../hooks/useMessageComposerMergedRefs';
4930
import { useDraft } from './hooks/useDraft';
5031
import { useMessageBoxAutoFocus } from './hooks/useMessageBoxAutoFocus';
5132
import { useMessageBoxPlaceholder } from './hooks/useMessageBoxPlaceholder';
33+
import { getImageExtensionFromMime } from '../../../../../lib/getImageExtensionFromMime';
5234

5335
const reducer = (_: unknown, event: ChangeEvent<HTMLInputElement>): boolean => {
5436
const target = event.target as HTMLInputElement;
5537

5638
return Boolean(target.value.trim());
5739
};
5840

59-
const handleFormattingShortcut = (event: KeyboardEvent, formattingButtons: FormattingButton[], composer: ComposerAPI) => {
60-
const isMacOS = navigator.platform.indexOf('Mac') !== -1;
61-
const isCmdOrCtrlPressed = (isMacOS && event.metaKey) || (!isMacOS && event.ctrlKey);
62-
63-
if (!isCmdOrCtrlPressed) {
64-
return false;
65-
}
66-
67-
const key = event.key.toLowerCase();
68-
69-
const formatter = formattingButtons.find((formatter) => 'command' in formatter && formatter.command === key);
70-
71-
if (!formatter || !('pattern' in formatter)) {
72-
return false;
73-
}
74-
75-
composer.wrapSelection(formatter.pattern);
76-
return true;
77-
};
78-
79-
const emptySubscribe = () => () => undefined;
80-
const getEmptyFalse = () => false;
81-
const a: any[] = [];
82-
const getEmptyArray = () => a;
83-
8441
export type MessageBoxProps = {
8542
tmid?: IMessage['_id'];
8643
onSend?: (params: { value: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean }) => Promise<void>;
8744
onJoin?: () => Promise<void>;
8845
onResize?: () => void;
8946
onTyping?: () => void;
47+
onUploadFiles?: (files: File[]) => void;
9048
onEscape?: () => void;
9149
onNavigateToPreviousMessage?: () => void;
9250
onNavigateToNextMessage?: () => void;
@@ -103,6 +61,7 @@ const MessageBox = ({
10361
onJoin,
10462
onNavigateToNextMessage,
10563
onNavigateToPreviousMessage,
64+
onUploadFiles,
10665
onEscape,
10766
onTyping,
10867
tshow,
@@ -370,7 +329,7 @@ const MessageBox = ({
370329

371330
if (files.length) {
372331
event.preventDefault();
373-
handleUploadFiles?.(files);
332+
(onUploadFiles ?? handleUploadFiles)(files);
374333
}
375334
});
376335

@@ -418,45 +377,33 @@ const MessageBox = ({
418377
const shouldPopupPreview = useEnablePopupPreview(popup.filter, popup.option);
419378

420379
return (
421-
<>
422-
{chat.composer?.quotedMessages && <MessageBoxReplies />}
423-
{shouldPopupPreview && popup.option && (
424-
<ComposerBoxPopup
425-
select={popup.select}
426-
items={popup.items}
427-
focused={popup.focused}
428-
title={popup.option.title}
429-
renderItem={popup.option.renderItem}
430-
/>
431-
)}
432-
{/*
433-
SlashCommand Preview popup works in a weird way
434-
There is only one trigger for all the commands: "/"
435-
After that we need to the slashcommand list and check if the command exists and provide the preview
436-
if not the query is `suspend` which means the slashcommand is not found or doesn't have a preview
437-
*/}
438-
{popup.option?.preview && (
439-
<ComposerBoxPopupPreview
440-
select={popup.select}
441-
items={popup.items as any}
442-
focused={popup.focused as any}
443-
title={popup.option.title}
444-
renderItem={popup.option.renderItem}
445-
ref={popup.commandsRef}
446-
rid={room._id}
447-
tmid={tmid}
448-
suspended={popup.suspended}
449-
/>
450-
)}
451-
<MessageBoxHint
452-
isEditing={isEditing}
453-
e2eEnabled={e2eEnabled}
454-
unencryptedMessagesAllowed={unencryptedMessagesAllowed}
455-
isMobile={isMobile}
456-
/>
457-
{isRecordingVideo && <VideoMessageRecorder reference={messageComposerRef} rid={room._id} tmid={tmid} />}
458-
<MessageComposer ref={messageComposerRef} variant={isEditing ? 'editing' : undefined}>
459-
{isRecordingAudio && <AudioMessageRecorder rid={room._id} isMicrophoneDenied={isMicrophoneDenied} />}
380+
<MessageBoxBase
381+
rid={room._id}
382+
tmid={tmid}
383+
composer={chat.composer}
384+
messageComposerRef={messageComposerRef}
385+
popup={popup}
386+
shouldPopupPreview={shouldPopupPreview}
387+
isEditing={isEditing}
388+
isRecording={isRecording}
389+
isRecordingAudio={isRecordingAudio}
390+
isRecordingVideo={isRecordingVideo}
391+
isMicrophoneDenied={isMicrophoneDenied}
392+
formatters={formatters}
393+
canSend={canSend}
394+
useEmojis={useEmojis}
395+
sendEnabled={canSend && !isUploading && !isProcessingUploads && (typing || isEditing || hasUploads)}
396+
sendActive={typing || isEditing || hasUploads}
397+
inlineSize={sizes.inlineSize}
398+
e2eEnabled={e2eEnabled}
399+
unencryptedMessagesAllowed={unencryptedMessagesAllowed}
400+
isMobile={isMobile}
401+
joinPending={joinMutation.isPending}
402+
onEmojiClick={handleOpenEmojiPicker}
403+
onSend={handleSendMessage}
404+
onJoin={onJoin}
405+
closeEditing={closeEditing}
406+
input={
460407
<MessageComposerInputExpandable
461408
dimensions={sizes}
462409
ref={mergedRefs}
@@ -469,58 +416,9 @@ const MessageBox = ({
469416
onPaste={handlePaste}
470417
aria-activedescendant={popup.focused ? `popup-item-${popup.focused._id}` : undefined}
471418
/>
472-
<MessageComposerFiles />
473-
<MessageComposerToolbar>
474-
<MessageComposerToolbarActions aria-label={t('Message_composer_toolbox_primary_actions')}>
475-
<MessageComposerAction
476-
icon='emoji'
477-
disabled={!useEmojis || isRecording || !canSend}
478-
onClick={handleOpenEmojiPicker}
479-
title={t('Emoji')}
480-
/>
481-
<MessageComposerActionsDivider />
482-
{chat.composer && formatters.length > 0 && (
483-
<MessageBoxFormattingToolbar
484-
composer={chat.composer}
485-
variant={sizes.inlineSize < 480 ? 'small' : 'large'}
486-
items={formatters}
487-
disabled={isRecording || !canSend}
488-
/>
489-
)}
490-
<MessageBoxActionsToolbar
491-
canSend={canSend}
492-
isMicrophoneDenied={isMicrophoneDenied}
493-
rid={room._id}
494-
tmid={tmid}
495-
isRecording={isRecording}
496-
variant={sizes.inlineSize < 480 ? 'small' : 'large'}
497-
isEditing={isEditing}
498-
/>
499-
</MessageComposerToolbarActions>
500-
<MessageComposerToolbarSubmit>
501-
{!canSend && (
502-
<MessageComposerButton primary onClick={onJoin} loading={joinMutation.isPending}>
503-
{t('Join')}
504-
</MessageComposerButton>
505-
)}
506-
{canSend && (
507-
<>
508-
{isEditing && <MessageComposerButton onClick={closeEditing}>{t('Cancel')}</MessageComposerButton>}
509-
<MessageComposerAction
510-
aria-label={t('Send')}
511-
icon='send'
512-
disabled={!canSend || isUploading || isProcessingUploads || (!typing && !isEditing && !hasUploads)}
513-
onClick={handleSendMessage}
514-
secondary={typing || isEditing || hasUploads}
515-
info={typing || isEditing || hasUploads}
516-
/>
517-
</>
518-
)}
519-
</MessageComposerToolbarSubmit>
520-
</MessageComposerToolbar>
521-
</MessageComposer>
522-
<ComposerUserActionIndicator rid={room._id} tmid={tmid} />
523-
</>
419+
}
420+
files={<MessageComposerFiles />}
421+
/>
524422
);
525423
};
526424

0 commit comments

Comments
 (0)