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(renderText): allow custom remark and rehype plugin composition #2142

Merged
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ Custom function to render message text content.

| Type | Default |
| -------- | -------------------------------------------------------------------------------------- |
| function | [renderText](https://github.com/GetStream/stream-chat-react/blob/master/src/utils.tsx) |
| function | <GHComponentLink text='renderText' path='/Message/renderText/renderText.tsx'/> |

### setEditingState

Expand Down
52 changes: 48 additions & 4 deletions docusaurus/docs/React/components/core-components/message-list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,20 @@ The `MessageList` internally creates a mapping of message id to a style group. T

### Default behaviour

The output of the default [`renderText`](#render-text) function is a message text processed by the `ReactMarkdown` component with [`remark`](https://github.com/remarkjs/remark/blob/main/doc/plugins.md) [`remark-gfm`](https://github.com/remarkjs/remark-gfm) plugin and custom [`rehype`](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md) plugins for mentions and emojis.
The default [`renderText`](#render-text) function parses a markdown string and outputs a `ReactElement`. Under the hood, the output is generated by the `ReactMarkdown` component from [react-markdown library](https://github.com/remarkjs/react-markdown). The component transforms the markdown to `ReactElement` by using [`remark` parser](https://github.com/remarkjs/remark/tree/main) and [`remark`](https://github.com/remarkjs/remark/blob/main/doc/plugins.md) and [`rehype`](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md) plugins.

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

### Overriding defaults

#### Custom `renderText` function

If you don't want your chat implementation to support markdown syntax by default you can override the default behaviour by creating a custom `renderText` function which returns a React node and passing it down to the `MessageList` or `MessageSimple` component via `renderText` property.

For this particular example we'll create a very primitive one which takes the message text passed down to it as a first argument and returns it wrapped in `span` element:
Expand Down Expand Up @@ -112,10 +122,12 @@ const App = () => (
);
```

If you feel like the default output is sufficient but you'd like to adjust how certain [ReactMarkdown components](https://github.com/remarkjs/react-markdown#appendix-b-components) look like (like `strong` element generated by typing \*\*strong\*\*) you can do so by passing down options to a third argument of the default `renderText` function:
#### Custom element rendering

If you feel like the default output is sufficient, but you'd like to adjust how certain [ReactMarkdown components](https://github.com/remarkjs/react-markdown#appendix-b-components) look like (like `strong` element generated by typing \*\*strong\*\*) you can do so by passing down options to a third argument of the default `renderText` function:

:::note
Types `mention` and `emoji` are special case component types generated by our custom rehype plugins. Currently we do not allow to add custom rehype/remark plugins to our default `renderText` function due to compatibility reasons regarding our custom plugins.
Types `mention` and `emoji` are special case component types generated by our SDK's custom rehype plugins.
:::

```tsx
Expand Down Expand Up @@ -146,6 +158,38 @@ const App = () => (
);
```

#### Custom remark and rehype plugins

If you would like to extend the array of plugins used to parse the markdown, you can provide your own lists of remark resp. rehype plugins. The logic that determines what plugins are used and in which order can be specified in custom `getRehypePlugins` and `getRemarkPlugins` functions. These receive the default array of rehype and remark plugins for further customization. Both custom functions ought to be passed to the third `renderText()` parameter. An example follows:

:::note
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';

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

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

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

## Props

### additionalMessageInputProps
Expand Down Expand Up @@ -434,7 +478,7 @@ Custom function to render message text content.

| Type | Default |
| -------- | -------------------------------------------------------------------------------------- |
| function | [renderText](https://github.com/GetStream/stream-chat-react/blob/master/src/utils.tsx) |
| function | <GHComponentLink text='renderText' path='/Message/renderText/renderText.tsx'/> |

### retrySendMessage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ Custom function to render message text content (overrides the function stored in

| Type | Default |
| -------- | -------------------------------------------------------------------------------------- |
| function | [renderText](https://github.com/GetStream/stream-chat-react/blob/master/src/utils.tsx) |
| function | <GHComponentLink text='renderText' path='/Message/renderText/renderText.tsx'/> |

### setEditingState

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ Custom function to render message text content.

| Type | Default |
| -------- | -------------------------------------------------------------------------------------- |
| function | [renderText](https://github.com/GetStream/stream-chat-react/blob/master/src/utils.tsx) |
| function | <GHComponentLink text='renderText' path='/Message/renderText/renderText.tsx'/> |

### retrySendMessage

Expand Down
115 changes: 0 additions & 115 deletions src/__tests__/utils.test.js

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/AutoCompleteTextarea/List.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import clsx from 'clsx';

import { useComponentContext } from '../../context/ComponentContext';
import { useChatContext } from '../../context/ChatContext';
import { escapeRegExp } from '../../utils';

import { Item } from './Item';
import { DefaultSuggestionListHeader } from './Header';
import { escapeRegExp } from '../Message/renderText';

export const List = ({
className,
Expand Down
3 changes: 1 addition & 2 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react';

import { getWholeChar } from '../../utils';
import { getWholeChar } from '../../utils/getWholeChar';

import type { UserResponse } from 'stream-chat';

Expand Down
2 changes: 1 addition & 1 deletion src/components/Message/FixedHeightMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useChatContext } from '../../context/ChatContext';
import { useComponentContext } from '../../context/ComponentContext';
import { useMessageContext } from '../../context/MessageContext';
import { useTranslationContext } from '../../context/TranslationContext';
import { renderText } from '../../utils';
import { renderText } from './renderText';

import type { TranslationLanguages } from 'stream-chat';

Expand Down
4 changes: 2 additions & 2 deletions src/components/Message/MessageText.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useMemo } from 'react';

import { QuotedMessage as DefaultQuotedMessage } from './QuotedMessage';
import { messageHasAttachments } from './utils';
import { isOnlyEmojis, messageHasAttachments } from './utils';

import { useComponentContext, useMessageContext, useTranslationContext } from '../../context';
import { renderText as defaultRenderText, isOnlyEmojis } from '../../utils';
import { renderText as defaultRenderText } from './renderText';

import type { TranslationLanguages } from 'stream-chat';
import type { MessageContextValue, StreamMessage } from '../../context';
Expand Down
1 change: 1 addition & 0 deletions src/components/Message/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export * from './MessageStatus';
export * from './MessageText';
export * from './MessageTimestamp';
export * from './QuotedMessage';
export * from './renderText';
export * from './types';
export * from './utils';
21 changes: 21 additions & 0 deletions src/components/Message/renderText/Anchor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import clsx from 'clsx';
import React, { ComponentProps } from 'react';
import { ReactMarkdownProps } from 'react-markdown/lib/complex-types';

export const Anchor = ({ children, href }: ComponentProps<'a'> & ReactMarkdownProps) => {
const isEmail = href?.startsWith('mailto:');
const isUrl = href?.startsWith('http');

if (!href || (!isEmail && !isUrl)) return <>{children}</>;

return (
<a
className={clsx({ 'str-chat__message-url-link': isUrl })}
href={href}
rel='nofollow noreferrer noopener'
target='_blank'
>
{children}
</a>
);
};
8 changes: 8 additions & 0 deletions src/components/Message/renderText/Emoji.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react';
import { ReactMarkdownProps } from 'react-markdown/lib/complex-types';

export const Emoji = ({ children }: ReactMarkdownProps) => (
<span className='inline-text-emoji' data-testid='inline-text-emoji'>
{children}
</span>
);
31 changes: 31 additions & 0 deletions src/components/Message/renderText/Mention.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';

import type { ReactMarkdownProps } from 'react-markdown/lib/complex-types';
import type { UserResponse } from 'stream-chat';
import type { DefaultStreamChatGenerics } from '../../../types/types';

export type MentionProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = ReactMarkdownProps & {
/**
* @deprecated will be removed in the next major release, transition to using `node.mentionedUser` instead
*/
mentioned_user: UserResponse<StreamChatGenerics>;
node: {
/**
* @deprecated will be removed in the next major release, transition to using `node.mentionedUser` instead
*/
mentioned_user: UserResponse<StreamChatGenerics>;
mentionedUser: UserResponse<StreamChatGenerics>;
};
};
export const Mention = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({
children,
node: { mentionedUser },
}: MentionProps<StreamChatGenerics>) => (
<span className='str-chat__message-mention' data-user-id={mentionedUser.id}>
{children}
</span>
);
Loading
Loading