From a574c3e53366bb6542b0e649a93186b39d66722e Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Wed, 19 Nov 2025 15:33:05 +0530 Subject: [PATCH 1/3] fix: export hooks and add message info read and delivery UI --- .../src/components/MessageInfoBottomSheet.tsx | 102 ++++++++++++++++++ .../SampleApp/src/screens/ChannelScreen.tsx | 25 ++++- .../SampleApp/src/utils/messageActions.tsx | 16 ++- package/src/components/Message/Message.tsx | 6 +- .../Message/hooks/useMessageDeliveryData.ts | 16 +-- .../Message/hooks/useMessageReadData.ts | 16 +-- package/src/components/index.ts | 2 + 7 files changed, 164 insertions(+), 19 deletions(-) create mode 100644 examples/SampleApp/src/components/MessageInfoBottomSheet.tsx 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..47cae07964 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) { @@ -205,6 +222,7 @@ export const ChannelScreen: React.FC = ({ audioRecordingEnabled={true} AttachmentPickerSelectionBar={CustomAttachmentPickerSelectionBar} channel={channel} + enableSwipeToReply={false} onPressMessage={onPressMessage} disableTypingIndicator enforceUniqueReaction @@ -232,6 +250,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..a45945fb6e 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -93,6 +93,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'; From 100b622de7e09c4445a4347bbb8ab8ba97deab5e Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Wed, 19 Nov 2025 15:34:16 +0530 Subject: [PATCH 2/3] fix: export hooks and add message info read and delivery UI --- examples/SampleApp/src/screens/ChannelScreen.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx index 47cae07964..c978ccfe5a 100644 --- a/examples/SampleApp/src/screens/ChannelScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelScreen.tsx @@ -222,7 +222,6 @@ export const ChannelScreen: React.FC = ({ audioRecordingEnabled={true} AttachmentPickerSelectionBar={CustomAttachmentPickerSelectionBar} channel={channel} - enableSwipeToReply={false} onPressMessage={onPressMessage} disableTypingIndicator enforceUniqueReaction From a3b3a0b82c317976730aca4ee847497d34b5669d Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Wed, 19 Nov 2025 16:24:31 +0530 Subject: [PATCH 3/3] fix: add more exports of hooks --- package/src/components/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/package/src/components/index.ts b/package/src/components/index.ts index a45945fb6e..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';