diff --git a/examples/SampleApp/src/components/MessageInfoBottomSheet.tsx b/examples/SampleApp/src/components/MessageInfoBottomSheet.tsx
new file mode 100644
index 0000000000..b989c32203
--- /dev/null
+++ b/examples/SampleApp/src/components/MessageInfoBottomSheet.tsx
@@ -0,0 +1,102 @@
+import React, { useMemo } from 'react';
+import BottomSheet, { BottomSheetFlatList } from '@gorhom/bottom-sheet';
+import { BottomSheetView } from '@gorhom/bottom-sheet';
+import {
+ Avatar,
+ useChatContext,
+ useMessageDeliveredData,
+ useMessageReadData,
+ useTheme,
+} from 'stream-chat-react-native';
+import { LocalMessage, UserResponse } from 'stream-chat';
+import { StyleSheet, Text, View } from 'react-native';
+
+const renderUserItem = ({ item }: { item: UserResponse }) => (
+
+
+ {item.name ?? item.id}
+
+);
+
+const renderEmptyText = ({ text }: { text: string }) => (
+ {text}
+);
+
+export const MessageInfoBottomSheet = ({
+ message,
+ ref,
+}: {
+ message?: LocalMessage;
+ ref: React.RefObject;
+}) => {
+ const {
+ theme: { colors },
+ } = useTheme();
+ const { client } = useChatContext();
+ const deliveredStatus = useMessageDeliveredData({ message });
+ const readStatus = useMessageReadData({ message });
+
+ const otherDeliveredToUsers = useMemo(() => {
+ return deliveredStatus.filter((user: UserResponse) => user.id !== client?.user?.id);
+ }, [deliveredStatus, client?.user?.id]);
+
+ const otherReadUsers = useMemo(() => {
+ return readStatus.filter((user: UserResponse) => user.id !== client?.user?.id);
+ }, [readStatus, client?.user?.id]);
+
+ return (
+
+
+ Read
+ item.id}
+ style={styles.flatList}
+ ListEmptyComponent={renderEmptyText({ text: 'No one has read this message.' })}
+ />
+ Delivered
+ item.id}
+ style={styles.flatList}
+ ListEmptyComponent={renderEmptyText({ text: 'The message was not delivered to anyone.' })}
+ />
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 24,
+ justifyContent: 'center',
+ height: '100%',
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginVertical: 8,
+ },
+ flatList: {
+ borderRadius: 16,
+ },
+ userItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 8,
+ backgroundColor: 'white',
+ },
+ userName: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ marginLeft: 16,
+ },
+ emptyText: {
+ fontSize: 16,
+ marginVertical: 16,
+ textAlign: 'center',
+ },
+});
diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx
index fbd35467ce..c978ccfe5a 100644
--- a/examples/SampleApp/src/screens/ChannelScreen.tsx
+++ b/examples/SampleApp/src/screens/ChannelScreen.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { LocalMessage, Channel as StreamChatChannel } from 'stream-chat';
import { RouteProp, useFocusEffect, useNavigation } from '@react-navigation/native';
import {
@@ -33,6 +33,8 @@ import { channelMessageActions } from '../utils/messageActions.tsx';
import { MessageLocation } from '../components/LocationSharing/MessageLocation.tsx';
import { useStreamChatContext } from '../context/StreamChatContext.tsx';
import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx';
+import BottomSheet from '@gorhom/bottom-sheet';
+import { MessageInfoBottomSheet } from '../components/MessageInfoBottomSheet.tsx';
export type ChannelScreenNavigationProp = NativeStackNavigationProp<
StackNavigatorParamList,
@@ -115,19 +117,20 @@ const ChannelHeader: React.FC = ({ channel }) => {
// Either provide channel or channelId.
export const ChannelScreen: React.FC = ({
+ navigation,
route: {
params: { channel: channelFromProp, channelId, messageId },
},
}) => {
const { chatClient, messageListImplementation, messageListMode, messageListPruning } =
useAppContext();
- const navigation = useNavigation();
const { bottom } = useSafeAreaInsets();
const {
theme: { colors },
} = useTheme();
const { t } = useTranslationContext();
const { setThread } = useStreamChatContext();
+ const [selectedMessage, setSelectedMessage] = useState(undefined);
const [channel, setChannel] = useState(channelFromProp);
@@ -170,6 +173,9 @@ export const ChannelScreen: React.FC = ({
const onThreadSelect = useCallback(
(thread: LocalMessage | null) => {
+ if (!thread || !channel) {
+ return;
+ }
setSelectedThread(thread);
setThread(thread);
navigation.navigate('ThreadScreen', {
@@ -180,6 +186,16 @@ export const ChannelScreen: React.FC = ({
[channel, navigation, setThread],
);
+ const messageInfoBottomSheetRef = useRef(null);
+
+ const handleMessageInfo = useCallback(
+ (message: LocalMessage) => {
+ setSelectedMessage(message);
+ messageInfoBottomSheetRef.current?.snapToIndex(1);
+ },
+ [messageInfoBottomSheetRef],
+ );
+
const messageActions = useCallback(
(params: MessageActionsParams) => {
if (!chatClient) {
@@ -190,9 +206,10 @@ export const ChannelScreen: React.FC = ({
chatClient,
t,
colors,
+ handleMessageInfo,
});
},
- [chatClient, colors, t],
+ [chatClient, colors, t, handleMessageInfo],
);
if (!channel || !chatClient) {
@@ -232,6 +249,7 @@ export const ChannelScreen: React.FC = ({
)}
+
);
diff --git a/examples/SampleApp/src/utils/messageActions.tsx b/examples/SampleApp/src/utils/messageActions.tsx
index 7dafc4ff79..6398bd12af 100644
--- a/examples/SampleApp/src/utils/messageActions.tsx
+++ b/examples/SampleApp/src/utils/messageActions.tsx
@@ -1,8 +1,10 @@
+import React from 'react';
import { Alert } from 'react-native';
-import { StreamChat } from 'stream-chat';
+import { LocalMessage, StreamChat } from 'stream-chat';
import {
Colors,
Delete,
+ Eye,
messageActions,
MessageActionsParams,
Time,
@@ -15,11 +17,13 @@ export function channelMessageActions({
chatClient,
colors,
t,
+ handleMessageInfo,
}: {
params: MessageActionsParams;
chatClient: StreamChat;
t: TranslationContextValue['t'];
colors?: typeof Colors;
+ handleMessageInfo: (message: LocalMessage) => void;
}) {
const { dismissOverlay, deleteForMeMessage } = params;
const actions = messageActions(params);
@@ -111,5 +115,15 @@ export function channelMessageActions({
title: t('Delete for me'),
});
+ actions.push({
+ action: () => {
+ dismissOverlay();
+ handleMessageInfo(params.message);
+ },
+ actionType: 'messageInfo',
+ icon: ,
+ title: 'Message Info',
+ });
+
return actions;
}
diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx
index 699d10cdd5..d1460cb61d 100644
--- a/package/src/components/Message/Message.tsx
+++ b/package/src/components/Message/Message.tsx
@@ -982,7 +982,7 @@ export const Message = (props: MessageProps) => {
const { openThread } = useThreadContext();
const { t } = useTranslationContext();
const readBy = useMessageReadData({ message });
- const deliveredToCount = useMessageDeliveredData({ message });
+ const deliveredTo = useMessageDeliveredData({ message });
const { setQuotedMessage, setEditingState } = useMessageComposerAPIContext();
return (
@@ -991,13 +991,13 @@ export const Message = (props: MessageProps) => {
{...{
channel,
chatContext,
- deliveredToCount,
+ deliveredToCount: deliveredTo.length,
dismissKeyboard,
enforceUniqueReaction,
members,
messagesContext,
openThread,
- readBy,
+ readBy: readBy.length,
setEditingState,
setQuotedMessage,
t,
diff --git a/package/src/components/Message/hooks/useMessageDeliveryData.ts b/package/src/components/Message/hooks/useMessageDeliveryData.ts
index daf640ea19..6a4202fe77 100644
--- a/package/src/components/Message/hooks/useMessageDeliveryData.ts
+++ b/package/src/components/Message/hooks/useMessageDeliveryData.ts
@@ -1,25 +1,29 @@
import { useCallback, useEffect, useState } from 'react';
-import { Event, LocalMessage } from 'stream-chat';
+import { Event, LocalMessage, UserResponse } from 'stream-chat';
import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
-export const useMessageDeliveredData = ({ message }: { message: LocalMessage }) => {
+export const useMessageDeliveredData = ({ message }: { message?: LocalMessage }) => {
const { channel } = useChannelContext();
const { client } = useChatContext();
const calculate = useCallback(() => {
- if (!message.created_at) {
- return 0;
+ if (!message?.created_at) {
+ return [];
}
const messageRef = {
msgId: message.id,
timestampMs: new Date(message.created_at).getTime(),
};
- return channel.messageReceiptsTracker.deliveredForMessage(messageRef).length;
+ return channel.messageReceiptsTracker.deliveredForMessage(messageRef);
}, [channel, message]);
- const [deliveredToCount, setDeliveredToCount] = useState(calculate());
+ const [deliveredToCount, setDeliveredToCount] = useState([]);
+
+ useEffect(() => {
+ setDeliveredToCount(calculate());
+ }, [calculate]);
useEffect(() => {
const { unsubscribe } = channel.on('message.delivered', (event: Event) => {
diff --git a/package/src/components/Message/hooks/useMessageReadData.ts b/package/src/components/Message/hooks/useMessageReadData.ts
index 8e0086a232..8f79963692 100644
--- a/package/src/components/Message/hooks/useMessageReadData.ts
+++ b/package/src/components/Message/hooks/useMessageReadData.ts
@@ -1,26 +1,30 @@
import { useCallback, useEffect, useState } from 'react';
-import { Event, LocalMessage } from 'stream-chat';
+import { Event, LocalMessage, UserResponse } from 'stream-chat';
import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
-export const useMessageReadData = ({ message }: { message: LocalMessage }) => {
+export const useMessageReadData = ({ message }: { message?: LocalMessage }) => {
const { channel } = useChannelContext();
const { client } = useChatContext();
const calculate = useCallback(() => {
- if (!message.created_at) {
- return 0;
+ if (!message?.created_at) {
+ return [];
}
const messageRef = {
msgId: message.id,
timestampMs: new Date(message.created_at).getTime(),
};
- return channel.messageReceiptsTracker.readersForMessage(messageRef).length;
+ return channel.messageReceiptsTracker.readersForMessage(messageRef);
}, [channel, message]);
- const [readBy, setReadBy] = useState(calculate());
+ const [readBy, setReadBy] = useState([]);
+
+ useEffect(() => {
+ setReadBy(calculate());
+ }, [calculate]);
useEffect(() => {
const { unsubscribe } = channel.on('message.read', (event: Event) => {
diff --git a/package/src/components/index.ts b/package/src/components/index.ts
index 108f0471c8..407bcd1893 100644
--- a/package/src/components/index.ts
+++ b/package/src/components/index.ts
@@ -64,6 +64,7 @@ export * from './ChannelPreview/hooks/useChannelPreviewDisplayPresence';
export * from './ChannelPreview/hooks/useLatestMessagePreview';
export * from './ChannelPreview/hooks/useChannelPreviewData';
export * from './ChannelPreview/hooks/useIsChannelMuted';
+export * from './ChannelPreview/hooks/useMessageDeliveryStatus';
export * from './Chat/Chat';
export * from './Chat/hooks/useCreateChatClient';
@@ -93,6 +94,8 @@ export * from './Message/hooks/useCreateMessageContext';
export * from './Message/hooks/useMessageActions';
export * from './Message/hooks/useMessageActionHandlers';
export * from './Message/hooks/useStreamingMessage';
+export * from './Message/hooks/useMessageDeliveryData';
+export * from './Message/hooks/useMessageReadData';
export * from './Message/Message';
export * from './Message/MessageSimple/MessageAvatar';
export * from './Message/MessageSimple/MessageBounce';