# Attachment previews

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

The composer shows what a message will carry above the input: each attachment with its upload progress, a voice recording, link previews and a location. This recipe replaces those previews, one kind at a time. How attachments are added and uploaded is explained in [Attachments and uploads](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/attachments-and-uploads/), and where the previews sit in [Message composer UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/message-composer-ui/#previews).

## Best practices

- Replace one kind of preview through the props of `AttachmentPreviewList`, and register the wrapped list under the `AttachmentPreviewList` key. The other kinds keep their defaults.
- To change only the upload progress or the file sizes, override the small parts first: `ProgressIndicator`, `UploadedSizeIndicator` and `FileSizeIndicator`. They apply to every preview and to message attachments.
- Keep the ✕ and the retry button working in a preview of your own: call `removeAttachments` and `handleRetry`, which every preview receives.
- Render attachment types of your own in `UnsupportedAttachmentPreview`, and fall back to the default for the rest.
- Replace voice recording previews through `VoiceRecordingPreviewSlot`. They aren't part of `AttachmentPreviewList`.
- Keep previews compact, so they don't push the input out of view.

## Default attachment preview types

`AttachmentPreviewList` renders one preview per attachment, picked by the attachment's type. Each component is a prop of the list:

| Attachment                            | Prop                           | Default                                                                    |
| ------------------------------------- | ------------------------------ | -------------------------------------------------------------------------- |
| An image                              | `ImageAttachmentPreview`       | A thumbnail with the progress ring over it. Clicking it opens the gallery. |
| A video                               | `VideoAttachmentPreview`       | The same as an image                                                       |
| An audio file                         | `AudioAttachmentPreview`       | The same as a file                                                         |
| Any other file                        | `FileAttachmentPreview`        | A row with the file's icon, name, progress ring and size                   |
| An attachment of a type your app adds | `UnsupportedAttachmentPreview` | A row with a generic icon and the ✕                                        |

Three previews are rendered outside the list:

- Voice recordings, by `VoiceRecordingPreviewSlot`, above the list. It shows the first recording.
- Link previews, by `LinkPreviewList`. See [Link previews](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/link-previews/).
- The location to share. It has no override key and isn't exported. Only a composer layout of your own, as in [Custom composer](https://getstream.io/chat/docs/sdk/react/v15/recipes/composer/custom-composer/), can show it differently.

Every preview receives the attachment and two functions:

- `removeAttachments(ids)` removes attachments from the composer. Removing one that is still uploading cancels its upload.
- `handleRetry(attachment)` uploads a failed attachment again.

Image and video previews also receive `openPreview()`, which opens the gallery. The upload state is in `attachment.localMetadata`: `uploadState` is `pending`, `uploading`, `finished`, `failed` or `blocked`, and `uploadProgress` goes from 0 to 100.

## Customize default attachment previews

Of the defaults in the table, only `UnsupportedAttachmentPreview` is exported. A replacement for the image, video, audio or file preview renders the whole preview. This one shows images as small square thumbnails:

```tsx title="src/ImageAttachmentPreview.tsx"
import type { ImageAttachmentPreviewProps } from "stream-chat-react";
import {
  RemoveAttachmentPreviewButton,
  UploadProgressIndicator,
} from "stream-chat-react";

export const ImageAttachmentPreview = ({
  attachment,
  handleRetry,
  openPreview,
  removeAttachments,
}: ImageAttachmentPreviewProps) => {
  const { id, previewUri, uploadProgress, uploadState } =
    attachment.localMetadata;
  // The local file until the upload finishes, then the uploaded image.
  const src = attachment.image_url || previewUri;

  return (
    <div className="image-preview">
      <button
        aria-label="Open image"
        className="image-preview__thumbnail"
        disabled={uploadState !== "finished"}
        onClick={openPreview}
        type="button"
      >
        {src && <img alt="" src={src} />}
      </button>
      {uploadState === "uploading" && (
        <UploadProgressIndicator uploadProgress={uploadProgress} />
      )}
      {uploadState === "failed" && (
        <button onClick={() => handleRetry(attachment)} type="button">
          Retry
        </button>
      )}
      <RemoveAttachmentPreviewButton
        onClick={() => removeAttachments([id])}
        uploadState={uploadState}
      />
    </div>
  );
};
```

```css title="src/ImageAttachmentPreview.css"
.image-preview {
  position: relative;
}

.image-preview__thumbnail {
  border: 0;
  border-radius: 8px;
  height: 48px;
  overflow: hidden;
  padding: 0;
  width: 48px;
}

.image-preview__thumbnail img {
  height: 100%;
  object-fit: cover;
  width: 100%;
}
```

`RemoveAttachmentPreviewButton` is the SDK's ✕. Its label says "Cancel upload" while the file is uploading, and "Remove attachment" after. `UploadProgressIndicator` shows the progress ring, or a spinner while the progress is unknown.

Pass the preview to `AttachmentPreviewList`, and register the list:

```tsx title="src/ChannelWithImagePreviews.tsx"
import {
  AttachmentPreviewList,
  Channel,
  MessageComposer,
  MessageList,
  WithComponents,
} from "stream-chat-react";
import { ImageAttachmentPreview } from "./ImageAttachmentPreview";
import "./ImageAttachmentPreview.css";

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

// Declared once, so WithComponents gets the same object on every render.
const overrides = { AttachmentPreviewList: CustomAttachmentPreviewList };

export const ChannelWithImagePreviews = () => (
  <WithComponents overrides={overrides}>
    <Channel channel={channel}>
      <MessageList />
      <MessageComposer />
    </Channel>
  </WithComponents>
);
```

The props types of the image, file and voice recording previews are exported: `ImageAttachmentPreviewProps`, `FileAttachmentPreviewProps` and `VoiceRecordingPreviewProps`. Those of the video and audio previews aren't.

## Upload progress and size UI

The default previews build their upload UI from small parts, each with an override key:

| Override key            | Shows                                                                               | Receives                     |
| ----------------------- | ----------------------------------------------------------------------------------- | ---------------------------- |
| `ProgressIndicator`     | The progress ring while a file uploads                                              | `percent`, from 0 to 100     |
| `UploadedSizeIndicator` | "1.2 MB / 3 MB" on a file row while it uploads                                      | `uploadedBytes`, `fullBytes` |
| `FileSizeIndicator`     | A file's size, on a file row once it's uploaded and on file attachments in messages | `fileSize`                   |

While the progress is unknown, or every byte is sent and the server hasn't answered yet, the previews show `LoadingIndicator` instead of the ring. That key also replaces the SDK's other spinners.

The same parts show the progress on message attachments when [sending with pending uploads](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/sending-with-pending-uploads/) is turned on, so an attachment looks the same before and after the user presses send. To replace that whole block on message attachments, the ring and the sizes together, override `AttachmentUploadProgressIndicator`. It receives the `attachment`, or a gallery's `attachments`.

This override shows the progress as a percentage:

```tsx title="src/UploadPercent.tsx"
import type { PropsWithChildren } from "react";
import type { ProgressIndicatorProps } from "stream-chat-react";
import { WithComponents } from "stream-chat-react";

const UploadPercent = ({ percent }: ProgressIndicatorProps) => (
  <span className="upload-percent">{Math.round(percent)}%</span>
);

const overrides = { ProgressIndicator: UploadPercent };

export const AppComponents = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);
```

## Customize custom attachment previews

An attachment your app adds to the composer, for example with `AttachmentManager.upsertAttachments()`, has a type the list doesn't know. It goes to `UnsupportedAttachmentPreview`. Branch on your own type there, and render the default for everything else:

```tsx title="src/CustomAttachmentPreviewList.tsx"
import type { UnsupportedAttachmentPreviewProps } from "stream-chat-react";
import {
  AttachmentPreviewList,
  UnsupportedAttachmentPreview,
} from "stream-chat-react";

const EventOrUnsupportedPreview = (
  props: UnsupportedAttachmentPreviewProps,
) => {
  const { attachment, removeAttachments } = props;

  if (attachment.type !== "event") {
    return <UnsupportedAttachmentPreview {...props} />;
  }

  return (
    <div className="event-preview">
      <span>📅 {attachment.title}</span>
      <button
        aria-label="Remove event"
        onClick={() => removeAttachments([attachment.localMetadata.id])}
        type="button"
      >
        ✕
      </button>
    </div>
  );
};

export const CustomAttachmentPreviewList = () => (
  <AttachmentPreviewList
    UnsupportedAttachmentPreview={EventOrUnsupportedPreview}
  />
);
```

Register `CustomAttachmentPreviewList` under the `AttachmentPreviewList` key, as in the image example. To render the same attachment in the message list, see [Change how attachments render](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/attachments-and-uploads/#change-how-attachments-render).

## Customize voice recording previews

A voice recording waiting to be sent shows in `VoiceRecordingPreviewSlot`, above the other previews. Give it your own `VoiceRecordingPreview`:

```tsx title="src/ChannelWithVoicePreview.tsx"
import type { VoiceRecordingPreviewProps } from "stream-chat-react";
import {
  Channel,
  MessageComposer,
  MessageList,
  RemoveAttachmentPreviewButton,
  VoiceRecordingPreviewSlot,
  WithComponents,
} from "stream-chat-react";

const CompactVoiceRecordingPreview = ({
  attachment,
  removeAttachments,
}: VoiceRecordingPreviewProps) => {
  const { id, previewUri, uploadState } = attachment.localMetadata;

  return (
    <div className="voice-preview">
      <audio controls src={attachment.asset_url || previewUri} />
      <RemoveAttachmentPreviewButton
        onClick={() => removeAttachments([id])}
        uploadState={uploadState}
      />
    </div>
  );
};

const CustomVoiceRecordingPreviewSlot = () => (
  <VoiceRecordingPreviewSlot
    VoiceRecordingPreview={CompactVoiceRecordingPreview}
  />
);

const overrides = {
  VoiceRecordingPreviewSlot: CustomVoiceRecordingPreviewSlot,
};

export const ChannelWithVoicePreview = () => (
  <WithComponents overrides={overrides}>
    <Channel channel={channel}>
      <MessageList />
      <MessageComposer audioRecordingEnabled />
    </Channel>
  </WithComponents>
);
```

Voice recording is off until `MessageComposer` gets `audioRecordingEnabled`. [Voice recordings and audio playback](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/voice-recordings-and-audio-playback/) covers recording and the players.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/recipes/composer/attachment-previews/](https://getstream.io/chat/docs/sdk/react/v15/recipes/composer/attachment-previews/).