Sending with pending uploads
What users get
By default, the composer won't send a message while one of its attachments is still uploading: the send button stays disabled and Enter does nothing until every upload finishes. With pending uploads turned on, the user sends right away. The message appears in the list with live upload progress, and the request goes out once the uploads finish.
- Send while uploading. The send button works as soon as a file is attached, and the composer clears at once, so the user can write the next message.
- Progress in the message list. Each attachment shows a progress ring and the uploaded and total size. A gallery shows one ring for all its items. Once every byte is sent but the server hasn't confirmed the upload, the ring turns into a spinner.
- Local preview. Images, video and audio render from the user's own file until the upload finishes.
- Cancel. Before sending, the ✕ on an attachment preview cancels its upload.
Turn it on when users attach large files on slow connections, where waiting for an upload before writing the next message is what frustrates them. Leave it off if your app needs a message to be on the server the moment it appears in the list.
How it works across the layers
The user attaches a 40 MB video to a message in "Design", types "Here's the cut", and presses Enter while the upload is at 30%:
- The composer lets them send. With
pendingUploadsEnabledon, the composer counts an attachment that is still uploading, or whose upload failed, as content.composer.allowsPendingUploadsreports the setting. - The composer clears.
MessageComposer.send()builds the message and clears the composer before the request, as it always does. The video's upload keeps running. - The message appears at once.
channel.messageOperations.send()adds the message with statussending. Its attachment still carries the local file, so the list renders the video from it. - The request waits for the upload. Message operations wait for the message's uploads to finish, then add the uploaded URLs to the message and send it.
- Messages keep their order. While pending uploads are on, the sends of a channel and its threads run one after another. A short text sent right after the video can't arrive first. Edits and deletes don't wait.
If any upload of the message fails, the whole message fails: no request is made, and the message is marked failed. Resend uploads only the files that didn't make it. Uploads that succeeded keep their URLs.
An attachment that the server's upload settings rejected before its upload started (blocked) doesn't count. A message whose only attachment was blocked can't be sent.
The upload states and the composer's attachment part are explained on Message composer, and failed sends on Message operations.
Progress in the UI
The file, audio, voice recording, image, video and gallery attachments render AttachmentUploadProgressIndicator. It reuses the composer's upload preview, so an attachment looks the same before and after the user presses send:
- a progress ring while the size of the transfer is known;
- a spinner once every byte has been sent but the server hasn't confirmed the upload;
- the uploaded and total size next to it, except in a gallery.
The indicator reads client.uploadManager, which holds a record only while a request runs. It shows nothing for an attachment the server returned, and it stops by itself if the upload is cancelled or client.disconnectUser() resets the upload manager.
Limitations
- A page reload loses messages that are still uploading. The browser's
Filehandles can't be saved, so a reload in the middle of an upload leaves nothing to send. client.disconnectUser()cancels running uploads, which marks their messagesfailed.- A message can't be edited while it uploads. A message with status
sendinghas no message actions.
Turning it on or off
Pending uploads are off by default. Turn them on with messageComposer.attachments.pendingUploadsEnabled in client.config:
client.config.set({
messageComposer: { attachments: { pendingUploadsEnabled: true } },
});The setting is client only. It applies to composers that already exist and to those created later, in channels, threads and edits alike, so it doesn't have to run before Chat renders. To turn it on for one composer instead, call MessageComposer.updateConfig({ attachments: { pendingUploadsEnabled: true } }).
Uploads themselves must be allowed: see Attachments and uploads.
Customizing
Replacing the progress UI
AttachmentUploadProgressIndicator is an override key. Read the upload state with useAttachmentUploadState(attachment):
import type { AttachmentUploadProgressIndicatorProps } from "stream-chat-react";
import { useAttachmentUploadState, WithComponents } from "stream-chat-react";
const UploadPercent = ({
attachment,
}: AttachmentUploadProgressIndicatorProps) => {
const { isUploading, progress, uploadConfirmationPending } =
useAttachmentUploadState(attachment);
if (!isUploading) return null;
if (uploadConfirmationPending || progress === undefined) {
return <span className="upload-status">Finishing…</span>;
}
return <span className="upload-status">{progress}%</span>;
};
export const App = () => (
<WithComponents
overrides={{ AttachmentUploadProgressIndicator: UploadPercent }}
>
{/* the rest of the app */}
</WithComponents>
);The indicator's props:
| Prop | Description | Type |
|---|---|---|
attachment |
The attachment being uploaded. The default indicator renders nothing unless its upload is running. | Attachment | LocalAttachment |
attachments |
Several attachments sharing one indicator, used by galleries. Their progress is averaged, and no size is shown. Ignored when attachment is set. |
(Attachment | LocalAttachment)[] |
className |
Additional class names. | string |
variant |
"inline" sits in the row that normally holds the file size; "overlay" sits over the media itself. |
"inline" | "overlay" |
Reading the upload state
useAttachmentUploadState(attachment) returns three values:
| Value | Type | Meaning |
|---|---|---|
isUploading |
boolean |
A request for this attachment is running now. Render progress only while it's true. |
progress |
number | undefined |
0 to 100 while the size of the transfer is known, undefined otherwise. |
uploadConfirmationPending |
boolean |
Every byte has been sent, but the server hasn't answered yet. Show an indeterminate indicator. |
Read progress and uploadConfirmationPending together. Browser upload progress counts the bytes written to the connection, not the bytes the server acknowledged, so it reaches 100 as soon as the request body is sent. The server then still has to store the file and answer, which takes a while for a large file on a slow link. A bar stuck at 100% during that time says the upload is done when it isn't.
For a gallery, useAttachmentsUploadState(attachments) combines several attachments: isUploading is true while any of them uploads, and progress is their average once every upload reports a number.
Telling users what a pending message means
A message that is still uploading isn't on the server yet. The default UI shows this through the progress on the attachment. A custom attachment or message UI should keep that signal, and should handle the failed state, where the user has to resend.
Reference
- State: Message composer, Message operations and the configuration tree.
- UI: Message composer UI and Overriding components.
- Props: Attachments and Composer UI components.
- Hooks:
useAttachmentUploadStateanduseAttachmentsUploadState.