Message actions
Message actions are what a user can do with one message: react to it, reply to it, quote it, pin it, edit it, delete it and more. This page lists the actions the SDK ships, what each one does and who sees it. To add, remove or replace actions, see Actions.
What users get
When a user points at a message or moves focus into it, buttons appear beside it: one to add a reaction, one to reply in a thread, and a "more" button that opens the message menu.
Say the user opens the menu on a message from Bob. They see Add reaction, Thread Reply, Quote Reply, Pin, Copy Message, Mark as unread, Remind me, Save for later, Flag, Mute and Block User. On their own message they see Edit Message and Delete message instead of Mark as unread, Flag, Mute and Block User. Most actions confirm the result with a notification, such as "Message pinned" or "Saved for later".
How it works across the layers
MessageUI renders MessageActions on every message that isn't deleted. MessageActions takes an action set, defaultMessageActionSet unless you pass another one. Each item in the set has three fields:
typenames the action, for examplepin;placementputs it next to the message (quick) or in the menu (dropdown); one item with the placementquick-dropdown-toggleis the "more" button;Componentrenders it.
Before rendering, useBaseMessageActionSetFilter drops the actions the user can't take on this message. The remaining quick actions render beside the message, and the dropdown actions render in the menu.
Each action calls the state layer. The table lists the default set in menu order, with what each action calls and when the filter keeps it. "Own" means the connected user sent the message, and names in code font such as send-reaction are channel capabilities.
| Action | type |
What it calls | Shown when |
|---|---|---|---|
| Add reaction (quick and in the menu) | react |
Opens the reaction selector | send-reaction |
| Reply (quick), Thread Reply (menu) | reply |
Opens the message's thread through useWorkspaceNavigation().openThread |
send-reply, and the message isn't a thread reply |
| Quote Reply | quote |
MessageComposer.setQuotedMessage(message) |
quote-message |
| Download Attachment | download |
Downloads the file, or opens a submenu when there are several | The message has a file, image, video, audio or voice recording |
| Pin, Unpin | pin |
client.pinMessage(message), client.unpinMessage(message) |
The message isn't a thread reply |
| Copy Message | copyMessageText |
Copies message.text to the clipboard |
The message has text |
| Resend | resendMessage |
MessageOperations.retry |
The message failed to send and can be retried, and send-message |
| Edit Message | edit |
Loads the message into the composer | update-any-message, or update-own-message on an own message; never a poll or a Giphy |
| Mark as unread | markUnread |
Channel.markUnread({ message_id }) |
Not own, read-events, and the message isn't a thread reply |
| Remind me, Remove reminder | remindMe |
client.reminders.upsertReminder, client.reminders.deleteReminder |
Reminders are on for the channel, and the message isn't saved for later |
| Save for later, Remove save for later | saveForLater |
client.reminders.createReminder, client.reminders.deleteReminder |
Reminders are on for the channel, and the message has no scheduled reminder |
| Flag | flag |
client.moderation.flagMessage({ message_id: message.id }) |
Not own, and flag-message |
| Mute, Unmute | mute |
client.moderation.mute or client.moderation.unmute for the author |
Not own, and mute-channel |
| Delete message | delete |
Asks to confirm, then MessageOperations.delete |
delete-any-message, or delete-own-message on an own message; the message isn't deleted already |
| Block User, Unblock | blockUser |
client.blockUser, client.unblockUser for the author |
Not own |
Some messages get no actions at all: a message that is still sending, a system or ephemeral message, and a bounced message. A message that failed to send keeps at most Resend, Edit Message and Delete message, and it keeps Edit Message and Delete message only when the failure was a network error.
A few things the table implies:
- Updates on screen. Reactions, deletes and resends change the message list before the server answers. Pinning doesn't at the state layer, but the Pin action updates the message on screen at once and puts it back if the request fails. What's optimistic and what isn't has the full list.
- Notifications. Pin, Mark as unread, the reminder actions, Flag, Mute and Delete report their result through
client.notifications. See Notifications. - Layout. Reply and Thread Reply open the thread through the layout. Without
ChatView, or aWorkspaceNavigationProviderof your own, they do nothing. See Moving between channels, threads and messages.
Each action has its own feature page: Reactions, Replies and threads, Quoted replies, Pinned messages, Sending, editing and deleting, Read state, unread and receipts, Reminders and save for later and Flagging, muting, blocking and bans.
Turning it on or off
Message actions are always on. Who sees which action depends on two things:
- Capabilities. The server gives the connected user a set of capabilities on each channel, derived from their roles and the channel type's permissions. The filter reads them for every message. Change them on the server, see Permissions reference.
- Channel settings. Remind me and Save for later also need
channel.config.userMessageReminders.enabled, see Reminders and save for later.
The filter doesn't check a capability for Pin, Download or Block User. Pin shows even for users without the pin-message capability, and the server rejects their request. Pinned messages shows how to hide it from them.
To remove the actions from every message, replace MessageActions with a component that renders nothing:
import type { Channel as ChannelType } from "stream-chat";
import { Channel, WithComponents } from "stream-chat-react";
const NoMessageActions = () => null;
export const ChannelWithoutActions = ({
channel,
}: {
channel: ChannelType;
}) => (
<WithComponents overrides={{ MessageActions: NoMessageActions }}>
<Channel channel={channel}>{/* MessageList, MessageComposer */}</Channel>
</WithComponents>
);Customizing
To change the actions, pass your own action set to MessageActions and register the wrapper with WithComponents. Start from defaultMessageActionSet to keep the built-in actions. The base filter runs on your set too, so the actions you keep stay hidden from users who can't take them.
This example removes Block User and keeps everything else:
import type { Channel as ChannelType } from "stream-chat";
import {
Channel,
MessageActions,
WithComponents,
defaultMessageActionSet,
} from "stream-chat-react";
// Keep the "more" button, which has no type, and every action except blocking.
const messageActionSet = defaultMessageActionSet.filter(
(action) =>
action.placement === "quick-dropdown-toggle" || action.type !== "blockUser",
);
const CustomMessageActions = () => (
<MessageActions messageActionSet={messageActionSet} />
);
export const ChannelWithCustomActions = ({
channel,
}: {
channel: ChannelType;
}) => (
<WithComponents overrides={{ MessageActions: CustomMessageActions }}>
<Channel channel={channel}>{/* MessageList, MessageComposer */}</Channel>
</WithComponents>
);Actions explains how action sets work and how to add quick and dropdown actions of your own. Custom message actions walks through a complete example. Message translation adds a Translate action.
Reference
- Message UI components: the
MessageActionsprops,messageActionSetanddisableBaseMessageActionSetFilter. - ComponentContext: the
MessageActionsoverride key. - Message hooks: the handlers behind the actions, such as
usePinHandleranduseMarkUnreadHandler. - Actions: every action set in the SDK.