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 upload with an upload state:pending,uploading,finished,failedorblocked. An attachment with nothing to upload, such as a GIF, has none. Sending puts the finished uploads and those attachments 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
The user adds a PDF. AttachmentManager.uploadFiles() checks it against the app's upload settings and uploads it, to Stream's CDN by default. localMetadata.uploadProgress follows the upload, and its localMetadata.uploadState ends as finished. On send, MessageComposer.send() puts it into 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.
Attachments explains each upload step, the upload states and the notifications. 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. When File is the only action offered, the selector is a plain upload button instead of a menu. Attachment Selector lists when each entry is offered. 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 and uploads the files dropped on it. Drag and drop explains which files it refuses and when it turns off.- 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, set in the Stream Dashboard or through the server API, and the user'supload-filecapability.
To turn uploads off in every composer:
client.config.set({
messageComposer: { attachments: { enabled: false } },
});With messageComposer.attachments.customCdn: true in client.config, files go to storage Stream doesn't host, so the server's uploads setting and the upload-file capability don't apply, and messageComposer.attachments.enabled decides alone. Configuration lists every setting both sides decide.
attachmentManager.isUploadEnabled says whether the composer accepts a file now. The drop zone and AttachmentManager.uploadFiles() follow it, so a UI of your own should read it too. Uploads lists what it checks. Attachment Selector shows how the File item behaves at the file limit.
Customizing
Limit what users can attach
messageComposer.attachments.acceptedFiles in client.config sets which files the file picker offers and the drop zone takes. messageComposer.attachments.maxNumberOfFilesPerMessage in client.config 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. Uploading to your own CDN has the example.
Change the attachment menu
The menu is an action set, defaultAttachmentSelectorActionSet, that you can filter, reorder or extend, for example to leave out Location or add an entry that opens a dialog of your own. Custom action sets shows how. The <AttachmentSelector/> props, such as getModalPortalDestination for where its dialogs open, are in the Attachment Selector reference.
Drop files on a larger area
To take dropped files anywhere in a larger area, such as the whole channel, wrap that area in <WithDragAndDropUpload/>. Drag and drop has the example and explains which composers a drop uploads to.
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 load. 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.