Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

Attachments and uploads

Attachments are the files, images and videos a message carries. This page covers how a user adds them in the composer, how they're uploaded, and how the message list shows them.

What users get

The user opens the attachment menu with the + button before the input and picks File. They select two photos and a PDF. Each file shows above the input at once, with its upload progress. They remove one photo with its ✕ and send. In the message list, the photo shows as an image they can open in a gallery, and the PDF as a row with its name, size and a download button.

They can also drop files on the composer or paste them into the input. A file whose upload fails shows a retry button. A file the app's upload settings refuse isn't uploaded: a file row shows "Upload blocked", or "File too large" when it's over the size limit, and an image or video shows a warning icon.

How it works across the layers

  • State. Each composer has an attachment manager, messageComposer.attachmentManager. Its store holds the attachments of the message being written, each with an upload state: pending, uploading, finished, failed or blocked. Sending puts the finished uploads into the message.
  • UI. In the composer, AttachmentSelector opens the file picker, AttachmentPreviewList shows the files and their progress, and WithDragAndDropUpload takes dropped files. In the message list, Attachment renders what a message carries.
  • Layout. Not involved.

Adding and uploading a file

Info:

Example

Take the user's PDF:

  1. The file input, a drop or a paste calls AttachmentManager.uploadFiles(files). Files beyond attachments.maxNumberOfFilesPerMessage (10 by default) are left out.
  2. The pre-upload middleware checks the file against the app's upload settings: allowed and blocked file types, and the size limit. A refused file stays in the composer as blocked, and a validation:attachment:upload:blocked notification is published.
  3. The file uploads through doUploadRequest: Stream's CDN by default, or your own upload function. Its state is uploading, with localMetadata.uploadProgress from 0 to 100.
  4. The post-upload middleware sets the attachment's URL and thumbnail from the response. A failed upload becomes failed, and an api:attachment:upload:failed notification is published.
  5. On send, the composition middleware adds the finished attachments to the message. By default the message can't be sent while an upload is running. Sending with pending uploads lets it go out at once.

The upload states, the custom upload function and the limits are explained on Message composer. The upload middleware and their ids are on Composer middleware.

In the composer

  • AttachmentSelector opens a menu with File, Poll, Location and Commands. File clicks a hidden file input. Each entry is shown only when it's available: File when uploads are allowed, Poll and Location outside threads when they're turned on (Poll also needs the send-poll capability), Commands when the channel type has commands. When File is the only entry, the selector is a plain upload button. The selector is disabled during a slow-mode cooldown, and hidden while a command is active.
  • AttachmentPreviewList shows each attachment with its progress, a ✕ to remove it and a retry button after a failure. Images and videos open in a gallery. Voice recordings, link previews and a shared location have previews of their own.
  • WithDragAndDropUpload wraps the composer UI. While files are dragged over it, it shows "Drag your files here". It refuses the file types attachments.acceptedFiles doesn't list, and turns off during a slow-mode cooldown and when uploads aren't allowed.
  • Paste. Pasting files into the input uploads them. Pasted text is inserted as text.

Message composer UI shows where each part sits and which store it reads.

In the message list

Attachment sorts a message's attachments into groups and renders each group with its own component. Each component is a prop of Attachment, so you can replace it:

Attachment Rendered by
One image Image
One video Media
Several images or videos ModalGallery
A Giphy Giphy, with AttachmentActions for the preview's buttons
A link preview: has og_scrape_url or title_link Card
A file, an audio file, a voice recording File, Audio, VoiceRecording
A shared location, from message.shared_location Geolocation
Anything else UnsupportedAttachment

The default MessageUI renders Attachment for every message that has attachments or a shared location. A Giphy renders inline and doesn't open the gallery.

Turning it on or off

Attachments are on by default. Two sides decide whether a user can upload:

  • The client: messageComposer.attachments.enabled, true by default.
  • The server: the channel type's uploads setting, and the user's upload-file capability.

To turn uploads off in every composer:

client.config.set({
  messageComposer: { attachments: { enabled: false } },
});

With attachments.customCdn: true, files go to storage Stream doesn't host, so the server's uploads setting and the upload-file capability don't apply, and attachments.enabled decides alone. Configuration lists every setting both sides decide.

attachmentManager.isUploadEnabled combines all of it with the free slots left in the message. The selector, the drop zone and AttachmentManager.uploadFiles() all follow it, so a UI of your own should read it too.

Customizing

Limit what users can attach

acceptedFiles sets which files the file picker offers and the drop zone takes. maxNumberOfFilesPerMessage caps the number per message:

client.config.set({
  messageComposer: {
    attachments: {
      acceptedFiles: ["image/*", "application/pdf"],
      maxNumberOfFilesPerMessage: 5,
    },
  },
});

acceptedFiles doesn't stop a pasted file or one added in code. To refuse a file whatever its source, add a pre-upload middleware. This one blocks videos larger than 50 MB, so the video stays in the composer as blocked, its preview shows the warning icon, and the built-in notification is published:

import type { AttachmentPreUploadMiddleware } from "stream-chat";

const MAX_VIDEO_BYTES = 50 * 1024 * 1024;

const videoSizeLimit: AttachmentPreUploadMiddleware = {
  id: "my-app/video-size-limit",
  handlers: {
    prepare: ({ forward, next, state }) => {
      const { attachment } = state;
      const { file } = attachment.localMetadata;
      if (!file.type.startsWith("video/") || file.size <= MAX_VIDEO_BYTES) {
        return forward();
      }

      return next({
        ...state,
        attachment: {
          ...attachment,
          localMetadata: {
            ...attachment.localMetadata,
            uploadPermissionCheck: {
              reason: "size_limit",
              uploadBlocked: true,
            },
            uploadState: "blocked",
          },
        },
      });
    },
  },
};

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  const { preUploadMiddlewareExecutor } = composer.attachmentManager;

  preUploadMiddlewareExecutor.insert({
    middleware: [videoSizeLimit],
    position: {
      after: "stream-io/attachment-manager-middleware/file-upload-config-check",
    },
    unique: true,
  });

  return () => preUploadMiddlewareExecutor.remove(videoSizeLimit.id);
});

There's one setup function per kind of object, so add this to your app's composer setup function if it has one. Composer middleware explains positions, unique and the teardown.

Upload to your own storage

Register your own upload function as attachments.doUploadRequest, and set customCdn: true when the files never reach Stream. Message composer has the example.

Change the attachment menu

The menu is an action set: defaultAttachmentSelectorActionSet, passed to AttachmentSelector as attachmentSelectorActionSet. This selector leaves out Location:

import {
  AttachmentSelector,
  Channel,
  defaultAttachmentSelectorActionSet,
  MessageComposer,
  MessageList,
  WithComponents,
} from "stream-chat-react";

const actionSet = defaultAttachmentSelectorActionSet.filter(
  (action) => action.type !== "addLocation",
);

const AttachmentSelectorWithoutLocation = () => (
  <AttachmentSelector attachmentSelectorActionSet={actionSet} />
);

const overrides = { AttachmentSelector: AttachmentSelectorWithoutLocation };

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

Actions shows how to add an action of your own, with a dialog. The AttachmentSelector props, such as getModalPortalDestination for where its dialogs open, are in the Attachment Selector reference.

Drop files on a larger area

The composer UI already takes dropped files. To take them anywhere in a larger area, such as the whole channel, wrap that area in WithDragAndDropUpload, as Message composer UI shows. Three things to know:

  • Files dropped on the wrapper go to every composer inside it. Wrap a channel and a thread separately, or a file dropped on one uploads to both.
  • Only the outermost wrapper handles drops. A WithDragAndDropUpload inside another one, such as the composer's own, renders only its element.
  • The wrapper gets the class str-chat__dropzone-root, which sets position: relative and flex: 1, so the overlay covers the wrapper. Add your own layout with className or style.

Change how attachments render

Replace one renderer by passing it to Attachment, and register the result as the Attachment override. Here, attachments of a type of your own, product, get a card, and everything else renders as before. A product attachment reaches UnsupportedAttachment only without og_scrape_url and title_link, because either one makes it a link preview for Card:

import type {
  AttachmentProps,
  UnsupportedAttachmentProps,
} from "stream-chat-react";
import {
  Attachment,
  Channel,
  MessageComposer,
  MessageList,
  UnsupportedAttachment,
  WithComponents,
} from "stream-chat-react";

const ProductOrUnsupported = ({ attachment }: UnsupportedAttachmentProps) => {
  if (attachment.type !== "product") {
    return <UnsupportedAttachment />;
  }

  return (
    <div className="product-card">
      {attachment.image_url && <img alt="" src={attachment.image_url} />}
      <span>{attachment.title}</span>
    </div>
  );
};

const CustomAttachment = (props: AttachmentProps) => (
  <Attachment {...props} UnsupportedAttachment={ProductOrUnsupported} />
);

const overrides = { Attachment: CustomAttachment };

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

Attachment also takes imageAttachmentSizeHandler and videoAttachmentSizeHandler, which pick the URL to load for an image or a video at its rendered size, and giphyVersion, the Giphy rendition to show. The CSS variable --str-chat__attachment-max-width sets the largest size of images and videos; it must resolve to pixels. To replace the previews in the composer, see the Attachment previews recipe. Overriding components explains how overrides apply.

Reference