# Attachments

An attachment is a file, or other content, that goes out with a message: an image, a video, an audio file, a voice recording or any other file, or content you build without an upload. While the user writes the message, the composer keeps its attachments in the attachment manager, `messageComposer.attachmentManager`. Previews of links in the text are kept apart, by the [link previews manager](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/link-previews/).

## What it holds

The attachment manager's state is in `attachmentManager.state`. Its one field, `attachments`, is the list of **local attachments**. A local attachment is the attachment the message will carry, plus a `localMetadata` field that the composer uses while the message is written. `attachmentManager.attachments` reads the current list.

In React Native, a file is a `FileReference`: an object with the file's `uri`, `name`, `type` (its MIME type) and `size`, as the SDK's pickers return it. A `FileReference` can also carry `height` and `width` for an image, `thumb_url` for a video and `duration` for a voice recording.

Each attachment has a `type`. For a file the user adds, the attachment manager sets it from the MIME type, and fills in the fields below. The URL fields are set when the upload finishes:

| `type`                       | Uploaded | What it is                                                                     | Fields                                                                                                                                                                                                              |
| ---------------------------- | -------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `image`                      | yes      | A file whose MIME type starts with `image/`, except Photoshop files.           | `image_url`, `fallback` (the file name), `original_width` and `original_height` when the file reference has `width` and `height`                                                                                    |
| `video`                      | yes      | A file whose MIME type contains `video/`.                                      | `asset_url`, `title` (the file name), `thumb_url` when the file reference has one                                                                                                                                   |
| `audio`                      | yes      | A file whose MIME type contains `audio/`.                                      | `asset_url`, `title`                                                                                                                                                                                                |
| `file`                       | yes      | Any other file.                                                                | `asset_url`, `title`                                                                                                                                                                                                |
| `voiceRecording`             | yes      | A recording made in the app.                                                   | `asset_url`, `title`, `custom.duration`, `custom.waveform_data`                                                                                                                                                     |
| any other, such as `product` | no       | An attachment of your own, added with `AttachmentManager.upsertAttachments()`. | Whichever you set. Data of your own goes in `custom`, typed by adding its fields to `CustomAttachmentData`, as [Data conventions](https://getstream.io/chat/docs/sdk/react-native/state/state-and-entities/data-conventions/) explains. |

Every type that comes from a file also gets `custom.file_size` and `custom.mime_type`, and `thumb_url` when the upload returns a thumbnail.

Here is what each one looks like in `attachmentManager.attachments` once its upload has finished:

**image**

```ts
import type { LocalImageAttachment } from "stream-chat";

const image: LocalImageAttachment = {
  type: "image",
  image_url: "https://us-east.stream-io-cdn.com/1234/images/beach.jpg",
  fallback: "beach.jpg",
  original_width: 1600,
  original_height: 1200,
  custom: { file_size: 482133, mime_type: "image/jpeg" },
  localMetadata: {
    id: "4f1c2a90-6b3e-4d55-9a1e-0c7b2f8d3e11",
    file: {
      name: "beach.jpg",
      size: 482133,
      type: "image/jpeg",
      uri: "file:///data/user/0/com.example/cache/beach.jpg",
    },
    uploadState: "finished",
  },
};
```

**video**

```ts
import type { LocalVideoAttachment } from "stream-chat";

const video: LocalVideoAttachment = {
  type: "video",
  asset_url: "https://us-east.stream-io-cdn.com/1234/attachments/trip.mp4",
  thumb_url: "https://us-east.stream-io-cdn.com/1234/images/trip-thumb.jpg",
  title: "trip.mp4",
  custom: { file_size: 8204311, mime_type: "video/mp4" },
  localMetadata: {
    id: "9b7e1d24-3c8f-4a61-b2d0-5e6f7a8b9c01",
    file: {
      name: "trip.mp4",
      size: 8204311,
      type: "video/mp4",
      uri: "file:///data/user/0/com.example/cache/trip.mp4",
    },
    uploadState: "finished",
  },
};
```

**audio**

```ts
import type { LocalAudioAttachment } from "stream-chat";

const audio: LocalAudioAttachment = {
  type: "audio",
  asset_url: "https://us-east.stream-io-cdn.com/1234/attachments/podcast.mp3",
  title: "podcast.mp3",
  custom: { file_size: 3411002, mime_type: "audio/mpeg" },
  localMetadata: {
    id: "2c4d6e8f-1a3b-4c5d-8e9f-0a1b2c3d4e5f",
    file: {
      name: "podcast.mp3",
      size: 3411002,
      type: "audio/mpeg",
      uri: "file:///data/user/0/com.example/cache/podcast.mp3",
    },
    uploadState: "finished",
  },
};
```

**file**

```ts
import type { LocalFileAttachment } from "stream-chat";

const file: LocalFileAttachment = {
  type: "file",
  asset_url: "https://us-east.stream-io-cdn.com/1234/attachments/agenda.pdf",
  title: "agenda.pdf",
  custom: { file_size: 120577, mime_type: "application/pdf" },
  localMetadata: {
    id: "7a8b9c0d-1e2f-4a3b-9c4d-5e6f7a8b9c0d",
    file: {
      name: "agenda.pdf",
      size: 120577,
      type: "application/pdf",
      uri: "file:///data/user/0/com.example/cache/agenda.pdf",
    },
    uploadState: "finished",
  },
};
```

**voiceRecording**

```ts
import type { LocalVoiceRecordingAttachment } from "stream-chat";

const recording: LocalVoiceRecordingAttachment = {
  type: "voiceRecording",
  asset_url: "https://us-east.stream-io-cdn.com/1234/attachments/recording.aac",
  title: "recording.aac",
  custom: {
    duration: 4.2,
    file_size: 33602,
    mime_type: "audio/aac",
    waveform_data: [0.12, 0.4, 0.85, 0.6, 0.3, 0.1],
  },
  localMetadata: {
    id: "5d6e7f80-9a1b-4c2d-8e3f-4a5b6c7d8e9f",
    file: {
      duration: 4.2,
      name: "recording.aac",
      size: 33602,
      type: "audio/aac",
      uri: "file:///data/user/0/com.example/cache/recording.aac",
      waveform_data: [0.12, 0.4, 0.85, 0.6, 0.3, 0.1],
    },
    uploadState: "finished",
  },
};
```

**giphy**

A Giphy attachment comes from the `/giphy` command: the server adds it to the message, and the attachment manager never uploads one. This is how a sent message carries it:

```ts
import type { GiphyAttachment } from "stream-chat";

const size = (width: string, height: string, url: string) => ({
  frames: "24",
  height,
  size: "301233",
  url,
  width,
});

const giphy: GiphyAttachment = {
  type: "giphy",
  custom: {},
  title: "hello",
  title_link: "https://giphy.com/gifs/hello-3o7TKSjRrfIPjeiVyM",
  thumb_url: "https://media.giphy.com/media/3o7TKSjRrfIPjeiVyM/giphy.gif",
  giphy: {
    original: size(
      "480",
      "270",
      "https://media.giphy.com/media/3o7TKSjRrfIPjeiVyM/giphy.gif",
    ),
    fixed_height: size(
      "356",
      "200",
      "https://media.giphy.com/media/3o7TKSjRrfIPjeiVyM/200.gif",
    ),
    fixed_height_downsampled: size(
      "356",
      "200",
      "https://media.giphy.com/media/3o7TKSjRrfIPjeiVyM/200_d.gif",
    ),
    fixed_height_still: size(
      "356",
      "200",
      "https://media.giphy.com/media/3o7TKSjRrfIPjeiVyM/200_s.gif",
    ),
    fixed_width: size(
      "200",
      "113",
      "https://media.giphy.com/media/3o7TKSjRrfIPjeiVyM/200w.gif",
    ),
    fixed_width_downsampled: size(
      "200",
      "113",
      "https://media.giphy.com/media/3o7TKSjRrfIPjeiVyM/200w_d.gif",
    ),
    fixed_width_still: size(
      "200",
      "113",
      "https://media.giphy.com/media/3o7TKSjRrfIPjeiVyM/200w_s.gif",
    ),
  },
};
```

A shared location isn't an attachment: it goes out in the message's `shared_location` field, as [Location](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/location/) explains.

`localMetadata` holds:

| Field                       | What it holds                                                                                                                                                                               |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                        | The attachment's id in the composer. The only field every local attachment has. `AttachmentManager.removeAttachments()` and `AttachmentManager.upsertAttachments()` find attachments by it. |
| `file`                      | The `FileReference` to upload. Only attachments that are uploaded have one, and an attachment restored from a draft or a message to edit has none.                                          |
| `previewUri`                | A local address of the file, the `uri` of its `FileReference`, for showing it before it's uploaded. It's removed when the upload finishes.                                                  |
| `uploadState`               | Where the upload is, as [Uploads](#uploads) explains. An attachment with nothing to upload has none.                                                                                        |
| `uploadProgress`            | How far the upload is, from 0 to 100, while progress tracking is on.                                                                                                                        |
| `uploadConfirmationPending` | `true` when the whole file is sent but the server hasn't confirmed it yet. Show progress without a percentage then.                                                                         |
| `uploadPermissionCheck`     | Why the upload was blocked: `{ uploadBlocked, reason }`, where `reason` names the upload setting that refused it.                                                                           |

Fields of your own can go into `localMetadata` too. They stay in the composer and are never sent.

A draft or a message to edit that is loaded into the composer brings its attachments back. An uploaded one, such as an image or a video, comes back with `uploadState: 'finished'` and no `file`, so a component that renders `localMetadata.file` has to allow for a missing one. The other attachments come back with only an `id`.

## Adding and removing attachments

A file the user adds goes through two **attachment middleware** pipelines, which work like the [text middleware](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/text/#changing-the-text):

- `prepare` runs before the upload, in `attachmentManager.preUploadMiddlewareExecutor`. It checks the file against the app's upload settings and can block it. To accept only some files, add a `prepare` function that discards the others.
- `postProcess` runs after the upload, in `attachmentManager.postUploadMiddlewareExecutor`, and sets the attachment's URL.

[Composer middleware](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/composer-middleware/#uploads) lists the built-in functions. The last column says which pipelines a call runs.

What an attachment picker and its previews need, and the call for each:

| To...                                                                | Call                                               | What happens                                                                                                                                                       | Attachment middleware         |
| -------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- |
| add files the user picked or took                                    | `AttachmentManager.uploadFiles(files)`             | Adds each file and uploads it at once, up to the free slots. Does nothing while `attachmentManager.isUploadEnabled` is `false`, as [Uploads](#uploads) explains.   | `prepare`, then `postProcess` |
| add one file                                                         | `AttachmentManager.uploadFile(file)`               | Adds the file and uploads it at once. It doesn't check `attachmentManager.isUploadEnabled` or the free slots: check them first.                                    | `prepare`, then `postProcess` |
| upload an attachment again, such as a failed one                     | `AttachmentManager.uploadFile(attachment)`         | Uploads it in place, as [Retrying a failed upload](#retrying-a-failed-upload) explains. It doesn't check `attachmentManager.isUploadEnabled`: check it first.      | `prepare`, then `postProcess` |
| remove attachments                                                   | `AttachmentManager.removeAttachments(localIds)`    | Removes them by `localMetadata.id` and cancels their uploads.                                                                                                      | none                          |
| add an attachment that needs no upload, such as one built from a URL | `AttachmentManager.upsertAttachments(attachments)` | Adds them, or merges them into the ones with the same `localMetadata.id`. An attachment without a local id gets a new one each time, so it can't be updated later. | none                          |

The SDK's pickers and the camera go through `useMessageInputContext().uploadNewFile(file)`. It gives a file with no MIME type one worked out from its name, compresses an image with `compressImageQuality`, calls `AttachmentManager.uploadFiles()`, and removes an attachment that came back `blocked`, after the notification for it was published. `pickFile()`, `pickAndUploadImageFromNativePicker()` and `takeAndUploadImage()` of the same context call it, and they stop with an alert when `attachmentManager.availableUploadSlots` is 0.

The composer sends an attachment that has nothing to upload as it is. Give it only the `localMetadata.id` that identifies it, with no `file` and no `uploadState`:

```ts
import { generateRandomId } from "stream-chat-react-native";
import type { LocalAttachment, MessageComposer } from "stream-chat";

export const addProductCard = (composer: MessageComposer) => {
  const product: LocalAttachment = {
    type: "product",
    title: "Desk lamp",
    image_url: "https://shop.example.com/images/desk-lamp.jpg",
    custom: {},
    localMetadata: { id: generateRandomId() },
  };

  composer.attachmentManager.upsertAttachments([product]);
};
```

The predicates `isNonUploadAttachment()`, `isFinishedUpload()`, `isSendableAttachment()` and `isResolvableUpload()` are exported from `stream-chat`. An attachment is sendable when its upload is finished or when it has nothing to upload. `isResolvableUpload()` is true for an upload that is `pending` or `uploading`, which a send can wait for, and for one that is `failed`, which a send retries, as [Sending attachments](#sending-attachments) explains.

## Uploads

Only files go through an upload: the `image`, `video`, `audio`, `file` and `voiceRecording` attachments. An upload puts the file on a CDN, so that the message can carry a link to it, in five steps:

1. **The file is added.** Call `AttachmentManager.uploadFiles(files)`. The attachment manager creates a local attachment for each file, sets its `type` from the MIME type, and gives it a `previewUri`, so a UI can show the file at once. Its `uploadState` is `pending`.
2. **The file is checked.** The `prepare` middleware compares the file with the app's upload settings, such as the allowed types and size. A file they don't allow becomes `blocked` and is never uploaded.
3. **The file is uploaded,** to Stream's CDN, or to your own storage, as [Uploading to your own CDN](#uploading-to-your-own-cdn) explains. Its `uploadState` is `uploading`, and `localMetadata.uploadProgress` follows it.
4. **The URL is added to the attachment.** The `postProcess` middleware writes the URL the upload returned into `image_url` or `asset_url`, and `thumb_url` when there is one. The `uploadState` becomes `finished`. If the upload fails, it becomes `failed`.
5. **The message is sent.** Call `MessageComposer.send()`. It puts every sendable attachment into the message, without its `localMetadata`. While an upload is still running, the send waits or is refused, as [Sending attachments](#sending-attachments) explains.

An attachment of your own skips steps 1 to 4. You add it with `AttachmentManager.upsertAttachments()`, as [Adding and removing attachments](#adding-and-removing-attachments) shows, and step 5 sends it with the rest. To drop a file at any step, call `AttachmentManager.removeAttachments([id])`, which also cancels its upload.

The SDK's audio recorder makes a `voiceRecording` attachment from a recording. Unless `audioRecordingSendOnComplete` is on, it uploads it with `AttachmentManager.uploadFile()`, while `attachmentManager.isUploadEnabled` is `true`. With it on, the recorder adds the attachment as a `pending` upload and sends the message, so the send has to upload it, which needs the `pendingUploadsEnabled` [setting](#settings).

Each attachment's `localMetadata.uploadState` tells where its upload is:

- `pending`: added, not uploading yet.
- `uploading`: on its way. `localMetadata.uploadProgress` goes from 0 to 100 while the `trackUploadProgress` [setting](#settings) is on, which is the default.
- `finished`: uploaded, with its URL set.
- `failed`: the request failed, and an `api:attachment:upload:failed` notification was published.
- `blocked`: refused before uploading, because the Stream app's upload settings refuse it. These are set in the Dashboard: the allowed and blocked file types, and the size limit, 100 MB when none is set. A `validation:attachment:upload:blocked` notification was published.

These getters count and list the attachments in each state:

| `uploadState` | Count                                      | Attachments                                        |
| ------------- | ------------------------------------------ | -------------------------------------------------- |
| `pending`     | `attachmentManager.pendingUploadsCount`    | `AttachmentManager.getUploadsByState("pending")`   |
| `uploading`   | `attachmentManager.uploadsInProgressCount` | `AttachmentManager.getUploadsByState("uploading")` |
| `finished`    | `attachmentManager.successfulUploadsCount` | `attachmentManager.successfulUploads`              |
| `failed`      | `attachmentManager.failedUploadsCount`     | `AttachmentManager.getUploadsByState("failed")`    |
| `blocked`     | `attachmentManager.blockedUploadsCount`    | `AttachmentManager.getUploadsByState("blocked")`   |

`attachmentManager.isUploadEnabled` says whether a file can be added now. Three things must hold: attachments are enabled, which also needs the channel type's `uploads` setting unless `customCdn` is set; a slot is free under the `maxNumberOfFilesPerMessage` setting; and the user has the `upload-file` capability, unless `customCdn` is set. `attachmentManager.availableUploadSlots` says how many more files fit. Each finished or running upload takes a slot. Pending, failed and blocked uploads, and attachments with nothing to upload, take none.

In a component, `useAttachmentManagerState()` returns the attachments with these counts, `isUploadEnabled` and `availableUploadSlots`.

### Retrying a failed upload

`AttachmentManager.uploadFile()` also takes an attachment already in the composer, and uploads it again in place. The attachment keeps its `localMetadata.id`, its fields, such as a voice recording's `custom.waveform_data` and `custom.duration`, and its position in `attachmentManager.attachments`. The retry runs the [attachment middleware](#adding-and-removing-attachments) again, so your own `prepare` and `postProcess` middleware runs on the retry too. It uploads `localMetadata.file`. If a `prepare` middleware of yours replaced the file on the first try, for example with a compressed image, it now runs on that replaced file, so make it skip a file it already changed.

`AttachmentManager.uploadFile()` also uploads an attachment you build yourself, with fields you set before the upload. The SDK's voice recorder works this way: it builds a `voiceRecording` attachment with `custom.waveform_data` and `custom.duration`, and uploads it with `AttachmentManager.uploadFile()`. Give the attachment's `localMetadata` the `file` and an `uploadState`, such as `'pending'`: a `file` without an `uploadState` doesn't compile. The attachment manager adds it and uploads `localMetadata.file`, and the attachment keeps the fields you set.

`AttachmentManager.uploadFile()` doesn't check `attachmentManager.isUploadEnabled`, so a retry button of your own checks it first. A failed attachment takes no upload slot, so retrying it needs a free one, and `attachmentManager.isUploadEnabled` is `false` when none is left. The SDK's attachment preview list retries this way and does nothing while `attachmentManager.isUploadEnabled` is `false`. `AttachmentManager.uploadFile()` always resolves to the attachment, so read its `localMetadata.uploadState` to know whether the upload worked:

```ts
import type { LocalUploadAttachment, MessageComposer } from "stream-chat";

const retryUpload = async (
  composer: MessageComposer,
  attachment: LocalUploadAttachment,
) => {
  const { attachmentManager } = composer;
  if (!attachmentManager.isUploadEnabled) return false;
  const result = await attachmentManager.uploadFile(attachment);
  return result.localMetadata.uploadState === "finished";
};
```

An attachment without `localMetadata.file` or `localMetadata.id` has nothing to upload. `AttachmentManager.uploadFile()` returns it unchanged and publishes an error notification.

### Uploading to your own CDN

By default, files go to Stream's CDN. To upload them to your own storage, register an upload request. It resolves with the file's URL, and it can report progress and stop when the user removes the file. Set `customCdn: true` when the files never reach Stream, so that Stream's `uploads` setting and the `upload-file` capability don't apply:

```ts
import { isFileReference, StreamChat } from "stream-chat";
import type { UploadRequestFn } from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

const uploadToMyStorage: UploadRequestFn = async (file, options) => {
  if (!isFileReference(file)) throw new Error("Expected a file reference");

  const body = new FormData();
  body.append("file", { name: file.name, type: file.type, uri: file.uri });

  const response = await fetch("https://uploads.example.com/files", {
    body,
    method: "POST",
    signal: options?.abortSignal,
  });
  const { url } = await response.json();

  return { file: url };
};

client.config.set({
  messageComposer: {
    attachments: { customCdn: true, doUploadRequest: uploadToMyStorage },
  },
});
```

[Attachments and uploads](https://getstream.io/chat/docs/sdk/react-native/features/rich-content/attachments-and-uploads/#upload-through-the-native-uploader) explains how the SDK uploads through the platform's native uploader instead.

## Sending attachments

When the message is built, the attachments that are sendable go into the message's `attachments`, in the composer's order: the ones whose upload is `finished`, and the ones with nothing to upload. `localMetadata` is left out, so the server receives each attachment without it. Blocked attachments, and by default failed ones, aren't sent.

By default a message can't be sent while one of its uploads is running: `MessageComposer.send()` publishes a `validation:attachment:upload:in-progress` warning and returns `'nothing-to-send'`. With `messageComposer.attachments.pendingUploadsEnabled` turned on in `client.config`, the message appears in the list at once and its request waits for the uploads, and a failed upload is retried. If one of them fails, the message fails. `<Chat enableOfflineSupport>` turns `pendingUploadsEnabled` on, unless your app has set it in `client.config`. See [Sending with pending uploads](https://getstream.io/chat/docs/sdk/react-native/features/messaging/sending-with-pending-uploads/).

A [draft](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/message-composer/#drafts) saves the sendable attachments. When a draft or a message to edit is loaded into the composer, its attachments come back, as [What it holds](#what-it-holds) explains.

## Settings

Set them for every composer in `client.config`, under `messageComposer.attachments`:

```ts
client.config.set({
  messageComposer: {
    attachments: { maxNumberOfFilesPerMessage: 5, trackUploadProgress: false },
  },
});
```

To change a setting for one composer only, set it on that composer's attachment manager, for example `channel.messageComposer.attachmentManager.maxNumberOfFilesPerMessage = 3`. `doUploadRequest` and `trackUploadProgress` are the exception: every upload in a channel reads them from the channel's composer, so set them in `client.config`.

| Setting                      | Default          | What it does                                                                                                                                      |
| ---------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`                    | `true`           | `false` turns off attachments. Combined with the channel type's `uploads` setting, unless `customCdn` is set.                                     |
| `maxNumberOfFilesPerMessage` | `10`             | How many attachments a message can have.                                                                                                          |
| `acceptedFiles`              | `[]`, every type | The file types a file picker should offer. Neither `stream-chat` nor the SDK's pickers check it.                                                  |
| `fileUploadFilter`           | takes every file | Returns `false` for a file that shouldn't be added. Deprecated: a `prepare` function that discards the file does the same.                        |
| `trackUploadProgress`        | `true`           | Sets `localMetadata.uploadProgress` during uploads.                                                                                               |
| `pendingUploadsEnabled`      | `false`          | Lets a message be sent while its uploads run, as [Sending attachments](#sending-attachments) explains. `<Chat enableOfflineSupport>` turns it on. |
| `doUploadRequest`            | Stream's upload  | Uploads to your own storage, as [Uploading to your own CDN](#uploading-to-your-own-cdn) explains.                                                 |
| `customCdn`                  | `false`          | `true` when `doUploadRequest` stores files outside Stream, as [Uploading to your own CDN](#uploading-to-your-own-cdn) explains.                   |

## Used in

- [Attachments and uploads](https://getstream.io/chat/docs/sdk/react-native/features/rich-content/attachments-and-uploads/)
- [Sending with pending uploads](https://getstream.io/chat/docs/sdk/react-native/features/messaging/sending-with-pending-uploads/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/attachments/](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/attachments/).