Skip to content

Commit 8ee3bac

Browse files
committed
feat: add delivery count to the message status
1 parent f531c00 commit 8ee3bac

File tree

14 files changed

+316
-170
lines changed

14 files changed

+316
-170
lines changed

package/src/components/ChannelPreview/ChannelPreviewStatus.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { StyleSheet, Text, View } from 'react-native';
33

44
import { ChannelPreviewProps } from './ChannelPreview';
55
import type { ChannelPreviewMessengerPropsWithContext } from './ChannelPreviewMessenger';
6-
import { MessageReadStatus } from './hooks/useLatestMessagePreview';
6+
7+
import { MessageDeliveryStatus } from './hooks/useMessageDeliveryStatus';
78

89
import { useTheme } from '../../contexts/themeContext/ThemeContext';
910
import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
@@ -38,7 +39,7 @@ export const ChannelPreviewStatus = (props: ChannelPreviewStatusProps) => {
3839
},
3940
} = useTheme();
4041

41-
const created_at = latestMessagePreview.messageObject?.created_at;
42+
const created_at = latestMessagePreview?.created_at;
4243
const latestMessageDate = created_at ? new Date(created_at) : new Date();
4344

4445
const formattedDate = useMemo(
@@ -55,9 +56,11 @@ export const ChannelPreviewStatus = (props: ChannelPreviewStatusProps) => {
5556

5657
return (
5758
<View style={styles.flexRow}>
58-
{status === MessageReadStatus.READ ? (
59+
{status === MessageDeliveryStatus.READ ? (
5960
<CheckAll pathFill={accent_blue} {...checkAllIcon} />
60-
) : status === MessageReadStatus.UNREAD ? (
61+
) : status === MessageDeliveryStatus.DELIVERED ? (
62+
<CheckAll pathFill={grey} {...checkAllIcon} />
63+
) : status === MessageDeliveryStatus.SENT ? (
6164
<Check pathFill={grey} {...checkIcon} />
6265
) : null}
6366
<Text style={[styles.date, { color: grey }, date]}>

package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import type {
1313
UserResponse,
1414
} from 'stream-chat';
1515

16+
import { MessageDeliveryStatus, useMessageDeliveryStatus } from './useMessageDeliveryStatus';
17+
1618
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
1719
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
1820

@@ -28,7 +30,7 @@ export type LatestMessagePreview = {
2830
text: string;
2931
draft?: boolean;
3032
}[];
31-
status: number;
33+
status?: MessageDeliveryStatus;
3234
created_at?: string | Date;
3335
};
3436

@@ -194,46 +196,20 @@ export enum MessageReadStatus {
194196
DELIVERED = 3,
195197
}
196198

197-
const getLatestMessageReadStatus = (
198-
channel: Channel,
199-
client: StreamChat,
200-
lastMessage: LocalMessage | undefined,
201-
readEvents: boolean,
202-
): MessageReadStatus => {
203-
const currentUserId = client.userID;
204-
const isLastMessageOwn = currentUserId === lastMessage?.user?.id;
205-
if (!lastMessage || !isLastMessageOwn || !readEvents) {
206-
return MessageReadStatus.NOT_SENT_BY_CURRENT_USER;
207-
}
208-
209-
const readList = { ...channel.state.read };
210-
if (currentUserId) {
211-
delete readList[currentUserId];
212-
}
213-
214-
const messageUpdatedAt = lastMessage.updated_at
215-
? typeof lastMessage.updated_at === 'string'
216-
? new Date(lastMessage.updated_at)
217-
: lastMessage.updated_at
218-
: undefined;
219-
220-
return Object.values(readList).some(
221-
({ last_read }) => messageUpdatedAt && messageUpdatedAt < last_read,
222-
)
223-
? MessageReadStatus.READ
224-
: MessageReadStatus.UNREAD;
225-
};
226-
227199
const getLatestMessagePreview = (params: {
228200
channel: Channel;
229201
client: StreamChat;
230202
draftMessage?: DraftMessage;
231203
pollState: LatestMessagePreviewSelectorReturnType | undefined;
232-
readEvents: boolean;
233-
t: TFunction;
204+
/**
205+
* @deprecated This parameter is no longer used and will be removed in the next major release.
206+
*/
207+
readEvents?: boolean;
234208
lastMessage?: LocalMessage;
209+
status?: MessageDeliveryStatus;
210+
t: TFunction;
235211
}) => {
236-
const { channel, client, draftMessage, lastMessage, pollState, readEvents, t } = params;
212+
const { channel, client, draftMessage, lastMessage, pollState, status, t } = params;
237213

238214
const messages = channel.state.messages;
239215

@@ -247,7 +223,7 @@ const getLatestMessagePreview = (params: {
247223
text: t('Nothing yet...'),
248224
},
249225
],
250-
status: MessageReadStatus.NOT_SENT_BY_CURRENT_USER,
226+
status: MessageDeliveryStatus.NOT_SENT_BY_CURRENT_USER,
251227
};
252228
}
253229

@@ -259,7 +235,7 @@ const getLatestMessagePreview = (params: {
259235
created_at: message?.created_at,
260236
messageObject: message,
261237
previews: getLatestMessageDisplayText(channel, client, draftMessage, message, t, pollState),
262-
status: getLatestMessageReadStatus(channel, client, message, readEvents),
238+
status,
263239
};
264240
};
265241

@@ -274,6 +250,9 @@ const stateSelector = (state: AttachmentManagerState) => ({
274250
/**
275251
* Hook to set the display preview for latest message on channel.
276252
*
253+
* FIXME: This hook is very poorly implemented and needs to be refactored with granular hooks
254+
* to avoid unnecessary re-renders and to make the code more readable.
255+
*
277256
* @param {*} channel Channel object
278257
*
279258
* @returns {object} latest message preview e.g.. { text: 'this was last message ...', created_at: '11/12/2020', messageObject: { originalMessageObject } }
@@ -327,6 +306,12 @@ export const useLatestMessagePreview = (
327306
return read_events;
328307
}, [channelConfigExists, channel]);
329308

309+
const { status } = useMessageDeliveryStatus({
310+
channel,
311+
isReadEventsEnabled: readEvents,
312+
lastMessage: lastMessage as LocalMessage,
313+
});
314+
330315
const pollId = lastMessage?.poll_id ?? '';
331316
const poll = client.polls.fromState(pollId);
332317
const pollState: LatestMessagePreviewSelectorReturnType =
@@ -340,15 +325,15 @@ export const useLatestMessagePreview = (
340325
draftMessage,
341326
lastMessage: translatedLastMessage,
342327
pollState,
343-
readEvents,
328+
status,
344329
t,
345330
});
346331
// eslint-disable-next-line react-hooks/exhaustive-deps
347332
}, [
348333
channelLastMessageString,
334+
status,
349335
draftMessage,
350336
forceUpdate,
351-
readEvents,
352337
latestVotesByOption,
353338
createdBy,
354339
name,
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
3+
import { Channel, Event, LocalMessage, MessageResponse } from 'stream-chat';
4+
5+
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
6+
7+
export enum MessageDeliveryStatus {
8+
NOT_SENT_BY_CURRENT_USER = 'not_sent_by_current_user',
9+
DELIVERED = 'delivered',
10+
READ = 'read',
11+
SENT = 'sent',
12+
}
13+
14+
type MessageDeliveryStatusProps = {
15+
channel: Channel;
16+
lastMessage: LocalMessage;
17+
isReadEventsEnabled: boolean;
18+
};
19+
20+
export const useMessageDeliveryStatus = ({
21+
channel,
22+
lastMessage,
23+
isReadEventsEnabled = true,
24+
}: MessageDeliveryStatusProps) => {
25+
const { client } = useChatContext();
26+
const [status, setStatus] = useState<MessageDeliveryStatus | undefined>(undefined);
27+
28+
const isOwnMessage = useCallback(
29+
(message: LocalMessage | MessageResponse) =>
30+
client.user && message && message.user?.id === client.user.id,
31+
[client],
32+
);
33+
34+
useEffect(() => {
35+
if (!lastMessage) {
36+
setStatus(undefined);
37+
}
38+
39+
if (!isReadEventsEnabled) {
40+
setStatus(MessageDeliveryStatus.NOT_SENT_BY_CURRENT_USER);
41+
return;
42+
}
43+
44+
if (!lastMessage?.created_at || !isOwnMessage(lastMessage)) {
45+
return;
46+
}
47+
48+
const msgRef = {
49+
msgId: lastMessage.id,
50+
timestampMs: new Date(lastMessage.created_at).getTime(),
51+
};
52+
setStatus(
53+
channel.messageReceiptsTracker.readersForMessage(msgRef).length > 1
54+
? MessageDeliveryStatus.READ
55+
: channel.messageReceiptsTracker.deliveredForMessage(msgRef).length > 1
56+
? MessageDeliveryStatus.DELIVERED
57+
: MessageDeliveryStatus.SENT,
58+
);
59+
}, [channel, isOwnMessage, isReadEventsEnabled, lastMessage]);
60+
61+
useEffect(() => {
62+
const handleMessageNew = (event: Event) => {
63+
// the last message is not mine, so do not show the delivery status
64+
if (event.message && !isOwnMessage(event.message)) {
65+
return setStatus(undefined);
66+
}
67+
return setStatus(MessageDeliveryStatus.SENT);
68+
};
69+
const { unsubscribe } = channel.on('message.new', handleMessageNew);
70+
return unsubscribe;
71+
}, [channel, isOwnMessage]);
72+
73+
useEffect(() => {
74+
if (!isOwnMessage(lastMessage)) return;
75+
const handleMessageDelivered = (event: Event) => {
76+
if (
77+
event.user?.id !== client.user?.id &&
78+
lastMessage &&
79+
lastMessage.id === event.last_delivered_message_id
80+
)
81+
setStatus(MessageDeliveryStatus.DELIVERED);
82+
};
83+
84+
const handleMarkRead = (event: Event) => {
85+
if (event.user?.id !== client.user?.id) setStatus(MessageDeliveryStatus.READ);
86+
};
87+
88+
const listeners = [
89+
channel.on('message.delivered', handleMessageDelivered),
90+
channel.on('message.read', handleMarkRead),
91+
];
92+
93+
return () => listeners.forEach((l) => l.unsubscribe());
94+
}, [channel, client, isOwnMessage, lastMessage]);
95+
96+
return { status };
97+
};

package/src/components/Message/Message.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { Attachment, LocalMessage, UserResponse } from 'stream-chat';
66
import { useCreateMessageContext } from './hooks/useCreateMessageContext';
77
import { useMessageActionHandlers } from './hooks/useMessageActionHandlers';
88
import { useMessageActions } from './hooks/useMessageActions';
9+
import { useMessageDeliveredData } from './hooks/useMessageDeliveryData';
10+
import { useMessageReadData } from './hooks/useMessageReadData';
911
import { useProcessReactions } from './hooks/useProcessReactions';
1012
import { messageActions as defaultMessageActions } from './utils/messageActions';
1113

@@ -46,7 +48,6 @@ import {
4648
MessageStatusTypes,
4749
} from '../../utils/utils';
4850
import type { Thumbnail } from '../Attachment/utils/buildGallery/types';
49-
import { getReadState } from '../MessageList/utils/getReadState';
5051

5152
export type TouchableEmitter =
5253
| 'fileAttachment'
@@ -142,10 +143,18 @@ export type MessagePropsWithContext = Pick<
142143
Partial<
143144
Omit<
144145
MessageContextValue,
145-
'groupStyles' | 'handleReaction' | 'message' | 'isMessageAIGenerated' | 'readBy'
146+
| 'groupStyles'
147+
| 'handleReaction'
148+
| 'message'
149+
| 'isMessageAIGenerated'
150+
| 'deliveredBy'
151+
| 'readBy'
146152
>
147153
> &
148-
Pick<MessageContextValue, 'groupStyles' | 'message' | 'isMessageAIGenerated' | 'readBy'> &
154+
Pick<
155+
MessageContextValue,
156+
'groupStyles' | 'message' | 'isMessageAIGenerated' | 'readBy' | 'deliveredBy'
157+
> &
149158
Pick<
150159
MessagesContextValue,
151160
| 'sendReaction'
@@ -219,6 +228,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
219228
chatContext,
220229
deleteMessage: deleteMessageFromContext,
221230
deleteReaction,
231+
deliveredBy,
222232
dismissKeyboard,
223233
dismissKeyboardOnMessageTouch,
224234
enableLongPress = true,
@@ -617,6 +627,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
617627
actionsEnabled,
618628
alignment,
619629
channel,
630+
deliveredBy,
620631
dismissOverlay,
621632
files: attachments.files,
622633
goToMessage,
@@ -758,6 +769,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
758769
const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWithContext) => {
759770
const {
760771
chatContext: { mutedUsers: prevMutedUsers },
772+
deliveredBy: prevDeliveredBy,
761773
goToMessage: prevGoToMessage,
762774
groupStyles: prevGroupStyles,
763775
isAttachmentEqual,
@@ -772,6 +784,7 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit
772784
} = prevProps;
773785
const {
774786
chatContext: { mutedUsers: nextMutedUsers },
787+
deliveredBy: nextDeliveredBy,
775788
goToMessage: nextGoToMessage,
776789
groupStyles: nextGroupStyles,
777790
isTargetedMessage: nextIsTargetedMessage,
@@ -784,6 +797,11 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit
784797
t: nextT,
785798
} = nextProps;
786799

800+
const deliveredByEqual = prevDeliveredBy === nextDeliveredBy;
801+
if (!deliveredByEqual) {
802+
return false;
803+
}
804+
787805
const readByEqual = prevReadBy === nextReadBy;
788806
if (!readByEqual) {
789807
return false;
@@ -948,13 +966,14 @@ export type MessageProps = Partial<
948966
*/
949967
export const Message = (props: MessageProps) => {
950968
const { message } = props;
951-
const { channel, enforceUniqueReaction, members, read } = useChannelContext();
969+
const { channel, enforceUniqueReaction, members } = useChannelContext();
952970
const chatContext = useChatContext();
953971
const { dismissKeyboard } = useKeyboardContext();
954972
const messagesContext = useMessagesContext();
955973
const { openThread } = useThreadContext();
956974
const { t } = useTranslationContext();
957-
const readBy = useMemo(() => getReadState(message, read), [message, read]);
975+
const readBy = useMessageReadData({ message });
976+
const deliveredBy = useMessageDeliveredData({ message });
958977
const { setQuotedMessage, setEditingState } = useMessageComposerAPIContext();
959978

960979
return (
@@ -963,6 +982,7 @@ export const Message = (props: MessageProps) => {
963982
{...{
964983
channel,
965984
chatContext,
985+
deliveredBy,
966986
dismissKeyboard,
967987
enforceUniqueReaction,
968988
members,

0 commit comments

Comments
 (0)