# Custom message 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/).

Message actions are the buttons beside a message and the items in its menu. This recipe builds one action set from the SDK's default, step by step: it adds an item to the menu, adds a button beside the message, and removes two default actions. How action sets work is explained in [Actions](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/actions/), and what each default action does in [Message actions](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/message-actions/).

## Best practices

- Start from `defaultMessageActionSet` and add, remove or replace items. A list written from scratch loses the built-in actions, and the "more" button with them.
- Declare the set once, outside any component. The base filter recomputes whenever it gets a new list.
- Keep the base filter on. It hides the default actions from users who can't take them, and lets your own actions through.
- Check permissions in your own actions. The filter doesn't know their types, so it always shows them.
- Close the menu when a menu action is done, with `closeMenu()` from `useContextMenuContext()`.
- Register the wrapper on the `MessageActions` key with `WithComponents`, so it applies in every message list, in channels and in threads.
- Check the result in a thread too. The filter removes `pin`, `reply` and `markUnread` from thread replies.

## Built-in actions

`defaultMessageActionSet` holds these items, in this order. The order of the list is the order on screen.

| Placement               | Types                                                                                                                                                                     |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quick-dropdown-toggle` | none: the "more" button that opens the menu                                                                                                                               |
| `quick`                 | `reply`, `react`                                                                                                                                                          |
| `dropdown`              | `react`, `reply`, `quote`, `download`, `pin`, `copyMessageText`, `resendMessage`, `edit`, `markUnread`, `remindMe`, `saveForLater`, `flag`, `mute`, `delete`, `blockUser` |

`quick` items render beside the message, and `dropdown` items in the menu. Before rendering, the base filter drops what doesn't apply to the message and the user. For example, `markUnread` shows only on other users' messages, and only with the `read-events` capability. [Message actions](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/message-actions/#how-it-works-across-the-layers) lists when each one shows.

The filter doesn't check the `pin-message` capability: Pin shows to every user on channel messages. [Pinned messages](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/pinned-messages/#turning-it-on-or-off) shows how to hide it from users who can't pin.

## Add a custom dropdown action

The menu gets a "Yell" item that shows the message's text in capitals. A dropdown item's component renders inside the menu with no props, and reads what it needs from context:

```tsx title="src/CustomMessageActions.tsx"
import type { PropsWithChildren } from "react";
import type { MessageActionSetItem } from "stream-chat-react";
import {
  ContextMenuButton,
  defaultMessageActionSet,
  MessageActions,
  useComponentContextIcons,
  useContextMenuContext,
  useMessageContext,
  WithComponents,
} from "stream-chat-react";

const YellAction = () => {
  const { IconMegaphone } = useComponentContextIcons();
  const { closeMenu } = useContextMenuContext();
  const { message } = useMessageContext();

  if (!message.text) return null;

  return (
    <ContextMenuButton
      Icon={IconMegaphone}
      onClick={() => {
        window.alert(message.text?.toUpperCase());
        closeMenu();
      }}
    >
      Yell
    </ContextMenuButton>
  );
};

// Declared once: the base filter recomputes whenever it gets a new list.
const messageActionSet: MessageActionSetItem[] = [
  ...defaultMessageActionSet,
  { Component: YellAction, placement: "dropdown", type: "yell" },
];

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

const overrides = { MessageActions: CustomMessageActions };

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

Wrap the channel, or the whole app, in `WithCustomMessageActions`.

1. The default `MessageUI` renders the `MessageActions` key on every message that isn't deleted. `CustomMessageActions` renders the SDK's `MessageActions` with the new list.
2. The base filter runs on the list. `yell` is a type it doesn't know, so the filter lets the item through, and `YellAction` itself returns `null` for a message without text.
3. The item goes last in the menu, because it's last in the list. Put it elsewhere in the list to move it.
4. `ContextMenuButton` gives it the look of the other menu items, and `useComponentContextIcons()` gives it an icon that follows [icon overrides](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/overriding-components/#icons).

The "more" button is the `quick-dropdown-toggle` item of the default set, so spreading the default set keeps it.

## Add a quick action

A quick action renders beside the message, next to the reply and reaction buttons. This one copies the message's id. It has no menu to close:

```tsx title="src/CustomMessageActions.tsx" add="17"
const CopyIdAction = () => {
  const { IconCopy } = useComponentContextIcons();
  const { message } = useMessageContext();

  return (
    <QuickMessageActionsButton
      aria-label="Copy message ID"
      onClick={() => void navigator.clipboard.writeText(message.id)}
    >
      <IconCopy />
    </QuickMessageActionsButton>
  );
};

const messageActionSet: MessageActionSetItem[] = [
  ...defaultMessageActionSet,
  { Component: CopyIdAction, placement: "quick", type: "copyId" },
  { Component: YellAction, placement: "dropdown", type: "yell" },
];
```

`QuickMessageActionsButton` is the button the default quick actions use. Give it an `aria-label`: the icon is its only content.

## Filter the defaults

To remove default actions, filter them out by type. This set drops Mark as unread and Block User, and keeps the two actions from above. It's the whole file:

```tsx title="src/CustomMessageActions.tsx" add="48,52-56"
import type { PropsWithChildren } from "react";
import type { MessageActionSetItem } from "stream-chat-react";
import {
  ContextMenuButton,
  defaultMessageActionSet,
  MessageActions,
  QuickMessageActionsButton,
  useComponentContextIcons,
  useContextMenuContext,
  useMessageContext,
  WithComponents,
} from "stream-chat-react";

const YellAction = () => {
  const { IconMegaphone } = useComponentContextIcons();
  const { closeMenu } = useContextMenuContext();
  const { message } = useMessageContext();

  if (!message.text) return null;

  return (
    <ContextMenuButton
      Icon={IconMegaphone}
      onClick={() => {
        window.alert(message.text?.toUpperCase());
        closeMenu();
      }}
    >
      Yell
    </ContextMenuButton>
  );
};

const CopyIdAction = () => {
  const { IconCopy } = useComponentContextIcons();
  const { message } = useMessageContext();

  return (
    <QuickMessageActionsButton
      aria-label="Copy message ID"
      onClick={() => void navigator.clipboard.writeText(message.id)}
    >
      <IconCopy />
    </QuickMessageActionsButton>
  );
};

const hiddenTypes = new Set(["blockUser", "markUnread"]);

// Declared once: the base filter recomputes whenever it gets a new list.
const messageActionSet: MessageActionSetItem[] = [
  // The "more" button has no type, so it's kept explicitly.
  ...defaultMessageActionSet.filter(
    (item) =>
      item.placement === "quick-dropdown-toggle" || !hiddenTypes.has(item.type),
  ),
  { Component: CopyIdAction, placement: "quick", type: "copyId" },
  { Component: YellAction, placement: "dropdown", type: "yell" },
];

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

const overrides = { MessageActions: CustomMessageActions };

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

The base filter still runs on what's left, so the remaining default actions keep following the user's capabilities. Some things to know when you go further:

- **Replacing an action.** Keep its `type` and swap its `Component`. The filter still treats it as that action. [Actions](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/actions/#adding-removing-and-replacing-actions) replaces Pin with a version that asks for confirmation and checks the `pin-message` capability.
- **Turning the filter off.** `MessageActions` takes `disableBaseMessageActionSetFilter`, which shows your list as it is. Pass it only when your components check every condition the filter checks.
- **A list from scratch.** Include a `quick-dropdown-toggle` item, or the menu items have no button to open them.

---

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