Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add customizable reactions sorting #2289

Merged
merged 7 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docusaurus/docs/React/components/contexts/message-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,14 @@ When true, show the reactions list component.
| ------- |
| boolean |

### sortReactions

Comparator function to sort reactions. Should have the same signature as the `sort` method for a string array.

| Type | Default |
| -------------------------------------------------------- | ------------------ |
| (this: ReactionSummary, that: ReactionSummary) => number | alphabetical order |

### threadList

If true, indicates that the current `MessageList` component is part of a `Thread`.
Expand Down
49 changes: 27 additions & 22 deletions docusaurus/docs/React/components/core-components/message-list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ The default `remark` plugins used by SDK are:
1. [`remark-gfm`](https://github.com/remarkjs/remark-gfm) - a third party plugin to add GitHub-like markdown support

The default `rehype` plugins (both specific to this SDK) are:

1. plugin to render user mentions
2. plugin to render emojis

Expand Down Expand Up @@ -166,56 +167,54 @@ If you would like to extend the array of plugins used to parse the markdown, you
It is important to understand what constitutes a rehype or remark plugin. A good start is to learn about the library called [`react-remark`](https://github.com/remarkjs/react-remark) which is used under the hood in our `renderText()` function.
:::


```tsx
import { renderText, RenderTextPluginConfigurator } from 'stream-chat-react';
import {customRehypePlugin} from './rehypePlugins';
import {customRemarkPlugin} from './remarkPlugins';
import { customRehypePlugin } from './rehypePlugins';
import { customRemarkPlugin } from './remarkPlugins';

const getRehypePlugins: RenderTextPluginConfigurator = (plugins) => {
return [customRehypePlugin, ...plugins];
}
return [customRehypePlugin, ...plugins];
};
const getRemarkPlugins: RenderTextPluginConfigurator = (plugins) => {
return [customRemarkPlugin, ...plugins];
}
return [customRemarkPlugin, ...plugins];
};

const customRenderText = (text, mentionedUsers) =>
renderText(text, mentionedUsers, {
getRehypePlugins,
getRemarkPlugins
getRemarkPlugins,
});

const CustomMessageList = () => (
<MessageList renderText={customRenderText}/>
);
const CustomMessageList = () => <MessageList renderText={customRenderText} />;
```

It is also possible to define your custom set of allowed tag names for the elements rendered from the parsed markdown. To perform the tree transformations, you will need to use libraries like [`unist-builder`](https://github.com/syntax-tree/unist-builder) to build the trees and [`unist-util-visit`](https://github.com/syntax-tree/unist-util-visit-parents) or [`hast-util-find-and-replace`](https://github.com/syntax-tree/hast-util-find-and-replace) to traverse the tree:

```tsx
import { findAndReplace } from 'hast-util-find-and-replace';
import { u } from 'unist-builder';
import { defaultAllowedTagNames, renderText, RenderTextPluginConfigurator } from 'stream-chat-react';
import {
defaultAllowedTagNames,
renderText,
RenderTextPluginConfigurator,
} from 'stream-chat-react';

// wraps every letter b in <xxx></xxx> tags
const customTagName = 'xxx';
const replace = (match) => u('element', { tagName: customTagName }, [u('text', match)]);
const customRehypePlugin = () => (tree) => findAndReplace(tree, /b/, replace);

const getRehypePlugins: RenderTextPluginConfigurator = (plugins) => {
return [customRehypePlugin, ...plugins];
}

return [customRehypePlugin, ...plugins];
};

const customRenderText = (text, mentionedUsers) =>
renderText(text, mentionedUsers, {
allowedTagNames: [...defaultAllowedTagNames, customTagName],
getRehypePlugins,
});

const CustomMessageList = () => (
<MessageList renderText={customRenderText}/>
);
const CustomMessageList = () => <MessageList renderText={customRenderText} />;
```

#### Custom message list rendering
Expand All @@ -233,9 +232,7 @@ const customRenderMessages: MessageRenderer<StreamChatGenerics> = (options) => {
return elements;
};

const CustomMessageList = () => (
<MessageList renderMessages={customRenderMessages}/>
);
const CustomMessageList = () => <MessageList renderMessages={customRenderMessages} />;
```

Make sure that the elements you return have `key`, as they will be rendered as an array. It's also a good idea to wrap each element with `<li>` to keep your markup semantically correct.
Expand Down Expand Up @@ -464,7 +461,7 @@ Function called when more messages are to be loaded, provide your own function t
When enabled, the channel will be marked read when a user scrolls to the bottom. Ignored when scrolled to the bottom of a thread message list.

| Type | Default |
|---------|---------|
| ------- | ------- |
| boolean | false |

### Message
Expand Down Expand Up @@ -623,6 +620,14 @@ channel, the `MessageNotification` component displays when new messages arrive.
| ------ | ------- |
| number | 200 |

### sortReactions

Comparator function to sort reactions. Should have the same signature as the `sort` method for a string array.

| Type | Default |
| -------------------------------------------------------- | ------------------ |
| (this: ReactionSummary, that: ReactionSummary) => number | alphabetical order |

### threadList

If true, indicates that the current `MessageList` component is part of a `Thread`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ Function called when more messages are to be loaded, provide your own function t
When enabled, the channel will be marked read when a user scrolls to the bottom. Ignored when scrolled to the bottom of a thread message list.

| Type | Default |
|---------|---------|
| ------- | ------- |
| boolean | false |

### Message
Expand Down Expand Up @@ -263,6 +263,14 @@ The scroll-to behavior when new messages appear. Use `'smooth'` for regular chat
| ------------------ | -------- |
| 'smooth' \| 'auto' | 'smooth' |

### sortReactions

Comparator function to sort reactions. Should have the same signature as the `sort` method for a string array.

| Type | Default |
| -------------------------------------------------------- | ------------------ |
| (this: ReactionSummary, that: ReactionSummary) => number | alphabetical order |

### threadList

If true, indicates that the current `VirtualizedMessageList` component is part of a `Thread`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,14 @@ Custom action handler to retry sending a message after a failed request.
| -------- | -------------------------------------------------------------------------------------------------------- |
| function | [ChannelActionContextValue['retrySendMessage']](../contexts/channel-action-context.mdx#retrysendmessage) |

### sortReactions

Comparator function to sort reactions. Should have the same signature as the `sort` method for a string array.

| Type | Default |
| -------------------------------------------------------- | ------------------ |
| (this: ReactionSummary, that: ReactionSummary) => number | alphabetical order |

### threadList

If true, indicates that the current `MessageList` component is part of a `Thread`.
Expand Down
33 changes: 33 additions & 0 deletions docusaurus/docs/React/components/message-components/reactions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,31 @@ const CustomReactionsList = (props) => {
</Chat>;
```

## Sorting reactions

By default, reactions are sorted alphabetically by type. You can change this behavior by passing a `sortReactions` prop to the `MessageList` (or `VirtualizedMessageList`).

In this example, we sort the reactions in the descending order by the number of users:

```jsx
function sortByReactionCount(a, b) {
return b.reactionCount - a.reactionCount;
}

<Chat client={client}>
<Channel
channel={channel}
ReactionSelector={CustomReactionSelector}
ReactionsList={CustomReactionsList}
>
<MessageList sortReactions={sortByReactionCount} />
<MessageInput />
</Channel>
</Chat>;
```

For better performance, keep this function memoized with `useCallback`, or declare it in either global or module scope.

## ReactionSelector Props

### additionalEmojiProps (removed in `11.0.0`)
Expand Down Expand Up @@ -276,6 +301,14 @@ If true, adds a CSS class that reverses the horizontal positioning of the select
| ------- | ------- |
| boolean | false |

### sortReactions

Comparator function to sort reactions. Should have the same signature as the `sort` method for a string array. This prop overrides the function stored in `MessageContext`.

| Type | Default |
| -------------------------------------------------------- | ------------------ |
| (this: ReactionSummary, that: ReactionSummary) => number | alphabetical order |

## SimpleReactionsList Props

### additionalEmojiProps (removed in `11.0.0`)
Expand Down
16 changes: 13 additions & 3 deletions examples/typescript/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ import {
import './App.css';

const apiKey = process.env.REACT_APP_STREAM_KEY as string;
const userId = process.env.REACT_APP_USER_ID as string;
const userToken = process.env.REACT_APP_USER_TOKEN as string;
let userId = process.env.REACT_APP_USER_ID as string;
let userToken = process.env.REACT_APP_USER_TOKEN as string;

const userOverride = new URLSearchParams(window.location.search).get('user');
if (userOverride) {
[userId, userToken] = userOverride.split(':');
}

const filters: ChannelFilters = { type: 'messaging', members: { $in: [userId] } };
const options: ChannelOptions = { state: true, presence: true, limit: 10 };
Expand Down Expand Up @@ -53,7 +58,12 @@ const App = () => (
<Channel>
<Window>
<ChannelHeader />
<MessageList />
<MessageList
sortReactions={(a, b) => {
console.log([a, b]);
return 0;
}}
/>
<MessageInput focus />
</Window>
<Thread />
Expand Down
5 changes: 4 additions & 1 deletion src/components/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ type MessageContextPropsToPick =
| 'onMentionsHoverMessage'
| 'onReactionListClick'
| 'reactionSelectorRef'
| 'showDetailedReactions';
| 'showDetailedReactions'
| 'sortReactions';

type MessageWithContextProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
Expand Down Expand Up @@ -207,6 +208,7 @@ export const Message = <
openThread: propOpenThread,
pinPermissions,
retrySendMessage: propRetrySendMessage,
sortReactions,
} = props;

const { addNotification } = useChannelActionContext<StreamChatGenerics>('Message');
Expand Down Expand Up @@ -308,6 +310,7 @@ export const Message = <
readBy={props.readBy}
renderText={props.renderText}
showDetailedReactions={showDetailedReactions}
sortReactions={sortReactions}
threadList={props.threadList}
unsafeHTML={props.unsafeHTML}
userRoles={userRoles}
Expand Down
3 changes: 3 additions & 0 deletions src/components/Message/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { MessageActionsArray } from './utils';

import type { GroupStyle } from '../MessageList/utils';
import type { MessageInputProps } from '../MessageInput/MessageInput';
import type { ReactionsComparator } from '../Reactions/types';

import type { ChannelActionContextValue } from '../../context/ChannelActionContext';
import type { StreamMessage } from '../../context/ChannelStateContext';
Expand Down Expand Up @@ -97,6 +98,8 @@ export type MessageProps<
) => JSX.Element | null;
/** Custom retry send message handler to override default in [ChannelActionContext](https://getstream.io/chat/docs/sdk/react/contexts/channel_action_context/) */
retrySendMessage?: ChannelActionContextValue<StreamChatGenerics>['retrySendMessage'];
/** Comparator function to sort reactions, defaults to alphabetical order */
sortReactions?: ReactionsComparator;
/** Whether the Message is in a Thread */
threadList?: boolean;
/** render HTML instead of markdown. Posting HTML is only allowed server-side */
Expand Down
1 change: 1 addition & 0 deletions src/components/MessageList/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ type PropsDrilledToMessage =
| 'pinPermissions' // @deprecated in favor of `channelCapabilities` - TODO: remove in next major release
| 'renderText'
| 'retrySendMessage'
| 'sortReactions'
| 'unsafeHTML';

export type MessageListProps<
Expand Down
6 changes: 5 additions & 1 deletion src/components/MessageList/VirtualizedMessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type VirtualizedMessageListPropsForContext =
| 'Message'
| 'messageActions'
| 'shouldGroupByUser'
| 'sortReactions'
| 'threadList';

/**
Expand Down Expand Up @@ -194,6 +195,7 @@ const VirtualizedMessageListWithContext = <
scrollToLatestMessageOnFocus = false,
separateGiphyPreview = false,
shouldGroupByUser = false,
sortReactions,
stickToBottomScrollBehavior = 'smooth',
suppressAutoscroll,
threadList,
Expand Down Expand Up @@ -442,6 +444,7 @@ const VirtualizedMessageListWithContext = <
ownMessagesReadByOthers,
processedMessages,
shouldGroupByUser,
sortReactions,
threadList,
unreadMessageCount: channelUnreadUiState?.unread_messages,
UnreadMessagesSeparator,
Expand Down Expand Up @@ -486,7 +489,8 @@ const VirtualizedMessageListWithContext = <
type PropsDrilledToMessage =
| 'additionalMessageInputProps'
| 'customMessageActions'
| 'messageActions';
| 'messageActions'
| 'sortReactions';

export type VirtualizedMessageListProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export const messageRenderer = <
ownMessagesReadByOthers,
processedMessages: messageList,
shouldGroupByUser,
sortReactions,
unreadMessageCount = 0,
UnreadMessagesSeparator,
virtuosoRef,
Expand Down Expand Up @@ -189,6 +190,7 @@ export const messageRenderer = <
Message={MessageUIComponent}
messageActions={messageActions}
readBy={ownMessagesReadByOthers[message.id] || []}
sortReactions={sortReactions}
/>
{showUnreadSeparator && (
<div className='str-chat__unread-messages-separator-wrapper'>
Expand Down
3 changes: 3 additions & 0 deletions src/components/Reactions/ReactionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useProcessReactions } from './hooks/useProcessReactions';
import type { ReactEventHandler } from '../Message/types';
import type { DefaultStreamChatGenerics } from '../../types/types';
import type { ReactionOptions } from './reactionOptions';
import type { ReactionsComparator } from './types';
import { ReactionsListModal } from './ReactionsListModal';
import { MessageContextValue, useTranslationContext } from '../../context';
import { MAX_MESSAGE_REACTIONS_TO_FETCH } from '../Message/hooks';
Expand All @@ -27,6 +28,8 @@ export type ReactionsListProps<
reactions?: ReactionResponse<StreamChatGenerics>[];
/** Display the reactions in the list in reverse order, defaults to false */
reverse?: boolean;
/** Comparator function to sort reactions, defaults to alphabetical order */
sortReactions?: ReactionsComparator;
};

const UnMemoizedReactionsList = <
Expand Down
Loading