Skip to content

Commit

Permalink
feat: allow custom attachments (#2383)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinCupela authored May 9, 2024
1 parent 6a3a7f8 commit c751670
Show file tree
Hide file tree
Showing 52 changed files with 2,035 additions and 594 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ Function that runs onSubmit to the underlying `textarea` component.
Allows to hide MessageInput's send button. Used by `MessageSimple` to hide the send button in `EditMessageForm`. Received from `MessageInputProps`.

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

### imageOrder
Expand Down Expand Up @@ -438,6 +438,28 @@ If true, triggers typing events on text input keystroke.
| ------- | ------- |
| boolean | true |

### removeAttachments

Function to remove an attachment objects from the `attachments` array in the `MessageInputState`.

| Type |
| ---------------------- |
| (id: string[]) => void |

```jsx
const Component = () => {
const { attachments, removeAttachments } = useMessageInputContext();

return (
<div>
{attachments.map((att) => (
<button onClick={() => removeAttachments([att.localMetadata.id])}>Remove</button>
))}
</div>
);
};
```

### removeFile

Function to remove a file from the `fileUploads` mapping.
Expand Down Expand Up @@ -526,6 +548,33 @@ Function to upload an array of files to the `fileUploads` and `imageUploads` map
| ----------------------------------- |
| (files: FileList \| File[]) => void |

### upsertAttachments

Function that adds or updates `attachments` array in `MessageInputState`. Accepts an array of objects.

| Type |
| -------------------------------------------------------------------------------------------------- |
| `(attachments: (Attachment<StreamChatGenerics> \| LocalAttachment<StreamChatGenerics>)[]) => void` |

```jsx
const Component = () => {
const { upsertAttachments } = useMessageInputContext();

const handleSelect = (location) => {
upsertAttachments([
{
type: 'geolocation',
longitude: location.longitude,
latitude: location.latitude,
name: location.name,
},
]);
};

// ...
};
```

### useMentionsTransliteration

If true, will use an optional dependency to support transliteration in the input for mentions. See: https://github.com/sindresorhus/transliterate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ The following UI components are available for use:
- [`SendButton`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/icons.tsx) - on click, sends a
message to the currently active channel

- [`UploadsPreview`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/UploadsPreview.tsx) - displays
a list of uploaded files prior to sending the message
- [`AttachmentPreviewList`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/AttachmentPreviewList/AttachmentPreviewList.tsx) - displays
a list of message attachments

## ChatAutoComplete Props

Expand Down Expand Up @@ -155,8 +155,46 @@ Function to send a message to the currently active channel.
| ----------------------------------------- |
| (event: React.BaseSyntheticEvent) => void |

## UploadsPreview
## AttachmentPreviewList

:::note
`UploadsPreview` only consumes context and does not accept any optional props.
:::
Renders message attachments in preview. The default (supported) message attachment types are:

- `audio`
- `video`
- `image`
- `voiceRecording`
- `file`

If the attachment object has property `og_scrape_url` or `title_link`, then it is rendered be [`LinkPreviewList` component](#linkpreviewlist-props) and not `AttachmentPreviewList`.

### FileAttachmentPreview

Custom component to be rendered for attachments of types `'file'`, `'video'`, `'audio'`.

| Type |
| ------------------------------------------- |
| `ComponentType<FileAttachmentPreviewProps>` |

### ImageAttachmentPreview

Custom component to be rendered for uploaded `'image'` attachment.

| Type |
| -------------------------------------------- |
| `ComponentType<ImageAttachmentPreviewProps>` |

### UnsupportedAttachmentPreview

Custom component to be rendered for attachment that is not recognized as any of the default types.

| Type |
| -------------------------------------------------- |
| `ComponentType<UnsupportedAttachmentPreviewProps>` |

### VoiceRecordingPreview

Custom component to preview audio recorded using [`AudioRecorder` component](../audio_recorder).

| Type |
| ------------------------------------------- |
| `ComponentType<VoiceRecordingPreviewProps>` |
75 changes: 75 additions & 0 deletions docusaurus/docs/React/guides/message-input/attachment-previews.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
id: attachment_previews
title: Attachment Previews in Message Input
---

In this section we will focus on how to customize attachment previews display in `MessageInput` component. The attachment previews are rendered by [`AttachmentPreviewList` component](../../../components/message-input-components/ui_components#attachmentpreviewlist).

## Customize the rendering of default attachment previews

The default attachment types recognized by `AttachmentPreviewList` are:

- `audio`
- `video`
- `image`
- `voiceRecording`
- `file`

If the attachment object has property `og_scrape_url` or `title_link`, then it is rendered by [`LinkPreviewList` component](#linkpreviewlist-props) and not `AttachmentPreviewList`.

To customize attachment previews we need to override `AttachmentsPreviewList` component.

```jsx
import { VideoAttachmentPreview } from './AttachmentPreviews';

const CustomAttachmentPreviewList = () => (
<AttachmentPreviewList VideoAttachmentPreview={VideoAttachmentPreview} />
);
```

And pass it to `Channel` component.

```jsx
<Channel AttachmentPreviewList={CustomAttachmentPreviewList} />
```

We can customize the following preview components:

- `AudioAttachmentPreview`
- `FileAttachmentPreview`
- `ImageAttachmentPreview`
- `UnsupportedAttachmentPreview`
- `VideoAttachmentPreview`
- `VoiceRecordingPreview`

## Customize the rendering of custom attachment previews

It is possible to add custom attachments (objects) to composed messages via [`upsertAttachments` function](../../../components/contexts/message-input-context#upsertattachments) provided by `MessageInputContext`.

The custom attachments are not recognized by `AttachmentPreviewList` component and therefore rendered via `UnsupportedAttachmentPreview` component within `AttachmentPreviewList`. The component `UnsupportedAttachmentPreview` can be customized and handle all the custom attachment objects added to the message attachments.

```tsx
import { GeolocationPreview } from './GeolocationAttachmentPreview';
import type { UnsupportedAttachmentPreviewProps } from 'stream-chat-react';

const CustomAttachmentsPreview = (props: UnsupportedAttachmentPreviewProps) => {
const { attachment } = props;
if (attachment.type === 'geolocation') {
return <GeolocationPreview {...props} />;
}
// more conditions follow...
};
```

The custom component is then passed to custom `AttachmentsPreviewList` component which purpose is just to specify the custom `UnsupportedAttachmentPreview` component.

```jsx
import { CustomAttachmentsPreview } from './AttachmentPreviewList';
const CustomAttachmentPreviewList = () => (
<AttachmentPreviewList UnsupportedAttachmentPreview={CustomAttachmentsPreview} />
);
```

```jsx
<Channel AttachmentPreviewList={CustomAttachmentPreviewList} />
```
1 change: 1 addition & 0 deletions docusaurus/sidebars-react.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"Message Input": [
"guides/theming/input_ui",
"guides/customization/link-previews",
"guides/message-input/attachment_previews",
"guides/customization/override_submit_handler",
"guides/customization/persist_input_text_in_localstorage",
"guides/customization/suggestion_list",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,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.14.0",
"@stream-io/stream-chat-css": "^4.16.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^13.1.1",
"@testing-library/react-hooks": "^8.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Attachment/AttachmentContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { VoiceRecording as DefaultVoiceRecording } from './VoiceRecording';
import { Gallery as DefaultGallery, ImageComponent as DefaultImage } from '../Gallery';
import { Card as DefaultCard } from './Card';
import { FileAttachment as DefaultFile } from './FileAttachment';
import { NullComponent as DefaultUnsupportedAttachment } from './UnsupportedAttachment';
import { UnsupportedAttachment as DefaultUnsupportedAttachment } from './UnsupportedAttachment';
import {
AttachmentContainerProps,
isGalleryAttachmentType,
Expand Down
24 changes: 17 additions & 7 deletions src/components/Attachment/UnsupportedAttachment.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from 'react';
import { FileIcon } from '../ReactFileUtilities';
import { useTranslationContext } from '../../context';
import type { Attachment } from 'stream-chat';
import type { DefaultStreamChatGenerics } from '../../types/types';

Expand All @@ -12,13 +14,21 @@ export const UnsupportedAttachment = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({
attachment,
}: UnsupportedAttachmentProps<StreamChatGenerics>) => (
<div>
<div>
Unsupported attachment type <strong>{attachment.type ?? 'unknown'}</strong>
}: UnsupportedAttachmentProps<StreamChatGenerics>) => {
const { t } = useTranslationContext('UnsupportedAttachment');
return (
<div className='str-chat__message-attachment-unsupported' data-testid='attachment-unsupported'>
<FileIcon className='str-chat__file-icon' version={'2'} />
<div className='str-chat__message-attachment-unsupported__metadata'>
<div
className='str-chat__message-attachment-unsupported__title'
data-testid='unsupported-attachment-title'
>
{attachment.title || t<string>('Unsupported attachment')}
</div>
</div>
</div>
<code>{JSON.stringify(attachment, null, 4)}</code>;
</div>
);
);
};

export const NullComponent = () => null;
35 changes: 20 additions & 15 deletions src/components/Attachment/__tests__/Attachment.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {
import { Attachment } from '../Attachment';
import { SUPPORTED_VIDEO_FORMATS } from '../utils';
import { generateScrapedVideoAttachment } from '../../../mock-builders';
import { ChannelStateProvider } from '../../../context';

const UNSUPPORTED_ATTACHMENT_TEST_ID = 'attachment-unsupported';

const Audio = (props) => <div data-testid='audio-attachment'>{props.customTestId}</div>;
const Card = (props) => <div data-testid='card-attachment'>{props.customTestId}</div>;
Expand Down Expand Up @@ -46,23 +49,25 @@ const ATTACHMENTS = {

const renderComponent = (props) =>
render(
<Attachment
AttachmentActions={AttachmentActions}
Audio={Audio}
Card={Card}
File={File}
Gallery={Gallery}
Image={Image}
Media={Media}
{...props}
/>,
<ChannelStateProvider value={{}}>
<Attachment
AttachmentActions={AttachmentActions}
Audio={Audio}
Card={Card}
File={File}
Gallery={Gallery}
Image={Image}
Media={Media}
{...props}
/>
</ChannelStateProvider>,
);

describe('attachment', () => {
describe('non-scraped content', () => {
it('should render empty attachment list if unrecognized type', () => {
const { container } = renderComponent({ attachments: [{}] });
expect(container.firstChild).toBeEmptyDOMElement();
it('should render unsupported attachment if unrecognized type', () => {
renderComponent({ attachments: [{}] });
expect(screen.getByTestId(UNSUPPORTED_ATTACHMENT_TEST_ID)).toBeInTheDocument();
});

const cases = {
Expand Down Expand Up @@ -122,8 +127,8 @@ describe('attachment', () => {
og_scrape_url: undefined,
title_link: undefined,
});
const { container } = renderComponent({ attachments: [attachment] });
expect(container.firstChild).toBeEmptyDOMElement();
renderComponent({ attachments: [attachment] });
expect(screen.getByTestId(UNSUPPORTED_ATTACHMENT_TEST_ID)).toBeInTheDocument();
});

const cases = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ exports[`QuotedVoiceRecording should render the component 1`] = `
<div
class="str-chat__message-attachment__voice-recording-widget__title"
data-testid="voice-recording-title"
title="audio_recording_Mon Feb 05 16:21:34 PST 2024"
title="audio_recording_Mon Feb 05 16:21:34 PST 2024.aac"
>
audio_recording_Mon Feb 05 16:21:34 PST 2024
audio_recording_Mon Feb 05 16:21:34 PST 2024.aac
</div>
<div
class="str-chat__message-attachment__voice-recording-widget__audio-state"
Expand Down
Loading

0 comments on commit c751670

Please sign in to comment.