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
sendReadReceipt and listReadReceipts)
- User B closes the thread
- User B reopens the thread —
listReadReceipts iterates receipts, calling addReadReceipt for each one
- User B's own read receipt passes the
readReceipt.sender !== this.getState().userId check (because it's a different object instance)
latestReadTime is updated with User B's own readOn timestamp
- 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} />
Describe the bug
ChatContext.addReadReceiptuses object reference comparison (!==) to determine whether a read receipt belongs to the current user. SinceCommunicationIdentifierKindobjects are always different instances, this check never filters out the current user's own receipts, causinglatestReadTimeto be polluted with the current user's own read receipt timestamps.This results in the
MessageThreadcomponent incorrectly showing "seen" status on sent messages — particularly visible when reopening a chat thread, sincelistReadReceipts(via the decoratedProxyChatThreadClient) callsaddReadReceiptfor each receipt during iteration.Reproduction
sendReadReceiptandlistReadReceipts)listReadReceiptsiterates receipts, callingaddReadReceiptfor each onereadReceipt.sender !== this.getState().userIdcheck (because it's a different object instance)latestReadTimeis updated with User B's ownreadOntimestampcreatedOn <= latestReadTimeRoot cause
In
ChatContext(src/chat-stateful-client/ChatContext.ts), theaddReadReceiptmethod:This compares two
CommunicationIdentifierKindobjects by reference. They are always different instances, so the condition is always true.Suggested fix
Use
toFlatCommunicationIdentifierfor string-based comparison, consistent with howmessageThreadSelectoralready handles it inreadReceiptsBySenderId:Note:
messageThreadSelector.tsalready does this correctly at the selector level (line ~214):But this doesn't help because
latestReadTimeis already corrupted at theChatContextlevel before the selector runs.Package
@azure/communication-react
Version
Verified in both
v1.26.0andv1.31.0(latest stable). The code is identical in both versions, including a// TODO: fix comparisoncomment near the problematic line.Environment
Workaround
We work around this by recomputing the correct
latestReadTimefrom raw read receipts (excluding the current user viatoFlatCommunicationIdentifier), then correcting the messages array before passing it to<MessageThread>: