Skip to content

Commit

Permalink
Merge branch 'master' into rc
Browse files Browse the repository at this point in the history
  • Loading branch information
arnautov-anton committed Nov 22, 2023
2 parents 70b28c9 + 3f7acda commit a330233
Show file tree
Hide file tree
Showing 11 changed files with 431 additions and 106 deletions.
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,25 @@

* **emoji-mart:** `reactionOptions` signature has changed, see [release guide](https://github.com/GetStream/stream-chat-react/blob/7a19e386aa3adcc5741a7f0d92bc816a1a424094/docusaurus/docs/React/release-guides/new-reactions.mdx) for more information

## [10.20.1](https://github.com/GetStream/stream-chat-react/compare/v10.20.0...v10.20.1) (2023-11-20)


### Bug Fixes

* calculate pagination stop from custom channel query message limit ([#2180](https://github.com/GetStream/stream-chat-react/issues/2180)) ([8374af1](https://github.com/GetStream/stream-chat-react/commit/8374af1048b81c307d0687d7730df6a96633b7e6))

## [10.20.0](https://github.com/GetStream/stream-chat-react/compare/v10.19.0...v10.20.0) (2023-11-16)


### Bug Fixes

* lift notifications above modal overlay ([#2175](https://github.com/GetStream/stream-chat-react/issues/2175)) ([17d98f4](https://github.com/GetStream/stream-chat-react/commit/17d98f40eaea0a134a501deea14605b71d965871))


### Features

* allow to configure channel query options ([#2177](https://github.com/GetStream/stream-chat-react/issues/2177)) ([4f91d9a](https://github.com/GetStream/stream-chat-react/commit/4f91d9a65e752f4bcab2000f5d633b57ae4d6b0e))

## [10.19.0](https://github.com/GetStream/stream-chat-react/compare/v10.18.0...v10.19.0) (2023-11-14)


Expand Down Expand Up @@ -2720,4 +2739,4 @@ We've already been on a v1 release for a while but never updated our versioning.

- Support for @mentions for @mention interactions `Channel` now accepts the following props
- `onMentionsHover={(event, user) => console.log(event, user)}`
- `onMentionsClick={(event, user) => console.log(event, user)}`
- `onMentionsClick={(event, user) => console.log(event, user)}`
33 changes: 33 additions & 0 deletions docusaurus/docs/React/components/core-components/channel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,39 @@ Custom UI component to display a user's avatar.
| --------- | ---------------------------------------------------------- |
| component | <GHComponentLink text='Avatar' path='/Avatar/Avatar.tsx'/> |

### channelQueryOptions

Optional configuration parameters used for the initial channel query. Applied only if the value of `channel.initialized` is false. If the channel instance has already been initialized (channel has been queried), then the channel query will be skipped and channelQueryOptions will not be applied.

In the example below, we specify, that the first page of messages when a channel is queried should have 20 messages (the default is 100). Note that the `channel` prop has to be passed along `channelQueryOptions`.

```tsx
import {ChannelQueryOptions} from "stream-chat";
import {Channel, useChatContext} from "stream-chat-react";

const channelQueryOptions: ChannelQueryOptions = {
messages: { limit: 20 },
};

type ChannelRendererProps = {
id: string;
type: string;
};

const ChannelRenderer = ({id, type}: ChannelRendererProps) => {
const { client } = useChatContext();
return (
<Channel channel={client.channel(type, id)} channelQueryOptions={channelQueryOptions}>
{/* Channel children */}
</Channel>
);
}
```

| Type |
|-----------------------|
| `ChannelQueryOptions` |

### CooldownTimer

Custom UI component to display the slow mode cooldown timer.
Expand Down
187 changes: 103 additions & 84 deletions src/components/Channel/Channel.tsx

Large diffs are not rendered by default.

273 changes: 257 additions & 16 deletions src/components/Channel/__tests__/Channel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ jest.mock('../../Loading', () => ({
LoadingIndicator: jest.fn(() => <div>loading</div>),
}));

const queryChannelWithNewMessages = (newMessages, channel) =>
// generate new channel mock from existing channel with new messages added
getOrCreateChannelApi(
generateChannel({
channel: {
config: channel.getConfig(),
id: channel.id,
type: channel.type,
},
messages: newMessages,
}),
);

const MockAvatar = ({ name }) => (
<div className='avatar' data-testid='custom-avatar'>
{name}
Expand Down Expand Up @@ -269,7 +282,103 @@ describe('Channel', () => {
renderComponent({ channel, chatClient });
});

await waitFor(() => expect(watchSpy).toHaveBeenCalledTimes(1));
await waitFor(() => {
expect(watchSpy).toHaveBeenCalledTimes(1);
expect(watchSpy).toHaveBeenCalledWith(undefined);
});
});

it('should apply channelQueryOptions to channel watch call', async () => {
const { channel, chatClient } = await initClient();
const watchSpy = jest.spyOn(channel, 'watch');
const channelQueryOptions = {
messages: { limit: 20 },
};
await act(() => {
renderComponent({ channel, channelQueryOptions, chatClient });
});

await waitFor(() => {
expect(watchSpy).toHaveBeenCalledTimes(1);
expect(watchSpy).toHaveBeenCalledWith(channelQueryOptions);
});
});

it('should set hasMore state to false if the initial channel query returns less messages than the default initial page size', async () => {
const { channel, chatClient } = await initClient();
useMockedApis(chatClient, [queryChannelWithNewMessages([generateMessage()], channel)]);
let hasMore;
await act(() => {
renderComponent({ channel, chatClient }, ({ hasMore: contextHasMore }) => {
hasMore = contextHasMore;
});
});

await waitFor(() => {
expect(hasMore).toBe(false);
});
});

it('should set hasMore state to true if the initial channel query returns count of messages equal to the default initial page size', async () => {
const { channel, chatClient } = await initClient();
useMockedApis(chatClient, [
queryChannelWithNewMessages(Array.from({ length: 25 }, generateMessage), channel),
]);
let hasMore;
await act(() => {
renderComponent({ channel, chatClient }, ({ hasMore: contextHasMore }) => {
hasMore = contextHasMore;
});
});

await waitFor(() => {
expect(hasMore).toBe(true);
});
});

it('should set hasMore state to false if the initial channel query returns less messages than the custom query channels options message limit', async () => {
const { channel, chatClient } = await initClient();
useMockedApis(chatClient, [queryChannelWithNewMessages([generateMessage()], channel)]);
let hasMore;
const channelQueryOptions = {
messages: { limit: 10 },
};
await act(() => {
renderComponent(
{ channel, channelQueryOptions, chatClient },
({ hasMore: contextHasMore }) => {
hasMore = contextHasMore;
},
);
});

await waitFor(() => {
expect(hasMore).toBe(false);
});
});

it('should set hasMore state to true if the initial channel query returns count of messages equal custom query channels options message limit', async () => {
const { channel, chatClient } = await initClient();
const equalCount = 10;
useMockedApis(chatClient, [
queryChannelWithNewMessages(Array.from({ length: equalCount }, generateMessage), channel),
]);
let hasMore;
const channelQueryOptions = {
messages: { limit: equalCount },
};
await act(() => {
renderComponent(
{ channel, channelQueryOptions, chatClient },
({ hasMore: contextHasMore }) => {
hasMore = contextHasMore;
},
);
});

await waitFor(() => {
expect(hasMore).toBe(true);
});
});

it('should not call watch the current channel on mount if channel is initialized', async () => {
Expand Down Expand Up @@ -372,7 +481,7 @@ describe('Channel', () => {

// first, wait for the effect in which the channel is watched,
// so we know the event listener is added to the document.
await waitFor(() => expect(watchSpy).toHaveBeenCalledWith());
await waitFor(() => expect(watchSpy).toHaveBeenCalledWith(undefined));
setTimeout(() => fireEvent(document, new Event('visibilitychange')), 0);

await waitFor(() => expect(markReadSpy).toHaveBeenCalledWith());
Expand Down Expand Up @@ -556,19 +665,6 @@ describe('Channel', () => {
});

describe('loading more messages', () => {
const queryChannelWithNewMessages = (newMessages, channel) =>
// generate new channel mock from existing channel with new messages added
getOrCreateChannelApi(
generateChannel({
channel: {
config: channel.getConfig(),
id: channel.id,
type: channel.type,
},
messages: newMessages,
}),
);

const limit = 10;
it('should be able to load more messages', async () => {
const { channel, chatClient } = await initClient();
Expand Down Expand Up @@ -616,7 +712,7 @@ describe('Channel', () => {
useMockedApis(chatClient, [queryChannelWithNewMessages(newMessages, channel)]);
loadMore(limit);
} else {
// If message has been added, set our checker variable so we can verify if hasMore is false.
// If message has been added, set our checker variable, so we can verify if hasMore is false.
channelHasMore = hasMore;
}
},
Expand Down Expand Up @@ -664,6 +760,151 @@ describe('Channel', () => {
});
await waitFor(() => expect(isLoadingMore).toBe(true));
});

it('should not load the second page, if the previous query has returned less then default limit messages', async () => {
const { channel, chatClient } = await initClient();
const firstPageOfMessages = [generateMessage()];
useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageOfMessages, channel)]);
let queryNextPageSpy;
let contextMessageCount;
await act(() => {
renderComponent({ channel, chatClient }, ({ loadMore, messages: contextMessages }) => {
queryNextPageSpy = jest.spyOn(channel, 'query');
contextMessageCount = contextMessages.length;
loadMore();
});
});

await waitFor(() => {
expect(queryNextPageSpy).not.toHaveBeenCalled();
expect(chatClient.axiosInstance.post).toHaveBeenCalledTimes(1);
expect(chatClient.axiosInstance.post.mock.calls[0][1]).toMatchObject(
expect.objectContaining({ data: {}, presence: false, state: true, watch: false }),
);
expect(contextMessageCount).toBe(firstPageOfMessages.length);
});
});
it('should load the second page, if the previous query has returned message count equal default messages limit', async () => {
const { channel, chatClient } = await initClient();
const firstPageMessages = Array.from({ length: 25 }, generateMessage);
const secondPageMessages = Array.from({ length: 15 }, generateMessage);
useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageMessages, channel)]);
let queryNextPageSpy;
let contextMessageCount;
await act(() => {
renderComponent({ channel, chatClient }, ({ loadMore, messages: contextMessages }) => {
queryNextPageSpy = jest.spyOn(channel, 'query');
contextMessageCount = contextMessages.length;
useMockedApis(chatClient, [queryChannelWithNewMessages(secondPageMessages, channel)]);
loadMore();
});
});

await waitFor(() => {
expect(queryNextPageSpy).toHaveBeenCalledTimes(1);
expect(chatClient.axiosInstance.post).toHaveBeenCalledTimes(2);
expect(chatClient.axiosInstance.post.mock.calls[0][1]).toMatchObject({
data: {},
presence: false,
state: true,
watch: false,
});
expect(chatClient.axiosInstance.post.mock.calls[1][1]).toMatchObject(
expect.objectContaining({
data: {},
messages: { id_lt: firstPageMessages[0].id, limit: 100 },
state: true,
watchers: { limit: 100 },
}),
);
expect(contextMessageCount).toBe(firstPageMessages.length + secondPageMessages.length);
});
});
it('should not load the second page, if the previous query has returned less then custom limit messages', async () => {
const { channel, chatClient } = await initClient();
const channelQueryOptions = {
messages: { limit: 10 },
};
const firstPageOfMessages = [generateMessage()];
useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageOfMessages, channel)]);
let queryNextPageSpy;
let contextMessageCount;
await act(() => {
renderComponent(
{ channel, channelQueryOptions, chatClient },
({ loadMore, messages: contextMessages }) => {
queryNextPageSpy = jest.spyOn(channel, 'query');
contextMessageCount = contextMessages.length;
loadMore(channelQueryOptions.messages.limit);
},
);
});

await waitFor(() => {
expect(queryNextPageSpy).not.toHaveBeenCalled();
expect(chatClient.axiosInstance.post).toHaveBeenCalledTimes(1);
expect(chatClient.axiosInstance.post.mock.calls[0][1]).toMatchObject({
data: {},
messages: {
limit: channelQueryOptions.messages.limit,
},
presence: false,
state: true,
watch: false,
});
expect(contextMessageCount).toBe(firstPageOfMessages.length);
});
});
it('should load the second page, if the previous query has returned message count equal custom messages limit', async () => {
const { channel, chatClient } = await initClient();
const equalCount = 10;
const channelQueryOptions = {
messages: { limit: equalCount },
};
const firstPageMessages = Array.from({ length: equalCount }, generateMessage);
const secondPageMessages = Array.from({ length: equalCount - 1 }, generateMessage);
useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageMessages, channel)]);
let queryNextPageSpy;
let contextMessageCount;

await act(() => {
renderComponent(
{ channel, channelQueryOptions, chatClient },
({ loadMore, messages: contextMessages }) => {
queryNextPageSpy = jest.spyOn(channel, 'query');
contextMessageCount = contextMessages.length;
useMockedApis(chatClient, [queryChannelWithNewMessages(secondPageMessages, channel)]);
loadMore(channelQueryOptions.messages.limit);
},
);
});

await waitFor(() => {
expect(queryNextPageSpy).toHaveBeenCalledTimes(1);
expect(chatClient.axiosInstance.post).toHaveBeenCalledTimes(2);
expect(chatClient.axiosInstance.post.mock.calls[0][1]).toMatchObject({
data: {},
messages: {
limit: channelQueryOptions.messages.limit,
},
presence: false,
state: true,
watch: false,
});
expect(chatClient.axiosInstance.post.mock.calls[1][1]).toMatchObject(
expect.objectContaining({
data: {},
messages: {
id_lt: firstPageMessages[0].id,
limit: channelQueryOptions.messages.limit,
},
state: true,
watchers: { limit: channelQueryOptions.messages.limit },
}),
);
expect(contextMessageCount).toBe(firstPageMessages.length + secondPageMessages.length);
});
});
});

describe('Sending/removing/updating messages', () => {
Expand Down
Loading

0 comments on commit a330233

Please sign in to comment.