Azure / Azure/communication-ui-library

[communication-react] ChatContext.addReadReceipt uses object reference comparison for userId, causing false "seen" status

Open
#6,043 3 comments 0 reactions 1 assignee Claimed by @mgamis-msft View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.