Attachment previews
The composer shows what a message will carry above the input: each attachment with its upload progress, a voice recording, link previews and a location. This recipe replaces those previews, one kind at a time. How attachments are added and uploaded is explained in Attachments and uploads, and where the previews sit in Message composer UI.
Best practices
- Replace one kind of preview through the props of
AttachmentPreviewList, and register the wrapped list under theAttachmentPreviewListkey. The other kinds keep their defaults. - To change only the upload progress or the file sizes, override the small parts first:
ProgressIndicator,UploadedSizeIndicatorandFileSizeIndicator. They apply to every preview and to message attachments. - Keep the ✕ and the retry button working in a preview of your own: call
removeAttachmentsandhandleRetry, which every preview receives. - Render attachment types of your own in
UnsupportedAttachmentPreview, and fall back to the default for the rest. - Replace voice recording previews through
VoiceRecordingPreviewSlot. They aren't part ofAttachmentPreviewList. - Keep previews compact, so they don't push the input out of view.
Default attachment preview types
AttachmentPreviewList renders one preview per attachment, picked by the attachment's type. Each component is a prop of the list:
| Attachment | Prop | Default |
|---|---|---|
| An image | ImageAttachmentPreview |
A thumbnail with the progress ring over it. Clicking it opens the gallery. |
| A video | VideoAttachmentPreview |
The same as an image |
| An audio file | AudioAttachmentPreview |
The same as a file |
| Any other file | FileAttachmentPreview |
A row with the file's icon, name, progress ring and size |
| An attachment of a type your app adds | UnsupportedAttachmentPreview |
A row with a generic icon and the ✕ |
Three previews are rendered outside the list:
- Voice recordings, by
VoiceRecordingPreviewSlot, above the list. It shows the first recording. - Link previews, by
LinkPreviewList. See Link previews. - The location to share. It has no override key and isn't exported. Only a composer layout of your own, as in Custom composer, can show it differently.
Every preview receives the attachment and two functions:
removeAttachments(ids)removes attachments from the composer. Removing one that is still uploading cancels its upload.handleRetry(attachment)uploads a failed attachment again.
Image and video previews also receive openPreview(), which opens the gallery. The upload state is in attachment.localMetadata: uploadState is pending, uploading, finished, failed or blocked, and uploadProgress goes from 0 to 100.
Customize default attachment previews
Of the defaults in the table, only UnsupportedAttachmentPreview is exported. A replacement for the image, video, audio or file preview renders the whole preview. This one shows images as small square thumbnails:
import type { ImageAttachmentPreviewProps } from "stream-chat-react";
import {
RemoveAttachmentPreviewButton,
UploadProgressIndicator,
} from "stream-chat-react";
export const ImageAttachmentPreview = ({
attachment,
handleRetry,
openPreview,
removeAttachments,
}: ImageAttachmentPreviewProps) => {
const { id, previewUri, uploadProgress, uploadState } =
attachment.localMetadata;
// The local file until the upload finishes, then the uploaded image.
const src = attachment.image_url || previewUri;
return (
<div className="image-preview">
<button
aria-label="Open image"
className="image-preview__thumbnail"
disabled={uploadState !== "finished"}
onClick={openPreview}
type="button"
>
{src && <img alt="" src={src} />}
</button>
{uploadState === "uploading" && (
<UploadProgressIndicator uploadProgress={uploadProgress} />
)}
{uploadState === "failed" && (
<button onClick={() => handleRetry(attachment)} type="button">
Retry
</button>
)}
<RemoveAttachmentPreviewButton
onClick={() => removeAttachments([id])}
uploadState={uploadState}
/>
</div>
);
};.image-preview {
position: relative;
}
.image-preview__thumbnail {
border: 0;
border-radius: 8px;
height: 48px;
overflow: hidden;
padding: 0;
width: 48px;
}
.image-preview__thumbnail img {
height: 100%;
object-fit: cover;
width: 100%;
}RemoveAttachmentPreviewButton is the SDK's ✕. Its label says "Cancel upload" while the file is uploading, and "Remove attachment" after. UploadProgressIndicator shows the progress ring, or a spinner while the progress is unknown.
Pass the preview to AttachmentPreviewList, and register the list:
import {
AttachmentPreviewList,
Channel,
MessageComposer,
MessageList,
WithComponents,
} from "stream-chat-react";
import { ImageAttachmentPreview } from "./ImageAttachmentPreview";
import "./ImageAttachmentPreview.css";
const CustomAttachmentPreviewList = () => (
<AttachmentPreviewList ImageAttachmentPreview={ImageAttachmentPreview} />
);
// Declared once, so WithComponents gets the same object on every render.
const overrides = { AttachmentPreviewList: CustomAttachmentPreviewList };
export const ChannelWithImagePreviews = () => (
<WithComponents overrides={overrides}>
<Channel channel={channel}>
<MessageList />
<MessageComposer />
</Channel>
</WithComponents>
);The props types of the image, file and voice recording previews are exported: ImageAttachmentPreviewProps, FileAttachmentPreviewProps and VoiceRecordingPreviewProps. Those of the video and audio previews aren't.
Upload progress and size UI
The default previews build their upload UI from small parts, each with an override key:
| Override key | Shows | Receives |
|---|---|---|
ProgressIndicator |
The progress ring while a file uploads | percent, from 0 to 100 |
UploadedSizeIndicator |
"1.2 MB / 3 MB" on a file row while it uploads | uploadedBytes, fullBytes |
FileSizeIndicator |
A file's size, on a file row once it's uploaded and on file attachments in messages | fileSize |
While the progress is unknown, or every byte is sent and the server hasn't answered yet, the previews show LoadingIndicator instead of the ring. That key also replaces the SDK's other spinners.
The same parts show the progress on message attachments when sending with pending uploads is turned on, so an attachment looks the same before and after the user presses send. To replace that whole block on message attachments, the ring and the sizes together, override AttachmentUploadProgressIndicator. It receives the attachment, or a gallery's attachments.
This override shows the progress as a percentage:
import type { PropsWithChildren } from "react";
import type { ProgressIndicatorProps } from "stream-chat-react";
import { WithComponents } from "stream-chat-react";
const UploadPercent = ({ percent }: ProgressIndicatorProps) => (
<span className="upload-percent">{Math.round(percent)}%</span>
);
const overrides = { ProgressIndicator: UploadPercent };
export const AppComponents = ({ children }: PropsWithChildren) => (
<WithComponents overrides={overrides}>{children}</WithComponents>
);Customize custom attachment previews
An attachment your app adds to the composer, for example with AttachmentManager.upsertAttachments(), has a type the list doesn't know. It goes to UnsupportedAttachmentPreview. Branch on your own type there, and render the default for everything else:
import type { UnsupportedAttachmentPreviewProps } from "stream-chat-react";
import {
AttachmentPreviewList,
UnsupportedAttachmentPreview,
} from "stream-chat-react";
const EventOrUnsupportedPreview = (
props: UnsupportedAttachmentPreviewProps,
) => {
const { attachment, removeAttachments } = props;
if (attachment.type !== "event") {
return <UnsupportedAttachmentPreview {...props} />;
}
return (
<div className="event-preview">
<span>📅 {attachment.title}</span>
<button
aria-label="Remove event"
onClick={() => removeAttachments([attachment.localMetadata.id])}
type="button"
>
✕
</button>
</div>
);
};
export const CustomAttachmentPreviewList = () => (
<AttachmentPreviewList
UnsupportedAttachmentPreview={EventOrUnsupportedPreview}
/>
);Register CustomAttachmentPreviewList under the AttachmentPreviewList key, as in the image example. To render the same attachment in the message list, see Change how attachments render.
Customize voice recording previews
A voice recording waiting to be sent shows in VoiceRecordingPreviewSlot, above the other previews. Give it your own VoiceRecordingPreview:
import type { VoiceRecordingPreviewProps } from "stream-chat-react";
import {
Channel,
MessageComposer,
MessageList,
RemoveAttachmentPreviewButton,
VoiceRecordingPreviewSlot,
WithComponents,
} from "stream-chat-react";
const CompactVoiceRecordingPreview = ({
attachment,
removeAttachments,
}: VoiceRecordingPreviewProps) => {
const { id, previewUri, uploadState } = attachment.localMetadata;
return (
<div className="voice-preview">
<audio controls src={attachment.asset_url || previewUri} />
<RemoveAttachmentPreviewButton
onClick={() => removeAttachments([id])}
uploadState={uploadState}
/>
</div>
);
};
const CustomVoiceRecordingPreviewSlot = () => (
<VoiceRecordingPreviewSlot
VoiceRecordingPreview={CompactVoiceRecordingPreview}
/>
);
const overrides = {
VoiceRecordingPreviewSlot: CustomVoiceRecordingPreviewSlot,
};
export const ChannelWithVoicePreview = () => (
<WithComponents overrides={overrides}>
<Channel channel={channel}>
<MessageList />
<MessageComposer audioRecordingEnabled />
</Channel>
</WithComponents>
);Voice recording is off until MessageComposer gets audioRecordingEnabled. Voice recordings and audio playback covers recording and the players.