Skip to content

Commit 2b789e5

Browse files
authored
fix: make consistent use of message composition related props (#2695)
1 parent 666be5e commit 2b789e5

14 files changed

+153
-170
lines changed

src/components/Channel/Channel.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ type ChannelPropsForwardedToComponentContext = Pick<
142142
| 'ReactionsListModal'
143143
| 'SendButton'
144144
| 'StartRecordingAudioButton'
145+
| 'TextareaComposer'
145146
| 'ThreadHead'
146147
| 'ThreadHeader'
147148
| 'ThreadStart'
@@ -1229,6 +1230,7 @@ const ChannelInner = (
12291230
StartRecordingAudioButton: props.StartRecordingAudioButton,
12301231
StopAIGenerationButton: props.StopAIGenerationButton,
12311232
StreamedMessageText: props.StreamedMessageText,
1233+
TextareaComposer: props.TextareaComposer,
12321234
ThreadHead: props.ThreadHead,
12331235
ThreadHeader: props.ThreadHeader,
12341236
ThreadStart: props.ThreadStart,
@@ -1291,6 +1293,7 @@ const ChannelInner = (
12911293
props.StartRecordingAudioButton,
12921294
props.StopAIGenerationButton,
12931295
props.StreamedMessageText,
1296+
props.TextareaComposer,
12941297
props.ThreadHead,
12951298
props.ThreadHeader,
12961299
props.ThreadStart,

src/components/Channel/__tests__/Channel.test.js

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { Thread } from '../../Thread';
3232
import { MessageProvider } from '../../../context';
3333
import { MessageActionsBox } from '../../MessageActions';
3434
import { DEFAULT_THREAD_PAGE_SIZE } from '../../../constants/limits';
35+
import { generateMessageDraft } from '../../../mock-builders/generator/messageDraft';
3536

3637
jest.mock('../../Loading', () => ({
3738
LoadingErrorIndicator: jest.fn(() => <div />),
@@ -173,6 +174,9 @@ describe('Channel', () => {
173174
pinnedMessages,
174175
user,
175176
}));
177+
jest.spyOn(channel, 'getDraft').mockResolvedValue({
178+
draft: generateMessageDraft({ channel, channel_cid: channel.cid }),
179+
});
176180
});
177181

178182
afterEach(() => {

src/components/MessageInput/EditMessageForm.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ export const EditMessageModal = ({
8686
>
8787
<MessageInput
8888
clearEditingState={clearEditingState}
89-
grow
9089
hideSendButton
9190
Input={EditMessageInput}
9291
{...additionalMessageInputProps}

src/components/MessageInput/MessageInput.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export type MessageInputProps = {
3838
*/
3939
additionalTextareaProps?: Omit<
4040
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
41-
'defaultValue'
41+
'defaultValue' | 'style' | 'disabled' | 'value'
4242
>;
4343
/**
4444
* When enabled, recorded messages won’t be sent immediately.
@@ -55,8 +55,6 @@ export type MessageInputProps = {
5555
emojiSearchIndex?: ComponentContextValue['emojiSearchIndex'];
5656
/** If true, focuses the text input on component mount */
5757
focus?: boolean;
58-
/** If true, expands the text input vertically for new lines */
59-
grow?: boolean;
6058
/** Allows to hide MessageInput's send button. */
6159
hideSendButton?: boolean;
6260
/** Custom UI component handling how the message input is rendered, defaults to and accepts the same props as [MessageInputFlat](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/MessageInputFlat.tsx) */

src/components/MessageInput/MessageInputFlat.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
QuotedMessagePreviewHeader,
2020
} from './QuotedMessagePreview';
2121
import { LinkPreviewList as DefaultLinkPreviewList } from './LinkPreviewList';
22-
import { TextareaComposer } from '../TextareaComposer';
22+
import { TextareaComposer as DefaultTextareaComposer } from '../TextareaComposer';
2323
import { AIStates, useAIState } from '../AIStateIndicator';
2424
import { RecordingAttachmentType } from '../MediaRecorder/classes';
2525

@@ -53,7 +53,8 @@ export const MessageInputFlat = () => {
5353
SendButton = DefaultSendButton,
5454
StartRecordingAudioButton = DefaultStartRecordingAudioButton,
5555
StopAIGenerationButton: StopAIGenerationButtonOverride,
56-
} = useComponentContext('MessageInputFlat');
56+
TextareaComposer = DefaultTextareaComposer,
57+
} = useComponentContext();
5758
const { channel } = useChatContext('MessageInputFlat');
5859
const { aiState } = useAIState(channel);
5960

src/components/MessageInput/hooks/useCreateMessageInputContext.ts

-4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ export const useCreateMessageInputContext = (value: MessageInputContextValue) =>
1212
cooldownRemaining,
1313
emojiSearchIndex,
1414
focus,
15-
grow,
1615
handleSubmit,
1716
hideSendButton,
18-
insertText,
1917
isThreadInput,
2018
maxRows,
2119
minRows,
@@ -39,10 +37,8 @@ export const useCreateMessageInputContext = (value: MessageInputContextValue) =>
3937
cooldownRemaining,
4038
emojiSearchIndex,
4139
focus,
42-
grow,
4340
handleSubmit,
4441
hideSendButton,
45-
insertText,
4642
isThreadInput,
4743
maxRows,
4844
minRows,

src/components/MessageInput/hooks/useMessageInputControls.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type React from 'react';
2-
import { useMessageInputText } from './useMessageInputText';
2+
import { useTextareaRef } from './useTextareaRef';
33
import { useSubmitHandler } from './useSubmitHandler';
44
import { usePasteHandler } from './usePasteHandler';
55
import { useMediaRecorder } from '../../MediaRecorder/hooks/useMediaRecorder';
@@ -12,7 +12,6 @@ export type MessageInputHookProps = {
1212
event?: React.BaseSyntheticEvent,
1313
customMessageData?: Omit<UpdatedMessage, 'mentioned_users'>,
1414
) => void;
15-
insertText: (textToInsert: string) => void;
1615
onPaste: (event: React.ClipboardEvent<HTMLTextAreaElement>) => void;
1716
recordingController: RecordingController;
1817
textareaRef: React.MutableRefObject<HTMLTextAreaElement | null | undefined>;
@@ -24,7 +23,7 @@ export const useMessageInputControls = (
2423
const { asyncMessagesMultiSendEnabled, audioRecordingConfig, audioRecordingEnabled } =
2524
props;
2625

27-
const { insertText, textareaRef } = useMessageInputText(props);
26+
const { textareaRef } = useTextareaRef(props);
2827

2928
const { handleSubmit } = useSubmitHandler(props);
3029

@@ -35,11 +34,10 @@ export const useMessageInputControls = (
3534
recordingConfig: audioRecordingConfig,
3635
});
3736

38-
const { onPaste } = usePasteHandler(insertText);
37+
const { onPaste } = usePasteHandler();
3938

4039
return {
4140
handleSubmit,
42-
insertText,
4341
onPaste,
4442
recordingController,
4543
textareaRef,

src/components/MessageInput/hooks/useMessageInputText.ts

-58
This file was deleted.

src/components/MessageInput/hooks/usePasteHandler.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { useCallback } from 'react';
22
import { useMessageComposer } from './useMessageComposer';
33
import { dataTransferItemsToFiles } from '../../ReactFileUtilities';
44

5-
export const usePasteHandler = (insertText: (textToInsert: string) => void) => {
6-
const { attachmentManager } = useMessageComposer();
5+
export const usePasteHandler = () => {
6+
const { attachmentManager, textComposer } = useMessageComposer();
77
const onPaste = useCallback(
88
(clipboardEvent: React.ClipboardEvent<HTMLTextAreaElement>) => {
99
(async (event) => {
@@ -29,13 +29,13 @@ export const usePasteHandler = (insertText: (textToInsert: string) => void) => {
2929

3030
if (plainTextPromise) {
3131
const pastedText = await plainTextPromise;
32-
insertText(pastedText);
32+
textComposer.insertText({ text: pastedText });
3333
} else {
3434
attachmentManager.uploadFiles(fileLikes);
3535
}
3636
})(clipboardEvent);
3737
},
38-
[attachmentManager, insertText],
38+
[attachmentManager, textComposer],
3939
);
4040

4141
return { onPaste };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useEffect, useRef } from 'react';
2+
import type { MessageInputProps } from '../MessageInput';
3+
4+
export const useTextareaRef = (props: MessageInputProps) => {
5+
const { focus } = props;
6+
const textareaRef = useRef<HTMLTextAreaElement>(undefined);
7+
// Focus
8+
useEffect(() => {
9+
if (focus && textareaRef.current) {
10+
textareaRef.current.focus();
11+
}
12+
}, [focus]);
13+
14+
return {
15+
textareaRef,
16+
};
17+
};

src/components/TextareaComposer/TextareaComposer.tsx

+37-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
import debounce from 'lodash.debounce';
12
import clsx from 'clsx';
2-
import type { ChangeEventHandler, TextareaHTMLAttributes, UIEventHandler } from 'react';
3+
import type {
4+
ChangeEventHandler,
5+
SyntheticEvent,
6+
TextareaHTMLAttributes,
7+
UIEventHandler,
8+
} from 'react';
9+
import { useMemo } from 'react';
310
import React, { useCallback, useEffect, useRef, useState } from 'react';
411
import Textarea from 'react-textarea-autosize';
512
import { useMessageComposer } from '../MessageInput';
@@ -40,52 +47,49 @@ const configStateSelector = (state: MessageComposerConfig) => ({
4047
const defaultShouldSubmit = (event: React.KeyboardEvent<HTMLTextAreaElement>) =>
4148
event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing;
4249

43-
export type TextComposerProps = Omit<
50+
export type TextareaComposerProps = Omit<
4451
TextareaHTMLAttributes<HTMLTextAreaElement>,
45-
'style' | 'defaultValue' | 'disabled'
52+
'style' | 'defaultValue' | 'disabled' | 'value'
4653
> & {
4754
closeSuggestionsOnClickOutside?: boolean;
4855
containerClassName?: string;
49-
dropdownClassName?: string;
50-
grow?: boolean;
51-
itemClassName?: string;
5256
listClassName?: string;
5357
maxRows?: number;
58+
minRows?: number;
5459
shouldSubmit?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => boolean;
5560
};
5661

5762
export const TextareaComposer = ({
5863
className,
5964
closeSuggestionsOnClickOutside,
6065
containerClassName,
61-
// dropdownClassName, // todo: X find a different way to prevent prop drilling
62-
grow: growProp,
63-
// itemClassName, // todo: X find a different way to prevent prop drilling
6466
listClassName,
6567
maxRows: maxRowsProp = 1,
68+
minRows: minRowsProp,
6669
onBlur,
6770
onChange,
6871
onKeyDown,
6972
onScroll,
73+
onSelect,
7074
placeholder: placeholderProp,
7175
shouldSubmit: shouldSubmitProp,
72-
...restProps
73-
}: TextComposerProps) => {
76+
...restTextareaProps
77+
}: TextareaComposerProps) => {
7478
const { t } = useTranslationContext();
7579
const { AutocompleteSuggestionList = DefaultSuggestionList } = useComponentContext();
7680
const {
7781
additionalTextareaProps,
7882
cooldownRemaining,
79-
grow: growContext,
8083
handleSubmit,
8184
maxRows: maxRowsContext,
85+
minRows: minRowsContext,
8286
onPaste,
8387
shouldSubmit: shouldSubmitContext,
8488
textareaRef,
8589
} = useMessageInputContext();
8690

87-
const grow = growProp ?? growContext;
8891
const maxRows = maxRowsProp ?? maxRowsContext;
92+
const minRows = minRowsProp ?? minRowsContext;
8993
const placeholder = placeholderProp ?? additionalTextareaProps?.placeholder;
9094
const shouldSubmit = shouldSubmitProp ?? shouldSubmitContext ?? defaultShouldSubmit;
9195

@@ -205,6 +209,22 @@ export const TextareaComposer = ({
205209
[onScroll, textComposer],
206210
);
207211

212+
const setSelectionDebounced = useMemo(
213+
() =>
214+
debounce(
215+
(e: SyntheticEvent<HTMLTextAreaElement>) => {
216+
onSelect?.(e);
217+
textComposer.setSelection({
218+
end: (e.target as HTMLTextAreaElement).selectionEnd,
219+
start: (e.target as HTMLTextAreaElement).selectionStart,
220+
});
221+
},
222+
100,
223+
{ leading: false, trailing: true },
224+
),
225+
[onSelect, textComposer],
226+
);
227+
208228
useEffect(() => {
209229
// FIXME: find the real reason for cursor being set to the end on each change
210230
// This is a workaround to prevent the cursor from jumping
@@ -235,7 +255,7 @@ export const TextareaComposer = ({
235255
ref={containerRef}
236256
>
237257
<Textarea
238-
{...restProps}
258+
{...{ ...additionalTextareaProps, ...restTextareaProps }}
239259
aria-label={cooldownRemaining ? t('Slow Mode ON') : placeholder}
240260
className={clsx(
241261
'rta__textarea',
@@ -244,14 +264,16 @@ export const TextareaComposer = ({
244264
)}
245265
data-testid='message-input'
246266
disabled={!enabled || !!cooldownRemaining}
247-
maxRows={grow ? maxRows : 1}
267+
maxRows={maxRows}
268+
minRows={minRows}
248269
onBlur={onBlur}
249270
onChange={changeHandler}
250271
onCompositionEnd={onCompositionEnd}
251272
onCompositionStart={onCompositionStart}
252273
onKeyDown={keyDownHandler}
253274
onPaste={onPaste}
254275
onScroll={scrollHandler}
276+
onSelect={setSelectionDebounced}
255277
placeholder={placeholder || t('Type your message')}
256278
ref={(ref) => {
257279
textareaRef.current = ref;

0 commit comments

Comments
 (0)