# Read state, unread and receipts

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/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/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. Delivered only moves forward and is never behind read. Read moves forward too, except when the current user marks a message unread, which moves their read position back. A member has read every message up to their read position.

>
> **Info:** **Example**
>
> The user sends "Lunch?" and then "At 1?" to a channel with Bob and Cleo. Both messages are delivered to both, so the user sees two checks. Bob reads both, and Cleo reads only "Lunch?". The readers of "Lunch?" are Bob and Cleo; the only reader of "At 1?" is Bob.
>

The tracker answers per message, for example `MessageReceiptsTracker.readersForMessage()`. [Methods](https://getstream.io/chat/docs/sdk/react/reference/state/message-receipts-tracker/#methods) lists them, and [Staying current](https://getstream.io/chat/docs/sdk/react/reference/state/message-receipts-tracker/#staying-current) shows how to re-render when a position moves.

#### 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

Receipts work in channels only, for now: inside a thread, `<MessageStatus/>` shows only the sending clock. Delivered needs delivery events on the server, and only loaded messages resolve a position given as a time. [Limits](https://getstream.io/chat/docs/sdk/react/reference/state/message-receipts-tracker/#limits) explains each.

## 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`. [Turning read events off](https://getstream.io/chat/docs/sdk/react/state/channels-and-threads/read-state-and-unread-counts/#turning-read-events-off) has the details for read events.

| Switch                                                                | What turning it off does                                                                                                                                                                               |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `read_events` (server)                                                | The server keeps no read state for the channel, so nothing counts as unread, unless `localUnreadCountEnabled` is on. `Channel.markRead()` and `Channel.markUnread()` throw, as with the client switch. |
| `channel.readEvents.enabled` (client)                                 | This client sends no read state: the lists stop marking the channel read, and `Channel.markRead()` and `Channel.markUnread()` throw. Unread counts keep rising.                                        |
| `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/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,
} 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>;
};

// The SDK renders the `MessageStatus` key without props, so the override passes its own.
const CustomMessageStatus = () => <MessageStatus 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.

The notification's arrow points toward the unread messages, up or down. A notification of your own gets the direction as the `unreadIsBelow` prop, as [Unread and scroll controls](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/message-list/unread-and-scroll-controls/) shows.

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/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/reference/state/message-receipts-tracker/): every method of `channel.messageReceiptsTracker`.
- [Channel and channel state](https://getstream.io/chat/docs/sdk/react/reference/state/channel-and-channel-state/): `channel.state.read` and the mark-read methods.
- [Message paginator](https://getstream.io/chat/docs/sdk/react/reference/state/message-paginator/): `unreadStateSnapshot` and `jumpToTheFirstUnreadMessage`.
- [Message UI components](https://getstream.io/chat/docs/sdk/react/component-reference/message/message-ui-components/): the `MessageStatus` props.
- [MessageList](https://getstream.io/chat/docs/sdk/react/component-reference/message-list/messagelist/) and [VirtualizedMessageList](https://getstream.io/chat/docs/sdk/react/component-reference/message-list/virtualizedmessagelist/): `returnAllReadData` and `showUnreadNotificationAlways`.
- [ComponentContext](https://getstream.io/chat/docs/sdk/react/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/features/messaging/read-state-unread-and-receipts/](https://getstream.io/chat/docs/sdk/react/features/messaging/read-state-unread-and-receipts/).