# Read state, unread and receipts

> **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/).

This page covers what a user knows about reading: which channels have unread messages, where the unread part of a channel starts, and whether the people in a channel have received and read the user's own messages.

## What users get

- **Unread counts.** Each row in the channel list shows how many messages the user hasn't read. A muted channel shows none.
- **Where unread starts.** The user opens a channel with three new messages. A separator reading "3 unread" sits above the first of them. While the separator is out of view, a floating "3 unread" button jumps back to it. The × on either one marks the channel read.
- **New messages while scrolled up.** When messages arrive while the user reads older ones, a "new messages" notification and the scroll-to-latest button take them down.
- **Marking read.** The channel is marked read when the user reaches the latest message with the tab in front. Mark as unread in the menu of someone else's message marks the channel unread from that message on.
- **Receipts.** The user's own messages show their status next to the timestamp: a clock while sending, one check once sent, two checks once delivered, and two checks in the accent color once read, with the number of readers when more than one has read it. Hovering shows who read it. By default only their latest message shows sent, delivered or read; the clock shows on every message that is sending. The channel list row shows the status of their last message too.

## How it works across the layers

- **State.**
  - `channel.state.read` holds each member's read position: when they last read (`last_read`), the last message they read, their unread count, and the last message delivered to them. `message.read`, `notification.mark_read`, `notification.mark_unread`, `message.new` and `message.delivered` events update it.
  - `Channel.markRead()` and `Channel.markUnread({ message_id })` move the user's own position. Both need read events on, and the state changes when the server's event arrives.
  - The message list keeps an unread snapshot, `messagePaginator.unreadStateSnapshot`: where the user's unread part started when the channel opened. Marking the channel read doesn't move it, so the separator stays where the user left off. `MessagePaginator.jumpToTheFirstUnreadMessage()` loads that part and jumps to it.
  - `channel.messageReceiptsTracker` turns the read positions into receipts, see [Receipts](#receipts).
- **UI.**
  - `MessageList` and `VirtualizedMessageList` mark the channel read when the user is at the latest message, the tab is visible and no newer messages wait beyond the loaded ones. The calls go through `client.messageDeliveryReporter.throttledMarkRead`, at most one a second. While the user is there, new messages don't raise the unread count, so the separator and the count don't flash before the list marks them read. After Mark as unread, the lists don't mark the channel read again until the user dismisses the separator or opens the channel again.
  - The lists render `UnreadMessagesSeparator` above the first unread message and `UnreadMessagesNotification` while the separator is out of view, both from the unread snapshot. The notification's main button calls `MessagePaginator.jumpToTheFirstUnreadMessage()`. Thread lists show no unread notification.
  - `NewMessageNotification` and `ScrollToLatestMessageButton` handle messages that arrive while the user is scrolled up.
  - `MessageStatus` renders the receipts on the user's own messages, and `ChannelListItem` renders the unread count and the last message's status.
- **Layout.** Not involved.

[Read state and unread counts](https://getstream.io/chat/docs/sdk/react/v15/state/channels-and-threads/read-state-and-unread-counts/) explains the read state in full: what counts as unread, every way to mark read and unread, and the differently shaped read state of threads. [Message paginators](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/message-paginators/) covers jumps and the unread snapshot.

### Receipts

A receipt answers two questions about one of the user's messages: who has received it, and who has read it. Receipts are worked out for channels only, for now.

#### Who received and read a message

`channel.messageReceiptsTracker` keeps, for each member, the last message delivered to them and the last message they read. Read and delivered only move forward, so a member has read every message up to their read position.

>
> **Info:** **Example**
>
> Take the user's messages "Lunch?" at 12:00 and "At 1?" at 12:01, in a channel with Bob and Cleo:
>
> 1. Bob's and Cleo's devices receive both messages and report them delivered. The tracker moves both delivered positions to "At 1?". The user sees two checks on "At 1?".
> 2. Bob opens the channel and reads both. His read position moves to "At 1?". The user sees the read checks, and the tooltip says Bob.
> 3. Cleo reads only up to "Lunch?". `MessageReceiptsTracker.readersForMessage()` for "Lunch?" returns Bob and Cleo; for "At 1?" it returns Bob.
>

The tracker answers per message, given its id and `created_at`:

- `MessageReceiptsTracker.readersForMessage(ref)` and `MessageReceiptsTracker.deliveredForMessage(ref)` return the users who read or received it; `MessageReceiptsTracker.deliveredNotReadForMessage(ref)` returns those who received it and haven't read it;
- `MessageReceiptsTracker.hasUserRead(ref, userId)` and `MessageReceiptsTracker.hasUserDelivered(ref, userId)` answer for one user;
- `MessageReceiptsTracker.getUserProgress(userId)` returns one user's positions.

The lists can include the user's own read position, so filter out `client.user.id` when you show them. The tracker publishes a snapshot to `snapshotStore` on every change; subscribe to it to re-render. [Message receipts tracker](https://getstream.io/chat/docs/sdk/react/v15/reference/state/message-receipts-tracker/) lists every method.

#### Delivery reports

The client reports delivery for the user by itself. When messages arrive, or a channel loads, `client.messageDeliveryReporter` notes the newest message the user hasn't read or confirmed yet in that channel. It waits a second, then sends the notes for all channels in one request, up to 100 channels at a time. The server then sends `message.delivered` to the channel, and every member's tracker moves.

The client sends no report:

- for a channel with delivery events off, see [Turning it on or off](#turning-it-on-or-off);
- when the user's `privacy_settings.delivery_receipts.enabled` is `false`;
- for messages the user has already read;
- for thread replies.

In the client configuration, `client.messageDelivery.markAsDeliveredBufferTimeoutMs` changes the one-second wait, and `client.messageDelivery.maxDeliveredMessageCountInPayload` the 100.

#### Limits

- **Channels only, for now.** Threads have no tracker, delivery isn't reported for thread replies, and inside a thread `MessageStatus` shows only the sending clock.
- **Only loaded messages resolve.** When the server reports a read or delivered position without a message id, the tracker looks for the message at that time among the loaded messages. If that message isn't loaded, the position is skipped.
- **Delivered needs the server.** On the server, delivery events are off by default for a channel type; see [Message delivery and read status](https://getstream.io/chat/docs/javascript/message-delivery-and-read-status/) to turn them on.

## Turning it on or off

Each switch has a client side and a server side, and a server "no" wins. The resolved values are in `channel.config`.

| Switch                                                                | What turning it off does                                                                                                                                            |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `channel.readEvents.enabled` (client), `read_events` (server)         | No read state is sent. The lists stop marking the channel read, and `Channel.markRead()` and `Channel.markUnread()` throw.                                          |
| `channel.readEvents.localUnreadCountEnabled` (client)                 | Off by default. On, it keeps an unread count on the device while read events are off, and the lists reset it with `Channel.markReadLocally()` instead of a request. |
| `channel.deliveryEvents.enabled` (client), `delivery_events` (server) | No delivery reports for those channels. Messages go from sent to read.                                                                                              |
| The user's `privacy_settings.read_receipts.enabled`                   | The server doesn't tell other members when this user reads.                                                                                                         |
| The user's `privacy_settings.delivery_receipts.enabled`               | The client doesn't report delivery for this user, and the server doesn't show it to others.                                                                         |

Mark as unread also needs the `read-events` capability, which the server grants per channel.

To turn delivery reports off for every channel:

```ts
client.config.set({ channel: { deliveryEvents: { enabled: false } } });
```

A user sets their own privacy settings when connecting:

```tsx
import { useCreateChatClient } from "stream-chat-react";

export const useClientWithPrivateReceipts = () =>
  useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: {
      id: userId,
      privacy_settings: {
        delivery_receipts: { enabled: false },
        read_receipts: { enabled: false },
      },
    },
  });
```

[Configuration](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/configuration/) explains `client.config`, and the [privacy settings](https://getstream.io/chat/docs/javascript/init-and-users/#privacy-settings) section of the JavaScript client docs covers the user side.

## Customizing

### A status on every message

`MessageStatus` shows on the latest own message only. Pass `returnAllReadData` to the list to show it on every own message. This example also replaces the read status with the readers' names:

```tsx
import type { Channel as ChannelType } from "stream-chat";
import {
  Channel,
  MessageList,
  MessageStatus,
  WithComponents,
  useChatContext,
  useMessageContext,
  type MessageStatusProps,
} from "stream-chat-react";

const SeenBy = () => {
  const { client } = useChatContext();
  const { readBy = [] } = useMessageContext();
  const names = readBy
    .filter((user) => user.id !== client.user?.id)
    .map((user) => user.name || user.id);

  return <span className="seen-by">Seen by {names.join(", ")}</span>;
};

const CustomMessageStatus = (props: MessageStatusProps) => (
  <MessageStatus {...props} MessageReadStatus={SeenBy} />
);

export const ChannelWithSeenBy = ({ channel }: { channel: ChannelType }) => (
  <WithComponents overrides={{ MessageStatus: CustomMessageStatus }}>
    <Channel channel={channel}>
      <MessageList returnAllReadData />
    </Channel>
  </WithComponents>
);
```

`MessageStatus` also takes `MessageSendingStatus`, `MessageSentStatus` and `MessageDeliveredStatus` for the other states.

### Receipts in your own components

Ask the tracker directly, and subscribe to its snapshot so the component re-renders when someone's position moves. Place it inside `Channel`:

```tsx
import type { LocalMessage, MessageReceiptsSnapshot } from "stream-chat";
import { useChannel, useChatContext, useStateStore } from "stream-chat-react";

const revisionSelector = ({ revision }: MessageReceiptsSnapshot) => ({
  revision,
});

export const ReadByCount = ({ message }: { message: LocalMessage }) => {
  const { client } = useChatContext();
  const channel = useChannel();
  const tracker = channel.messageReceiptsTracker;

  // Re-renders whenever a read or delivered position moves
  useStateStore(tracker.snapshotStore, revisionSelector);

  const ref = { msgId: message.id, timestamp: message.created_at };
  const readers = tracker
    .readersForMessage(ref)
    .filter((user) => user.id !== client.user?.id);
  const waiting = tracker.deliveredNotReadForMessage(ref).length;

  return (
    <span>
      Read by {readers.length}, delivered to {waiting} more
    </span>
  );
};
```

### The unread separator and notification

Replace them through `WithComponents`. The separator receives the unread count:

```tsx
import type { Channel as ChannelType } from "stream-chat";
import {
  Channel,
  UnreadMessagesNotification,
  WithComponents,
  type UnreadMessagesNotificationProps,
  type UnreadMessagesSeparatorProps,
} from "stream-chat-react";

const CustomUnreadMessagesSeparator = ({
  unreadCount,
}: UnreadMessagesSeparatorProps) => (
  <div className="custom-unread-separator">
    {unreadCount ? `${unreadCount} new messages` : "New messages"}
  </div>
);

// Keeps the default notification, and loads 50 messages when it jumps
const CustomUnreadMessagesNotification = (
  props: UnreadMessagesNotificationProps,
) => <UnreadMessagesNotification {...props} queryMessageLimit={50} />;

export const ChannelWithCustomUnread = ({
  channel,
}: {
  channel: ChannelType;
}) => (
  <WithComponents
    overrides={{
      UnreadMessagesNotification: CustomUnreadMessagesNotification,
      UnreadMessagesSeparator: CustomUnreadMessagesSeparator,
    }}
  >
    <Channel channel={channel}>{/* MessageList, MessageComposer */}</Channel>
  </WithComponents>
);
```

The default separator and notification show the count unless you pass `showCount={false}`, and both have a button that marks the channel read. A custom separator without that button leaves marking read to the list.

To jump from your own button, call `channel.messagePaginator.jumpToTheFirstUnreadMessage()`. A thread's `messagePaginator` has the same method, but it takes the unread position from the channel's read state, not the thread's. [Message list UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/message-list-ui/) explains the separators and what the list shows after a jump.

## Reference

- [Message receipts tracker](https://getstream.io/chat/docs/sdk/react/v15/reference/state/message-receipts-tracker/): every method of `channel.messageReceiptsTracker`.
- [Channel and channel state](https://getstream.io/chat/docs/sdk/react/v15/reference/state/channel-and-channel-state/): `channel.state.read` and the mark-read methods.
- [Message paginator](https://getstream.io/chat/docs/sdk/react/v15/reference/state/message-paginator/): `unreadStateSnapshot` and `jumpToTheFirstUnreadMessage`.
- [Message UI components](https://getstream.io/chat/docs/sdk/react/v15/component-reference/message/message-ui-components/): the `MessageStatus` props.
- [MessageList](https://getstream.io/chat/docs/sdk/react/v15/component-reference/message-list/messagelist/) and [VirtualizedMessageList](https://getstream.io/chat/docs/sdk/react/v15/component-reference/message-list/virtualizedmessagelist/): `returnAllReadData` and `showUnreadNotificationAlways`.
- [ComponentContext](https://getstream.io/chat/docs/sdk/react/v15/component-reference/channel/componentcontext/): the `UnreadMessagesSeparator`, `UnreadMessagesNotification`, `NewMessageNotification` and `MessageStatus` override keys.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/features/messaging/read-state-unread-and-receipts/](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/read-state-unread-and-receipts/).