Attachment Selector

<AttachmentSelector/> is the button before the composer's input. It opens a menu to upload a file, create a poll, share a location or pick a command. The menu is an action set: a list of actions, each with a type and a component. Attachments and uploads explains uploads, and Actions explains how action sets work.

Best practices

  • Change the menu by passing your own attachmentSelectorActionSet, built from defaultAttachmentSelectorActionSet. Register the selector that renders it under the AttachmentSelector key.
  • Decide who can upload, create polls and share locations on the server and in client.config. The selector offers a built-in action only when the user can use it, and that filter can't be turned off.
  • Give a custom action a type of its own. The four built-in types are filtered, whatever component you give them.
  • Set getModalPortalDestination when the selector renders outside a <Channel/>, or when the channel's container clips its dialogs.

Default behavior

The default menu has four actions, in this order:

Type Label What it does Offered when
uploadFile File Opens the file picker The user can upload files, as Enabling features explains
createPoll Poll Opens the poll creation dialog Polls are turned on, the user has the send-poll capability, and it's not a thread
addLocation Location Opens the location sharing dialog Location sharing is turned on, and it's not a thread
selectCommand Commands Opens a submenu of the channel's commands The channel type has commands

<AttachmentSelector/> applies this filter itself, every time it renders, to the default set and to any set you pass. An action of any other type is always offered.

The File item stays in the menu, disabled, once the message reaches the file limit (messageComposer.attachments.maxNumberOfFilesPerMessage in client.config). Uploads says which files count.

What the selector renders depends on the actions it offers:

  • No actions: nothing.
  • One action that has a StandaloneButton: that button alone, with no menu. The uploadFile action has FileUploadButton by default, so File alone renders a plain upload button. Standalone buttons explains them.
  • Otherwise: the button and its menu. A lone action without a StandaloneButton is the only item of the menu.

The button that opens the menu is disabled during a slow-mode cooldown. While a command is selected, the selector is hidden with a CSS transition and taken out of the tab order. A dialog opened from the menu closes once its action is no longer offered, or once a standalone button replaces the menu.

While the composer edits a message (it renders inside that message's MessageContext), the default layout renders <SimpleAttachmentSelector/> in place of <AttachmentSelector/>, which shows FileUploadButton alone, under the File action's rule and file limit.

Enabling features

Each action follows the settings below. Configuration explains how the server and client.config combine.

  • File: offered while both of these hold. See Attachments and uploads.
    • Attachments are enabled. That needs messageComposer.attachments.enabled in client.config, and the channel type's uploads setting, set in the Stream Dashboard or through the server API. The uploads setting doesn't count when messageComposer.attachments.customCdn in client.config is true.
    • messageComposer.attachments.customCdn in client.config is true, or the user has the upload-file capability.
  • Poll: messageComposer.polls.enabled in client.config, the channel type's polls setting (set in the Stream Dashboard or through the server API), and the send-poll capability. See Polls.
  • Location: messageComposer.location.enabled in client.config, and the channel type's shared_locations setting, set in the Stream Dashboard or through the server API. See Location sharing.
  • Commands: the channel type's commands, set in the Stream Dashboard or through the server API. See Commands and Giphy.

The filter decides only what the selector offers. It doesn't grant what an action needs:

  • An action with a type of its own is offered to every user, such as an upload action for a user without the upload-file capability.
  • AttachmentManager.uploadFiles() still drops files while attachmentManager.isUploadEnabled is false, so an upload action that goes through the file input or AttachmentManager.uploadFiles() uploads nothing.
  • Stream's server still refuses a poll from a user without the send-poll capability.

Custom action sets

<AttachmentSelector/> props:

Prop Type Default Description
attachmentSelectorActionSet AttachmentSelectorAction[] defaultAttachmentSelectorActionSet The actions of the menu, top to bottom. The filter in Default behavior still applies.
buttonProps Omit<ButtonProps, "onClick"> none Props for the button that opens the menu, or for a standalone button. The disabled you pass is ignored: the menu button and a standalone button are disabled during a slow-mode cooldown.
getModalPortalDestination () => HTMLElement | null the <Channel/> container The element the action dialogs render into. See Custom modal portal destination.

An AttachmentSelectorAction has these fields:

Field Type Description
type "uploadFile" | "createPoll" | "addLocation" | "selectCommand" | string What the action is. Also the key passed to openModalForAction.
ActionButton ComponentType<AttachmentSelectorActionProps> The menu item. It gets openModalForAction(type), and submenuItems and submenuHeader when the action has a submenu.
ModalContent ComponentType<AttachmentSelectorModalContentProps> What openModalForAction(type) opens in a modal. It gets close.
Submenu ContextMenuSubmenu A submenu, as the commands action has.
Header ContextMenuHeaderComponent The header of the submenu.
id string An identifier of your own. The selector doesn't read it.
StandaloneButton ComponentType<AttachmentSelectorStandaloneButtonProps> What renders in place of the menu when this is the only action offered. uploadFile defaults to FileUploadButton. See Standalone buttons.

A menu item closes the menu with closeMenu() from useContextMenuContext(). The default items are exported as DefaultAttachmentSelectorComponents: File, Poll, Location and Command.

This selector leaves out Location:

import type { PropsWithChildren } from "react";
import {
  AttachmentSelector,
  defaultAttachmentSelectorActionSet,
  WithComponents,
} from "stream-chat-react";

const actionSet = defaultAttachmentSelectorActionSet.filter(
  ({ type }) => type !== "addLocation",
);

const AttachmentSelectorWithoutLocation = () => (
  <AttachmentSelector attachmentSelectorActionSet={actionSet} />
);

const overrides = { AttachmentSelector: AttachmentSelectorWithoutLocation };

export const WithoutLocationAction = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

Standalone buttons adds an action of its own, and the Product card recipe adds one that opens a picker in a dialog with openModalForAction().

Standalone buttons

When the selector offers only one action and that action has a StandaloneButton, the selector renders that button instead of a menu with one item.

  • Where it renders: inside the selector's root element, .str-chat__attachment-selector, in place of the menu button. It collapses while a command is selected.
  • Props: buttonProps, the selector's buttonProps with disabled set while a slow-mode cooldown runs. The type is AttachmentSelectorStandaloneButtonProps.
  • No dialog: there's no menu, so it can't call openModalForAction.
  • Width: the selector's space beside the input fits a round icon button. A wider button needs the --str-chat__message-composer-attachment-slot-width CSS variable.

The uploadFile action's standalone button is FileUploadButton. It opens the file picker, and it's disabled at the file limit. An action with a type of your own has no standalone button by default; an upload action of your own sets StandaloneButton: FileUploadButton to get the same plain button. Enabling features says what such an action doesn't grant.

In this example, the set's only action is one of your own, which inserts a bug report template, so the selector always renders its labeled standalone button. The action still needs an ActionButton for the menu, which shows once the set has more actions:

import type { PropsWithChildren } from "react";
import {
  AttachmentSelector,
  Button,
  ContextMenuButton,
  useContextMenuContext,
  useMessageComposerController,
  WithComponents,
} from "stream-chat-react";
import type {
  AttachmentSelectorAction,
  AttachmentSelectorStandaloneButtonProps,
} from "stream-chat-react";

const template = "Steps to reproduce:\nExpected:\nActual:";

const BugReportMenuItem = () => {
  const { closeMenu } = useContextMenuContext();
  const messageComposer = useMessageComposerController();

  return (
    <ContextMenuButton
      onClick={() => {
        void messageComposer.textComposer.insertText({ text: template });
        closeMenu();
      }}
    >
      Bug report
    </ContextMenuButton>
  );
};

const BugReportButton = ({
  buttonProps,
}: AttachmentSelectorStandaloneButtonProps) => {
  const messageComposer = useMessageComposerController();

  return (
    <Button
      {...buttonProps}
      appearance="outline"
      onClick={() =>
        void messageComposer.textComposer.insertText({ text: template })
      }
      size="lg"
      variant="secondary"
    >
      Bug report
    </Button>
  );
};

const actionSet: AttachmentSelectorAction[] = [
  {
    ActionButton: BugReportMenuItem,
    StandaloneButton: BugReportButton,
    type: "bugReportTemplate",
  },
];

const BugReportSelector = () => (
  <AttachmentSelector attachmentSelectorActionSet={actionSet} />
);

const overrides = { AttachmentSelector: BugReportSelector };

export const WithBugReportButton = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);
.str-chat__message-composer {
  --str-chat__message-composer-attachment-slot-width: 8rem;
}

Poll and location dialogs

The createPoll and addLocation actions open the PollCreationDialog and ShareLocationDialog overrides, unless the action has a ModalContent of its own. Both dialogs get close. Opening the poll dialog starts a new poll in the composer's pollComposer.

import type { PropsWithChildren } from "react";
import type { PollCreationDialogProps } from "stream-chat-react";
import { PollCreationDialog, WithComponents } from "stream-chat-react";

const PollDialogWithIntro = (props: PollCreationDialogProps) => (
  <div className="poll-dialog">
    <p>Polls close after a week.</p>
    <PollCreationDialog {...props} />
  </div>
);

const overrides = { PollCreationDialog: PollDialogWithIntro };

export const WithPollDialogIntro = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

The dialogs' own props are on Poll components and Location sharing.

Custom modal portal destination

The action dialogs render into the <Channel/> container, the element with the str-chat__channel id. Outside a <Channel/>, or when that container clips them, pass getModalPortalDestination:

import { AttachmentSelector } from "stream-chat-react";

const getModalPortalDestination = () =>
  document.getElementById("chat-modal-root");

export const AttachmentSelectorWithModalRoot = () => (
  <AttachmentSelector getModalPortalDestination={getModalPortalDestination} />
);

Register it under the AttachmentSelector key. Dialogs explains modals.

AttachmentSelectorContext

The menu items of <AttachmentSelector/> can read AttachmentSelectorContext with useAttachmentSelectorContext():

Value Type Description
fileInput HTMLInputElement | null The hidden file input of the selector. Call fileInput.click() to open the file picker. null until the input renders, and outside the selector.

<SimpleAttachmentSelector/> and standalone buttons don't provide this context.

Hooks