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<string>("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",
},
});