Skip to content

Commit

Permalink
feat: show full list of reactions in a modal (#2249)
Browse files Browse the repository at this point in the history
### 🎯 Goal

We're updating UI for displaying reactions. Instead of showing the
latest reactions only, we're adding a separate modal window full the
full list of reactions.

Although not a breaking change in terms of API, it's a breaking change
in terms of UI and styling.

### 🛠 Implementation details

1. New component `ReactionsListModal` is added (rendered by default
`ReactionsList`).
2. New handler added on message context level: `handleFetchReactions`.
It's expected to load all reactions for current message (e.g. using our
paged endpoint for fetching reactions). As usual, it can be overriden on
`Message` or `ReactionsList` level.
3. Instead of showing counts for the latest reactions only, we show
counts for all supported reactions in the `ReactionsList`.

### 🎨 UI Changes


![image](https://github.com/GetStream/stream-chat-react/assets/975978/69b93a0b-571f-4823-9f21-ceb744cfb4af)

### To-Do

- [x] Tests for ReactionsListModal
- [x] Add translations
  • Loading branch information
myandrienko authored Jan 30, 2024
1 parent a40809c commit 0ebdbc6
Show file tree
Hide file tree
Showing 36 changed files with 759 additions and 258 deletions.
14 changes: 12 additions & 2 deletions docusaurus/docs/React/components/contexts/message-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ Function that edits a message.
| ----------------------------------------------------------- |
| (event: React.BaseSyntheticEvent) => Promise<void\> \| void |

### handleFetchReactions

Function that loads the reactions for a message.

| Type |
| ------------------------------------- |
| () => Promise<ReactionResponse[]\> \ |

This function limits the number of loaded reactions to 1200. To customize this behavior, you can pass [a custom `ReactionsList` component](../message-components/reactions.mdx#handlefetchreactions).

### handleFlag

Function that flags a message.
Expand Down Expand Up @@ -339,8 +349,8 @@ An array of users that have read the current message.

Custom function to render message text content.

| Type | Default |
| -------- | -------------------------------------------------------------------------------------- |
| Type | Default |
| -------- | ------------------------------------------------------------------------------ |
| function | <GHComponentLink text='renderText' path='/Message/renderText/renderText.tsx'/> |

### setEditingState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,15 @@ deleted [message object](https://getstream.io/chat/docs/javascript/message_forma
| ---------------------------------- |
| (message: StreamMessage) => string |

### getFetchReactionsErrorNotification

Function that returns the notification text to be displayed when loading message reactions fails. This function receives the
current [message object](https://getstream.io/chat/docs/javascript/message_format/?language=javascript) as its argument.

| Type |
| ---------------------------------- |
| (message: StreamMessage) => string |

### getFlagMessageErrorNotification

Function that returns the notification text to be displayed when a flag message request fails. This function receives the
Expand Down
15 changes: 13 additions & 2 deletions docusaurus/docs/React/components/message-components/message-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,16 @@ Function that edits a message (overrides the function stored in `MessageContext`
| ----------------------------------------------------------- | ------------------------------------------------------------------------------- |
| (event: React.BaseSyntheticEvent) => Promise<void\> \| void | [MessageContextValue['handleEdit']](../contexts/message-context.mdx#handleedit) |

### handleFetchReactions

Function that loads the reactions for a message (overrides the function stored in `MessageContext`).

| Type | Default |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| (event: React.BaseSyntheticEvent) => Promise<void\> \| void | [MessageContextValue['handleFetchReactions']](../contexts/message-context.mdx#handlhandlefetchreactions) |

This function limits the number of loaded reactions to 1200. To customize this behavior, you can pass [a custom `ReactionsList` component](./reactions.mdx#handlefetchreactions).

### handleFlag

Function that flags a message (overrides the function stored in `MessageContext`).
Expand Down Expand Up @@ -352,6 +362,7 @@ Function that returns whether a message belongs to the current user (overrides t
| () => boolean |

### isReactionEnabled (deprecated)

If true, sending reactions is enabled in the currently active channel (overrides the value stored in `MessageContext`).

| Type | Default |
Expand Down Expand Up @@ -458,8 +469,8 @@ An array of users that have read the current message (overrides the value stored

Custom function to render message text content (overrides the function stored in `MessageContext`).

| Type | Default |
| -------- | -------------------------------------------------------------------------------------- |
| Type | Default |
| -------- | ------------------------------------------------------------------------------ |
| function | <GHComponentLink text='renderText' path='/Message/renderText/renderText.tsx'/> |

### setEditingState
Expand Down
13 changes: 11 additions & 2 deletions docusaurus/docs/React/components/message-components/message.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ deleted [message object](https://getstream.io/chat/docs/javascript/message_forma
| ---------------------------------- |
| (message: StreamMessage) => string |

### getFetchReactionsErrorNotification

Function that returns the notification text to be displayed when loading message reactions fails. This function receives the
current [message object](https://getstream.io/chat/docs/javascript/message_format/?language=javascript) as its argument.

| Type |
| ---------------------------------- |
| (message: StreamMessage) => string |

### getFlagMessageErrorNotification

Function that returns the notification text to be displayed when a flag message request fails. This function receives the
Expand Down Expand Up @@ -324,8 +333,8 @@ An array of users that have read the current message.

Custom function to render message text content.

| Type | Default |
| -------- | -------------------------------------------------------------------------------------- |
| Type | Default |
| -------- | ------------------------------------------------------------------------------ |
| function | <GHComponentLink text='renderText' path='/Message/renderText/renderText.tsx'/> |

### retrySendMessage
Expand Down
31 changes: 31 additions & 0 deletions docusaurus/docs/React/components/message-components/reactions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,29 @@ Additional props to be passed to the [`NimbleEmoji`](https://github.com/missive/
| ------ |
| object |

### handleFetchReactions

Function that loads the message reactions (overrides the function stored in `MessageContext`).

| Type | Default |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| () => Promise<void\> | [MessageContextValue['handleFetchReactions']](../contexts/message-context.mdx#handlefetchreactions) |

The default implementation of `handleFetchReactions`, provided via the [`MessageContext`](../contexts/message-context.mdx#handlefetchreactions), limits the number of loaded reactions to 1200. Use this prop to provide your own loading implementation:

```jsx
const MyCustomReactionsList = (props) => {
const { channel } = useChannelStateContext();
const { message } = useMessageContext();

function fetchReactions() {
return channel.getReactions(message.id, { limit: 42 });
}

return <ReactionsList handleFetchReactions={fetchReactions} />;
};
```

### onClick

Custom on click handler for an individual reaction in the list (overrides the function stored in `MessageContext`).
Expand Down Expand Up @@ -263,6 +286,14 @@ Additional props to be passed to the [`NimbleEmoji`](https://github.com/missive/
| ------ |
| object |

### handleFetchReactions

Function that loads the message reactions (overrides the function stored in `MessageContext`).

| Type | Default |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| () => Promise<void\> | [MessageContextValue['handleFetchReactions']](../contexts/message-context.mdx#handlefetchreactions) |

### handleReaction

Function that adds/removes a reaction on a message (overrides the function stored in `MessageContext`).
Expand Down
31 changes: 31 additions & 0 deletions docusaurus/docs/React/hooks/message-hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,37 @@ const MyCustomMessageComponent = () => {
};
```
### useReactionsFetcher
A [custom hook](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/hooks/useReactionsFetcher.ts) to handle loading message reactions. Returns an async function that loads and returns message reactions.
```jsx
import { useQuery } from 'react-query';
const MyCustomReactionsList = () => {
const { message } = useMessageContext();
const { addNotification } = useChannelActionContext();
const handleFetchReactions = useReactionsFetcher(message, { notify: addNotification });
// This example relies on react-query - but you can use you preferred method
// of fetching data instead
const { data } = useQuery(['message', message.id, 'reactions'], handleFetchReactions);
if (!data) {
return null;
}
return (
<>
{data.map((reaction) => (
<span key={reaction.type}>reaction.type</span>
))}
</>
);
};
```
This function limits the number of loaded reactions to 1200. To customize this behavior, provide [your own loader function](../message-components/reactions.mdx#handlefetchreactions) instead.
### useRetryHandler
A [custom hook](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/hooks/useRetryHandler.ts) to handle the retry of sending a message.
Expand Down
9 changes: 9 additions & 0 deletions src/components/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
usePinHandler,
useReactionClick,
useReactionHandler,
useReactionsFetcher,
useRetryHandler,
useUserHandler,
useUserRole,
Expand Down Expand Up @@ -38,6 +39,7 @@ type MessageContextPropsToPick =
| 'handleOpenThread'
| 'handlePin'
| 'handleReaction'
| 'handleFetchReactions'
| 'handleRetry'
| 'isReactionEnabled'
| 'mutes'
Expand Down Expand Up @@ -158,6 +160,7 @@ export const Message = <
closeReactionSelectorOnClick,
disableQuotedMessages,
getDeleteMessageErrorNotification,
getFetchReactionsErrorNotification,
getFlagMessageErrorNotification,
getFlagMessageSuccessNotification,
getMuteUserErrorNotification,
Expand All @@ -183,6 +186,11 @@ export const Message = <
const handleRetry = useRetryHandler(propRetrySendMessage);
const userRoles = useUserRole(message, onlySenderCanEdit, disableQuotedMessages);

const handleFetchReactions = useReactionsFetcher(message, {
getErrorNotification: getFetchReactionsErrorNotification,
notify: addNotification,
});

const handleDelete = useDeleteHandler(message, {
getErrorNotification: getDeleteMessageErrorNotification,
notify: addNotification,
Expand Down Expand Up @@ -233,6 +241,7 @@ export const Message = <
groupStyles={props.groupStyles}
handleAction={handleAction}
handleDelete={handleDelete}
handleFetchReactions={handleFetchReactions}
handleFlag={handleFlag}
handleMute={handleMute}
handleOpenThread={handleOpenThread}
Expand Down
11 changes: 7 additions & 4 deletions src/components/Message/__tests__/MessageSimple.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
TranslationProvider,
} from '../../../context';
import {
countReactions,
generateChannel,
generateMessage,
generateReaction,
Expand Down Expand Up @@ -224,9 +225,10 @@ describe('<MessageSimple />', () => {

// FIXME: test relying on deprecated channel config parameter
it('should render reaction list even though sending reactions is disabled in channel config', async () => {
const bobReaction = generateReaction({ user: bob });
const reactions = [generateReaction({ user: bob })];
const message = generateAliceMessage({
latest_reactions: [bobReaction],
latest_reactions: reactions,
reaction_counts: countReactions(reactions),
text: undefined,
});

Expand All @@ -240,9 +242,10 @@ describe('<MessageSimple />', () => {
});

it('should render reaction list with custom component when one is given', async () => {
const bobReaction = generateReaction({ type: 'cool-reaction', user: bob });
const reactions = [generateReaction({ type: 'cool-reaction', user: bob })];
const message = generateAliceMessage({
latest_reactions: [bobReaction],
latest_reactions: reactions,
reaction_counts: countReactions(reactions),
text: undefined,
});
const CustomReactionsList = ({ reactions = [] }) => (
Expand Down
30 changes: 7 additions & 23 deletions src/components/Message/__tests__/MessageText.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
TranslationProvider,
} from '../../../context';
import {
countReactions,
generateChannel,
generateMessage,
generateReaction,
Expand Down Expand Up @@ -101,7 +102,6 @@ async function renderMessageText({
}

const messageTextTestId = 'message-text-inner-wrapper';
const reactionSelectorTestId = 'reaction-selector';

describe('<MessageText />', () => {
beforeEach(jest.clearAllMocks);
Expand Down Expand Up @@ -230,9 +230,10 @@ describe('<MessageText />', () => {
});

it('should show reaction list if message has reactions and detailed reactions are not displayed', async () => {
const bobReaction = generateReaction({ user: bob });
const reactions = [generateReaction({ user: bob })];
const message = generateAliceMessage({
latest_reactions: [bobReaction],
latest_reactions: reactions,
reaction_counts: countReactions(reactions),
});

let container;
Expand All @@ -248,9 +249,10 @@ describe('<MessageText />', () => {

// FIXME: test relying on deprecated channel config parameter
it('should show reaction list even though sending reactions is disabled in channelConfig', async () => {
const bobReaction = generateReaction({ user: bob });
const reactions = [generateReaction({ user: bob })];
const message = generateAliceMessage({
latest_reactions: [bobReaction],
latest_reactions: reactions,
reaction_counts: countReactions(reactions),
});
const { container, queryByTestId } = await renderMessageText({
channelCapabilitiesOverrides: { 'send-reaction': false },
Expand All @@ -262,24 +264,6 @@ describe('<MessageText />', () => {
expect(results).toHaveNoViolations();
});

it('should show reaction selector when message has reaction and reaction list is clicked', async () => {
const bobReaction = generateReaction({ user: bob });
const message = generateAliceMessage({
latest_reactions: [bobReaction],
});
const { container, getByTestId, queryByTestId } = await renderMessageText({
customProps: { message },
});
expect(queryByTestId(reactionSelectorTestId)).not.toBeInTheDocument();
await act(() => {
fireEvent.click(getByTestId('reaction-list'));
});

expect(getByTestId(reactionSelectorTestId)).toBeInTheDocument();
const results = await axe(container);
expect(results).toHaveNoViolations();
});

it('should render message options', async () => {
const { container } = await renderMessageText();
expect(MessageOptionsMock).toHaveBeenCalledTimes(1);
Expand Down
10 changes: 8 additions & 2 deletions src/components/Message/__tests__/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { generateMessage, generateReaction, generateUser } from 'mock-builders';
import { getTestClientWithUser, mockTranslatorFunction } from '../../../mock-builders';
import {
countReactions,
getTestClientWithUser,
mockTranslatorFunction,
} from '../../../mock-builders';
import {
areMessagePropsEqual,
areMessageUIPropsEqual,
Expand Down Expand Up @@ -236,8 +240,10 @@ describe('Message utils', () => {
expect(messageHasReactions(message)).toBe(false);
});
it('should return true if message has reactions', () => {
const reactions = [generateReaction()];
const message = generateMessage({
latest_reactions: [generateReaction()],
latest_reactions: reactions,
reaction_counts: countReactions(reactions),
});
expect(messageHasReactions(message)).toBe(true);
});
Expand Down
Loading

0 comments on commit 0ebdbc6

Please sign in to comment.