Skip to content

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

@beumerr

Description

@beumerr

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:

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} />

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions