# Actions

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

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. To change a menu, you pass the component that renders it a different list. There are six action sets, in four places:

| Action set               | Default                                | Passed to                                                | Base filter                                  |
| ------------------------ | -------------------------------------- | -------------------------------------------------------- | -------------------------------------------- |
| Message actions          | `defaultMessageActionSet`              | `MessageActions`, as `messageActionSet`                  | `useBaseMessageActionSetFilter`              |
| Channel-list row actions | built in, not exported                 | no prop: replace `ChannelListItemActionButtons`          | built in, not exported                       |
| Attachment selector      | `defaultAttachmentSelectorActionSet`   | `AttachmentSelector`, as `attachmentSelectorActionSet`   | built in, not exported                       |
| Channel management       | `defaultChannelManagementActionSet`    | `ChannelManagementView`, as `channelManagementActionSet` | `useBaseChannelManagementActionSetFilter`    |
| Channel member           | `defaultChannelMemberActionSet`        | `ChannelMemberDetail`, as `channelMemberActionSet`       | `useBaseChannelMemberActionSetFilter`        |
| Channel members header   | `defaultChannelMembersHeaderActionSet` | `ChannelMembersView`, as `headerActionSet`               | `useBaseChannelMembersHeaderActionSetFilter` |

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

Take the message actions. `defaultMessageActionSet` starts like this:

```ts
[
  { 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
];
```

When a message renders its actions:

1. **The base filter drops what doesn't apply.** It reads the message, the user's capabilities in the channel and the channel's settings. For a message the user didn't send, `edit` and `delete` go, unless the user may edit or delete other people's messages.
2. **The placement decides where an item goes.** `quick` items render next to the message. `dropdown` items render in the menu. The `quick-dropdown-toggle` item renders the button that opens the menu.
3. **The order of the list is the order on screen.**

Each item has a `type`, which is how the base filter recognizes it, and a `Component`, which renders it with no props: it reads what it needs from context. 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.

The sets differ in their placements:

- Message and channel-list row actions have `quick`, `dropdown` and `quick-dropdown-toggle`.
- The channel members header has `quick` and `menu`, and its items name their component `component` rather than `Component`.
- The attachment selector, channel management and channel member sets have no placement: they're one list.

## Message actions

`MessageActions` renders the actions of one message. The default `MessageUI` renders it, and it reads `MessageActions` from `ComponentContext`, so a component set on that key replaces it. To change the set, set a component that renders `MessageActions` with your list:

```tsx
import {
  ContextMenuButton,
  defaultMessageActionSet,
  MessageActions,
  useComponentContextIcons,
  useContextMenuContext,
  useMessageContext,
  WithComponents,
} from "stream-chat-react";
import type { MessageActionSetItem } from "stream-chat-react";
import type { PropsWithChildren } from "react";

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 };

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

What the base filter does for message actions:

- It removes every action from a bounced message, a system or ephemeral message, a message that is still sending, and the parent message shown above a thread's replies.
- On a reply in a thread, it removes `pin`, `reply` and `markUnread`.
- On a failed message, it keeps only `resendMessage`, and `edit` and `delete` where they apply.
- Otherwise, each type needs its capability: `delete`, `edit`, `flag`, `markUnread`, `mute`, `quote`, `react` and `reply` check the user's capabilities in the channel, `blockUser` is hidden on the user's own messages, and `remindMe` and `saveForLater` need message reminders turned on for the channel type. `copyMessageText` needs text, and `download` needs a downloadable attachment.
- `pin` isn't checked. For users without the `pin-message` capability, hide it yourself, as `PinWithConfirmation` below does.

`MessageActions` takes `disableBaseMessageActionSetFilter` to show your list as it is. Pass it only when your components check every one of those conditions themselves. To build your own actions component, run `useBaseMessageActionSetFilter(set)` on your list, as `MessageActions` does.

A dropdown item's component renders inside the menu: read `closeMenu` from `useContextMenuContext()` and call it when the action is done. A quick item renders next to the message and has no menu to close. If you build a set from scratch, include a `quick-dropdown-toggle` item, or the dropdown items have no button to open them.

What each default action does, and the actions that come with flagging, muting and reminders, are on [Message actions](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/message-actions/).

## Channel-list row actions

Each row of a channel list has actions: a quick mute button, and a menu with archive, ban (in a direct message channel), pin and leave. The row's base filter keeps `archive` and `pin` for members, `mute` with the `mute-channel` capability, `ban` with `ban-channel-members` in a direct message channel, and `leave` with `leave-channel`.

This set has no prop: `ChannelListItemActionButtons` always renders the default set, and neither the set nor its filter is exported. To change the row actions, set your own component on the `ChannelListItemActionButtons` key. It renders inside the row, and `useChannelListItemContext()` gives it the row's channel:

```tsx
import type { OwnCapabilitiesState } from "stream-chat";
import {
  Button,
  useChannelListItemContext,
  useComponentContextIcons,
  useStateStore,
  WithComponents,
} from "stream-chat-react";
import type { PropsWithChildren } from "react";

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

const MarkReadRowAction = () => {
  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 rowOverrides = { ChannelListItemActionButtons: MarkReadRowAction };

export const WithRowActions = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={rowOverrides}>{children}</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](https://getstream.io/chat/docs/sdk/react/v15/features/channels/channel-lists/).

## Attachment selector actions

The attachment selector is the button beside the composer's input. Its menu has four actions by default, in this order:

| Type            | What it does                      | Shown when                                                                     |
| --------------- | --------------------------------- | ------------------------------------------------------------------------------ |
| `uploadFile`    | opens the file picker             | uploads are enabled                                                            |
| `createPoll`    | opens the poll creation dialog    | the user has `send-poll`, polls are enabled, and the composer isn't a thread's |
| `addLocation`   | opens the location sharing dialog | location sharing is enabled, and the composer isn't a thread's                 |
| `selectCommand` | opens a submenu of commands       | the channel type has commands                                                  |

That filter is built into `AttachmentSelector` and always runs. When only `uploadFile` is left, the selector renders a plain upload button instead of a menu.

An item has a `type` and an `ActionButton`. The button receives `openModalForAction(type)`, which opens the item's `ModalContent` in a modal, and the item's `Submenu` and `Header` when it has them. `ModalContent` receives `close`. To change the set, set a component on the `AttachmentSelector` key that renders `AttachmentSelector` with your list:

```tsx
import {
  AttachmentSelector,
  ContextMenuButton,
  defaultAttachmentSelectorActionSet,
  useComponentContextIcons,
  useContextMenuContext,
  useMessageComposerController,
  WithComponents,
} from "stream-chat-react";
import type { AttachmentSelectorAction } from "stream-chat-react";
import type { PropsWithChildren } from "react";

const BugReportTemplateButton = () => {
  const { IconFile } = useComponentContextIcons();
  const { closeMenu } = useContextMenuContext();
  const messageComposer = useMessageComposerController();

  return (
    <ContextMenuButton
      Icon={IconFile}
      onClick={() => {
        void messageComposer.textComposer.insertText({
          text: "Steps to reproduce:\nExpected:\nActual:",
        });
        closeMenu();
      }}
    >
      Bug report
    </ContextMenuButton>
  );
};

const attachmentSelectorActionSet: AttachmentSelectorAction[] = [
  ...defaultAttachmentSelectorActionSet.filter(
    ({ type }) => type !== "addLocation",
  ),
  { ActionButton: BugReportTemplateButton, type: "bugReportTemplate" },
];

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

const composerOverrides = { AttachmentSelector: CustomAttachmentSelector };

export const WithCustomAttachmentActions = ({
  children,
}: PropsWithChildren) => (
  <WithComponents overrides={composerOverrides}>{children}</WithComponents>
);
```

The default poll and location items get their dialogs from the `PollCreationDialog` and `ShareLocationDialog` keys unless you give them a `ModalContent`. The [Attachment actions](https://getstream.io/chat/docs/sdk/react/v15/recipes/composer/attachment-actions/) recipe builds an action with its own dialog.

## 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`, as the example below does. They have three action sets:

- **Channel management**, at the bottom of the channel info view: `muteChannel`, `muteUser`, `blockUser`, `leaveChannel` and `deleteChat`. Its filter keeps `muteUser` and `blockUser` for direct message channels, `leaveChannel` for group channels, and checks `mute-channel`, `ban-channel-members`, `leave-channel` and `delete-channel`.
- **Channel member**, in a member's detail view: `sendMessage`, `muteUser`, `blockUser` and `removeUser`. Its filter removes them all on the user's own entry, and `removeUser` without `update-channel-members`.
- **Channel members header**, in the members view's header: `addMembers`, a `quick` button that needs `update-channel-members`. Your own header items can carry a `filter({ channel })` that decides whether they show.

All three filters always run. Each view takes its set as a prop, and you reach the view through the sections of `ChannelDetail`:

```tsx
import {
  ChannelHeader,
  ContextMenuButton,
  ListItemLayout,
} from "stream-chat-react";
import {
  AvatarWithChannelDetail,
  ChannelDetail,
  ChannelManagementView,
  ChannelMembersView,
  defaultChannelDetailSections,
  defaultChannelManagementActionSet,
  defaultChannelMembersHeaderActionSet,
  useChannelDetailContext,
} from "stream-chat-react/channel-detail";
import type {
  AvatarWithChannelDetailProps,
  ChannelDetailProps,
  ChannelManagementActionItem,
  ChannelMembersHeaderActionComponentProps,
  ChannelMembersHeaderActionItem,
  SectionNavigatorSection,
  SectionNavigatorSectionContentProps,
} from "stream-chat-react/channel-detail";

const ExportChatAction = () => {
  const { channel } = useChannelDetailContext();

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

const CopyInviteLinkAction = ({
  closeMenu,
}: ChannelMembersHeaderActionComponentProps) => {
  const { channel } = useChannelDetailContext();

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

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.
    filter: ({ channel }) =>
      channel.data?.own_capabilities?.includes("update-channel-members") ??
      false,
    placement: "menu",
    type: "copyInviteLink",
  },
];

const ManagementSection = (props: SectionNavigatorSectionContentProps) => (
  <ChannelManagementView
    {...props}
    channelManagementActionSet={channelManagementActionSet}
  />
);

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

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} />
);

const HeaderAvatar = (props: AvatarWithChannelDetailProps) => (
  <AvatarWithChannelDetail {...props} ChannelDetail={CustomChannelDetail} />
);

// Render inside a Channel.
export const CustomChannelHeader = () => (
  <ChannelHeader Avatar={HeaderAvatar} />
);
```

The member set has no such path yet. `ChannelMemberDetail` takes `channelMemberActionSet`, but `ChannelMembersView` opens it from the members list without one, so that view always shows the default member actions. Only a `ChannelMemberDetail` you render yourself uses your set.

The views and their other props are on [Channel Detail Views](https://getstream.io/chat/docs/sdk/react/v15/component-reference/channel-detail/channel-detail-views/), and the feature on [Channel details and members](https://getstream.io/chat/docs/sdk/react/v15/features/channels/channel-details-and-members/).

## Adding, removing and replacing actions

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

- **Remove** an action by filtering out its type.
- **Add** one by putting an item where you want it: the list's order is the order on screen.
- **Replace** one by keeping its `type` and swapping its component.

```tsx
// Asks before pinning or unpinning, and hides itself without the pin-message capability.
const PinWithConfirmation = () => {
  const channel = useChannel();
  const capabilities = useChannelCapabilities({ cid: channel.cid });
  const { closeMenu } = useContextMenuContext();
  const { handlePin, message } = useMessageContext();

  if (!capabilities.has("pin-message")) return null;

  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 above 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` is still removed from 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](https://getstream.io/chat/docs/sdk/react/v15/recipes/message/custom-message-actions/) and [Attachment actions](https://getstream.io/chat/docs/sdk/react/v15/recipes/composer/attachment-actions/) recipes walk through complete examples.

Used in: [Message actions](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/message-actions/), [Flagging, muting, blocking and bans](https://getstream.io/chat/docs/sdk/react/v15/features/moderation-and-safety/flagging-muting-blocking-and-bans/), [Channel lists](https://getstream.io/chat/docs/sdk/react/v15/features/channels/channel-lists/) and [Channel details and members](https://getstream.io/chat/docs/sdk/react/v15/features/channels/channel-details-and-members/).

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/actions/](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/actions/).