Skip to content

Commit eb66888

Browse files
committed
feat: allow to customize the generation of latest message preview
1 parent cc78d20 commit eb66888

File tree

7 files changed

+123
-20
lines changed

7 files changed

+123
-20
lines changed

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

+18-12
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ re-setting the list state, you can customize behavior and UI.
110110
| `channel.truncated` | Updates the channel | [onChannelTruncated](#onchanneltruncated) |
111111
| `channel.updated` | Updates the channel | [onChannelUpdated](#onchannelupdated) |
112112
| `channel.visible` | Adds channel to list | [onChannelVisible](#onchannelvisible) |
113-
| `connection.recovered` | Forces a component render | N/A |
113+
| `connection.recovered` | Forces a component render | N/A |
114114
| `message.new` | Moves channel to top of list | [onMessageNewHandler](#onmessagenewhandler) |
115115
| `notification.added_to_channel` | Moves channel to top of list and starts watching | [onAddedToChannel](#onaddedtochannel) |
116116
| `notification.message_new` | Moves channel to top of list and starts watching | [onMessageNew](#onmessagenew) |
@@ -225,28 +225,26 @@ Custom function that handles the channel pagination.
225225
Takes parameters:
226226

227227
| Parameter | Description |
228-
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
228+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
229229
| `currentChannels` | The state of loaded `Channel` objects queried thus far. Has to be set with `setChannels` (see below). |
230230
| `queryType` | A string indicating, whether the channels state has to be reset to the first page ('reload') or newly queried channels should be appended to the `currentChannels`. |
231231
| `setChannels` | Function that allows us to set the channels state reflected in `currentChannels`. |
232232
| `setHasNextPage` | Flag indicating whether there are more items to be loaded from the API. Should be infered from the comparison of the query result length and the query options limit. |
233233

234234
The function has to:
235+
235236
1. build / provide own query filters, sort and options parameters
236237
2. query and append channels to the current channels state
237238
3. update the `hasNext` pagination flag after each query with `setChannels` function
238239

239240
An example below implements a custom query function that uses different filters sequentially once a preceding filter is exhausted:
240241

241242
```ts
242-
import uniqBy from "lodash.uniqby";
243+
import uniqBy from 'lodash.uniqby';
243244
import throttle from 'lodash.throttle';
244-
import {useCallback, useRef} from 'react';
245-
import {ChannelFilters, ChannelOptions, ChannelSort, StreamChat} from 'stream-chat';
246-
import {
247-
CustomQueryChannelParams,
248-
useChatContext,
249-
} from 'stream-chat-react';
245+
import { useCallback, useRef } from 'react';
246+
import { ChannelFilters, ChannelOptions, ChannelSort, StreamChat } from 'stream-chat';
247+
import { CustomQueryChannelParams, useChatContext } from 'stream-chat-react';
250248

251249
const DEFAULT_PAGE_SIZE = 30 as const;
252250

@@ -312,7 +310,7 @@ export const useCustomQueryChannels = () => {
312310
It is recommended to control for duplicate requests by throttling the custom function calls.
313311

314312
| Type |
315-
|---------------------------------------------------------------------------------------------------|
313+
| ------------------------------------------------------------------------------------------------- |
316314
| <GHComponentLink text='CustomQueryChannelsFn' path='/ChannelList/hooks/usePaginatedChannels.ts'/> |
317315

318316
### EmptyStateIndicator
@@ -332,6 +330,14 @@ for more information.
332330
| ------ |
333331
| object |
334332

333+
### getLatestMessagePreview
334+
335+
Custom function that generates the message preview in ChannelPreview component.
336+
337+
| Type |
338+
| ------------------------------------------------------------------------------------------------------------------------------------- |
339+
| `(channel: Channel, t: TranslationContextValue['t'], userLanguage: TranslationContextValue['userLanguage']) => string \| JSX.Element` |
340+
335341
### List
336342

337343
Custom UI component to display the container for the queried channels.
@@ -425,7 +431,7 @@ Function to override the default behavior when a message is received on a channe
425431
Function to override the default behavior when a message is received on a channel being watched. Handles `message.new` event.
426432

427433
| Type |
428-
|-------------------------------------------------------------------------------------------------------------------------------------|
434+
| ----------------------------------------------------------------------------------------------------------------------------------- |
429435
| `(setChannels: React.Dispatch<React.SetStateAction<Array<Channel<StreamChatGenerics>>>>, event: Event<StreamChatGenerics>) => void` |
430436

431437
### onRemovedFromChannel
@@ -491,7 +497,7 @@ const App = () => (
491497
```
492498

493499
| Type | Default |
494-
|--------|---------|
500+
| ------ | ------- |
495501
| number | 5000 |
496502

497503
### renderChannels

docusaurus/docs/React/components/utility-components/channel-preview-ui.mdx

+8
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ Title of channel to display.
9595
| -------- |
9696
| `string` |
9797

98+
### getLatestMessagePreview
99+
100+
Custom function that generates the message preview in ChannelPreview component.
101+
102+
| Type |
103+
| ------------------------------------------------------------------------------------------------------------------------------------- |
104+
| `(channel: Channel, t: TranslationContextValue['t'], userLanguage: TranslationContextValue['userLanguage']) => string \| JSX.Element` |
105+
98106
### lastMessage
99107

100108
The last message received in a channel.

docusaurus/docs/React/components/utility-components/channel-preview.mdx

+8
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ Custom class for the channel preview root
5555
| -------- |
5656
| `string` |
5757

58+
### getLatestMessagePreview
59+
60+
Custom function that generates the message preview in ChannelPreview component.
61+
62+
| Type |
63+
| ------------------------------------------------------------------------------------------------------------------------------------- |
64+
| `(channel: Channel, t: TranslationContextValue['t'], userLanguage: TranslationContextValue['userLanguage']) => string \| JSX.Element` |
65+
5866
### onSelect
5967

6068
Custom handler invoked when the `ChannelPreview` is clicked. The SDK uses `ChannelPreview` to display items of channel search results. There, behind the scenes, the new active channel is set.

src/components/ChannelList/ChannelList.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { ChannelListContextProvider } from '../../context';
3535
import { useChatContext } from '../../context/ChatContext';
3636

3737
import type { Channel, ChannelFilters, ChannelOptions, ChannelSort, Event } from 'stream-chat';
38-
38+
import type { TranslationContextValue } from '../../context/TranslationContext';
3939
import type { DefaultStreamChatGenerics, PaginatorProps } from '../../types/types';
4040

4141
const DEFAULT_FILTERS = {};
@@ -70,6 +70,12 @@ export type ChannelListProps<
7070
EmptyStateIndicator?: React.ComponentType<EmptyStateIndicatorProps>;
7171
/** An object containing channel query filters */
7272
filters?: ChannelFilters<StreamChatGenerics>;
73+
/** Custom function that generates the message preview in ChannelPreview component */
74+
getLatestMessagePreview?: (
75+
channel: Channel<StreamChatGenerics>,
76+
t: TranslationContextValue['t'],
77+
userLanguage: TranslationContextValue['userLanguage'],
78+
) => string | JSX.Element;
7379
/** Custom UI component to display the container for the queried channels, defaults to and accepts same props as: [ChannelListMessenger](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelList/ChannelListMessenger.tsx) */
7480
List?: React.ComponentType<ChannelListMessengerProps<StreamChatGenerics>>;
7581
/** Custom UI component to display the loading error indicator, defaults to and accepts same props as: [ChatDown](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChatDown/ChatDown.tsx) */
@@ -168,6 +174,7 @@ const UnMemoizedChannelList = <
168174
customQueryChannels,
169175
EmptyStateIndicator = DefaultEmptyStateIndicator,
170176
filters,
177+
getLatestMessagePreview,
171178
LoadingErrorIndicator = ChatDown,
172179
LoadingIndicator = LoadingChannels,
173180
List = ChannelListMessenger,
@@ -333,6 +340,7 @@ const UnMemoizedChannelList = <
333340
channel: item,
334341
// forces the update of preview component on channel update
335342
channelUpdateCount,
343+
getLatestMessagePreview,
336344
key: item.cid,
337345
Preview,
338346
setActiveChannel,

src/components/ChannelList/__tests__/ChannelList.test.js

+32-2
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ const channelsQueryStateMock = {
6060
* to those components might end up breaking tests for ChannelList, which will be quite painful
6161
* to debug then.
6262
*/
63-
const ChannelPreviewComponent = ({ channel, channelUpdateCount, latestMessage }) => (
63+
const ChannelPreviewComponent = ({ channel, channelUpdateCount, latestMessagePreview }) => (
6464
<div data-testid={channel.id} role='listitem'>
6565
<div data-testid='channelUpdateCount'>{channelUpdateCount}</div>
6666
<div>{channel.data.name}</div>
67-
<div>{latestMessage}</div>
67+
<div>{latestMessagePreview}</div>
6868
</div>
6969
);
7070

@@ -457,6 +457,36 @@ describe('ChannelList', () => {
457457
});
458458
});
459459

460+
it('allows to customize latest message preview generation', async () => {
461+
const previewText = 'custom preview text';
462+
const getLatestMessagePreview = () => previewText;
463+
464+
useMockedApis(chatClient, [queryChannelsApi([testChannel1])]);
465+
const { rerender } = render(
466+
<Chat client={chatClient}>
467+
<ChannelList filters={{}} options={{ limit: 2 }} />
468+
</Chat>,
469+
);
470+
471+
await waitFor(() => {
472+
expect(screen.getByText('Nothing yet...')).toBeInTheDocument();
473+
});
474+
475+
rerender(
476+
<Chat client={chatClient}>
477+
<ChannelList
478+
filters={{}}
479+
getLatestMessagePreview={getLatestMessagePreview}
480+
options={{ limit: 2 }}
481+
/>
482+
</Chat>,
483+
);
484+
485+
await waitFor(() => {
486+
expect(screen.getByText(previewText)).toBeInTheDocument();
487+
});
488+
});
489+
460490
describe('Default and custom active channel', () => {
461491
let setActiveChannel;
462492
const watchersConfig = { limit: 20, offset: 0 };

src/components/ChannelPreview/ChannelPreview.tsx

+14-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import React, { useEffect, useMemo, useState } from 'react';
44
import { ChannelPreviewMessenger } from './ChannelPreviewMessenger';
55
import { useIsChannelMuted } from './hooks/useIsChannelMuted';
66
import { useChannelPreviewInfo } from './hooks/useChannelPreviewInfo';
7-
import { getLatestMessagePreview } from './utils';
7+
import { getLatestMessagePreview as defaultGetLatestMessagePreview } from './utils';
88

99
import { ChatContextValue, useChatContext } from '../../context/ChatContext';
1010
import { useTranslationContext } from '../../context/TranslationContext';
@@ -15,7 +15,7 @@ import type { Channel, Event } from 'stream-chat';
1515
import type { AvatarProps } from '../Avatar/Avatar';
1616

1717
import type { StreamMessage } from '../../context/ChannelStateContext';
18-
18+
import type { TranslationContextValue } from '../../context/TranslationContext';
1919
import type { DefaultStreamChatGenerics } from '../../types/types';
2020

2121
export type ChannelPreviewUIComponentProps<
@@ -52,6 +52,12 @@ export type ChannelPreviewProps<
5252
channelUpdateCount?: number;
5353
/** Custom class for the channel preview root */
5454
className?: string;
55+
/** Custom function that generates the message preview in ChannelPreview component */
56+
getLatestMessagePreview?: (
57+
channel: Channel<StreamChatGenerics>,
58+
t: TranslationContextValue['t'],
59+
userLanguage: TranslationContextValue['userLanguage'],
60+
) => string | JSX.Element;
5561
key?: string;
5662
/** Custom ChannelPreview click handler function */
5763
onSelect?: (event: React.MouseEvent) => void;
@@ -68,7 +74,12 @@ export const ChannelPreview = <
6874
>(
6975
props: ChannelPreviewProps<StreamChatGenerics>,
7076
) => {
71-
const { channel, Preview = ChannelPreviewMessenger, channelUpdateCount } = props;
77+
const {
78+
channel,
79+
Preview = ChannelPreviewMessenger,
80+
channelUpdateCount,
81+
getLatestMessagePreview = defaultGetLatestMessagePreview,
82+
} = props;
7283
const { channel: activeChannel, client, setActiveChannel } = useChatContext<StreamChatGenerics>(
7384
'ChannelPreview',
7485
);

src/components/ChannelPreview/__tests__/ChannelPreview.test.js

+34-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ const PreviewUIComponent = (props) => (
3636
</div>
3737
</>
3838
);
39+
const PreviewUIComponentWithLatestMessagePreview = (props) => (
40+
<>
41+
<div data-testid='channel-id'>{props.channel.id}</div>
42+
<div data-testid='unread-count'>{props.unread}</div>
43+
<div data-testid='last-event-message'>
44+
{props.lastMessage ? props.latestMessagePreview : EMPTY_CHANNEL_PREVIEW_TEXT}
45+
</div>
46+
</>
47+
);
3948

4049
const expectUnreadCountToBe = async (getByTestId, expectedValue) => {
4150
await waitFor(() => {
@@ -72,8 +81,14 @@ describe('ChannelPreview', () => {
7281
client = await getTestClientWithUser(user);
7382
useMockedApis(client, [
7483
queryChannelsApi([
75-
generateChannel({ messages: Array.from({ length: 5 }, generateMessage) }),
76-
generateChannel({ messages: Array.from({ length: 5 }, generateMessage) }),
84+
generateChannel({
85+
channel: { name: 'c0' },
86+
messages: Array.from({ length: 5 }, generateMessage),
87+
}),
88+
generateChannel({
89+
channel: { name: 'c1' },
90+
messages: Array.from({ length: 5 }, generateMessage),
91+
}),
7792
]),
7893
]);
7994

@@ -138,6 +153,22 @@ describe('ChannelPreview', () => {
138153
await expectUnreadCountToBe(getByTestId, newUnreadCount);
139154
});
140155

156+
it('allows to customize latest message preview generation', async () => {
157+
const getLatestMessagePreview = (channel) => channel.data.name;
158+
159+
const { getByTestId } = renderComponent(
160+
{
161+
activeChannel: c0,
162+
channel: c0,
163+
getLatestMessagePreview,
164+
Preview: PreviewUIComponentWithLatestMessagePreview,
165+
},
166+
render,
167+
);
168+
169+
await expectLastEventMessageToBe(getByTestId, c0.data.name);
170+
});
171+
141172
const eventCases = [
142173
['message.new', dispatchMessageNewEvent],
143174
['message.updated', dispatchMessageUpdatedEvent],
@@ -349,6 +380,7 @@ describe('ChannelPreview', () => {
349380
expectUnreadCountToBe(screen.getByTestId, unreadCount);
350381
});
351382
});
383+
352384
describe('notification.mark_unread', () => {
353385
it('should be ignored if not originated from the current user', () => {
354386
const unreadCount = 0;

0 commit comments

Comments
 (0)