# File compression

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

Stream accepts uploads up to 100 MB unless your app sets a lower limit, and a phone's photos and videos are often far larger than a chat needs. This UI cookbook makes uploads smaller before they leave the device: it compresses photos, scales down large ones, and compresses videos, and then sends them to Stream's CDN or to your own storage. How attachments are picked and uploaded is explained in [Attachments and uploads](https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/attachments-and-uploads/).

There are three places to step in:

| What you change            | Where                                                                                           | Applies to                                                         |
| -------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| The JPEG quality of images | `compressImageQuality` on `Channel` and `Thread`                                                | Images from the photo grid, the system image picker and the camera |
| How an image is compressed | The `compressImage` native handler                                                              | The same images, when `compressImageQuality` is set                |
| The upload of every file   | `messageComposer.attachments.doUploadRequest` in `client.config` or the composer setup function | Every file, video and voice recording the composer uploads         |

## Best practices

- Compress only what needs it. Re-encoding a small image or video costs time and quality, and saves little.
- Keep the file's `name` and `type`. Other clients pick how to render an attachment from them.
- Fall back to the original file when compression fails, so the upload still goes through.
- Wrap the default `compressImage` handler from `defaultNativeHandlers` rather than writing one from scratch.
- Register upload functions for all composers, through `client.config` or the composer setup function. Every upload in a channel goes through the channel composer's upload function, including the uploads of a thread or of a message being edited.

## Compress images

`compressImageQuality`, from 0 to 1, re-encodes each picked image as a JPEG of that quality before it's uploaded. Set it on `Channel` and on `Thread`, which take it separately:

```tsx title="src/ChannelScreen.tsx"
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageList,
} from "stream-chat-react-native";

export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => (
  <Channel channel={channel} compressImageQuality={0.7}>
    <MessageList />
    <MessageComposer />
  </Channel>
);
```

The SDK compresses an image only when the picker reported its width and height, and only when the quality is a number other than 1. Without the prop, images from the photo grid and the system image picker upload as they are. The camera applies its own quality: the value of the prop, or 0.8 on iOS with `react-native-image-picker` when the prop isn't set. A camera photo then goes through the same compression as the others, so with the prop set it's encoded twice.

Videos and files are never compressed by the SDK.

## Scale down large images

`compressImageQuality` changes the quality, not the size: a 4000 by 3000 photo stays 4000 by 3000. The SDK compresses each image by calling the `compressImage` native handler with the quality and the image's `width`, `height` and `uri`, and uploads the file it returns. Override the handler to pass a smaller size. Register the override once, at the top level of a module, after the import of the package:

**RN CLI**

The default handler of `stream-chat-react-native` resizes the image to the `width` and `height` it receives, so wrap it and pass a smaller size:

```ts title="src/nativeHandlers.ts"
import {
  defaultNativeHandlers,
  registerNativeHandlers,
} from "stream-chat-react-native";

const MAX_SIDE = 1600;
const defaultCompressImage = defaultNativeHandlers.compressImage;

if (defaultCompressImage) {
  registerNativeHandlers({
    compressImage: ({ height, width, ...params }) => {
      const scale = Math.min(1, MAX_SIDE / Math.max(width, height));

      return defaultCompressImage({
        ...params,
        height: Math.round(height * scale),
        width: Math.round(width * scale),
      });
    },
  });
}
```

**Expo**

The default handler of `stream-chat-expo` changes only the quality and ignores the size, so register a handler that resizes with `expo-image-manipulator`, which `stream-chat-expo` already needs:

```ts title="src/nativeHandlers.ts"
import { manipulateAsync, SaveFormat } from "expo-image-manipulator";
import { registerNativeHandlers } from "stream-chat-expo";

const MAX_SIDE = 1600;

registerNativeHandlers({
  compressImage: async ({ compressImageQuality, height, uri, width }) => {
    const scale = Math.min(1, MAX_SIDE / Math.max(width, height));
    try {
      const { uri: compressedUri } = await manipulateAsync(
        uri,
        scale < 1
          ? [
              {
                resize: {
                  height: Math.round(height * scale),
                  width: Math.round(width * scale),
                },
              },
            ]
          : [],
        { compress: compressImageQuality, format: SaveFormat.JPEG },
      );

      return compressedUri;
    } catch {
      // Upload the original image.
      return uri;
    }
  },
});
```

Import the file once, for example from the file that renders `OverlayProvider`, so the override is registered before the first upload. The handler runs only when `compressImageQuality` is set to a value other than 1, so set the prop too, for example to `0.8`. The RN CLI handler writes a JPEG over a white background, and returns the original `uri` when it fails. [Entry points and native handlers](https://getstream.io/chat/docs/sdk/react-native/v10/reference/entry-points-and-native-handlers/#overriding-a-handler) explains overriding handlers.

## Compress videos before upload

Videos and other files go to the composer's upload function, `attachments.doUploadRequest`, unchanged. Register one that compresses large videos and hands the result to the default upload. This one uses [`react-native-compressor`](https://github.com/numandev1/react-native-compressor), which you install yourself. It's a native module, so rebuild the app after adding it.

The default upload, `AttachmentManager.doDefaultUploadRequest()`, belongs to a composer, so register the function in the composer setup function, where the composer is at hand:

```ts title="src/setUpComposers.ts"
import { Video } from "react-native-compressor";
import { isFileReference } from "stream-chat";
import type { StreamChat, UploadRequestFn } from "stream-chat";

const MIN_VIDEO_SIZE = 10 * 1024 * 1024; // 10 MB

export const setUpComposers = (client: StreamChat) =>
  client.config.setSetupFunction("messageComposer", ({ composer }) => {
    const { attachmentManager } = composer;

    const compressAndUpload: UploadRequestFn = async (file, options) => {
      const isLargeVideo =
        isFileReference(file) &&
        file.type.startsWith("video/") &&
        // The photo grid of `stream-chat-expo` reports a size of 0.
        (file.size === 0 || file.size >= MIN_VIDEO_SIZE);

      if (!isFileReference(file) || !isLargeVideo) {
        return attachmentManager.doDefaultUploadRequest(file, options);
      }

      let { uri } = file;
      try {
        uri = await Video.compress(file.uri, { compressionMethod: "auto" });
      } catch {
        // Upload the original video.
      }

      return attachmentManager.doDefaultUploadRequest(
        { ...file, uri },
        options,
      );
    };

    composer.updateConfig({
      attachments: { doUploadRequest: compressAndUpload },
    });
  });
```

Call `setUpComposers(client)` once the client exists, before any `Channel` renders. There is one setup function per kind of object, so if your app already has a composer setup function, such as the one in the [Commands UI cookbook](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/commands-ui/#install-the-command-middleware), put the upload function in it.

Here is what happens to a large video:

1. The user picks it, and its preview shows the upload running while the video compresses.
2. `compressAndUpload` passes the compressed file's `uri`, with the original `name` and `type`, to the default upload. The default upload sends it to Stream's CDN, reports progress through `options.onProgress`, and stops when the user removes the attachment.
3. If compression fails, the original video uploads.

The function receives every file the composer uploads, voice recordings included, so check the type before compressing. `AttachmentManager.doDefaultUploadRequest()` uploads to Stream, so leave `customCdn` at `false`.

When your app has upload settings, the composer checks the file against them, the size limit included, before it calls the upload function. That check sees the original file, so a video over the limit is blocked and removed before it can be compressed.

## Compress and upload to your own storage

When the files go to your own storage instead, the upload function doesn't need the default upload, so register it for all composers in `client.config`. Set `customCdn: true`, so the composer stops applying the channel type's `uploads` setting and the `upload-file` capability to files that never reach Stream:

```ts title="src/uploads.ts"
import { Video } from "react-native-compressor";
import { isFileReference } from "stream-chat";
import type { StreamChat, UploadRequestFn } from "stream-chat";

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

  let { uri } = file;
  if (file.type.startsWith("video/")) {
    try {
      uri = await Video.compress(file.uri, { compressionMethod: "auto" });
    } catch {
      // Upload the original video.
    }
  }

  const body = new FormData();
  // React Native's FormData sends a local file given as { uri, name, type }.
  body.append("file", { name: file.name, type: file.type, uri });

  const response = await fetch("https://uploads.example.com/files", {
    body,
    method: "POST",
    signal: options?.abortSignal,
  });
  if (!response.ok) throw new Error(`Upload failed with ${response.status}`);
  const { url } = (await response.json()) as { url: string };

  return { file: url };
};

export const configureUploads = (client: StreamChat) =>
  client.config.set({
    messageComposer: {
      attachments: { customCdn: true, doUploadRequest: uploadToMyStorage },
    },
  });
```

The function resolves with `{ file }`, the URL to put in the message, and can add `thumb_url`. Throwing marks the upload as failed, and the preview offers a retry. Pass `options.abortSignal` to your request, so removing an attachment stops its upload, and call `options.onProgress` with a percentage if your upload reports progress. [Message composer](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/message-composer/#attachments-and-uploads) covers the upload states and settings.

`compressImageQuality` and a `compressImage` override still run before this function, because the SDK compresses images when they're picked. The app's upload settings, such as the size limit and the allowed file types, still apply with `customCdn: true`, and the composer checks them against the file before your upload function runs.

## Other uploads

The channel image in channel details doesn't go through the composer. `ChannelEditDetailsForm` takes its own `compressImageQuality` and `doFileUploadRequest`. See [Uploading the channel image to your own storage](https://getstream.io/chat/docs/sdk/react-native/v10/features/channels/channel-details-and-members/#uploading-the-channel-image-to-your-own-storage).

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/file-compression/](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/file-compression/).