Azure / Azure/communication-ui-library
[communication-react] ChatContext.addReadReceipt uses object reference comparison for userId, causing false "seen" status
- Dominant language
- TypeScript
- Stars
- 204
- Forks
- 84
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 4
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
1. User A sends a message to User B
2. User B opens the thread (triggering `sendReadReceipt` and `listReadReceipts`)
3. User B closes the thread
4. User B reopens the thread — `listReadReceipts` iterates receipts, calling `addReadReceipt` for each one
5. User B's own read receipt passes the `readReceipt.sender !== this.getState().userId` check (because it's a different object instance)
6. `latestReadTime` is updated with User B's own `readOn` timestamp
7. 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:
```ts
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`:
```ts
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):
```ts
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 ``:
```ts
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]);
```
Contributor guide
Assessment
This issue has not been assessed yet.