Actions

Every menu of actions in the SDK is built from an action set: a list of items, each naming an action and the component that renders it. Use this page to know which action sets there are, how the SDK filters them for each user, and how to add, remove or replace actions.

Action set Where it shows Prop that takes your set
Message actions Next to each message, and in its menu (Message UI) messageActionSet on <MessageActions/>
Channel list item actions On each channel list item (Channel list UI) None. Register your own component under the ChannelListItemActionButtons key with <WithComponents/>
Attachment selector In the menu beside the composer's input (Message composer UI) attachmentSelectorActionSet on <AttachmentSelector/>
Channel management At the bottom of the Channel info section of channel details (Channel details UI) channelManagementActionSet on <ChannelManagementView/>
Channel member In a member's detail view, opened from the Members section of channel details channelMemberActionSet on <ChannelMemberDetail/>
Channel members header In the header of the Members section of channel details headerActionSet on <ChannelMembersView/>

Each set has a default you can start from, and a base filter that removes the actions the user can't take. Each section names them.

The last three are channel details actions, exported from stream-chat-react/channel-detail. Everything else is exported from stream-chat-react.

How an action set works

An action set is a list of items. Every item has:

  • a type, the action's name, which the base filter uses to recognize it. The menu button item, described in Placements, has none;
  • a component that renders the action;
  • in the message, channel list item and channel members header sets, a placement that decides where the action renders.

Placements

Placement In the sets Where the item renders
quick Message, channel list item, channel members header As a button: next to the message, on the channel list item, or in the members header
dropdown Message, channel list item, channel members header In the menu
quick-dropdown-toggle Message, channel list item, channel members header As the menu button that opens the dropdown items: "…" on messages and channel list items, "Actions" in the members header. It's an item of its own, with no type, and it renders only while at least one dropdown item is left. Without it, the dropdown items can't be opened.

What happens when a menu renders

The same three steps apply to every set:

  1. The base filter drops the actions the user can't take. It reads the user's capabilities, the channel's settings, and what the action applies to: the message, the channel or the member. A type the filter doesn't know passes it, so your own actions always show, and checking whether the user may use them is up to your component.
  2. The placement, in the sets that have one, decides where an action goes, as in Placements.
  3. The list's order is the order in the menu.

When the filter leaves no action, the message actions, the attachment selector, the channel list item actions and the channel members header actions render nothing.

Message actions

Bob's message hovered: the quick actions next to it, react, reply and the menu button, and the open menu with Thread Reply, Quote Reply, Pin, Copy Message, Mark as unread, Flag, Mute and Block User

The menu of someone else's message. Edit and Delete show on the user's own messages, and Remind me and Save for later when reminders are on.

<MessageActions/> renders one message's actions from a message action set. By default, that's defaultMessageActionSet.

The message action set

A message action set is an array, and each item in it describes one action:

Field What it is
type The action's name, such as "edit". The base filter checks the default names, as in Checks per action, and lets any other name through. Code that changes a set finds an item by it. The menu button item has none.
placement Where the action renders: quick next to the message, dropdown in the menu, or quick-dropdown-toggle as the menu button.
Component The component that renders the action. It reads the message with useMessageContext(). A dropdown component gets the props of a menu item, ContextMenuItemProps, and the quick-dropdown-toggle component gets the props of a button. A quick component gets none.
isVisible Optional. A rule that gets the message, whether it's the user's own and whether it's a thread reply, and returns whether the action shows. It can only hide an action, as Rules on the items explains. The menu button item has none.

For each message, <MessageActions/> drops the items the base filter or their own isVisible rules out, then renders the rest by placement, in the set's order.

Info:

Example

The edit item of the default set has the type "edit", the placement dropdown, an Edit button as its Component, and an isVisible that hides it on a message with a poll or a giphy:

  1. The user opens the menu on someone else's message. The base filter drops edit, because the user may not edit it, and the menu has no Edit.
  2. The user opens the menu on their own text message. The base filter keeps edit, isVisible returns true, and Edit shows in the menu.
  3. The user opens the menu on their own poll message. The base filter keeps edit, but isVisible returns false, and the menu has no Edit.

The default set starts like this:

[
  { Component: QuickDropdownToggle, placement: "quick-dropdown-toggle" },
  { Component: QuickReply, placement: "quick", type: "reply" },
  { Component: QuickReact, placement: "quick", type: "react" },
  { Component: AddReaction, placement: "dropdown", type: "react" },
  { Component: ThreadReply, placement: "dropdown", type: "reply" },
  // quote, download, pin, copy, resend, edit, mark unread, remind me,
  // save for later, flag, mute, delete, block user
];

An item's placement decides where it renders, and what its component must do:

Placement Where it renders Your component
dropdown In the menu Closes the menu when the action is done: call closeMenu() from useContextMenuContext()
quick Next to the message Needs nothing extra: there's no menu to close
quick-dropdown-toggle As the menu button that opens the menu, when at least one dropdown item is left Keep one in your set, or the dropdown items can't be opened

Next to the message, the menu button always comes first, then the quick items in list order. The SDK's stylesheet lays them out from right to left.

To use your own set, register a component under the MessageActions key that renders <MessageActions messageActionSet={yourSet} />. Here is a ChannelWithCustomMessageActions component, a channel whose messages have no "Block user" action and a "Copy link" one:

import {
  Channel,
  ContextMenuButton,
  defaultMessageActionSet,
  MessageActions,
  MessageComposer,
  MessageList,
  useComponentContextIcons,
  useContextMenuContext,
  useMessageContext,
  WithComponents,
} from "stream-chat-react";
import type { MessageActionSetItem } from "stream-chat-react";
import type { Channel as StreamChannel } from "stream-chat";

const CopyLinkAction = () => {
  const { IconLink } = useComponentContextIcons();
  const { closeMenu } = useContextMenuContext();
  const { message } = useMessageContext();

  return (
    <ContextMenuButton
      Icon={IconLink}
      onClick={() => {
        void navigator.clipboard.writeText(
          `${window.location.origin}/messages/${message.id}`,
        );
        closeMenu();
      }}
    >
      Copy link
    </ContextMenuButton>
  );
};

// Declared once: the base filter recomputes whenever it gets a new list.
const messageActionSet: MessageActionSetItem[] = [
  ...defaultMessageActionSet.filter(
    (item) =>
      item.placement === "quick-dropdown-toggle" || item.type !== "blockUser",
  ),
  { Component: CopyLinkAction, placement: "dropdown", type: "copyLink" },
];

const CustomMessageActions = () => (
  <MessageActions messageActionSet={messageActionSet} />
);

const messageOverrides = { MessageActions: CustomMessageActions };

// The messages in this channel get the custom actions.
export const ChannelWithCustomMessageActions = ({
  channel,
}: {
  channel: StreamChannel;
}) => (
  <WithComponents overrides={messageOverrides}>
    <Channel channel={channel}>
      <MessageList />
      <MessageComposer />
    </Channel>
  </WithComponents>
);

The base filter gives some messages no actions, or only a few, such as a message still sending or one that failed to send; Message actions lists them. On a reply in a thread it drops pin, reply and markUnread. On any other message it keeps each action that passes the base filter's check in Checks per action, and never resendMessage.

Checks per action

Action Shown when Checked by
edit The user may edit the message base filter
edit The message has no poll and isn't a giphy the item's isVisible
delete The user may delete the message, and it isn't deleted already base filter
flag, markUnread, mute The message isn't the user's own, and the user has flag-message, read-events or mute-channel capability base filter
blockUser The message isn't the user's own base filter
quote, react, reply The user has the capability to quote, react or reply in the channel base filter
remindMe, saveForLater Message reminders are on for the channel: channel.configState's userMessageReminders.enabled base filter
copyMessageText The message has text the item's isVisible
download The message has a file, image, video, audio or voice recording attachment the item's isVisible
pin The user has the pin-message capability base filter

Rules on the items

An item can carry its own rule as isVisible(params). It gets MessageActionVisibilityFilterParams: isMyMessage, isThreadReply and the message. The action shows only while isVisible returns true.

An item's rule can only hide an action: one the base filter drops stays hidden. The rule also applies to a message that failed to send, and with disableBaseMessageActionSetFilter.

An item without isVisible shows wherever the base filter allows it. So a set that writes its own edit, copyMessageText or download item by hand loses the default rule: without it, edit shows on a poll message the user may update. Spread the default item, or copy its isVisible. To change a rule, replace the item's isVisible:

import { defaultMessageActionSet } from "stream-chat-react";
import type { MessageActionSetItem } from "stream-chat-react";

// Lets users edit the text and attachments sent with a poll; a giphy still can't be edited.
const messageActionSet: MessageActionSetItem[] = defaultMessageActionSet.map(
  (item) =>
    item.placement !== "quick-dropdown-toggle" && item.type === "edit"
      ? { ...item, isVisible: ({ message }) => message.command !== "giphy" }
      : item,
);

<MessageActions/> takes disableBaseMessageActionSetFilter to skip the base filter's checks. Pass it only when your components check the base filter's conditions in Checks per action themselves. To build your own actions component, run useBaseMessageActionSetFilter(set) on your list, as <MessageActions/> does.

What each default action does, and the actions that come with flagging, muting and reminders, are on Message actions.

Channel list item actions

The Bob channel list item hovered: the quick actions, the menu button and the mute button, and the open menu with Archive, Pin and Leave Channel

<ChannelListItemActionButtons/> renders each channel list item's actions. To customize them, replace it with your own component, as in Replacing the actions. It renders defaultChannelActionSet, filtered by useBaseChannelActionSetFilter.

The default set, in this order:

Type Placement Shown when
None: the menu button quick-dropdown-toggle At least one dropdown action is left
mute quick The user has the mute-channel capability
archive dropdown The user is a member of the channel
ban ("Block User") dropdown The channel is a direct message (the user is a member, and it has 2 members) and the user has the ban-channel-members capability
pin dropdown The user is a member of the channel
leave dropdown The user has the leave-channel capability

The placements work as in Message actions: quick on the channel list item, dropdown in the menu.

Replacing the actions

The set has no prop, and neither it nor its filter is exported, so your component starts from scratch:

  1. Write a component that renders the actions you want. It renders inside the channel list item, so useChannelListItemContext() gives it the item's channel.
  2. Register it under the ChannelListItemActionButtons key with <WithComponents/>, around the channel list.

Here is a ChannelListWithMarkAsRead component, a channel list whose items show a "Mark as read" button, MarkAsReadAction, instead of their actions:

import type { ChannelPaginator, OwnCapabilitiesState } from "stream-chat";
import {
  Button,
  ChannelList,
  useChannelListItemContext,
  useComponentContextIcons,
  useStateStore,
  WithComponents,
} from "stream-chat-react";

const capabilitiesSelector = ({ ownCapabilities }: OwnCapabilitiesState) => ({
  ownCapabilities,
});

const MarkAsReadAction = () => {
  const { channel } = useChannelListItemContext();
  const { IconChecks } = useComponentContextIcons();
  const { ownCapabilities } = useStateStore(
    channel.state,
    capabilitiesSelector,
  );

  if (!ownCapabilities.includes("read-events")) return null;

  return (
    <div className="str-chat__channel-list-item__action-buttons">
      <Button
        appearance="ghost"
        aria-label="Mark as read"
        circular
        onClick={() => void channel.markRead()}
        size="sm"
        variant="secondary"
      >
        <IconChecks />
      </Button>
    </div>
  );
};

const channelListItemOverrides = {
  ChannelListItemActionButtons: MarkAsReadAction,
};

// Every item of this channel list gets the "Mark as read" button instead of its actions.
export const ChannelListWithMarkAsRead = ({
  paginator,
}: {
  paginator: ChannelPaginator;
}) => (
  <WithComponents overrides={channelListItemOverrides}>
    <ChannelList paginator={paginator} />
  </WithComponents>
);

Your component replaces the default buttons, menu included, and does its own capability checks, here through channel.state. The actions themselves are on Channel lists.

Attachment selector actions

The composer with the attachment selector menu open: File, Location and Commands

The menu in a channel with polls disabled. Attachment Selector lists when Poll and the other actions show.

<AttachmentSelector/>, the button beside the composer's input, renders a menu of defaultAttachmentSelectorActionSet, filtered by a filter built into it. The filter isn't exported, and it always runs. An item of this set has a type and an ActionButton, its entry in the menu, but no placement.

To change the set, see Custom action sets, which lists the fields of an item. To give an action of your own a button that replaces the menu, see Standalone buttons.

Channel details actions

Channel details come from stream-chat-react/channel-detail. An app usually opens them from the channel header, by passing AvatarWithChannelDetail as its Avatar. They have three action sets, each rendered by one view. Using your own sets changes two of them in one example.

Channel management

Channel details, Group info: the channel management actions Mute Chat, Leave Chat and Delete Chat at the bottom

The actions of a group channel.

<ChannelManagementView/>, the Channel info section, renders them at its bottom: defaultChannelManagementActionSet, filtered by useBaseChannelManagementActionSetFilter. The filter always runs.

The channel management action set

An item has no placement, and these fields:

Field What it is
type The action's name, such as "deleteChat". The base filter checks the default names and lets any other name through.
Component The component that renders the action. It gets no props: it reads the channel with useChannelDetailContext().

The view renders the items the base filter keeps, in the set's order.

The default set, in this order:

Type Shown when
muteChannel The user has the mute-channel capability
muteUser The channel is a direct message with another user
blockUser The channel is a direct message with another user, and the user has the ban-channel-members capability
leaveChannel The channel is a group channel, and the user has the leave-channel capability
deleteChat The user has the delete-channel capability

Channel member

Channel details, Member detail for Bob: the channel member actions Send Direct Message, Mute User, Block User and Remove User

<ChannelMemberDetail/>, a member's detail view opened from the Members section, renders the member's actions: defaultChannelMemberActionSet, filtered by useBaseChannelMemberActionSetFilter. The filter always runs.

The channel member action set

An item has these fields:

Field What it is
type The action's name, such as "removeUser". The base filter checks the default names and lets any other name through.
Component The component that renders the action. It gets no props. It reads the member with useChannelMemberActionContext(), which gives member, memberDisplayName and targetUserId, and the channel with useChannelDetailContext().

The view renders the items the base filter keeps, in the set's order.

The default set, in this order:

Type Shown when
sendMessage, muteUser, blockUser The member isn't the user
removeUser The member isn't the user, and the user has the update-channel-members capability

<ChannelMembersView/> opens this view from the members list without passing a set, so the Members section always shows the default member actions. Only a <ChannelMemberDetail/> you render yourself uses your set.

Channel members header

Channel details, the Members section: the header with the Add button, the channel members header action

<ChannelMembersView/>, the Members section, renders these actions in its header, in browse mode: defaultChannelMembersHeaderActionSet, filtered by useBaseChannelMembersHeaderActionSetFilter.

The channel members header action set

An item has the same fields as a message action item:

Field What it is
type The action's name, such as "addMembers". The base filter checks only addMembers. The menu button item has none.
placement Where the action renders: quick as a button in the header, dropdown in the header's menu, or quick-dropdown-toggle as the menu button.
Component The component that renders the action. A quick or dropdown component gets modeController, which switches the Members section between its modes, such as browse and add. ChannelMembersView explains the modes. The quick-dropdown-toggle component is the menu button.
isVisible Optional. A rule that gets { channel } and returns whether the action shows. Use it for your own actions' permission checks. It can only hide an action. The menu button item has none.

The default header actions drop the items that the base filter or their own isVisible rules out, then render the rest by placement, in the set's order.

The default set:

Type Placement Shown when
(none) quick-dropdown-toggle At least one dropdown item is left
addMembers quick The user has the update-channel-members capability

Placements says where each placement renders. Two of them ask more of your component here:

  • A dropdown component closes the menu when its action is done: it calls closeMenu() from useContextMenuContext().
  • The quick-dropdown-toggle component is a forwardRef button. It gets a ref, aria-expanded and onClick, and spreads them onto its button. One that ignores them never opens the menu.

The default set already holds a menu button. Keep it when you build your set from the default one, and add one when you write a set from scratch, or the dropdown items can't be opened.

The filter runs in the default header actions only. <ChannelMembersView/> takes a HeaderActions component that replaces them, and it receives your headerActionSet unfiltered. To match the default, run useBaseChannelMembersHeaderActionSetFilter(set) in your component and render the dropdown items in a <ContextMenu/> anchored to the menu button, since useContextMenuContext() throws outside one. DefaultHeaderActions in ChannelMembersHeaderActions.defaults.tsx of the SDK shows the wiring.

Using your own sets

Each view takes its set as a prop, and you reach the view through the sections of <ChannelDetail/>. Using your own sets takes six steps:

  1. Write the components of your own actions.
  2. Build your sets from the defaults.
  3. Give each set to its view, and use that view as its section's content.
  4. Give the sections to <ChannelDetail/>.
  5. Open that <ChannelDetail/> from the channel header's avatar.
  6. Render that header in a channel.

Here is a ChannelWithCustomDetails component that follows them. It removes "Delete chat", adds "Export chat" to the channel management actions, and adds "Copy invite link" to the members header. The comments mark the steps:

import {
  Channel,
  ChannelHeader,
  ContextMenuButton,
  ListItemLayout,
  MessageComposer,
  MessageList,
  useContextMenuContext,
} from "stream-chat-react";
import type { Channel as StreamChannel } from "stream-chat";
import {
  AvatarWithChannelDetail,
  ChannelDetail,
  ChannelManagementView,
  ChannelMembersView,
  defaultChannelDetailSections,
  defaultChannelManagementActionSet,
  defaultChannelMembersHeaderActionSet,
  useChannelDetailContext,
} from "stream-chat-react/channel-detail";
import type {
  AvatarWithChannelDetailProps,
  ChannelDetailProps,
  ChannelManagementActionItem,
  ChannelMembersHeaderActionItem,
  SectionNavigatorSection,
  SectionNavigatorSectionContentProps,
} from "stream-chat-react/channel-detail";

// 1. Write the components of your own actions.
const ExportChatAction = () => {
  const { channel } = useChannelDetailContext();

  return (
    <ListItemLayout
      RootElement="button"
      rootProps={{
        onClick: () => window.open(`/exports/${channel.id}`),
        type: "button",
      }}
      title="Export chat"
    />
  );
};

const CopyInviteLinkAction = () => {
  const { channel } = useChannelDetailContext();
  const { closeMenu } = useContextMenuContext();

  return (
    <ContextMenuButton
      onClick={() => {
        void navigator.clipboard.writeText(
          `${window.location.origin}/join/${channel.id}`,
        );
        closeMenu();
      }}
    >
      Copy invite link
    </ContextMenuButton>
  );
};

// 2. Build your sets from the defaults.
const channelManagementActionSet: ChannelManagementActionItem[] = [
  ...defaultChannelManagementActionSet.filter(
    ({ type }) => type !== "deleteChat",
  ),
  { Component: ExportChatAction, type: "exportChat" },
];

const headerActionSet: ChannelMembersHeaderActionItem[] = [
  ...defaultChannelMembersHeaderActionSet,
  {
    Component: CopyInviteLinkAction,
    // An app's own action carries its own permission check.
    isVisible: ({ channel }) =>
      channel.data?.own_capabilities?.includes("update-channel-members") ??
      false,
    placement: "dropdown",
    type: "copyInviteLink",
  },
];

// 3. Give each set to its view, and use that view as its section's content.
const ManagementSection = (props: SectionNavigatorSectionContentProps) => (
  <ChannelManagementView
    {...props}
    channelManagementActionSet={channelManagementActionSet}
  />
);

const MembersSection = (props: SectionNavigatorSectionContentProps) => (
  <ChannelMembersView {...props} headerActionSet={headerActionSet} />
);

// 4. Give the sections to ChannelDetail.
const sections: SectionNavigatorSection[] = defaultChannelDetailSections.map(
  (section) => {
    if (section.id === "channel-info")
      return { ...section, SectionContent: ManagementSection };
    if (section.id === "channel-members")
      return { ...section, SectionContent: MembersSection };
    return section;
  },
);

const CustomChannelDetail = (props: ChannelDetailProps) => (
  <ChannelDetail {...props} sections={sections} />
);

// 5. Open that ChannelDetail from the channel header's avatar.
const HeaderAvatar = (props: AvatarWithChannelDetailProps) => (
  <AvatarWithChannelDetail {...props} ChannelDetail={CustomChannelDetail} />
);

const CustomChannelHeader = () => <ChannelHeader Avatar={HeaderAvatar} />;

// 6. Render the custom header in a channel.
export const ChannelWithCustomDetails = ({
  channel,
}: {
  channel: StreamChannel;
}) => (
  <Channel channel={channel}>
    <CustomChannelHeader />
    <MessageList />
    <MessageComposer />
  </Channel>
);

The views and their other props are on Channel Detail Views, and the feature on Channel details and members.

Adding, removing and replacing actions

Start from the default set and change it, rather than writing a list from scratch:

To Do
Remove an action Filter out its type
Add an action Put an item where you want it: the list's order is the order in the menu
Replace an action Keep its type and swap its component
Change when an action shows Keep its type and replace its isVisible, as in Rules on the items

Here is a PinWithConfirmation component that replaces the pin action, and a message action set that also adds CopyLinkAction from Message actions before "flag":

// Asks before pinning or unpinning.
const PinWithConfirmation = () => {
  const { closeMenu } = useContextMenuContext();
  const { handlePin, message } = useMessageContext();

  return (
    <ContextMenuButton
      onClick={(event) => {
        if (
          window.confirm(
            message.pinned ? "Unpin this message?" : "Pin this message?",
          )
        ) {
          void handlePin(event);
        }
        closeMenu();
      }}
    >
      {message.pinned ? "Unpin" : "Pin"}
    </ContextMenuButton>
  );
};

// Replace the pin action's component, and add CopyLinkAction (from the Message actions example) before "flag".
const messageActionSet: MessageActionSetItem[] =
  defaultMessageActionSet.flatMap((item): MessageActionSetItem[] => {
    if (item.placement === "dropdown" && item.type === "pin") {
      return [{ ...item, Component: PinWithConfirmation }];
    }
    if (item.placement === "dropdown" && item.type === "flag") {
      return [
        { Component: CopyLinkAction, placement: "dropdown", type: "copyLink" },
        item,
      ];
    }
    return [item];
  });

A replaced action keeps its type, so the base filter still treats it as that action: PinWithConfirmation still shows only to users with the pin-message capability, and not on thread replies. An added action has a type the filter doesn't know: it always passes, so its component checks whether the user may use it and returns null when not. Keep the base filter on, so the default actions keep following the user's capabilities.

The Custom message actions and Product card recipes walk through complete examples.

Used in