Skip to content

Commit

Permalink
feat: allow to customize the generation of latest message preview
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinCupela committed Oct 11, 2024
1 parent cc78d20 commit eb66888
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 20 deletions.
30 changes: 18 additions & 12 deletions docusaurus/docs/React/components/core-components/channel-list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ re-setting the list state, you can customize behavior and UI.
| `channel.truncated` | Updates the channel | [onChannelTruncated](#onchanneltruncated) |
| `channel.updated` | Updates the channel | [onChannelUpdated](#onchannelupdated) |
| `channel.visible` | Adds channel to list | [onChannelVisible](#onchannelvisible) |
| `connection.recovered` | Forces a component render | N/A |
| `connection.recovered` | Forces a component render | N/A |
| `message.new` | Moves channel to top of list | [onMessageNewHandler](#onmessagenewhandler) |
| `notification.added_to_channel` | Moves channel to top of list and starts watching | [onAddedToChannel](#onaddedtochannel) |
| `notification.message_new` | Moves channel to top of list and starts watching | [onMessageNew](#onmessagenew) |
Expand Down Expand Up @@ -225,28 +225,26 @@ Custom function that handles the channel pagination.
Takes parameters:

| Parameter | Description |
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `currentChannels` | The state of loaded `Channel` objects queried thus far. Has to be set with `setChannels` (see below). |
| `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`. |
| `setChannels` | Function that allows us to set the channels state reflected in `currentChannels`. |
| `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. |

The function has to:

1. build / provide own query filters, sort and options parameters
2. query and append channels to the current channels state
3. update the `hasNext` pagination flag after each query with `setChannels` function

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

```ts
import uniqBy from "lodash.uniqby";
import uniqBy from 'lodash.uniqby';
import throttle from 'lodash.throttle';
import {useCallback, useRef} from 'react';
import {ChannelFilters, ChannelOptions, ChannelSort, StreamChat} from 'stream-chat';
import {
CustomQueryChannelParams,
useChatContext,
} from 'stream-chat-react';
import { useCallback, useRef } from 'react';
import { ChannelFilters, ChannelOptions, ChannelSort, StreamChat } from 'stream-chat';
import { CustomQueryChannelParams, useChatContext } from 'stream-chat-react';

const DEFAULT_PAGE_SIZE = 30 as const;

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

| Type |
|---------------------------------------------------------------------------------------------------|
| ------------------------------------------------------------------------------------------------- |
| <GHComponentLink text='CustomQueryChannelsFn' path='/ChannelList/hooks/usePaginatedChannels.ts'/> |

### EmptyStateIndicator
Expand All @@ -332,6 +330,14 @@ for more information.
| ------ |
| object |

### getLatestMessagePreview

Custom function that generates the message preview in ChannelPreview component.

| Type |
| ------------------------------------------------------------------------------------------------------------------------------------- |
| `(channel: Channel, t: TranslationContextValue['t'], userLanguage: TranslationContextValue['userLanguage']) => string \| JSX.Element` |

### List

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

| Type |
|-------------------------------------------------------------------------------------------------------------------------------------|
| ----------------------------------------------------------------------------------------------------------------------------------- |
| `(setChannels: React.Dispatch<React.SetStateAction<Array<Channel<StreamChatGenerics>>>>, event: Event<StreamChatGenerics>) => void` |

### onRemovedFromChannel
Expand Down Expand Up @@ -491,7 +497,7 @@ const App = () => (
```

| Type | Default |
|--------|---------|
| ------ | ------- |
| number | 5000 |

### renderChannels
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ Title of channel to display.
| -------- |
| `string` |

### getLatestMessagePreview

Custom function that generates the message preview in ChannelPreview component.

| Type |
| ------------------------------------------------------------------------------------------------------------------------------------- |
| `(channel: Channel, t: TranslationContextValue['t'], userLanguage: TranslationContextValue['userLanguage']) => string \| JSX.Element` |

### lastMessage

The last message received in a channel.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ Custom class for the channel preview root
| -------- |
| `string` |

### getLatestMessagePreview

Custom function that generates the message preview in ChannelPreview component.

| Type |
| ------------------------------------------------------------------------------------------------------------------------------------- |
| `(channel: Channel, t: TranslationContextValue['t'], userLanguage: TranslationContextValue['userLanguage']) => string \| JSX.Element` |

### onSelect

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.
Expand Down
10 changes: 9 additions & 1 deletion src/components/ChannelList/ChannelList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { ChannelListContextProvider } from '../../context';
import { useChatContext } from '../../context/ChatContext';

import type { Channel, ChannelFilters, ChannelOptions, ChannelSort, Event } from 'stream-chat';

import type { TranslationContextValue } from '../../context/TranslationContext';
import type { DefaultStreamChatGenerics, PaginatorProps } from '../../types/types';

const DEFAULT_FILTERS = {};
Expand Down Expand Up @@ -70,6 +70,12 @@ export type ChannelListProps<
EmptyStateIndicator?: React.ComponentType<EmptyStateIndicatorProps>;
/** An object containing channel query filters */
filters?: ChannelFilters<StreamChatGenerics>;
/** Custom function that generates the message preview in ChannelPreview component */
getLatestMessagePreview?: (
channel: Channel<StreamChatGenerics>,
t: TranslationContextValue['t'],
userLanguage: TranslationContextValue['userLanguage'],
) => string | JSX.Element;
/** 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) */
List?: React.ComponentType<ChannelListMessengerProps<StreamChatGenerics>>;
/** 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) */
Expand Down Expand Up @@ -168,6 +174,7 @@ const UnMemoizedChannelList = <
customQueryChannels,
EmptyStateIndicator = DefaultEmptyStateIndicator,
filters,
getLatestMessagePreview,
LoadingErrorIndicator = ChatDown,
LoadingIndicator = LoadingChannels,
List = ChannelListMessenger,
Expand Down Expand Up @@ -333,6 +340,7 @@ const UnMemoizedChannelList = <
channel: item,
// forces the update of preview component on channel update
channelUpdateCount,
getLatestMessagePreview,
key: item.cid,
Preview,
setActiveChannel,
Expand Down
34 changes: 32 additions & 2 deletions src/components/ChannelList/__tests__/ChannelList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ const channelsQueryStateMock = {
* to those components might end up breaking tests for ChannelList, which will be quite painful
* to debug then.
*/
const ChannelPreviewComponent = ({ channel, channelUpdateCount, latestMessage }) => (
const ChannelPreviewComponent = ({ channel, channelUpdateCount, latestMessagePreview }) => (
<div data-testid={channel.id} role='listitem'>
<div data-testid='channelUpdateCount'>{channelUpdateCount}</div>
<div>{channel.data.name}</div>
<div>{latestMessage}</div>
<div>{latestMessagePreview}</div>
</div>
);

Expand Down Expand Up @@ -457,6 +457,36 @@ describe('ChannelList', () => {
});
});

it('allows to customize latest message preview generation', async () => {
const previewText = 'custom preview text';
const getLatestMessagePreview = () => previewText;

useMockedApis(chatClient, [queryChannelsApi([testChannel1])]);
const { rerender } = render(
<Chat client={chatClient}>
<ChannelList filters={{}} options={{ limit: 2 }} />
</Chat>,
);

await waitFor(() => {
expect(screen.getByText('Nothing yet...')).toBeInTheDocument();
});

rerender(
<Chat client={chatClient}>
<ChannelList
filters={{}}
getLatestMessagePreview={getLatestMessagePreview}
options={{ limit: 2 }}
/>
</Chat>,
);

await waitFor(() => {
expect(screen.getByText(previewText)).toBeInTheDocument();
});
});

describe('Default and custom active channel', () => {
let setActiveChannel;
const watchersConfig = { limit: 20, offset: 0 };
Expand Down
17 changes: 14 additions & 3 deletions src/components/ChannelPreview/ChannelPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { ChannelPreviewMessenger } from './ChannelPreviewMessenger';
import { useIsChannelMuted } from './hooks/useIsChannelMuted';
import { useChannelPreviewInfo } from './hooks/useChannelPreviewInfo';
import { getLatestMessagePreview } from './utils';
import { getLatestMessagePreview as defaultGetLatestMessagePreview } from './utils';

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

import type { StreamMessage } from '../../context/ChannelStateContext';

import type { TranslationContextValue } from '../../context/TranslationContext';
import type { DefaultStreamChatGenerics } from '../../types/types';

export type ChannelPreviewUIComponentProps<
Expand Down Expand Up @@ -52,6 +52,12 @@ export type ChannelPreviewProps<
channelUpdateCount?: number;
/** Custom class for the channel preview root */
className?: string;
/** Custom function that generates the message preview in ChannelPreview component */
getLatestMessagePreview?: (
channel: Channel<StreamChatGenerics>,
t: TranslationContextValue['t'],
userLanguage: TranslationContextValue['userLanguage'],
) => string | JSX.Element;
key?: string;
/** Custom ChannelPreview click handler function */
onSelect?: (event: React.MouseEvent) => void;
Expand All @@ -68,7 +74,12 @@ export const ChannelPreview = <
>(
props: ChannelPreviewProps<StreamChatGenerics>,
) => {
const { channel, Preview = ChannelPreviewMessenger, channelUpdateCount } = props;
const {
channel,
Preview = ChannelPreviewMessenger,
channelUpdateCount,
getLatestMessagePreview = defaultGetLatestMessagePreview,
} = props;
const { channel: activeChannel, client, setActiveChannel } = useChatContext<StreamChatGenerics>(
'ChannelPreview',
);
Expand Down
36 changes: 34 additions & 2 deletions src/components/ChannelPreview/__tests__/ChannelPreview.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ const PreviewUIComponent = (props) => (
</div>
</>
);
const PreviewUIComponentWithLatestMessagePreview = (props) => (
<>
<div data-testid='channel-id'>{props.channel.id}</div>
<div data-testid='unread-count'>{props.unread}</div>
<div data-testid='last-event-message'>
{props.lastMessage ? props.latestMessagePreview : EMPTY_CHANNEL_PREVIEW_TEXT}
</div>
</>
);

const expectUnreadCountToBe = async (getByTestId, expectedValue) => {
await waitFor(() => {
Expand Down Expand Up @@ -72,8 +81,14 @@ describe('ChannelPreview', () => {
client = await getTestClientWithUser(user);
useMockedApis(client, [
queryChannelsApi([
generateChannel({ messages: Array.from({ length: 5 }, generateMessage) }),
generateChannel({ messages: Array.from({ length: 5 }, generateMessage) }),
generateChannel({
channel: { name: 'c0' },
messages: Array.from({ length: 5 }, generateMessage),
}),
generateChannel({
channel: { name: 'c1' },
messages: Array.from({ length: 5 }, generateMessage),
}),
]),
]);

Expand Down Expand Up @@ -138,6 +153,22 @@ describe('ChannelPreview', () => {
await expectUnreadCountToBe(getByTestId, newUnreadCount);
});

it('allows to customize latest message preview generation', async () => {
const getLatestMessagePreview = (channel) => channel.data.name;

const { getByTestId } = renderComponent(
{
activeChannel: c0,
channel: c0,
getLatestMessagePreview,
Preview: PreviewUIComponentWithLatestMessagePreview,
},
render,
);

await expectLastEventMessageToBe(getByTestId, c0.data.name);
});

const eventCases = [
['message.new', dispatchMessageNewEvent],
['message.updated', dispatchMessageUpdatedEvent],
Expand Down Expand Up @@ -349,6 +380,7 @@ describe('ChannelPreview', () => {
expectUnreadCountToBe(screen.getByTestId, unreadCount);
});
});

describe('notification.mark_unread', () => {
it('should be ignored if not originated from the current user', () => {
const unreadCount = 0;
Expand Down

0 comments on commit eb66888

Please sign in to comment.