Skip to content

Commit cd07418

Browse files
authored
feat: allow to conditionally display MessageInput's send button through MessageInputProps (#2109)
1 parent 38796ac commit cd07418

File tree

10 files changed

+157
-42
lines changed

10 files changed

+157
-42
lines changed

docusaurus/docs/React/components/contexts/message-input-context.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,14 @@ Function that runs onSubmit to the underlying `textarea` component.
246246
| ---------------------------------------------------------------------- |
247247
| (event: React.BaseSyntheticEvent, customMessageData?: Message) => void |
248248

249+
### hideSendButton
250+
251+
Allows to hide MessageInput's send button. Used by `MessageSimple` to hide the send button in `EditMessageForm`. Received from `MessageInputProps`.
252+
253+
| Type | Default |
254+
|---------|---------|
255+
| boolean | false |
256+
249257
### imageOrder
250258

251259
The order in which image attachments have been added to the current message.

docusaurus/docs/React/components/message-input-components/message-input.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ If true, expands the text input vertically for new lines.
124124
| ------- | ------- |
125125
| boolean | true |
126126

127+
### hideSendButton
128+
129+
Allows to hide MessageInput's send button. Used by `MessageSimple` to hide the send button in `EditMessageForm`.
130+
131+
| Type | Default |
132+
|---------|---------|
133+
| boolean | false |
134+
127135
### Input
128136

129137
Custom UI component handling how the message input is rendered.

src/components/Message/MessageSimple.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ const MessageSimpleWithContext = <
124124
<MessageInput
125125
clearEditingState={clearEditingState}
126126
grow
127+
hideSendButton
127128
Input={EditMessageInput}
128129
message={message}
129130
{...additionalMessageInputProps}

src/components/MessageInput/MessageInput.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export type MessageInputProps<
6161
getDefaultValue?: () => string | string[];
6262
/** If true, expands the text input vertically for new lines */
6363
grow?: boolean;
64+
/** Allows to hide MessageInput's send button. */
65+
hideSendButton?: boolean;
6466
/** 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) */
6567
Input?: React.ComponentType<MessageInputProps<StreamChatGenerics, V>>;
6668
/** Max number of rows the underlying `textarea` component is allowed to grow */

src/components/MessageInput/MessageInputFlat.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ const MessageInputV1 = <
8181
cooldownRemaining,
8282
emojiPickerIsOpen,
8383
handleSubmit,
84+
hideSendButton,
8485
isUploadEnabled,
8586
maxFilesLeft,
8687
numberOfUploads,
@@ -164,7 +165,7 @@ const MessageInputV1 = <
164165
</div>
165166
)}
166167
</div>
167-
{!cooldownRemaining && <SendButton sendMessage={handleSubmit} />}
168+
{!(cooldownRemaining || hideSendButton) && <SendButton sendMessage={handleSubmit} />}
168169
</div>
169170
</ImageDropzone>
170171
</div>
@@ -188,6 +189,7 @@ const MessageInputV2 = <
188189
emojiPickerIsOpen,
189190
findAndEnqueueURLsToEnrich,
190191
handleSubmit,
192+
hideSendButton,
191193
isUploadEnabled,
192194
linkPreviews,
193195
maxFilesLeft,
@@ -304,8 +306,7 @@ const MessageInputV2 = <
304306
</div>
305307
</div>
306308
</div>
307-
{/* hide SendButton if this component is rendered in the edit message form */}
308-
{!message && (
309+
{!hideSendButton && (
309310
<>
310311
{cooldownRemaining ? (
311312
<CooldownTimer

src/components/MessageInput/MessageInputSmall.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const MessageInputSmall = <
5252
cooldownRemaining,
5353
emojiPickerIsOpen,
5454
handleSubmit,
55+
hideSendButton,
5556
isUploadEnabled,
5657
maxFilesLeft,
5758
numberOfUploads,
@@ -153,7 +154,7 @@ export const MessageInputSmall = <
153154
)}
154155
<EmojiPicker small />
155156
</div>
156-
{!cooldownRemaining && <SendButton sendMessage={handleSubmit} />}
157+
{!(cooldownRemaining || hideSendButton) && <SendButton sendMessage={handleSubmit} />}
157158
</div>
158159
</ImageDropzone>
159160
</div>

src/components/MessageInput/__tests__/LinkPreviewList.test.js

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,14 @@ import {
1313
useMockedApis,
1414
} from '../../../mock-builders';
1515
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
16-
import {
17-
ChatProvider,
18-
MessageProvider,
19-
useChatContext,
20-
useMessageInputContext,
21-
} from '../../../context';
16+
import { ChatProvider, MessageProvider, useChatContext } from '../../../context';
2217
import React, { useEffect } from 'react';
2318
import { Chat } from '../../Chat';
2419
import { Channel } from '../../Channel';
2520
import { MessageActionsBox } from '../../MessageActions';
2621
import { MessageInput } from '../MessageInput';
2722

2823
import '@testing-library/jest-dom';
29-
import { SendButton } from '../icons';
3024

3125
// Mock out lodash debounce implementation, so it calls the debounced method immediately
3226
jest.mock('lodash.debounce', () =>
@@ -107,17 +101,6 @@ const makeRenderFn = (InputComponent) => async ({
107101
messageContextOverrides = {},
108102
messageActionsBoxProps = {},
109103
} = {}) => {
110-
// circumvents not so good decision to render SendButton conditionally
111-
const InputContainer = () => {
112-
const { handleSubmit, message } = useMessageInputContext();
113-
return (
114-
<>
115-
<InputComponent />
116-
{!!message && <SendButton sendMessage={handleSubmit} />}
117-
</>
118-
);
119-
};
120-
121104
let renderResult;
122105
await act(() => {
123106
renderResult = render(
@@ -131,7 +114,7 @@ const makeRenderFn = (InputComponent) => async ({
131114
getMessageActions={defaultMessageContextValue.getMessageActions}
132115
/>
133116
</MessageProvider>
134-
<MessageInput Input={InputContainer} {...messageInputProps} />
117+
<MessageInput Input={InputComponent} {...messageInputProps} />
135118
</Channel>
136119
</ChatContextOverrider>
137120
</Chat>,

src/components/MessageInput/__tests__/MessageInput.test.js

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { MessageActionsBox } from '../../MessageActions';
1616

1717
import { MessageProvider } from '../../../context/MessageContext';
1818
import { useMessageInputContext } from '../../../context/MessageInputContext';
19-
import { useChatContext } from '../../../context/ChatContext';
19+
import { ChatProvider, useChatContext } from '../../../context/ChatContext';
2020
import {
2121
dispatchMessageDeletedEvent,
2222
dispatchMessageUpdatedEvent,
@@ -1117,12 +1117,52 @@ function axeNoViolations(container) {
11171117
});
11181118

11191119
[
1120-
{ InputComponent: MessageInputSmall, name: 'MessageInputSmall' },
1121-
{ InputComponent: MessageInputFlat, name: 'MessageInputFlat' },
1122-
].forEach(({ InputComponent, name: componentName }) => {
1120+
{ InputComponent: MessageInputSmall, name: 'MessageInputSmall', themeVersion: '1' },
1121+
{ InputComponent: MessageInputSmall, name: 'MessageInputSmall', themeVersion: '2' },
1122+
{ InputComponent: MessageInputFlat, name: 'MessageInputFlat', themeVersion: '1' },
1123+
{ InputComponent: MessageInputFlat, name: 'MessageInputFlat', themeVersion: '2' },
1124+
].forEach(({ InputComponent, name: componentName, themeVersion }) => {
1125+
const makeRenderFn = (InputComponent) => async ({
1126+
channelProps = {},
1127+
chatContextOverrides = {},
1128+
messageInputProps = {},
1129+
messageContextOverrides = {},
1130+
messageActionsBoxProps = {},
1131+
} = {}) => {
1132+
let renderResult;
1133+
await act(() => {
1134+
renderResult = render(
1135+
<ChatProvider
1136+
value={{
1137+
channel,
1138+
channelsQueryState: { error: null, queryInProgress: false },
1139+
client: chatClient,
1140+
latestMessageDatesByChannels: {},
1141+
...chatContextOverrides,
1142+
}}
1143+
>
1144+
{/*<ActiveChannelSetter activeChannel={channel} />*/}
1145+
<Channel
1146+
doSendMessageRequest={submitMock}
1147+
doUpdateMessageRequest={editMock}
1148+
{...channelProps}
1149+
>
1150+
<MessageProvider value={{ ...defaultMessageContextValue, ...messageContextOverrides }}>
1151+
<MessageActionsBox
1152+
{...messageActionsBoxProps}
1153+
getMessageActions={defaultMessageContextValue.getMessageActions}
1154+
/>
1155+
</MessageProvider>
1156+
<MessageInput Input={InputComponent} {...messageInputProps} />
1157+
</Channel>
1158+
</ChatProvider>,
1159+
);
1160+
});
1161+
return renderResult;
1162+
};
11231163
const renderComponent = makeRenderFn(InputComponent);
11241164

1125-
describe(`${componentName}`, () => {
1165+
describe(`${componentName}${themeVersion ? `(theme: ${themeVersion})` : ''}:`, () => {
11261166
beforeEach(async () => {
11271167
chatClient = await getTestClientWithUser({ id: user1.id });
11281168
useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannelData)]);
@@ -1131,17 +1171,39 @@ function axeNoViolations(container) {
11311171

11321172
afterEach(tearDown);
11331173

1134-
const render = async () => {
1174+
const render = async ({
1175+
chatContextOverrides = {},
1176+
messageContextOverrides = {},
1177+
messageInputProps = {},
1178+
} = {}) => {
11351179
const message =
11361180
componentName === 'MessageInputSmall' ? threadMessage : defaultMessageContextValue.message;
11371181

11381182
await renderComponent({
1139-
messageContextOverrides: { message },
1183+
chatContextOverrides: { themeVersion, ...chatContextOverrides },
1184+
messageContextOverrides: { message, ...messageContextOverrides },
1185+
messageInputProps,
11401186
});
11411187

11421188
return message;
11431189
};
11441190

1191+
const renderWithActiveCooldown = async ({ messageInputProps = {} } = {}) => {
1192+
channel = chatClient.channel('messaging', mockedChannelData.channel.id);
1193+
channel.data.cooldown = 30;
1194+
channel.initialized = true;
1195+
const lastSentSecondsAhead = 5;
1196+
await render({
1197+
chatContextOverrides: {
1198+
channel,
1199+
latestMessageDatesByChannels: {
1200+
[channel.cid]: new Date(new Date().getTime() + lastSentSecondsAhead * 1000),
1201+
},
1202+
},
1203+
messageInputProps,
1204+
});
1205+
};
1206+
11451207
const initQuotedMessagePreview = async (message) => {
11461208
await waitFor(() => expect(screen.queryByText(message.text)).not.toBeInTheDocument());
11471209

@@ -1154,7 +1216,9 @@ function axeNoViolations(container) {
11541216
};
11551217

11561218
const quotedMessagePreviewIsDisplayedCorrectly = async (message) => {
1157-
await waitFor(() => expect(screen.queryByText(/reply to message/i)).toBeInTheDocument());
1219+
await waitFor(() =>
1220+
expect(screen.queryByTestId('quoted-message-preview')).toBeInTheDocument(),
1221+
);
11581222
await waitFor(() => expect(screen.getByText(message.text)).toBeInTheDocument());
11591223
};
11601224

@@ -1174,17 +1238,19 @@ function axeNoViolations(container) {
11741238
const message = await render();
11751239
await initQuotedMessagePreview(message);
11761240
message.text = nanoid();
1177-
act(() => {
1241+
await act(() => {
11781242
dispatchMessageUpdatedEvent(chatClient, message, channel);
11791243
});
11801244
await quotedMessagePreviewIsDisplayedCorrectly(message);
11811245
});
11821246

11831247
it('is closed on close button click', async () => {
1248+
// skip trying to cancel reply for theme version 2 as that is not supported
1249+
if (themeVersion === '2') return;
11841250
const message = await render();
11851251
await initQuotedMessagePreview(message);
11861252
const closeBtn = screen.getByRole('button', { name: /cancel reply/i });
1187-
act(() => {
1253+
await act(() => {
11881254
fireEvent.click(closeBtn);
11891255
});
11901256
quotedMessagePreviewIsNotDisplayed(message);
@@ -1193,11 +1259,53 @@ function axeNoViolations(container) {
11931259
it('is closed on original message delete', async () => {
11941260
const message = await render();
11951261
await initQuotedMessagePreview(message);
1196-
act(() => {
1262+
await act(() => {
11971263
dispatchMessageDeletedEvent(chatClient, message, channel);
11981264
});
11991265
quotedMessagePreviewIsNotDisplayed(message);
12001266
});
12011267
});
1268+
1269+
describe('send button', () => {
1270+
const SEND_BTN_TEST_ID = 'send-button';
1271+
1272+
it('should be renderer for empty input', async () => {
1273+
await render();
1274+
expect(screen.getByTestId(SEND_BTN_TEST_ID)).toBeInTheDocument();
1275+
});
1276+
1277+
it('should be renderer when editing a message', async () => {
1278+
await render({ messageInputProps: { message: generateMessage() } });
1279+
expect(screen.getByTestId(SEND_BTN_TEST_ID)).toBeInTheDocument();
1280+
});
1281+
1282+
it('should not be renderer during active cooldown period', async () => {
1283+
await renderWithActiveCooldown();
1284+
expect(screen.queryByTestId(SEND_BTN_TEST_ID)).not.toBeInTheDocument();
1285+
});
1286+
1287+
it('should not be renderer if explicitly hidden', async () => {
1288+
await render({ messageInputProps: { hideSendButton: true } });
1289+
expect(screen.queryByTestId(SEND_BTN_TEST_ID)).not.toBeInTheDocument();
1290+
});
1291+
});
1292+
1293+
describe('cooldown timer', () => {
1294+
const COOLDOWN_TIMER_TEST_ID = 'cooldown-timer';
1295+
1296+
it('should be renderer during active cool-down period', async () => {
1297+
await renderWithActiveCooldown();
1298+
expect(screen.getByTestId(COOLDOWN_TIMER_TEST_ID)).toBeInTheDocument();
1299+
});
1300+
1301+
it('should not be renderer if send button explicitly hidden only for MessageInputFlat theme 2', async () => {
1302+
await renderWithActiveCooldown({ messageInputProps: { hideSendButton: true } });
1303+
if (componentName === 'MessageInputSmall' || themeVersion === '1') {
1304+
expect(screen.queryByTestId(COOLDOWN_TIMER_TEST_ID)).toBeInTheDocument();
1305+
} else {
1306+
expect(screen.queryByTestId(COOLDOWN_TIMER_TEST_ID)).not.toBeInTheDocument();
1307+
}
1308+
});
1309+
});
12021310
});
12031311
});

0 commit comments

Comments
 (0)