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

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 the AttachmentPreviewList key. The other kinds keep their defaults.
  • To change only the upload progress or the file sizes, override the small parts first: ProgressIndicator, UploadedSizeIndicator and FileSizeIndicator. They apply to every preview and to message attachments.
  • Keep the ✕ and the retry button working in a preview of your own: call removeAttachments and handleRetry, 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 of AttachmentPreviewList.
  • 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:

src/ImageAttachmentPreview.tsx
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>
  );
};
src/ImageAttachmentPreview.css
.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:

src/ChannelWithImagePreviews.tsx
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:

src/UploadPercent.tsx
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:

src/CustomAttachmentPreviewList.tsx
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:

src/ChannelWithVoicePreview.tsx
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.