# Message receipts tracker

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

`channel.messageReceiptsTracker` answers two questions about a message in a channel: who has received it, and who has read it. It's a `MessageReceiptsTracker` from `stream-chat`, and this page lists its API.

How receipts work end to end, with delivery reports and a "seen by" status, is on [Read state, unread and receipts](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/read-state-unread-and-receipts/#receipts).

## Getting the tracker

Every `Channel` creates its tracker when it's constructed, and starts it at once: `channel.messageReceiptsTracker`. The tracker stops when the channel is disposed. Don't construct one yourself.

## Who received and who read a message

The tracker keeps one record per member, built from the channel's read state, `channel.state.read`:

- the last message the member read;
- the last message delivered to the member.

Delivered only moves forward and is never behind read. Read moves forward too, except when the current user marks a message unread. So a member has read every message up to their read position, and received every message up to their delivered position. The tracker answers for any message by comparing its timestamp with those two positions.

>
> **Info:** **Example**
>
> Take the user's messages "Lunch?" at 12:00 and "At 1?" at 12:01, in a channel with Bob and Cleo. Bob has read both. Cleo has received both and read only "Lunch?":
>
> | Question                                 | Answer, leaving out the user |
> | ---------------------------------------- | ---------------------------- |
> | `readersForMessage` for "Lunch?"         | Bob, Cleo                    |
> | `readersForMessage` for "At 1?"          | Bob                          |
> | `deliveredForMessage` for "At 1?"        | Bob, Cleo                    |
> | `deliveredNotReadForMessage` for "At 1?" | Cleo                         |
>

The user's own record is tracked too. Sending a message moves the sender's read position to it, so the user appears among the readers of their own messages. Filter out `client.userId` when you show the lists.

### Staying current

The tracker follows `channel.state.read`. Every SDK write to it updates the tracker: a channel query, and the `message.read`, `notification.mark_read`, `notification.mark_unread`, `message.delivered` and `message.new` events, among others. App code never feeds the tracker.

After each change the tracker publishes a new value to `snapshotStore`. Read the tracker's methods after a change, and subscribe to `snapshotStore` to know when one happened:

```ts
const tracker = channel.messageReceiptsTracker;
const ref = { msgId: message.id, timestamp: message.created_at };

const unsubscribe = tracker.snapshotStore.subscribeWithSelector(
  ({ revision }) => ({ revision }),
  () => {
    const readers = tracker
      .readersForMessage(ref)
      .filter((user) => user.id !== client.userId);
    console.log(
      `Read by ${readers.map((user) => user.name ?? user.id).join(", ")}`,
    );
  },
);
```

`snapshotStore` holds a `MessageReceiptsSnapshot`:

| Field                      | Type                             | What it holds                                                                                               |
| -------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `revision`                 | `number`                         | Grows by one on every change. Select it to re-render on any change.                                         |
| `readersByMessageId`       | `Record<string, UserResponse[]>` | The users whose read position is exactly that message. A user appears once, at the message they stopped on. |
| `deliveredByMessageId`     | `Record<string, UserResponse[]>` | The same for delivered positions.                                                                           |
| `lastReadRefByOthers`      | `MsgRef` or `null`               | The furthest read position of any member other than the current user, or `null` when nobody has read yet.   |
| `lastDeliveredRefByOthers` | `MsgRef` or `null`               | The same for delivered positions.                                                                           |

`readersByMessageId` doesn't say who read a message: a reader of a later message has read this one too. Ask `readersForMessage` for that, or add up the buckets from the newest message down.

## Methods

A message is passed as a `MsgRef`, `{ msgId: string; timestamp: TimestampNS }`. Use the message's `id` and `created_at`. The tracker compares messages by `timestamp`.

| Method                               | Returns                          | What it answers                                                                                              |
| ------------------------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `readersForMessage(msgRef)`          | `UserResponse[]`                 | Who has read the message: everyone whose read position is at it or later.                                    |
| `deliveredForMessage(msgRef)`        | `UserResponse[]`                 | Who has received it, readers included.                                                                       |
| `deliveredNotReadForMessage(msgRef)` | `UserResponse[]`                 | Who has received it and not read it.                                                                         |
| `usersWhoseLastReadIs(msgRef)`       | `UserResponse[]`                 | Whose read position is exactly `msgRef`. Matches on `msgId` and on `timestamp`, see below.                   |
| `usersWhoseLastDeliveredIs(msgRef)`  | `UserResponse[]`                 | The same for delivered positions.                                                                            |
| `hasUserRead(msgRef, userId)`        | `boolean`                        | Whether that user has read the message.                                                                      |
| `hasUserDelivered(msgRef, userId)`   | `boolean`                        | Whether the message was delivered to that user.                                                              |
| `isReadByOthers(msgRef)`             | `boolean`                        | Whether any member other than the current user has read it.                                                  |
| `isDeliveredToOthers(msgRef)`        | `boolean`                        | Whether it was delivered to any member other than the current user.                                          |
| `getUserProgress(userId)`            | `UserProgress` or `null`         | The user's record: `{ user, lastReadRef, lastDeliveredRef }`, or `null` for a user the tracker doesn't know. |
| `groupUsersByLastReadMessage()`      | `Record<string, UserResponse[]>` | The users grouped by the message their read position is on. The same map as `readersByMessageId`.            |
| `groupUsersByLastDeliveredMessage()` | `Record<string, UserResponse[]>` | The same for delivered positions.                                                                            |

In a `UserProgress`, a position's `timestamp` is the time of the read or the delivery, not the message's `created_at`. A position that's unknown has the `timestamp` `-Infinity` and an empty `msgId`. So `MessageReceiptsTracker.usersWhoseLastReadIs()` and `MessageReceiptsTracker.usersWhoseLastDeliveredIs()` with a ref built from the message usually find nobody: to list who stopped at a message, read `readersByMessageId[msgId]` or `deliveredByMessageId[msgId]`, which match on the id alone.

`isReadByOthers` and `isDeliveredToOthers` read the snapshot's furthest positions, so an older message never looks less far along than a newer one. They suit a single "sent, delivered, read" status. The lists suit "seen by" details.

The tracker also has methods that write its records: `MessageReceiptsTracker.ingestInitial()`, `MessageReceiptsTracker.onMessageRead()`, `MessageReceiptsTracker.onMessageDelivered()`, `MessageReceiptsTracker.onNotificationMarkUnread()`, `MessageReceiptsTracker.reconcileFromReadStore()` and `MessageReceiptsTracker.setPendingReadStoreReconcileMeta()`. Its `MessageReceiptsTracker.registerSubscriptions()` and `MessageReceiptsTracker.unregisterSubscriptions()` start and stop following the read state. The channel calls what it needs and keeps the tracker in step with its read state by itself. Called from app code, the writing methods put the tracker out of step with `channel.state.read` until the next change.

## Limits

- **Only loaded messages resolve.** When the server reports a read or delivered position with a message id, the tracker uses it. When it reports only a time, the tracker looks for the last message at or before that time among the loaded messages of the latest window. If none is loaded there, the position stays unknown, so that member counts as having read or received nothing.
- **Channels only, for now.** Threads have no tracker of their own, read events for a thread don't move the channel's read positions, and delivery isn't reported for thread replies.
- **Delivered needs the server.** Delivery events are off by default for a channel type. With them off, every delivered position follows the read position. See [Message delivery and read status](https://getstream.io/chat/docs/javascript/message-delivery-and-read-status/).

## See also

- [Read state, unread and receipts](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/read-state-unread-and-receipts/): the feature end to end, including delivery reports and the settings that turn them off.
- [Read state and unread counts](https://getstream.io/chat/docs/sdk/react/v15/state/channels-and-threads/read-state-and-unread-counts/): `channel.state.read` and the events that change it.
- [Channel and channel state](https://getstream.io/chat/docs/sdk/react/v15/reference/state/channel-and-channel-state/#channelstate-fields): the fields of each read state entry.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/reference/state/message-receipts-tracker/](https://getstream.io/chat/docs/sdk/react/v15/reference/state/message-receipts-tracker/).