Skip to content

Commit

Permalink
feat: allow to conditionally display MessageInput's send button throu…
Browse files Browse the repository at this point in the history
…gh MessageInputProps
  • Loading branch information
MartinCupela committed Oct 2, 2023
1 parent 38796ac commit 6174573
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,14 @@ Function that runs onSubmit to the underlying `textarea` component.
| ---------------------------------------------------------------------- |
| (event: React.BaseSyntheticEvent, customMessageData?: Message) => void |

### hideSendButton

Allows to hide MessageInput's send button. Used by `MessageSimple` to hide the send button in `EditMessageForm`. Received from `MessageInputProps`.

| Type | Default |
|---------|---------|
| boolean | false |

### imageOrder

The order in which image attachments have been added to the current message.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ If true, expands the text input vertically for new lines.
| ------- | ------- |
| boolean | true |

### hideSendButton

Allows to hide MessageInput's send button. Used by `MessageSimple` to hide the send button in `EditMessageForm`.

| Type | Default |
|---------|---------|
| boolean | false |

### Input

Custom UI component handling how the message input is rendered.
Expand Down
1 change: 1 addition & 0 deletions src/components/Message/MessageSimple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const MessageSimpleWithContext = <
<MessageInput
clearEditingState={clearEditingState}
grow
hideSendButton
Input={EditMessageInput}
message={message}
{...additionalMessageInputProps}
Expand Down
2 changes: 2 additions & 0 deletions src/components/MessageInput/MessageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export type MessageInputProps<
getDefaultValue?: () => string | string[];
/** If true, expands the text input vertically for new lines */
grow?: boolean;
/** Allows to hide MessageInput's send button. */
hideSendButton?: boolean;
/** 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) */
Input?: React.ComponentType<MessageInputProps<StreamChatGenerics, V>>;
/** Max number of rows the underlying `textarea` component is allowed to grow */
Expand Down
7 changes: 4 additions & 3 deletions src/components/MessageInput/MessageInputFlat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const MessageInputV1 = <
cooldownRemaining,
emojiPickerIsOpen,
handleSubmit,
hideSendButton,
isUploadEnabled,
maxFilesLeft,
numberOfUploads,
Expand Down Expand Up @@ -164,7 +165,7 @@ const MessageInputV1 = <
</div>
)}
</div>
{!cooldownRemaining && <SendButton sendMessage={handleSubmit} />}
{!(cooldownRemaining || hideSendButton) && <SendButton sendMessage={handleSubmit} />}
</div>
</ImageDropzone>
</div>
Expand All @@ -188,6 +189,7 @@ const MessageInputV2 = <
emojiPickerIsOpen,
findAndEnqueueURLsToEnrich,
handleSubmit,
hideSendButton,
isUploadEnabled,
linkPreviews,
maxFilesLeft,
Expand Down Expand Up @@ -304,8 +306,7 @@ const MessageInputV2 = <
</div>
</div>
</div>
{/* hide SendButton if this component is rendered in the edit message form */}
{!message && (
{!hideSendButton && (
<>
{cooldownRemaining ? (
<CooldownTimer
Expand Down
3 changes: 2 additions & 1 deletion src/components/MessageInput/MessageInputSmall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const MessageInputSmall = <
cooldownRemaining,
emojiPickerIsOpen,
handleSubmit,
hideSendButton,
isUploadEnabled,
maxFilesLeft,
numberOfUploads,
Expand Down Expand Up @@ -153,7 +154,7 @@ export const MessageInputSmall = <
)}
<EmojiPicker small />
</div>
{!cooldownRemaining && <SendButton sendMessage={handleSubmit} />}
{!(cooldownRemaining || hideSendButton) && <SendButton sendMessage={handleSubmit} />}
</div>
</ImageDropzone>
</div>
Expand Down
21 changes: 2 additions & 19 deletions src/components/MessageInput/__tests__/LinkPreviewList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,14 @@ import {
useMockedApis,
} from '../../../mock-builders';
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
ChatProvider,
MessageProvider,
useChatContext,
useMessageInputContext,
} from '../../../context';
import { ChatProvider, MessageProvider, useChatContext } from '../../../context';
import React, { useEffect } from 'react';
import { Chat } from '../../Chat';
import { Channel } from '../../Channel';
import { MessageActionsBox } from '../../MessageActions';
import { MessageInput } from '../MessageInput';

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

// Mock out lodash debounce implementation, so it calls the debounced method immediately
jest.mock('lodash.debounce', () =>
Expand Down Expand Up @@ -107,17 +101,6 @@ const makeRenderFn = (InputComponent) => async ({
messageContextOverrides = {},
messageActionsBoxProps = {},
} = {}) => {
// circumvents not so good decision to render SendButton conditionally
const InputContainer = () => {
const { handleSubmit, message } = useMessageInputContext();
return (
<>
<InputComponent />
{!!message && <SendButton sendMessage={handleSubmit} />}
</>
);
};

let renderResult;
await act(() => {
renderResult = render(
Expand All @@ -131,7 +114,7 @@ const makeRenderFn = (InputComponent) => async ({
getMessageActions={defaultMessageContextValue.getMessageActions}
/>
</MessageProvider>
<MessageInput Input={InputContainer} {...messageInputProps} />
<MessageInput Input={InputComponent} {...messageInputProps} />
</Channel>
</ChatContextOverrider>
</Chat>,
Expand Down
130 changes: 119 additions & 11 deletions src/components/MessageInput/__tests__/MessageInput.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { MessageActionsBox } from '../../MessageActions';

import { MessageProvider } from '../../../context/MessageContext';
import { useMessageInputContext } from '../../../context/MessageInputContext';
import { useChatContext } from '../../../context/ChatContext';
import { ChatProvider, useChatContext } from '../../../context/ChatContext';
import {
dispatchMessageDeletedEvent,
dispatchMessageUpdatedEvent,
Expand Down Expand Up @@ -1117,12 +1117,52 @@ function axeNoViolations(container) {
});

[
{ InputComponent: MessageInputSmall, name: 'MessageInputSmall' },
{ InputComponent: MessageInputFlat, name: 'MessageInputFlat' },
].forEach(({ InputComponent, name: componentName }) => {
{ InputComponent: MessageInputSmall, name: 'MessageInputSmall', themeVersion: '1' },
{ InputComponent: MessageInputSmall, name: 'MessageInputSmall', themeVersion: '2' },
{ InputComponent: MessageInputFlat, name: 'MessageInputFlat', themeVersion: '1' },
{ InputComponent: MessageInputFlat, name: 'MessageInputFlat', themeVersion: '2' },
].forEach(({ InputComponent, name: componentName, themeVersion }) => {
const makeRenderFn = (InputComponent) => async ({
channelProps = {},
chatContextOverrides = {},
messageInputProps = {},
messageContextOverrides = {},
messageActionsBoxProps = {},
} = {}) => {
let renderResult;
await act(() => {
renderResult = render(
<ChatProvider
value={{
channel,
channelsQueryState: { error: null, queryInProgress: false },
client: chatClient,
latestMessageDatesByChannels: {},
...chatContextOverrides,
}}
>
{/*<ActiveChannelSetter activeChannel={channel} />*/}
<Channel
doSendMessageRequest={submitMock}
doUpdateMessageRequest={editMock}
{...channelProps}
>
<MessageProvider value={{ ...defaultMessageContextValue, ...messageContextOverrides }}>
<MessageActionsBox
{...messageActionsBoxProps}
getMessageActions={defaultMessageContextValue.getMessageActions}
/>
</MessageProvider>
<MessageInput Input={InputComponent} {...messageInputProps} />
</Channel>
</ChatProvider>,
);
});
return renderResult;
};
const renderComponent = makeRenderFn(InputComponent);

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

afterEach(tearDown);

const render = async () => {
const render = async ({
chatContextOverrides = {},
messageContextOverrides = {},
messageInputProps = {},
} = {}) => {
const message =
componentName === 'MessageInputSmall' ? threadMessage : defaultMessageContextValue.message;

await renderComponent({
messageContextOverrides: { message },
chatContextOverrides: { themeVersion, ...chatContextOverrides },
messageContextOverrides: { message, ...messageContextOverrides },
messageInputProps,
});

return message;
};

const renderWithActiveCooldown = async ({ messageInputProps = {} } = {}) => {
channel = chatClient.channel('messaging', mockedChannelData.channel.id);
channel.data.cooldown = 30;
channel.initialized = true;
const lastSentSecondsAhead = 5;
await render({
chatContextOverrides: {
channel,
latestMessageDatesByChannels: {
[channel.cid]: new Date(new Date().getTime() + lastSentSecondsAhead * 1000),
},
},
messageInputProps,
});
};

const initQuotedMessagePreview = async (message) => {
await waitFor(() => expect(screen.queryByText(message.text)).not.toBeInTheDocument());

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

const quotedMessagePreviewIsDisplayedCorrectly = async (message) => {
await waitFor(() => expect(screen.queryByText(/reply to message/i)).toBeInTheDocument());
await waitFor(() =>
expect(screen.queryByTestId('quoted-message-preview')).toBeInTheDocument(),
);
await waitFor(() => expect(screen.getByText(message.text)).toBeInTheDocument());
};

Expand All @@ -1174,17 +1238,19 @@ function axeNoViolations(container) {
const message = await render();
await initQuotedMessagePreview(message);
message.text = nanoid();
act(() => {
await act(() => {
dispatchMessageUpdatedEvent(chatClient, message, channel);
});
await quotedMessagePreviewIsDisplayedCorrectly(message);
});

it('is closed on close button click', async () => {
// skip trying to cancel reply for theme version 2 as that is not supported
if (themeVersion === '2') return;
const message = await render();
await initQuotedMessagePreview(message);
const closeBtn = screen.getByRole('button', { name: /cancel reply/i });
act(() => {
await act(() => {
fireEvent.click(closeBtn);
});
quotedMessagePreviewIsNotDisplayed(message);
Expand All @@ -1193,11 +1259,53 @@ function axeNoViolations(container) {
it('is closed on original message delete', async () => {
const message = await render();
await initQuotedMessagePreview(message);
act(() => {
await act(() => {
dispatchMessageDeletedEvent(chatClient, message, channel);
});
quotedMessagePreviewIsNotDisplayed(message);
});
});

describe('send button', () => {
const SEND_BTN_TEST_ID = 'send-button';

it('should be renderer for empty input', async () => {
await render();
expect(screen.getByTestId(SEND_BTN_TEST_ID)).toBeInTheDocument();
});

it('should be renderer when editing a message', async () => {
await render({ messageInputProps: { message: generateMessage() } });
expect(screen.getByTestId(SEND_BTN_TEST_ID)).toBeInTheDocument();
});

it('should not be renderer during active cooldown period', async () => {
await renderWithActiveCooldown();
expect(screen.queryByTestId(SEND_BTN_TEST_ID)).not.toBeInTheDocument();
});

it('should not be renderer if explicitly hidden', async () => {
await render({ messageInputProps: { hideSendButton: true } });
expect(screen.queryByTestId(SEND_BTN_TEST_ID)).not.toBeInTheDocument();
});
});

describe('cooldown timer', () => {
const COOLDOWN_TIMER_TEST_ID = 'cooldown-timer';

it('should be renderer during active cool-down period', async () => {
await renderWithActiveCooldown();
expect(screen.getByTestId(COOLDOWN_TIMER_TEST_ID)).toBeInTheDocument();
});

it('should not be renderer if send button explicitly hidden only for MessageInputFlat theme 2', async () => {
await renderWithActiveCooldown({ messageInputProps: { hideSendButton: true } });
if (componentName === 'MessageInputSmall' || themeVersion === '1') {
expect(screen.queryByTestId(COOLDOWN_TIMER_TEST_ID)).toBeInTheDocument();
} else {
expect(screen.queryByTestId(COOLDOWN_TIMER_TEST_ID)).not.toBeInTheDocument();
}
});
});
});
});
Loading

0 comments on commit 6174573

Please sign in to comment.