Actions
Every menu of actions in the SDK is built from a list of action items: each one has a label, an icon, a handler, and an identifier. To change a menu, you give the component that builds it a function that receives the default items and returns the list to render. There are three such lists:
| Actions | Where they show | Changed through | Default list |
|---|---|---|---|
| Message actions | the message menu, opened by a long press on a message | messageActions on Channel and Thread |
messageActions |
| Channel actions | a channel-list row's swipe actions and options sheet, channel details | getChannelActionItems on ChannelList, and on ChannelDetailsActionsSection |
buildDefaultChannelActionItems |
| Member actions | the sheet that opens on a member in channel details | getChannelMemberActionItems on ChannelMemberActionsSheet |
buildDefaultChannelMemberActionItems |
Each list also has hooks that run the same actions from components of your own. Everything on this page is exported from stream-chat-react-native.
Message actions
A long press on a message opens its menu: the reaction picker above the message, and the action list below it. The action list is built when the menu opens, not when the message renders, so a long list or an expensive check costs nothing until the user asks for it.
Channel and Thread take a messageActions function. When a menu opens, the SDK builds every default action for the message, passes them to the function with what it knows about the message, and renders the list it returns, in order. Without the prop, the SDK uses its own messageActions function, which is exported, so yours can start from its result:
import { useCallback } from "react";
import { Share } from "react-native";
import type { Channel as StreamChannel } from "stream-chat";
import {
asDynamicKey,
Channel,
MessageComposer,
MessageList,
messageActions as defaultMessageActions,
useComponentsContext,
useTheme,
useTranslationContext,
} from "stream-chat-react-native";
import type {
MessageActionsProp,
MessageActionType,
} from "stream-chat-react-native";
export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => {
const { icons } = useComponentsContext();
const { t } = useTranslationContext();
const {
theme: { semantics },
} = useTheme();
const messageActions = useCallback<MessageActionsProp>(
(params) => {
// The SDK's list for this message, without "Block User".
const actions = defaultMessageActions(params).filter(
(action) => action.actionType !== "blockUser",
);
const text = params.message.text;
if (!text || params.error) return actions;
const share: MessageActionType = {
action: () => {
void Share.share({ message: text });
},
actionType: "shareMessage",
icon: (
<icons.Share
height={20}
stroke={semantics.textSecondary}
width={20}
/>
),
title: t(asDynamicKey("myApp.message.share.label"), "Share"),
type: "standard",
};
return [share, ...actions];
},
[icons, semantics.textSecondary, t],
);
return (
<Channel channel={channel} messageActions={messageActions}>
<MessageList />
<MessageComposer />
</Channel>
);
};- The user long-presses a message, and the menu opens.
- The SDK calls
messageActionswith the message's actions and context. defaultMessageActions(params)returns the list the SDK would show: the actions that apply to this message and to the user's capabilities, in the SDK's order.- The function drops "Block User", and puts "Share" first when the message has text.
- Pressing an item closes the menu, and runs the item's
actiononce the menu has closed.
Thread doesn't inherit props from Channel, so pass the same messageActions to both.
What the function receives
| Field | What it is |
|---|---|
banUser, blockUser, copyMessage, deleteForMeMessage, deleteMessage, editMessage, flagMessage, markUnread, muteUser, pinMessage, quotedReply, retry, threadReply, unpinMessage |
one MessageActionType for each default action, whether or not it applies to this message |
message |
the message the menu is open on |
isMyMessage |
whether the current user sent it |
isThreadMessage |
whether the menu is inside a Thread, or the message is a reply |
error |
whether the message is an error or failed to send |
ownCapabilities |
the user's capabilities in the channel, as booleans such as pinMessage and deleteAnyMessage |
dismissOverlay |
closes the menu |
updateMessage |
updates the message in the message list |
The action objects are built for every message, so a function that returns them directly, such as ({ copyMessage, editMessage }) => [copyMessage, editMessage], shows them even where they don't apply: an edit action on someone else's message, a copy action on a message without text. Check the fields yourself, or filter the result of defaultMessageActions(params) as the example does.
What the default list holds
The SDK's messageActions adds an action when its condition holds, in this order:
| Action | Shown when |
|---|---|
retry |
the message is the user's own and failed to send |
threadReply |
the user has sendReply, outside a thread, and the message didn't fail |
quotedReply |
the user has quoteMessage, outside a thread, and the message didn't fail |
pinMessage, unpinMessage |
the user has pinMessage; one or the other, depending on whether the message is pinned |
editMessage |
the user may update this message (updateOwnMessage or updateAnyMessage), and it has no Giphy, Imgur or poll |
copyMessage |
a clipboard handler is available, the message has text, and it didn't fail |
markUnread |
the user has readEvents, the message is someone else's, outside a thread, and it didn't fail |
banUser |
the message is someone else's and the user has banChannelMembers |
deleteMessage |
the user may delete this message (deleteOwnMessage or deleteAnyMessage) |
flagMessage |
the message is someone else's and the user has flagMessage |
muteUser, blockUser |
the message is someone else's |
On a failed message, only retry, editMessage and deleteMessage stay. deleteForMeMessage, which deletes the message for the current user only, is never in the default list: add it in your function to offer it.
The menu renders the standard actions first and the destructive ones after a separator, each group in the order of your list. banUser, blockUser, deleteMessage and deleteForMeMessage are destructive.
MessageActionType
| Field | Type | Description |
|---|---|---|
action |
() => void |
Runs when the item is pressed, after the menu has closed. |
actionType |
ActionType | string |
Identifies the action. Your own actions use a string of their own. |
title |
string |
The label. Translate it yourself, as share does. |
icon |
ReactElement |
Optional. The SDK's actions use 20 by 20 icons from the icons key. |
titleStyle |
StyleProp<TextStyle> |
Optional. The style of the label. |
type |
'standard' | 'destructive' |
Which group of the menu it renders in. |
To change one default action, spread it and replace a field: { ...params.deleteMessage, title: "Remove" } keeps its handler and its confirmation dialog.
Running code when an action runs
Channel and Thread also take a handler for each default action, called with the message when the user picks the action, before the SDK's own handler runs. They add to the action, they don't replace it, which makes them the place for analytics or logging:
| Prop | Called for |
|---|---|
handleBan |
banUser |
handleBlockUser |
blockUser, with the user |
handleCopy |
copyMessage |
handleDelete |
deleteMessage |
handleDeleteForMe |
deleteForMeMessage |
handleEdit |
editMessage |
handleFlag |
flagMessage |
handleMarkUnread |
markUnread |
handleMute |
muteUser |
handlePinMessage |
pinMessage, unpinMessage |
handleQuotedReply |
quotedReply |
handleRetry |
retry |
handleThreadReply |
threadReply |
handleReaction |
a reaction the user picks, with its type |
import type { LocalMessage } from "stream-chat";
import type { Channel as StreamChannel } from "stream-chat";
import {
Channel,
MessageComposer,
MessageList,
} from "stream-chat-react-native";
const track = (event: string, message: LocalMessage) => {
console.log(event, message.id);
};
const handleDelete = async (message: LocalMessage) =>
track("delete-picked", message);
const handleFlag = async (message: LocalMessage) =>
track("flag-picked", message);
export const TrackedChannel = ({ channel }: { channel: StreamChannel }) => (
<Channel
channel={channel}
handleDelete={handleDelete}
handleFlag={handleFlag}
>
<MessageList />
<MessageComposer />
</Channel>
);Delete and flag ask for confirmation, and their handlers run before the dialog shows, so they run even when the user cancels. Treat them as "the user picked the action", not "the action happened". A handler set on a MessageActionType you build yourself, or an action you call from your own component through useMessageActionHandlers(), doesn't call these props.
Message actions in your own components
Two hooks run a message's actions from a component inside the message, such as a custom MessageItemView, one of the MessageContent*View slots, or a custom MessageActionList. Both take no arguments and read the message from context:
useMessageActions()returns the sameMessageActionTypeobjects the menu shows:{ banUser, blockUser, copyMessage, deleteForMeMessage, deleteMessage, editMessage, flagMessage, handleReaction, markUnread, muteUser, pinMessage, quotedReply, retry, threadReply, unpinMessage }. Theiractionruns thehandle*prop fromChannelfirst, as the menu does, andblockUserasks for confirmation. Building them subscribes to the author's mute state and translates every title, so the SDK calls this hook only while a menu is open. Don't call it in every row of a list.useMessageActionHandlers()returns the handlers behind the actions,MessageActionHandlerCallbacks.Messagebuilds them once per message and puts them inMessageContextasmessageActionHandlers, so every caller shares them and calling the hook costs nothing. They don't call thehandle*props.
| Handler | Does |
|---|---|
handleCopyMessage |
copies the text, translated into userLanguage when a translation exists |
handleDeleteMessage |
asks for confirmation, then deletes the message |
handleDeleteForMeMessage |
deletes the message for the current user |
handleEditMessage |
puts the message into the composer for editing |
handleFlagMessage |
asks for confirmation, then flags the message |
handleMarkUnreadMessage |
marks the channel unread from this message |
handleQuotedReplyMessage |
sets the message as the composer's quoted message |
handleResendMessage |
sends a failed message again |
handleToggleBanUser |
bans or unbans the author |
handleToggleBlockUser |
blocks or unblocks the user you pass, without asking |
handleToggleMuteUser |
mutes or unmutes the author |
handleTogglePinMessage |
pins or unpins the message |
handleToggleReaction |
adds or removes the user's reaction of the type you pass |
The copy, delete, flag, mark unread, mute and pin handlers publish a notification when they finish or fail: see Displaying notifications.
This one adds a reply button below every message's content:
import { Pressable, Text } from "react-native";
import {
useMessageActionHandlers,
useTranslationContext,
} from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";
const ReplyButton = () => {
const { handleQuotedReplyMessage } = useMessageActionHandlers();
const { t } = useTranslationContext();
return (
<Pressable accessibilityRole="button" onPress={handleQuotedReplyMessage}>
<Text>{t("message.reply.label", "Reply")}</Text>
</Pressable>
);
};
export const replyOverrides: ComponentOverrides = {
MessageContentBottomView: ReplyButton,
};The same handlers, under shorter names, reach the press handlers: onLongPressMessage and onPressMessage on Channel receive them as actionHandlers, together with the message and a defaultHandler. onLongPressMessage={({ actionHandlers }) => actionHandlers?.quotedReply()} makes a long press quote the message instead of opening the menu.
Replacing how the menu renders the actions
The action list is the MessageActionList key, and each item the MessageActionListItem key. MessageActionList receives the list your function returned as messageActions. MessageActionListItem receives one MessageActionType as its props. A replacement item closes the menu and runs the action after it closes, as the default does, with closeOverlay() and scheduleActionOnClose(action). The menu's other parts are on Overlays and bottom sheets.
What each default action does, and the actions that come with flagging, muting and reminders, are on Message actions.
Channel actions
A channel's actions show in three places:
- A channel-list row's swipe actions. Swiping a row left reveals a button that opens the row's options sheet, and the actions whose
placementis'swipe'or'both'. - The options sheet,
ChannelDetailsBottomSheet, with the actions whoseplacementis'sheet'or'both'. - Channel details, in
ChannelDetailsActionsSection, which ignoresplacement.
ChannelList shows the swipe actions unless swipeActionsEnabled is false. The props the options sheet and its header receive, for a replacement of either, are listed in The options sheet.
The SDK builds the default items with buildDefaultChannelActionItems:
id |
Label | placement |
Shown |
|---|---|---|---|
mute |
Mute or Unmute Chat, or Group | 'swipe' |
always |
pin |
Pin or Unpin Chat, or Group | 'sheet' |
in the channel list only |
muteUser |
Mute or Unmute User | 'sheet' |
in a direct message channel |
block |
Block or Unblock User | 'sheet' |
in a direct message channel; blocking asks for confirmation |
leave |
Leave Chat, or Group | 'sheet' |
always; asks for confirmation |
deleteChannel |
Delete Chat, or Group | 'sheet' |
when the user created the channel; asks for confirmation |
A direct message channel is one with exactly one member besides the current user. The items don't check the user's capabilities: a user without leave-channel sees "Leave", and the request fails with an error notification. Filter the items yourself where your app's permissions need it.
In channel details, mute and muteUser render as switches, and the others as rows.
Changing the items
getChannelActionItems receives { context, defaultItems } and returns the items to render. context holds what the defaults were built from:
| Field | What it is |
|---|---|
channel |
the channel |
actions |
ChannelActions: the handlers behind the items, see useChannelActions |
surface |
'list' in the channel list, 'details' in channel details |
isDirectChat |
whether the channel has exactly one member besides the current user |
isPinned, isArchived, channelMuteActive |
the channel's state for the current user |
userMuteActive, isBlocked |
in a direct message channel, the other member's state |
icons |
the icons in effect, with your overrides |
t |
the translation function |
This adds an archive action to the options sheet:
import type { ChannelPaginator } from "stream-chat";
import { asDynamicKey, ChannelList } from "stream-chat-react-native";
import type {
ChannelActionItem,
GetChannelActionItems,
} from "stream-chat-react-native";
const getChannelActionItems: GetChannelActionItems = ({
context,
defaultItems,
}) => {
const { actions, icons, isArchived, t } = context;
const archive: ChannelActionItem = {
action: isArchived ? actions.unarchive : actions.archive,
Icon: icons.Folder,
id: "archive",
label: isArchived
? t(asDynamicKey("myApp.channel.unarchive.label"), "Unarchive")
: t(asDynamicKey("myApp.channel.archive.label"), "Archive"),
placement: "sheet",
type: "standard",
};
return [...defaultItems, archive];
};
export const Channels = ({ paginator }: { paginator: ChannelPaginator }) => (
<ChannelList
getChannelActionItems={getChannelActionItems}
paginator={paginator}
/>
);A ChannelActionItem has an action, which receives optional { onSuccess, onFailure } callbacks, an Icon component that receives IconProps, an id, a label, a placement and a type, 'standard' or 'destructive'. Declare the function at module scope, or wrap it in useCallback: the list rebuilds its items when it changes.
To remove an item, filter it out by id. To replace one, map over defaultItems and keep its id: channel details still renders mute and muteUser as switches.
In channel details
ChannelDetails renders ChannelDetailsActionsSection with no props, so it takes your function through an override of that key:
import type { Channel as StreamChannel } from "stream-chat";
import {
ChannelDetails,
ChannelDetailsActionsSection,
ChannelDetailsContextProvider,
WithComponents,
} from "stream-chat-react-native";
import type {
ChannelDetailsActionsSectionProps,
ComponentOverrides,
GetChannelActionItems,
} from "stream-chat-react-native";
// Channel details has no pin item by default. This adds the list's.
const getChannelActionItems: GetChannelActionItems = ({
context,
defaultItems,
}) => {
const { actions, icons, isDirectChat, isPinned, t } = context;
const pinLabel = isDirectChat
? t("channel.pinChat.label", "Pin Chat")
: t("channel.pinGroup.label", "Pin Group");
const unpinLabel = isDirectChat
? t("channel.unpinChat.label", "Unpin Chat")
: t("channel.unpinGroup.label", "Unpin Group");
return [
...defaultItems,
{
action: isPinned ? actions.unpin : actions.pin,
Icon: isPinned ? icons.Unpin : icons.Pin,
id: "pin",
label: isPinned ? unpinLabel : pinLabel,
placement: "sheet",
type: "standard",
},
];
};
const ActionsSection = (props: ChannelDetailsActionsSectionProps) => (
<ChannelDetailsActionsSection
{...props}
getChannelActionItems={getChannelActionItems}
/>
);
const detailsOverrides: ComponentOverrides = {
ChannelDetailsActionsSection: ActionsSection,
};
export const ChannelDetailsScreen = ({
channel,
onBack,
}: {
channel: StreamChannel;
onBack: () => void;
}) => (
<ChannelDetailsContextProvider channel={channel}>
<WithComponents overrides={detailsOverrides}>
<ChannelDetails onBack={onBack} />
</WithComponents>
</ChannelDetailsContextProvider>
);ChannelDetailsActionsSection also takes onChannelDismiss, called once the channel is gone for the user, after leave, deleteChannel or block succeeds. Pass it to navigate away from the screen. It reaches the items by id, so keep those ids on replaced items.
The same function can serve both places: check context.surface to return different items in the list and in details.
Running channel and user actions
The hooks behind the items run the same actions from your own components:
useChannelActionItems({ channel, surface, getChannelActionItems })returns the items for a channel, as the list and details build them.useChannelActionItemsByIdreturns them keyed byid.useChannelActions(channel)returnsChannelActions:addMembers,removeMembers,archive,unarchive,pin,unpin,muteChannel,unmuteChannel,muteUser,unmuteUser,blockUser,unblockUser,leave,deleteChannel,updateNameandupdateImage. The user actions act on the other member of a direct message channel. They don't ask for confirmation.useUserActions(user)returnsUserActionsfor any user:muteUser,unmuteUser,blockUserandunblockUser.
Every action takes optional { onSuccess, onFailure }, publishes a success or error notification, and resolves without throwing. The UserActions call onSuccess but never onFailure. addMembers(memberIds, options), removeMembers(memberIds, options), updateName(name, options) and updateImage(image, options) take their value first.
import { Pressable, Text } from "react-native";
import type { Channel as StreamChannel } from "stream-chat";
import { useChannelActions } from "stream-chat-react-native";
export const ArchiveButton = ({
channel,
onDone,
}: {
channel: StreamChannel;
onDone: () => void;
}) => {
const { archive } = useChannelActions(channel);
return (
<Pressable
accessibilityRole="button"
onPress={() => archive({ onSuccess: onDone })}
>
<Text>Archive</Text>
</Pressable>
);
};The hooks publish their notifications through the notification target they sit in. Render them inside the screen whose notification list should show the result: see Displaying notifications.
Member actions
Pressing a member in channel details opens ChannelMemberActionsSheet, with the member's actions. The SDK builds them with buildDefaultChannelMemberActionItems:
id |
Label | Shown |
|---|---|---|
muteUser |
Mute or Unmute User | for every member but the current user |
block |
Block or Unblock User | for every member but the current user; blocking asks for confirmation |
removeMember |
Remove User | for other members, when the user has update-channel-members; asks for confirmation |
The sheet takes getChannelMemberActionItems, which receives { context, defaultItems } and returns the items. context holds channel, member, isCurrentUser, isBlocked, userMuteActive, ownCapabilities (the user's capabilities in the channel, as strings), channelActions, userActions, icons and t. A ChannelMemberActionItem has the fields of a channel action item without placement. Pressing an item runs its action and closes the sheet.
Both channel details screens that open the sheet, the member section and the full member list, read it from the ChannelMemberActionsSheet key, so one override changes both:
import { useCallback } from "react";
import { Linking } from "react-native";
import type { Channel as StreamChannel } from "stream-chat";
import {
ChannelDetails,
ChannelDetailsContextProvider,
ChannelMemberActionsSheet,
useChannelDetailsContext,
WithComponents,
} from "stream-chat-react-native";
import type {
ChannelMemberActionsSheetProps,
ComponentOverrides,
GetChannelMemberActionItems,
} from "stream-chat-react-native";
const MemberActionsSheet = (props: ChannelMemberActionsSheetProps) => {
const { closeModals } = useChannelDetailsContext();
const getChannelMemberActionItems = useCallback<GetChannelMemberActionItems>(
({ context, defaultItems }) => {
const userId = context.member.user?.id;
if (context.isCurrentUser || !userId) return defaultItems;
return [
{
action: () => {
// Close the member list before leaving the screen.
closeModals();
void Linking.openURL(`myapp://direct-message/${userId}`);
},
Icon: context.icons.MessageBubbleEmpty,
id: "sendDirectMessage",
label: "Send Direct Message",
type: "standard",
},
...defaultItems,
];
},
[closeModals],
);
return (
<ChannelMemberActionsSheet
{...props}
getChannelMemberActionItems={getChannelMemberActionItems}
/>
);
};
const detailsOverrides: ComponentOverrides = {
ChannelMemberActionsSheet: MemberActionsSheet,
};
export const ChannelDetailsScreen = ({
channel,
onBack,
}: {
channel: StreamChannel;
onBack: () => void;
}) => (
<ChannelDetailsContextProvider channel={channel}>
<WithComponents overrides={detailsOverrides}>
<ChannelDetails onBack={onBack} />
</WithComponents>
</ChannelDetailsContextProvider>
);MemberActionsSheet is declared at module scope and reads what it needs through hooks, so the override object never changes. The sheet closes itself after an item runs. closeModals() from useChannelDetailsContext() closes the screens channel details opened on top of itself, such as the full member list, so call it before you navigate away.
useChannelMemberActionItems({ channel, member, getChannelMemberActionItems }) returns a member's items for a sheet of your own, and useUserActions(member.user) the user actions alone.
The member list, the roles and adding members are on Channel details and members, and the UI cookbooks Custom channel actions and Custom member actions build complete examples.
Adding, removing and replacing actions
Start from the default list and change it, rather than writing one from scratch:
- Remove an action by filtering it out, by
actionTypefor message actions and byidfor channel and member actions. - Add one by putting an item where you want it. Message actions keep your order within their group,
standardordestructive; channel and member actions keep it as given. - Replace one by keeping its identifier and changing its fields.
An action you add has no condition: it shows on every message, channel or member your function returns it for. Check params or context, the user's capabilities among them, and leave it out where it doesn't apply. The Custom message actions UI cookbook walks through a complete example.
Pitfalls
- Returning the received action objects as they are. They exist for every message. Start from
defaultMessageActions(params), or check the conditions yourself. messageActionsonChannelonly. AThreaddoesn't inherit it. Pass it to both.- A
handle*prop used as "the action succeeded". It runs when the user picks the action, before any confirmation and before the request. useMessageActions()in every row. It subscribes and translates for each message. UseuseMessageActionHandlers()in rows, and keepuseMessageActions()for a custom menu.getChannelActionItemsonChannelListexpected in channel details. Channel details takes it through an override ofChannelDetailsActionsSection.- A replaced
mute,leaveorblockitem with a newid. Channel details matches these byidfor the switches and foronChannelDismiss.
Used in: Message actions, Flagging, muting, blocking and bans, Channel lists and Channel details and members.