import { Text, View } from "react-native";
import { MessageMenu } from "stream-chat-react-native";
const CustomMessageMenu = () => {
return (
<View>
<Text>Custom Message Menu</Text>
</View>
);
};
const App = () => {
return (
<Channel MessageMenu={CustomMessageMenu}>
{/** MessageList and MessageInput component here */}
</Channel>
);
};Custom Message Menu
The message menu is the contextual menu that appears when you long press a message. It allows you to perform actions on the message like replying, deleting, etc. Also, it allows you to react to the message/view the message reactions.
How to build a custom message menu
To build a custom message menu, you need to override the MessageMenu component in the Channel component.
Example
An example of a custom message menu is as follows. We will show a message menu with the reactions picker and the message actions.

For this example, we have used the default components whereever it was possible to keep the guide as simple.
We used the MessageActionListItem, MessageUserReactionsAvatar and MessageReactionPicker components with the default styles and wrapped them in a View component with the styles for the actions container and the reactions container.
The message actions are received from the props of the MessageActionsList component.
The MessageUserReactionsItem component is used to render the user reactions in the UI and this is imported from the useMessagesContext hook.
The message user reactions is fetched using the useFetchReactions hook.
We show the message/attachments which was long pressed in the message menu using the default components for the respective message types.
The types of the message to display can be shown using the messageContentOrder from the useMessagesContext hook.
For the text, we show the message text in the message menu using the MessageTextContainer component.
For the attachments, we show the attachments in the message menu using the Attachment component. For this we need otherAttachments from the useMessageContext hook.
For the quoted reply, we show the quoted message in the message menu using the Reply component. For this we need message.quoted_message. The message can be accessed from the useMessageContext hook.
For the gallery, we show the gallery in the message menu using the Gallery component. For this we need images from the useMessageContext hook.
For the files, we show the files in the message menu using the FileAttachmentGroup component. For this we need files from the useMessageContext hook.
import {
FlatList,
Modal,
StyleSheet,
Text,
TouchableWithoutFeedback,
View,
} from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import {
Attachment,
FileAttachmentGroup,
Gallery,
MessageActionListItem,
MessageActionListProps,
MessageAvatar,
MessageMenuProps,
MessageReactionPicker,
MessageTextContainer,
MessageUserReactionsAvatar,
MessageUserReactionsProps,
Poll,
Reaction,
Reply,
ReplyProps,
StreamingMessageView,
useChatContext,
useFetchReactions,
useMessageContext,
useMessagesContext,
useTheme,
useTranslationContext,
} from "stream-chat-react-native";
import Animated, {
useSharedValue,
useAnimatedStyle,
} from "react-native-reanimated";
import { useMemo } from "react";
const CustomMessageActionsList = (props: MessageActionListProps) => {
const { messageActions } = props;
return (
<View style={styles.actionsContainer}>
{messageActions?.map((action) => (
<MessageActionListItem key={action.title} {...action} />
))}
</View>
);
};
const CustomMessageReactions = (props: MessageUserReactionsProps) => {
const { reactions: propReactions, selectedReaction } = props;
const { supportedReactions, MessageUserReactionsItem } = useMessagesContext();
const { message } = useMessageContext();
const { loadNextPage, reactions: fetchedReactions } = useFetchReactions({
message,
reactionType: selectedReaction,
sort: {
created_at: -1,
},
});
const { t } = useTranslationContext();
const reactions = useMemo(
() =>
propReactions ||
(fetchedReactions.map((reaction) => ({
id: reaction.user?.id,
image: reaction.user?.image,
name: reaction.user?.name,
type: reaction.type,
})) as Reaction[]),
[propReactions, fetchedReactions],
);
const renderItem = ({ item }: { item: Reaction }) => (
<MessageUserReactionsItem
MessageUserReactionsAvatar={MessageUserReactionsAvatar}
reaction={item}
supportedReactions={supportedReactions ?? []}
/>
);
const renderHeader = () => (
<Text style={[styles.reactionsText]}>{t("Message Reactions")}</Text>
);
return (
<View style={styles.reactionsContainer}>
<FlatList
accessibilityLabel="reaction-flat-list"
columnWrapperStyle={[styles.flatListColumnContainer]}
contentContainerStyle={[styles.flatListContainer]}
data={reactions}
keyExtractor={(item) => item.id}
ListHeaderComponent={renderHeader}
numColumns={4}
onEndReached={loadNextPage}
renderItem={renderItem}
/>
</View>
);
};
export const MessageMenu = (props: MessageMenuProps) => {
const { client } = useChatContext();
const {
alignment,
files,
groupStyles,
images,
message,
dismissOverlay,
handleReaction,
onlyEmojis,
otherAttachments,
threadList,
videos,
} = useMessageContext();
const {
messageContentOrder,
messageTextNumberOfLines,
isMessageAIGenerated,
} = useMessagesContext();
const own_reactions =
message?.own_reactions?.map((reaction) => reaction.type) || [];
const {
messageActions,
showMessageReactions,
selectedReaction,
MessageUserReactionsItem,
} = props;
const {
theme: {
colors: {
blue_alice,
grey_gainsboro,
grey_whisper,
light_gray,
transparent,
white_snow,
},
messageSimple: {
content: {
container: { borderRadiusL, borderRadiusS },
receiverMessageBackgroundColor,
senderMessageBackgroundColor,
},
},
},
} = useTheme();
const groupStyle = `${alignment}_${(groupStyles?.[0] || "bottom").toLowerCase()}`;
const hasThreadReplies = !!message?.reply_count;
const messageHeight = useSharedValue(0);
const messageLayout = useSharedValue({ x: 0, y: 0 });
const messageWidth = useSharedValue(0);
return (
<View style={styles.wrapper}>
<Modal onRequestClose={dismissOverlay} transparent visible={true}>
<GestureHandlerRootView style={{ flex: 1 }}>
<TouchableWithoutFeedback
onPress={dismissOverlay}
style={{ flex: 1 }}
>
<View style={styles.overlay}>
<MessageReactionPicker
dismissOverlay={dismissOverlay}
handleReaction={handleReaction}
ownReactionTypes={own_reactions}
/>
<Animated.View
onLayout={({
nativeEvent: {
layout: { height: layoutHeight, width: layoutWidth, x, y },
},
}) => {
messageLayout.value = {
x: alignment === "left" ? x + layoutWidth : x,
y,
};
messageWidth.value = layoutWidth;
messageHeight.value = layoutHeight;
}}
style={[styles.alignEnd, styles.row]}
>
{alignment === "left" && MessageAvatar && (
<MessageAvatar
{...{ alignment, message, showAvatar: true }}
/>
)}
<View
style={[
styles.contentContainer,
{
backgroundColor:
onlyEmojis && !message.quoted_message
? transparent
: otherAttachments?.length
? otherAttachments[0].type === "giphy"
? !message.quoted_message
? transparent
: grey_gainsboro
: blue_alice
: alignment === "left"
? (receiverMessageBackgroundColor ?? white_snow)
: (senderMessageBackgroundColor ?? light_gray),
borderBottomLeftRadius:
(groupStyle === "left_bottom" ||
groupStyle === "left_single") &&
(!hasThreadReplies || threadList)
? borderRadiusS
: borderRadiusL,
borderBottomRightRadius:
(groupStyle === "right_bottom" ||
groupStyle === "right_single") &&
(!hasThreadReplies || threadList)
? borderRadiusS
: borderRadiusL,
borderColor: grey_whisper,
},
(onlyEmojis && !message.quoted_message) ||
otherAttachments?.length
? { borderWidth: 0 }
: {},
]}
>
{messageContentOrder.map(
(messageContentType, messageContentOrderIndex) => {
switch (messageContentType) {
case "quoted_reply":
return (
message.quoted_message && (
<View
key={`quoted_reply_${messageContentOrderIndex}`}
style={[styles.replyContainer]}
>
<Reply
quotedMessage={
message.quoted_message as ReplyProps["quotedMessage"]
}
/>
</View>
)
);
case "gallery":
return (
<Gallery
alignment={alignment}
groupStyles={groupStyles}
hasThreadReplies={!!message?.reply_count}
images={images}
key={`gallery_${messageContentOrderIndex}`}
message={message}
threadList={threadList}
videos={videos}
/>
);
case "files":
return (
<FileAttachmentGroup
files={files}
key={`file_attachment_group_${messageContentOrderIndex}`}
messageId={message.id}
/>
);
case "poll":
const pollId = message.poll_id;
const poll = pollId && client.polls.fromState(pollId);
return poll ? (
<Poll message={message} poll={poll} />
) : null;
case "ai_text": {
return isMessageAIGenerated?.(message) ? (
<StreamingMessageView
key={`ai_message_text_container_${messageContentOrderIndex}`}
message={message}
/>
) : null;
}
case "attachments":
return otherAttachments?.map(
(attachment, attachmentIndex) => (
<Attachment
attachment={attachment}
key={`${message.id}-${attachmentIndex}`}
/>
),
);
case "text":
default:
return (otherAttachments?.length &&
otherAttachments[0].actions) ||
isMessageAIGenerated?.(message) ? null : (
<MessageTextContainer
key={`message_text_container_${messageContentOrderIndex}`}
messageOverlay
messageTextNumberOfLines={
messageTextNumberOfLines
}
onlyEmojis={onlyEmojis}
/>
);
}
},
)}
</View>
</Animated.View>
{showMessageReactions ? (
<CustomMessageReactions
message={message}
MessageUserReactionsAvatar={MessageUserReactionsAvatar}
MessageUserReactionsItem={MessageUserReactionsItem}
selectedReaction={selectedReaction}
/>
) : (
<CustomMessageActionsList
dismissOverlay={dismissOverlay}
MessageActionListItem={MessageActionListItem}
messageActions={messageActions}
/>
)}
</View>
</TouchableWithoutFeedback>
</GestureHandlerRootView>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
alignEnd: { alignItems: "flex-end" },
wrapper: {
flex: 1,
},
overlay: {
flex: 1,
backgroundColor: "#000000CC",
justifyContent: "center",
alignItems: "center",
},
actionsContainer: {
backgroundColor: "white",
borderRadius: 8,
padding: 16,
},
contentContainer: {
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderWidth: 1,
overflow: "hidden",
marginVertical: 8,
},
replyContainer: {
flexDirection: "row",
paddingHorizontal: 8,
paddingTop: 8,
},
row: { flexDirection: "row" },
reactionsContainer: {
maxHeight: 200,
backgroundColor: "white",
borderRadius: 8,
padding: 16,
maxWidth: 500,
},
flatListColumnContainer: {
justifyContent: "space-evenly",
},
flatListContainer: {
justifyContent: "center",
},
reactionsText: {
fontSize: 16,
fontWeight: "bold",
marginVertical: 16,
textAlign: "center",
},
});