Skip to content

Commit

Permalink
feat: implement message bounce flow (#2254)
Browse files Browse the repository at this point in the history
### 🎯 Goal

πŸš‚ GetStream/stream-chat-js#1213
πŸš‚ GetStream/stream-chat-css#264

In chats with moderation rules set up, message can bounce if its content
is deemed potentially harmful. The author of a bounced message should
then be presented with four alternatives:

1. Edit the message and try sending it again
2. Try sending it again as-is (this is helpful for "bounce then flag"
flow)
3. Remove the message
4. Do nothing. Bounced messages are ephemeral, so it will soon disappear
on its own

### πŸ›  Implementation details

This PR introduces a couple of new components, including the
`MessageBounceModal` which is rendered by `MessageSimple` when a bounced
message is clicked.

The contents of the modal (`MessageBounceOption`) is an overridable
component that should ideally render three buttons for the first three
alternative options listed above. The callbacks for said buttons are
provided via `MessageBounceContext`.

### 🎨 UI Changes

The chat in the screenshot has a semantic filter set up which is
triggered by the word "midnight". Here's what a bounced message with the
word "midnight" looks like:


![image](https://github.com/GetStream/stream-chat-react/assets/975978/9476cde6-f310-41a8-bb6d-3e7ed0f58421)

Clicking on the bounced messages opens `MessageBounceModal`:


![image](https://github.com/GetStream/stream-chat-react/assets/975978/9bbb7201-3342-4ce7-88f2-9a3131c96878)

Clicking "Edit Message" opens the standard editing UI:


![image](https://github.com/GetStream/stream-chat-react/assets/975978/ef627801-9b3b-46e5-8d12-eb8b3fd08afd)

### To-Do and Next Steps

[The design
doc](https://www.figma.com/file/ekifwChR9tR7zRJg1QEzSM/Chat-UI-Kit-1.0-All-platforms?type=design&node-id=23638-313355&mode=design)
for this feature also features a notification banner with a button,
which is displayed when a message bounces. Clicking the button should
bring the user to the bounced message.

We don't have a way to have interactive elements within channel
notifications at the moment, but this is going to be implemented in
further PRs in two steps:

1. Allow passing arbitrary JSX to the notification, not just text
2. Implement a bounced message notification with a button to bring the
user to the message

- [x] Release `stream-chat-css` with udpated styles
- [x] Release `stream-chat-js` with updates types
- [x] Cover `MessageBounceModal` and `MessageBounceOptions` with tests
- [x] Document new components and customization options
- [x] Document the moderation flow
  • Loading branch information
myandrienko authored Feb 9, 2024
1 parent 6a928f6 commit 3878e2f
Show file tree
Hide file tree
Showing 31 changed files with 605 additions and 46 deletions.
22 changes: 16 additions & 6 deletions docusaurus/docs/React/components/contexts/component-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ Custom UI component to display a user's avatar.
Custom UI component to display image resp. a fallback in case of load error, in `<img/>` element. The default resp. custom (from `ComponentContext`) `BaseImage` component is rendered by:

- <GHComponentLink text='Image' path='/Gallery/Image.tsx' /> - single image attachment in message list
- <GHComponentLink text='Gallery' path='/Gallery/Gallery.tsx' /> - group of image attachments in message list
- <GHComponentLink text='AttachmentPreviewList' path='/MessageInput/AttachmentPreviewList.tsx' /> - image uploads preview in message input (composer)
- <GHComponentLink text='Gallery' path='/Gallery/Gallery.tsx' /> - group of image attachments in message
list
- <GHComponentLink text='AttachmentPreviewList' path='/MessageInput/AttachmentPreviewList.tsx' /> - image
uploads preview in message input (composer)

The `BaseImage` component accepts the same props as `<img/>` element.

Expand Down Expand Up @@ -261,6 +263,14 @@ Custom UI component to display a timestamp on a message.
| --------- | ------------------------------------------------------------------------------- |
| component | <GHComponentLink text='MessageTimestamp' path='/Message/MessageTimestamp.tsx'/> |

### MessageBouncePrompt

Custom UI component for the content of the modal dialog for messages that got bounced by the moderation rules.

| Type | Default |
| --------- | ------------------------------------------------------------------------------------------- |
| component | <GHComponentLink text='MessageBouncePrompt' path='/MessageBounce/MessageBouncePrompt.tsx'/> |

### ModalGallery

Custom UI component for viewing message's image attachments.
Expand Down Expand Up @@ -369,16 +379,16 @@ Custom UI component for the typing indicator.

Custom UI component that indicates a user is viewing unread messages. It disappears once the user scrolls to `UnreadMessagesSeparator`.

| Type | Default |
| --------- | ------------------------------------------------------------------------------------- |
| Type | Default |
| --------- | ------------------------------------------------------------------------------------------------------- |
| component | <GHComponentLink text='UnreadMessagesNotification' path='/MessageList/UnreadMessagesNotification.tsx'/> |

### UnreadMessagesSeparator

Custom UI component inserted before the first message marked unread.

| Type | Default |
| --------- | ------------------------------------------------------------------------------------- |
| Type | Default |
| --------- | ------------------------------------------------------------------------------------------------- |
| component | <GHComponentLink text='UnreadMessagesSeparator' path='/MessageList/UnreadMessagesSeparator.tsx'/> |

### VirtualMessage
Expand Down
152 changes: 152 additions & 0 deletions docusaurus/docs/React/components/contexts/message-bounce-context.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
id: message_bounce_context
sidebar_position: 11
title: MessageBounceContext
---

The `MessageBounceContext` is available inside the modal rendered by the default message component for messages that got bounced by the moderation rules. This context provides callbacks that can be used to deal with the bounced message.

## Basic Usage

In most cases when using the default Message UI component implementation you are not going to deal with the `MessageBounceContext` directly. However if you are customizing the Message UI component, or providing a custom `MessageBouncePrompt`, the callbacks provided by this context come in handy.

Get values from context with our custom hook:

```jsx
const { message, handleEdit, handleSend, handleDelete } = useMessageBounceContext();
```

Use these callbacks to implement your custom `MessageBouncePrompt`. Normally this component displays three options: edit the message before sending it again, send the message again without changes (this can be useful if you are using the "Bounce then flag" moderation flow), and delete the message.

```jsx
import { useMessageBounceContext } from 'stream-chat-react';

function MyCustomMessageBouncePrompt({ onClose }) {
const { message, handleEdit, handleSend, handleDelete } = useMessageBounceContext();
return (
<>
<p>Your message is in violation of our community rules.</p>
<p>Message id: "{message.id}"</p>
<button
type='button'
onClick={() => {
handleEdit();
onClose();
}}
>
Edit message
</button>
{/* ... */}
</>
);
}
```

Then override the default `MessageBouncePrompt` component with your custom one:

```jsx
<Channel MessageBouncePrompt={MyCustomMessageBouncePrompt}>
<Window>
<ChannelHeader />
<MessageList />
<MessageInput />
</Window>
<Thread />
</Channel>
```

## Usage in a Custom Message UI component

When implementing your own Message component from scratch, you should consider implementing UI for bounced messages, especially if you are using one of the moderation flows with message bouncing ("Bounce", "Bounce then flag", or "Bounce then block").

To do that, first check if the message is bounced:

```jsx
import { useMessageContext, isMessageBounced } from 'stream-chat-react';

function CustomMessage() {
const { message } = useMessageContext();
const isBounced = isMessageBounced(message);
// ...
}
```

Then, display custom UI in case the message is bounced. Don't forget to wrap the UI with the `MessageBounceProvider`, so that it has access to the callbacks used to deal with the bounced message:

```jsx
import { useMessageContext, isMessageBounced, MessageBounceProvider } from 'stream-chat-react';

function MyCustomMessage() {
const { message } = useMessageContext();
const isBounced = isMessageBounced(message);

return (
<div className='message-wrapper'>
{/* ... */}
<MessageText />
<MessageStatus />
{isBounced && (
<MessageBounceProvider>
<MyCustomMessageBouncePrompt />
</MessageBounceProvider>
)}
</div>
);
}

function MyCustomMessageBouncePrompt({ onClose }) {
const { message, handleEdit, handleSend, handleDelete } = useMessageBounceContext();
return (
<>
<button
type='button'
onClick={(e) => {
handleEdit(e);
onClose(e);
}}
>
Edit message
</button>
{/* ... */}
</>
);
}
```

It only makes sense to render `MessageBounceProvider` in the context of a bounced message, so you'll see a warning in the browser console if you try to render it for any other type of message.

Implementing a custom Message UI component from scratch is a larger topic, covered by the [Message UI Customization](../../guides/theming/message-ui.mdx) guide.

## Values

### message

The object representing the message that got bounced.

| Type |
| ------------- |
| StreamMessage |

### handleEdit

Call this function to switch the bounced message into editing mode.

| Type |
| ----------------- |
| ReactEventHandler |

### handleSend

Call this function to try sending the bounced message again without changes.

| Type |
| ----------------- |
| ReactEventHandler |

### handleDelete

Call this function to remove the bounced message from the message list.

| Type |
| ----------------- |
| ReactEventHandler |
19 changes: 13 additions & 6 deletions docusaurus/docs/React/components/core-components/channel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ export const MessageInput = (props: MessageInputProps) => {
Configuration parameter to mark the active channel as read when mounted (opened). By default, the channel is not marked read on mount.

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

### Input
Expand Down Expand Up @@ -589,6 +589,14 @@ Custom UI component to display a timestamp on a message.
| --------- | ------------------------------------------------------------------------------- |
| component | <GHComponentLink text='MessageTimestamp' path='/Message/MessageTimestamp.tsx'/> |

### MessageBouncePrompt

Custom UI component for the content of the modal dialog for messages that got bounced by the moderation rules.

| Type | Default |
| --------- | ------------------------------------------------------------------------------------------- |
| component | <GHComponentLink text='MessageBouncePrompt' path='/MessageBounce/MessageBouncePrompt.tsx'/> |

### ModalGallery

Custom UI component for viewing message's image attachments.
Expand Down Expand Up @@ -745,17 +753,16 @@ Custom UI component for the typing indicator.

Custom UI component that indicates a user is viewing unread messages. It disappears once the user scrolls to `UnreadMessagesSeparator`.

| Type | Default |
| --------- | ------------------------------------------------------------------------------------- |
| Type | Default |
| --------- | ------------------------------------------------------------------------------------------------------- |
| component | <GHComponentLink text='UnreadMessagesNotification' path='/MessageList/UnreadMessagesNotification.tsx'/> |


### UnreadMessagesSeparator

Custom UI component inserted before the first message marked unread.

| Type | Default |
| --------- | ------------------------------------------------------------------------------------- |
| Type | Default |
| --------- | ------------------------------------------------------------------------------------------------- |
| component | <GHComponentLink text='UnreadMessagesSeparator' path='/MessageList/UnreadMessagesSeparator.tsx'/> |

### videoAttachmentSizeHandler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ The following UI components are available for use:
- [`QuotedMessage`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/QuotedMessage.tsx) - shows a quoted
message UI wrapper when the sent message quotes a previous message

- [`MessageBouncePrompt`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageBounce/MessageBouncePrompt.tsx) -
presents options to deal with a message that got bounced by the moderation rules.

Besides the above there are also components that render reaction list and reaction selector. You can find more about them in [dedicated chapter](./reactions.mdx).

## MessageActions Props
Expand Down Expand Up @@ -415,3 +418,46 @@ The side of the message list to render MML components.
:::note
`QuotedMessage` only consumes context and does not accept any optional props.
:::

## MessageBouncePrompt

This component is rendered in a modal dialog for messages that got bounced by the moderation rules.

### MessageBouncePrompt children

| Type | Default |
| --------- | ----------------------------------------------------------------------- |
| ReactNode | Localized string for "This message did not meet our content guidelines" |

Use this prop to easily override the text displayed in the modal dialog for the bounced messages, without fully implementing a custom `MessageBouncePrompt` component:

```jsx
import { MessageBouncePrompt } from 'stream-react-chat';

function MyCustomMessageBouncePrompt(props) {
return <MessageBouncePrompt {...props}>My custom text</MessageBouncePrompt>;
}
```

Then override the default `MessageBouncePrompt` component with your custom one:

```jsx
<Channel MessageBouncePrompt={MyCustomMessageBouncePrompt}>
<Window>
<ChannelHeader />
<MessageList />
<MessageInput />
</Window>
<Thread />
</Channel>
```

If you need deeper customization, refer to the [`MessageBounceContext`](../contexts/message-bounce-context.mdx) documentation.

### onClose

The Message UI component will pass this callback to close the modal dialog `MessageBouncePrompt` are rendered in.

| Type |
| ----------------- |
| ReactEventHandler |
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
"emoji-mart": "^5.4.0",
"react": "^18.0.0 || ^17.0.0 || ^16.8.0",
"react-dom": "^18.0.0 || ^17.0.0 || ^16.8.0",
"stream-chat": "^8.0.0"
"stream-chat": "^8.15.0"
},
"peerDependenciesMeta": {
"emoji-mart": {
Expand Down Expand Up @@ -144,7 +144,7 @@
"@semantic-release/changelog": "^6.0.2",
"@semantic-release/git": "^10.0.1",
"@stream-io/rollup-plugin-node-builtins": "^2.1.5",
"@stream-io/stream-chat-css": "^4.6.3",
"@stream-io/stream-chat-css": "^4.7.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^13.1.1",
"@testing-library/react-hooks": "^8.0.0",
Expand Down
3 changes: 3 additions & 0 deletions src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ type ChannelPropsForwardedToComponentContext<
LoadingIndicator?: ComponentContextValue<StreamChatGenerics>['LoadingIndicator'];
/** Custom UI component to display a message in the standard `MessageList`, defaults to and accepts the same props as: [MessageSimple](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageSimple.tsx) */
Message?: ComponentContextValue<StreamChatGenerics>['Message'];
/** Custom UI component to display the contents of a bounced message modal. Usually it allows to retry, edit, or delete the message. Defaults to and accepts the same props as: [MessageBouncePrompt](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageBounce/MessageBouncePrompt.tsx) */
MessageBouncePrompt?: ComponentContextValue<StreamChatGenerics>['MessageBouncePrompt'];
/** Custom UI component for a deleted message, defaults to and accepts same props as: [MessageDeleted](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageDeleted.tsx) */
MessageDeleted?: ComponentContextValue<StreamChatGenerics>['MessageDeleted'];
/** Custom UI component that displays message and connection status notifications in the `MessageList`, defaults to and accepts same props as [DefaultMessageListNotifications](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/MessageListNotifications.tsx) */
Expand Down Expand Up @@ -1112,6 +1114,7 @@ const ChannelInner = <
LinkPreviewList: props.LinkPreviewList,
LoadingIndicator: props.LoadingIndicator,
Message: props.Message || MessageSimple,
MessageBouncePrompt: props.MessageBouncePrompt,
MessageDeleted: props.MessageDeleted,
MessageListNotifications: props.MessageListNotifications,
MessageNotification: props.MessageNotification,
Expand Down
38 changes: 38 additions & 0 deletions src/components/Message/MessageErrorText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

import { StreamMessage, useTranslationContext } from '../../context';
import { DefaultStreamChatGenerics } from '../../types/types';
import { isMessageBounced } from './utils';

export interface MessageErrorTextProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> {
message: StreamMessage<StreamChatGenerics>;
theme: string;
}

export function MessageErrorText<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({ message, theme }: MessageErrorTextProps<StreamChatGenerics>) {
const { t } = useTranslationContext('MessageText');

if (message.type === 'error' && !isMessageBounced(message)) {
return (
<div className={`str-chat__${theme}-message--error-message str-chat__message--error-message`}>
{t<string>('Error Β· Unsent')}
</div>
);
}

if (message.status === 'failed') {
return (
<div className={`str-chat__${theme}-message--error-message str-chat__message--error-message`}>
{message.errorStatusCode !== 403
? t<string>('Message Failed Β· Click to try again')
: t<string>('Message Failed Β· Unauthorized')}
</div>
);
}

return null;
}
Loading

0 comments on commit 3878e2f

Please sign in to comment.