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. 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:
[
{ 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:
- 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,
editanddeletego, unless the user may edit or delete other people's messages. - The placement decides where an item goes.
quickitems render next to the message.dropdownitems render in the menu. Thequick-dropdown-toggleitem renders the button that opens the menu. - 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,dropdownandquick-dropdown-toggle. - The channel members header has
quickandmenu, and its items name their componentcomponentrather thanComponent. - 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:
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,replyandmarkUnread. - On a failed message, it keeps only
resendMessage, andeditanddeletewhere they apply. - Otherwise, each type needs its capability:
delete,edit,flag,markUnread,mute,quote,reactandreplycheck the user's capabilities in the channel,blockUseris hidden on the user's own messages, andremindMeandsaveForLaterneed message reminders turned on for the channel type.copyMessageTextneeds text, anddownloadneeds a downloadable attachment. pinisn't checked. For users without thepin-messagecapability, hide it yourself, asPinWithConfirmationbelow 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.
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:
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.
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:
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 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,leaveChannelanddeleteChat. Its filter keepsmuteUserandblockUserfor direct message channels,leaveChannelfor group channels, and checksmute-channel,ban-channel-members,leave-channelanddelete-channel. - Channel member, in a member's detail view:
sendMessage,muteUser,blockUserandremoveUser. Its filter removes them all on the user's own entry, andremoveUserwithoutupdate-channel-members. - Channel members header, in the members view's header:
addMembers, aquickbutton that needsupdate-channel-members. Your own header items can carry afilter({ 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:
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, 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:
- 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
typeand swapping its component.
// 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 and Attachment actions recipes walk through complete examples.
Used in: Message actions, Flagging, muting, blocking and bans, Channel lists and Channel details and members.