Skip to content

Commit ba6b1a3

Browse files
authored
fix: get correct lastMessage and latestMessagePreview from channel state (#2535)
1 parent c852fe4 commit ba6b1a3

File tree

12 files changed

+241
-46
lines changed

12 files changed

+241
-46
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

+19-3
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ For even deeper customization of the channel list and channel previews, use the
1818
To customize the `ChannelList` item UI simply pass your custom `Preview` component to the `ChannelList`. See [The Preview Prop Component](../../guides/customization/channel-list-preview.mdx#the-preview-prop-component) for the extended guide.
1919

2020
```tsx
21-
const CustomChannelPreviewUI = ({ latestMessage, lastMessage }) => {
21+
const CustomChannelPreviewUI = ({ latestMessagePreview, lastMessage }) => {
2222
// "lastMessage" property is for the last
2323
// message that has been interacted with (pinned/edited/deleted)
2424

25-
// to display last message of the channel use "latestMessage" property
26-
return <span>{latestMessage}</span>;
25+
// to display last message of the channel use "latestMessagePreview" property
26+
return <span>{latestMessagePreview}</span>;
2727
};
2828

2929
<ChannelList Preview={CustomChannelPreviewUI} />;
@@ -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.
@@ -105,6 +113,14 @@ The last message received in a channel.
105113

106114
### latestMessage
107115

116+
Deprecated, use `latestMessagePreview` instead.
117+
118+
| Type |
119+
| ----------------------- |
120+
| `string \| JSX.Element` |
121+
122+
### latestMessagePreview
123+
108124
Latest message preview to display. Will be either a string or a JSX.Element rendering markdown.
109125

110126
| Type |

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.

docusaurus/docs/React/guides/customization/channel-list-preview.mdx

+6-6
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ Let's implement a simple custom preview:
5555
<TabItem value="js" label="React">
5656

5757
```jsx
58-
const CustomChannelPreview = ({ displayImage, displayTitle, latestMessage }) => (
58+
const CustomChannelPreview = ({ displayImage, displayTitle, latestMessagePreview }) => (
5959
<div className='channel-preview'>
6060
<img className='channel-preview__avatar' src={displayImage} alt='' />
6161
<div className='channel-preview__main'>
6262
<div className='channel-preview__header'>{displayTitle}</div>
63-
<div className='channel-preview__message'>{latestMessage}</div>
63+
<div className='channel-preview__message'>{latestMessagePreview}</div>
6464
</div>
6565
</div>
6666
);
@@ -122,7 +122,7 @@ message in the channel:
122122

123123
```jsx
124124
const CustomChannelPreview = (props) => {
125-
const { channel, displayImage, displayTitle, latestMessage } = props;
125+
const { channel, displayImage, displayTitle, latestMessagePreview } = props;
126126
const { userLanguage } = useTranslationContext();
127127
const latestMessageAt = channel.state.last_message_at;
128128

@@ -146,7 +146,7 @@ const CustomChannelPreview = (props) => {
146146
{timestamp}
147147
</time>
148148
</div>
149-
<div className='channel-preview__message'>{latestMessage}</div>
149+
<div className='channel-preview__message'>{latestMessagePreview}</div>
150150
</div>
151151
</div>
152152
);
@@ -217,7 +217,7 @@ const CustomChannelPreview = (props) => {
217217
activeChannel,
218218
displayImage,
219219
displayTitle,
220-
latestMessage,
220+
latestMessagePreview,
221221
setActiveChannel,
222222
} = props;
223223
const latestMessageAt = channel.state.last_message_at;
@@ -252,7 +252,7 @@ const CustomChannelPreview = (props) => {
252252
{timestamp}
253253
</time>
254254
</div>
255-
<div className='channel-preview__message'>{latestMessage}</div>
255+
<div className='channel-preview__message'>{latestMessagePreview}</div>
256256
</div>
257257
</button>
258258
);

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

+27-10
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<
@@ -29,8 +29,10 @@ export type ChannelPreviewUIComponentProps<
2929
displayTitle?: string;
3030
/** The last message received in a channel */
3131
lastMessage?: StreamMessage<StreamChatGenerics>;
32-
/** Latest message preview to display, will be a string or JSX element supporting markdown. */
32+
/** @deprecated Use latestMessagePreview prop instead. */
3333
latestMessage?: string | JSX.Element;
34+
/** Latest message preview to display, will be a string or JSX element supporting markdown. */
35+
latestMessagePreview?: string | JSX.Element;
3436
/** Status describing whether own message has been delivered or read by another. If the last message is not an own message, then the status is undefined. */
3537
messageDeliveryStatus?: MessageDeliveryStatus;
3638
/** Number of unread Messages */
@@ -50,6 +52,12 @@ export type ChannelPreviewProps<
5052
channelUpdateCount?: number;
5153
/** Custom class for the channel preview root */
5254
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;
5361
key?: string;
5462
/** Custom ChannelPreview click handler function */
5563
onSelect?: (event: React.MouseEvent) => void;
@@ -66,7 +74,12 @@ export const ChannelPreview = <
6674
>(
6775
props: ChannelPreviewProps<StreamChatGenerics>,
6876
) => {
69-
const { channel, Preview = ChannelPreviewMessenger, channelUpdateCount } = props;
77+
const {
78+
channel,
79+
Preview = ChannelPreviewMessenger,
80+
channelUpdateCount,
81+
getLatestMessagePreview = defaultGetLatestMessagePreview,
82+
} = props;
7083
const { channel: activeChannel, client, setActiveChannel } = useChatContext<StreamChatGenerics>(
7184
'ChannelPreview',
7285
);
@@ -123,26 +136,29 @@ export const ChannelPreview = <
123136
useEffect(() => {
124137
refreshUnreadCount();
125138

126-
const handleEvent = (event: Event<StreamChatGenerics>) => {
127-
if (event.message) setLastMessage(event.message);
139+
const handleEvent = () => {
140+
setLastMessage(channel.state.latestMessages[channel.state.latestMessages.length - 1]);
128141
refreshUnreadCount();
129142
};
130143

131144
channel.on('message.new', handleEvent);
132145
channel.on('message.updated', handleEvent);
133146
channel.on('message.deleted', handleEvent);
147+
channel.on('message.undeleted', handleEvent);
148+
channel.on('channel.truncated', handleEvent);
134149

135150
return () => {
136151
channel.off('message.new', handleEvent);
137152
channel.off('message.updated', handleEvent);
138153
channel.off('message.deleted', handleEvent);
154+
channel.off('message.undeleted', handleEvent);
155+
channel.off('channel.truncated', handleEvent);
139156
};
140-
// eslint-disable-next-line react-hooks/exhaustive-deps
141-
}, [refreshUnreadCount, channelUpdateCount]);
157+
}, [channel, refreshUnreadCount, channelUpdateCount]);
142158

143159
if (!Preview) return null;
144160

145-
const latestMessage = getLatestMessagePreview(channel, t, userLanguage);
161+
const latestMessagePreview = getLatestMessagePreview(channel, t, userLanguage);
146162

147163
return (
148164
<Preview
@@ -151,7 +167,8 @@ export const ChannelPreview = <
151167
displayImage={displayImage}
152168
displayTitle={displayTitle}
153169
lastMessage={lastMessage}
154-
latestMessage={latestMessage}
170+
latestMessage={latestMessagePreview}
171+
latestMessagePreview={latestMessagePreview}
155172
messageDeliveryStatus={messageDeliveryStatus}
156173
setActiveChannel={setActiveChannel}
157174
unread={unread}

0 commit comments

Comments
 (0)