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,failedorblocked. Sending puts the finished uploads into the message. - UI. In the composer,
AttachmentSelectoropens the file picker,AttachmentPreviewListshows the files and their progress, andWithDragAndDropUploadtakes dropped files. In the message list,Attachmentrenders what a message carries. - Layout. Not involved.
Adding and uploading a file
Example
Take the user's PDF:
- The file input, a drop or a paste calls
AttachmentManager.uploadFiles(files). Files beyondattachments.maxNumberOfFilesPerMessage(10 by default) are left out. - 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 avalidation:attachment:upload:blockednotification is published. - The file uploads through
doUploadRequest: Stream's CDN by default, or your own upload function. Its state isuploading, withlocalMetadata.uploadProgressfrom 0 to 100. - The post-upload middleware sets the attachment's URL and thumbnail from the response. A failed upload becomes
failed, and anapi:attachment:upload:failednotification is published. - 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
AttachmentSelectoropens 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 thesend-pollcapability), 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.AttachmentPreviewListshows 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.WithDragAndDropUploadwraps the composer UI. While files are dragged over it, it shows "Drag your files here". It refuses the file typesattachments.acceptedFilesdoesn'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,trueby default. - The server: the channel type's
uploadssetting, and the user'supload-filecapability.
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
WithDragAndDropUploadinside another one, such as the composer's own, renders only its element. - The wrapper gets the class
str-chat__dropzone-root, which setsposition: relativeandflex: 1, so the overlay covers the wrapper. Add your own layout withclassNameorstyle.
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
- Attachments: the
Attachmentprops. - Attachment Selector: the
AttachmentSelectorprops andAttachmentSelectorContext. - WithDragAndDropUpload: the drop zone's props.
- MessageComposer API: the attachment manager's methods and store.
- Configuration tree: every
messageComposer.attachmentssetting. - Composer hooks:
useAttachmentManagerStateand the other composer hooks. - Sending with pending uploads, Voice recordings and audio playback, Link previews and Location sharing: the attachments with features of their own.