-
Notifications
You must be signed in to change notification settings - Fork 80
Description
Describe the bug
ChatContext.addReadReceipt uses object reference comparison (!==) to determine whether a read receipt belongs to the current user. Since CommunicationIdentifierKind objects are always different instances, this check never filters out the current user's own receipts, causing latestReadTime to be polluted with the current user's own read receipt timestamps.
This results in the MessageThread component incorrectly showing "seen" status on sent messages — particularly visible when reopening a chat thread, since listReadReceipts (via the decorated ProxyChatThreadClient) calls addReadReceipt for each receipt during iteration.
Reproduction
- User A sends a message to User B
- User B opens the thread (triggering
sendReadReceiptandlistReadReceipts) - User B closes the thread
- User B reopens the thread —
listReadReceiptsiterates receipts, callingaddReadReceiptfor each one - User B's own read receipt passes the
readReceipt.sender !== this.getState().userIdcheck (because it's a different object instance) latestReadTimeis updated with User B's ownreadOntimestamp- User B sends a new message — it immediately shows "seen" status because
createdOn <= latestReadTime
Root cause
In ChatContext (src/chat-stateful-client/ChatContext.ts), the addReadReceipt method:
if (readReceipt.sender !== this.getState().userId) {
// update latestReadTime
} This compares two CommunicationIdentifierKind objects by reference. They are always different instances, so the condition is always true.
Suggested fix
Use toFlatCommunicationIdentifier for string-based comparison, consistent with how messageThreadSelector already handles it in readReceiptsBySenderId:
import { toFlatCommunicationIdentifier } from '@azure/communication-common';
if (toFlatCommunicationIdentifier(readReceipt.sender) !== toFlatCommunicationIdentifier(this.getState().userId)) {
// update latestReadTime
}Note: messageThreadSelector.ts already does this correctly at the selector level (line ~214):
const oderedReadReceiptsBySenderId = chatMessages && readReceiptsBySenderId(
Object.values(chatMessages),
readReceipts.filter((r) => toFlatCommunicationIdentifier(r.sender) !== oderedReadReceiptsBySenderId userId),
userId
);But this doesn't help because latestReadTime is already corrupted at the ChatContext level before the selector runs.
Package
@azure/communication-react
Version
Verified in both v1.26.0 and v1.31.0 (latest stable). The code is identical in both versions, including a // TODO: fix comparison comment near the problematic line.
Environment
- Node.js 20
- React 18
- TypeScript 5
- Vite
- macOS 15.5 Sequoia (Darwin 24.6.0)
- Chrome 144.0.7559.110
Workaround
We work around this by recomputing the correct latestReadTime from raw read receipts (excluding the current user via toFlatCommunicationIdentifier), then correcting the messages array before passing it to <MessageThread>:
const correctedMessages = useMemo(() => {
if (!chatClient || !messageThreadProps?.messages) return messageThreadProps?.messages;
const state = chatClient.getState();
const currentUserId = toFlatCommunicationIdentifier(state.userId);
const threadState = state.threads[chatThreadClient.threadId];
let correctLatestReadTime: Date | undefined;
if (threadState?.readReceipts?.length) {
for (const receipt of threadState.readReceipts) {
if (!receipt.sender || !receipt.readOn) continue;
const senderId = toFlatCommunicationIdentifier(receipt.sender);
if (senderId === currentUserId) continue;
if (!correctLatestReadTime || receipt.readOn > correctLatestReadTime) {
correctLatestReadTime = receipt.readOn;
}
}
}
return messageThreadProps.messages.map(msg => {
if (msg.messageType !== "chat" || !msg.mine || msg.status !== "seen") return msg;
if (correctLatestReadTime && msg.createdOn <= correctLatestReadTime) return msg;
return { ...msg, status: "delivered" as const };
});
}, [chatClient, messageThreadProps?.messages, chatThreadClient]);
<MessageThread {...messageThreadProps} messages={correctedMessages} />