Skip to content

Commit a330233

Browse files
Merge branch 'master' into rc
2 parents 70b28c9 + 3f7acda commit a330233

File tree

11 files changed

+431
-106
lines changed

11 files changed

+431
-106
lines changed

CHANGELOG.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,25 @@
8888

8989
* **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
9090

91+
## [10.20.1](https://github.com/GetStream/stream-chat-react/compare/v10.20.0...v10.20.1) (2023-11-20)
92+
93+
94+
### Bug Fixes
95+
96+
* 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))
97+
98+
## [10.20.0](https://github.com/GetStream/stream-chat-react/compare/v10.19.0...v10.20.0) (2023-11-16)
99+
100+
101+
### Bug Fixes
102+
103+
* 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))
104+
105+
106+
### Features
107+
108+
* 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))
109+
91110
## [10.19.0](https://github.com/GetStream/stream-chat-react/compare/v10.18.0...v10.19.0) (2023-11-14)
92111

93112

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

27212740
- Support for @mentions for @mention interactions `Channel` now accepts the following props
27222741
- `onMentionsHover={(event, user) => console.log(event, user)}`
2723-
- `onMentionsClick={(event, user) => console.log(event, user)}`
2742+
- `onMentionsClick={(event, user) => console.log(event, user)}`

docusaurus/docs/React/components/core-components/channel.mdx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,39 @@ Custom UI component to display a user's avatar.
176176
| --------- | ---------------------------------------------------------- |
177177
| component | <GHComponentLink text='Avatar' path='/Avatar/Avatar.tsx'/> |
178178

179+
### channelQueryOptions
180+
181+
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.
182+
183+
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`.
184+
185+
```tsx
186+
import {ChannelQueryOptions} from "stream-chat";
187+
import {Channel, useChatContext} from "stream-chat-react";
188+
189+
const channelQueryOptions: ChannelQueryOptions = {
190+
messages: { limit: 20 },
191+
};
192+
193+
type ChannelRendererProps = {
194+
id: string;
195+
type: string;
196+
};
197+
198+
const ChannelRenderer = ({id, type}: ChannelRendererProps) => {
199+
const { client } = useChatContext();
200+
return (
201+
<Channel channel={client.channel(type, id)} channelQueryOptions={channelQueryOptions}>
202+
{/* Channel children */}
203+
</Channel>
204+
);
205+
}
206+
```
207+
208+
| Type |
209+
|-----------------------|
210+
| `ChannelQueryOptions` |
211+
179212
### CooldownTimer
180213

181214
Custom UI component to display the slow mode cooldown timer.

src/components/Channel/Channel.tsx

Lines changed: 103 additions & 84 deletions
Large diffs are not rendered by default.

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

Lines changed: 257 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ jest.mock('../../Loading', () => ({
3232
LoadingIndicator: jest.fn(() => <div>loading</div>),
3333
}));
3434

35+
const queryChannelWithNewMessages = (newMessages, channel) =>
36+
// generate new channel mock from existing channel with new messages added
37+
getOrCreateChannelApi(
38+
generateChannel({
39+
channel: {
40+
config: channel.getConfig(),
41+
id: channel.id,
42+
type: channel.type,
43+
},
44+
messages: newMessages,
45+
}),
46+
);
47+
3548
const MockAvatar = ({ name }) => (
3649
<div className='avatar' data-testid='custom-avatar'>
3750
{name}
@@ -269,7 +282,103 @@ describe('Channel', () => {
269282
renderComponent({ channel, chatClient });
270283
});
271284

272-
await waitFor(() => expect(watchSpy).toHaveBeenCalledTimes(1));
285+
await waitFor(() => {
286+
expect(watchSpy).toHaveBeenCalledTimes(1);
287+
expect(watchSpy).toHaveBeenCalledWith(undefined);
288+
});
289+
});
290+
291+
it('should apply channelQueryOptions to channel watch call', async () => {
292+
const { channel, chatClient } = await initClient();
293+
const watchSpy = jest.spyOn(channel, 'watch');
294+
const channelQueryOptions = {
295+
messages: { limit: 20 },
296+
};
297+
await act(() => {
298+
renderComponent({ channel, channelQueryOptions, chatClient });
299+
});
300+
301+
await waitFor(() => {
302+
expect(watchSpy).toHaveBeenCalledTimes(1);
303+
expect(watchSpy).toHaveBeenCalledWith(channelQueryOptions);
304+
});
305+
});
306+
307+
it('should set hasMore state to false if the initial channel query returns less messages than the default initial page size', async () => {
308+
const { channel, chatClient } = await initClient();
309+
useMockedApis(chatClient, [queryChannelWithNewMessages([generateMessage()], channel)]);
310+
let hasMore;
311+
await act(() => {
312+
renderComponent({ channel, chatClient }, ({ hasMore: contextHasMore }) => {
313+
hasMore = contextHasMore;
314+
});
315+
});
316+
317+
await waitFor(() => {
318+
expect(hasMore).toBe(false);
319+
});
320+
});
321+
322+
it('should set hasMore state to true if the initial channel query returns count of messages equal to the default initial page size', async () => {
323+
const { channel, chatClient } = await initClient();
324+
useMockedApis(chatClient, [
325+
queryChannelWithNewMessages(Array.from({ length: 25 }, generateMessage), channel),
326+
]);
327+
let hasMore;
328+
await act(() => {
329+
renderComponent({ channel, chatClient }, ({ hasMore: contextHasMore }) => {
330+
hasMore = contextHasMore;
331+
});
332+
});
333+
334+
await waitFor(() => {
335+
expect(hasMore).toBe(true);
336+
});
337+
});
338+
339+
it('should set hasMore state to false if the initial channel query returns less messages than the custom query channels options message limit', async () => {
340+
const { channel, chatClient } = await initClient();
341+
useMockedApis(chatClient, [queryChannelWithNewMessages([generateMessage()], channel)]);
342+
let hasMore;
343+
const channelQueryOptions = {
344+
messages: { limit: 10 },
345+
};
346+
await act(() => {
347+
renderComponent(
348+
{ channel, channelQueryOptions, chatClient },
349+
({ hasMore: contextHasMore }) => {
350+
hasMore = contextHasMore;
351+
},
352+
);
353+
});
354+
355+
await waitFor(() => {
356+
expect(hasMore).toBe(false);
357+
});
358+
});
359+
360+
it('should set hasMore state to true if the initial channel query returns count of messages equal custom query channels options message limit', async () => {
361+
const { channel, chatClient } = await initClient();
362+
const equalCount = 10;
363+
useMockedApis(chatClient, [
364+
queryChannelWithNewMessages(Array.from({ length: equalCount }, generateMessage), channel),
365+
]);
366+
let hasMore;
367+
const channelQueryOptions = {
368+
messages: { limit: equalCount },
369+
};
370+
await act(() => {
371+
renderComponent(
372+
{ channel, channelQueryOptions, chatClient },
373+
({ hasMore: contextHasMore }) => {
374+
hasMore = contextHasMore;
375+
},
376+
);
377+
});
378+
379+
await waitFor(() => {
380+
expect(hasMore).toBe(true);
381+
});
273382
});
274383

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

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

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

558667
describe('loading more messages', () => {
559-
const queryChannelWithNewMessages = (newMessages, channel) =>
560-
// generate new channel mock from existing channel with new messages added
561-
getOrCreateChannelApi(
562-
generateChannel({
563-
channel: {
564-
config: channel.getConfig(),
565-
id: channel.id,
566-
type: channel.type,
567-
},
568-
messages: newMessages,
569-
}),
570-
);
571-
572668
const limit = 10;
573669
it('should be able to load more messages', async () => {
574670
const { channel, chatClient } = await initClient();
@@ -616,7 +712,7 @@ describe('Channel', () => {
616712
useMockedApis(chatClient, [queryChannelWithNewMessages(newMessages, channel)]);
617713
loadMore(limit);
618714
} else {
619-
// If message has been added, set our checker variable so we can verify if hasMore is false.
715+
// If message has been added, set our checker variable, so we can verify if hasMore is false.
620716
channelHasMore = hasMore;
621717
}
622718
},
@@ -664,6 +760,151 @@ describe('Channel', () => {
664760
});
665761
await waitFor(() => expect(isLoadingMore).toBe(true));
666762
});
763+
764+
it('should not load the second page, if the previous query has returned less then default limit messages', async () => {
765+
const { channel, chatClient } = await initClient();
766+
const firstPageOfMessages = [generateMessage()];
767+
useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageOfMessages, channel)]);
768+
let queryNextPageSpy;
769+
let contextMessageCount;
770+
await act(() => {
771+
renderComponent({ channel, chatClient }, ({ loadMore, messages: contextMessages }) => {
772+
queryNextPageSpy = jest.spyOn(channel, 'query');
773+
contextMessageCount = contextMessages.length;
774+
loadMore();
775+
});
776+
});
777+
778+
await waitFor(() => {
779+
expect(queryNextPageSpy).not.toHaveBeenCalled();
780+
expect(chatClient.axiosInstance.post).toHaveBeenCalledTimes(1);
781+
expect(chatClient.axiosInstance.post.mock.calls[0][1]).toMatchObject(
782+
expect.objectContaining({ data: {}, presence: false, state: true, watch: false }),
783+
);
784+
expect(contextMessageCount).toBe(firstPageOfMessages.length);
785+
});
786+
});
787+
it('should load the second page, if the previous query has returned message count equal default messages limit', async () => {
788+
const { channel, chatClient } = await initClient();
789+
const firstPageMessages = Array.from({ length: 25 }, generateMessage);
790+
const secondPageMessages = Array.from({ length: 15 }, generateMessage);
791+
useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageMessages, channel)]);
792+
let queryNextPageSpy;
793+
let contextMessageCount;
794+
await act(() => {
795+
renderComponent({ channel, chatClient }, ({ loadMore, messages: contextMessages }) => {
796+
queryNextPageSpy = jest.spyOn(channel, 'query');
797+
contextMessageCount = contextMessages.length;
798+
useMockedApis(chatClient, [queryChannelWithNewMessages(secondPageMessages, channel)]);
799+
loadMore();
800+
});
801+
});
802+
803+
await waitFor(() => {
804+
expect(queryNextPageSpy).toHaveBeenCalledTimes(1);
805+
expect(chatClient.axiosInstance.post).toHaveBeenCalledTimes(2);
806+
expect(chatClient.axiosInstance.post.mock.calls[0][1]).toMatchObject({
807+
data: {},
808+
presence: false,
809+
state: true,
810+
watch: false,
811+
});
812+
expect(chatClient.axiosInstance.post.mock.calls[1][1]).toMatchObject(
813+
expect.objectContaining({
814+
data: {},
815+
messages: { id_lt: firstPageMessages[0].id, limit: 100 },
816+
state: true,
817+
watchers: { limit: 100 },
818+
}),
819+
);
820+
expect(contextMessageCount).toBe(firstPageMessages.length + secondPageMessages.length);
821+
});
822+
});
823+
it('should not load the second page, if the previous query has returned less then custom limit messages', async () => {
824+
const { channel, chatClient } = await initClient();
825+
const channelQueryOptions = {
826+
messages: { limit: 10 },
827+
};
828+
const firstPageOfMessages = [generateMessage()];
829+
useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageOfMessages, channel)]);
830+
let queryNextPageSpy;
831+
let contextMessageCount;
832+
await act(() => {
833+
renderComponent(
834+
{ channel, channelQueryOptions, chatClient },
835+
({ loadMore, messages: contextMessages }) => {
836+
queryNextPageSpy = jest.spyOn(channel, 'query');
837+
contextMessageCount = contextMessages.length;
838+
loadMore(channelQueryOptions.messages.limit);
839+
},
840+
);
841+
});
842+
843+
await waitFor(() => {
844+
expect(queryNextPageSpy).not.toHaveBeenCalled();
845+
expect(chatClient.axiosInstance.post).toHaveBeenCalledTimes(1);
846+
expect(chatClient.axiosInstance.post.mock.calls[0][1]).toMatchObject({
847+
data: {},
848+
messages: {
849+
limit: channelQueryOptions.messages.limit,
850+
},
851+
presence: false,
852+
state: true,
853+
watch: false,
854+
});
855+
expect(contextMessageCount).toBe(firstPageOfMessages.length);
856+
});
857+
});
858+
it('should load the second page, if the previous query has returned message count equal custom messages limit', async () => {
859+
const { channel, chatClient } = await initClient();
860+
const equalCount = 10;
861+
const channelQueryOptions = {
862+
messages: { limit: equalCount },
863+
};
864+
const firstPageMessages = Array.from({ length: equalCount }, generateMessage);
865+
const secondPageMessages = Array.from({ length: equalCount - 1 }, generateMessage);
866+
useMockedApis(chatClient, [queryChannelWithNewMessages(firstPageMessages, channel)]);
867+
let queryNextPageSpy;
868+
let contextMessageCount;
869+
870+
await act(() => {
871+
renderComponent(
872+
{ channel, channelQueryOptions, chatClient },
873+
({ loadMore, messages: contextMessages }) => {
874+
queryNextPageSpy = jest.spyOn(channel, 'query');
875+
contextMessageCount = contextMessages.length;
876+
useMockedApis(chatClient, [queryChannelWithNewMessages(secondPageMessages, channel)]);
877+
loadMore(channelQueryOptions.messages.limit);
878+
},
879+
);
880+
});
881+
882+
await waitFor(() => {
883+
expect(queryNextPageSpy).toHaveBeenCalledTimes(1);
884+
expect(chatClient.axiosInstance.post).toHaveBeenCalledTimes(2);
885+
expect(chatClient.axiosInstance.post.mock.calls[0][1]).toMatchObject({
886+
data: {},
887+
messages: {
888+
limit: channelQueryOptions.messages.limit,
889+
},
890+
presence: false,
891+
state: true,
892+
watch: false,
893+
});
894+
expect(chatClient.axiosInstance.post.mock.calls[1][1]).toMatchObject(
895+
expect.objectContaining({
896+
data: {},
897+
messages: {
898+
id_lt: firstPageMessages[0].id,
899+
limit: channelQueryOptions.messages.limit,
900+
},
901+
state: true,
902+
watchers: { limit: channelQueryOptions.messages.limit },
903+
}),
904+
);
905+
expect(contextMessageCount).toBe(firstPageMessages.length + secondPageMessages.length);
906+
});
907+
});
667908
});
668909

669910
describe('Sending/removing/updating messages', () => {

0 commit comments

Comments
 (0)