Skip to content

Commit

Permalink
Merge pull request #2121 from GetStream/develop
Browse files Browse the repository at this point in the history
v10.13.0
  • Loading branch information
MartinCupela authored Oct 6, 2023
2 parents 02c5f0c + def7c42 commit e6809bc
Show file tree
Hide file tree
Showing 17 changed files with 282 additions and 58 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
59 changes: 54 additions & 5 deletions docusaurus/docs/React/components/core-components/channel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,14 @@ The Giphy version to render - check the keys of the [Image Object](https://devel
| ------ | -------------- |
| string | 'fixed_height' |

### HeaderComponent

Custom UI component to render at the top of the `MessageList`.

| Type | Default |
| --------- | ------- |
| component | none |

### imageAttachmentSizeHandler

A custom function to provide size configuration for image attachments
Expand All @@ -337,13 +345,54 @@ A custom function to provide size configuration for image attachments
| ---------------------------------------------------------------- |
| `(a: Attachment, e: HTMLElement) => ImageAttachmentConfigration` |

### HeaderComponent
### initializeOnMount

Allows to prevent triggering the `channel.watch()` (triggers channel query HTTP request) call when mounting the `Channel` component (the default behavior) with uninitialized (`channel.initialized`) `Channel` instance. That means that no channel data from the back-end will be received neither channel WS events will be delivered to the client. Preventing to initialize the channel on mount allows us to postpone the channel creation in the Stream's DB to a later point in time, for example, when a first message is sent:

```typescript jsx
import {useCallback} from "react";
import {
getChannel,
MessageInput as StreamMessageInput,
MessageInputProps, MessageToSend,
useChannelActionContext,
useChatContext
} from "stream-chat-react";
import {Message, SendMessageOptions} from "stream-chat";

import {useChannelInitContext} from "../../context/ChannelInitProvider";
import type { MyStreamChatGenerics } from "../../types";

export const MessageInput = (props: MessageInputProps) => {
const {client} = useChatContext();
const {sendMessage} = useChannelActionContext();
const { setInitializedChannelOnMount} = useChannelInitContext();

const submitHandler: MessageInputProps['overrideSubmitHandler'] = useCallback(async (
message: MessageToSend<MyStreamChatGenerics>,
channelCid: string,
customMessageData?: Partial<Message<MyStreamChatGenerics>>,
options?: SendMessageOptions,
) => {
const [channelType, channelId] = channelCid.split(":");
const channel = client.channel(channelType, channelId);
if (!channel.initialized) {
await getChannel({channel, client});
setInitializedChannelOnMount(true);
}

await sendMessage(message, customMessageData, options);
}, [client, sendMessage, setInitializedChannelOnMount]);

Custom UI component to render at the top of the `MessageList`.
return (
<StreamMessageInput {...props} overrideSubmitHandler={submitHandler} />
);
};
```

| Type | Default |
| --------- | ------- |
| component | none |
| Type | Default |
|---------|---------|
| boolean | true |

### Input

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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@
"rollup-plugin-url": "^3.0.1",
"rollup-plugin-visualizer": "^4.2.0",
"semantic-release": "^19.0.5",
"stream-chat": "^8.12.0",
"stream-chat": "^8.12.4",
"style-loader": "^2.0.0",
"ts-jest": "^26.5.1",
"typescript": "^4.7.4",
Expand Down
14 changes: 11 additions & 3 deletions src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ export type ChannelProps<
EmptyPlaceholder?: React.ReactElement;
/** Custom UI component to be displayed when the `MessageList` is empty, defaults to and accepts same props as: [EmptyStateIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/EmptyStateIndicator/EmptyStateIndicator.tsx) */
EmptyStateIndicator?: ComponentContextValue<StreamChatGenerics>['EmptyStateIndicator'];
/** A global flag to toggle the URL enrichment and link previews in `MessageInput` components.
/**
* A global flag to toggle the URL enrichment and link previews in `MessageInput` components.
* By default, the feature is disabled. Can be overridden on Thread, MessageList level through additionalMessageInputProps
* or directly on MessageInput level through urlEnrichmentConfig.
*/
Expand All @@ -169,6 +170,12 @@ export type ChannelProps<
HeaderComponent?: ComponentContextValue<StreamChatGenerics>['HeaderComponent'];
/** A custom function to provide size configuration for image attachments */
imageAttachmentSizeHandler?: ImageAttachmentSizeHandler;
/**
* Allows to prevent triggering the channel.watch() call when mounting the component.
* That means that no channel data from the back-end will be received neither channel WS events will be delivered to the client.
* Preventing to initialize the channel on mount allows us to postpone the channel creation to a later point in time.
*/
initializeOnMount?: 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?: ComponentContextValue<StreamChatGenerics>['Input'];
/** Custom component to render link previews in message input **/
Expand Down Expand Up @@ -312,6 +319,7 @@ const ChannelInner = <
dragAndDropWindow = false,
emojiData = defaultEmojiData,
enrichURLForPreviewConfig,
initializeOnMount = true,
LoadingErrorIndicator = DefaultLoadingErrorIndicator,
LoadingIndicator = DefaultLoadingIndicator,
maxNumberOfFiles,
Expand Down Expand Up @@ -478,7 +486,7 @@ const ChannelInner = <
};

(async () => {
if (!channel.initialized) {
if (!channel.initialized && initializeOnMount) {
try {
// if active channel has been set without id, we will create a temporary channel id from its member IDs
// to keep track of the /query request in progress. This is the same approach of generating temporary id
Expand Down Expand Up @@ -533,7 +541,7 @@ const ChannelInner = <
client.off('user.deleted', handleEvent);
notificationTimeouts.forEach(clearTimeout);
};
}, [channel.cid, doMarkReadRequest, channelConfig?.read_events]);
}, [channel.cid, doMarkReadRequest, channelConfig?.read_events, initializeOnMount]);

useEffect(() => {
if (!state.thread) return;
Expand Down
24 changes: 22 additions & 2 deletions src/components/Channel/__tests__/Channel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,13 +380,33 @@ describe('Channel', () => {
jest.spyOn(channel, 'countUnread').mockImplementationOnce(() => 1);
const doMarkReadRequest = jest.fn();

renderComponent({
doMarkReadRequest,
await act(() => {
renderComponent({
doMarkReadRequest,
});
});

await waitFor(() => expect(doMarkReadRequest).toHaveBeenCalledTimes(1));
});

it('should not query the channel from the backend when initializeOnMount is disabled', async () => {
const watchSpy = jest.spyOn(channel, 'watch').mockImplementationOnce();
await act(() => {
renderComponent({
initializeOnMount: false,
});
});
await waitFor(() => expect(watchSpy).not.toHaveBeenCalled());
});

it('should query the channel from the backend when initializeOnMount is enabled (the default)', async () => {
const watchSpy = jest.spyOn(channel, 'watch').mockImplementationOnce();
await act(() => {
renderComponent();
});
await waitFor(() => expect(watchSpy).toHaveBeenCalledTimes(1));
});

describe('Children that consume the contexts set in Channel', () => {
it('should expose the emoji config', async () => {
let context;
Expand Down
29 changes: 29 additions & 0 deletions src/components/ChannelList/__tests__/ChannelList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,35 @@ describe('ChannelList', () => {
expect(results).toHaveNoViolations();
});

it('should show unique channels', async () => {
useMockedApis(chatClientUthred, [queryChannelsApi([testChannel1, testChannel2])]);
const ChannelPreview = (props) => <div data-testid={props.channel.id} role='listitem' />;
render(
<Chat client={chatClientUthred}>
<ChannelList filters={{}} options={{ limit: 2 }} Preview={ChannelPreview} />
</Chat>,
);

await waitFor(() => {
expect(screen.getByTestId(testChannel1.channel.id)).toBeInTheDocument();
expect(screen.getByTestId(testChannel2.channel.id)).toBeInTheDocument();
expect(screen.getAllByRole('listitem')).toHaveLength(2);
});

useMockedApis(chatClientUthred, [queryChannelsApi([testChannel1, testChannel3])]);

await act(() => {
fireEvent.click(screen.getByTestId('load-more-button'));
});

await waitFor(() => {
expect(screen.getByTestId(testChannel1.channel.id)).toBeInTheDocument();
expect(screen.getByTestId(testChannel2.channel.id)).toBeInTheDocument();
expect(screen.getByTestId(testChannel3.channel.id)).toBeInTheDocument();
expect(screen.getAllByRole('listitem')).toHaveLength(3);
});
});

describe('Default and custom active channel', () => {
let setActiveChannel;
const watchersConfig = { limit: 20, offset: 0 };
Expand Down
5 changes: 4 additions & 1 deletion src/components/ChannelList/hooks/usePaginatedChannels.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import uniqBy from 'lodash.uniqby';

import { MAX_QUERY_CHANNELS_LIMIT } from '../utils';

Expand Down Expand Up @@ -52,7 +53,9 @@ export const usePaginatedChannels = <
const channelQueryResponse = await client.queryChannels(filters, sort || {}, newOptions);

const newChannels =
queryType === 'reload' ? channelQueryResponse : [...channels, ...channelQueryResponse];
queryType === 'reload'
? channelQueryResponse
: uniqBy([...channels, ...channelQueryResponse], 'cid');

setChannels(newChannels);
setHasNextPage(channelQueryResponse.length >= newOptions.limit);
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
Loading

0 comments on commit e6809bc

Please sign in to comment.