# Read state and unread counts

Every channel and every thread keeps a read state for each of its members: when they last read it, and how many messages they haven't read since.

>
> **Info:** **Example**
>
> The user last read "Design" on Monday at 9:00. Since then Bob and Cara posted 12 messages, so the user's read state says 12 unread. Bob's says 0, because posting a message counts as reading the channel.
>

Channel lists, unread badges, unread separators and "jump to first unread" all build on this state. Channels and threads keep it in different shapes, so this page covers channels first, then threads.

## Read state per user

A channel's read state is the `read` field of `channel.state`, one entry per user id:

| Field                                            | What it holds                                                                                                                                      |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `last_read`                                      | When the user last read the channel, in nanoseconds.                                                                                               |
| `unread_messages`                                | How many messages the user hasn't read.                                                                                                            |
| `last_read_message_id`                           | The last message the user read.                                                                                                                    |
| `first_unread_message_id`                        | The message the user marked the channel unread from.                                                                                               |
| `user`                                           | The user.                                                                                                                                          |
| `last_delivered_at`, `last_delivered_message_id` | Delivery receipts, explained in [Read state, unread and receipts](https://getstream.io/chat/docs/sdk/react-native/features/messaging/read-state-unread-and-receipts/). |

The current user's count is also `channel.state.unreadCount`, and `Channel.countUnread()` returns the same. Both read the current value without subscribing. To follow it, subscribe to `channel.state` and select the current user's entry:

```ts
import { nsToDate } from "stream-chat";
import type { Channel } from "stream-chat";

export const followOwnReadState = (channel: Channel) => {
  const ownUserId = channel.getClient().userId;

  return channel.state.subscribeWithSelector(
    ({ read }) => ({ ownRead: ownUserId ? read[ownUserId] : undefined }),
    ({ ownRead }) => {
      if (!ownRead) return;
      const lastRead = nsToDate(ownRead.last_read).toLocaleString();
      console.log(`${ownRead.unread_messages} unread since ${lastRead}`);
    },
  );
};
```

Timestamps are nanosecond numbers. `nsToDate` turns them into a `Date`, as [Data conventions](https://getstream.io/chat/docs/sdk/react-native/state/state-and-entities/data-conventions/) explains. In a component, select `read` with `useStateStore`, as the unread badge on [Reading state in components](https://getstream.io/chat/docs/sdk/react-native/ui/reading-state-in-components/) does.

A few more reads:

- `Channel.lastRead()` is the current user's `last_read`, or `null` if they never read the channel.
- `Channel.countUnread(lastRead)` counts the loaded newest messages after `lastRead` that count as unread.
- `Channel.countUnreadMentions()` counts the unread messages that mention the current user.

## What changes the read state

- **The state changes on its own, through events.** That includes changes the user makes on another device.
- **Other members' entries in `channel.state.read` update too.** When someone posts, their entry becomes read and everyone else's count goes up. Read receipts depend on this.
- **Channels the client doesn't watch still get the user's own read changes.** They don't get other members' read changes.
- **The server's count wins.** The client's count is an estimate, and each time the channel loads, the server's count replaces it.

Each event changes it this way:

| What happens                                      | What changes                                                                                                                                                                                                                                     |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| The channel loads, from a query or a channel list | Every entry comes from the server. If the server sends none for the current user, they get one with nothing unread.                                                                                                                              |
| `message.new` from Bob                            | Bob's entry: read at the message's time, 0 unread. Every other entry: 1 more unread. The current user's count goes up only if the message counts as unread (next section).                                                                       |
| `message.read` from Bob                           | Bob's entry: read at the event's time, 0 unread, `first_unread_message_id` cleared. Only watched channels receive it.                                                                                                                            |
| `notification.mark_read`                          | The same, for the current user, also in channels the client doesn't watch. Marking every channel read sets every loaded channel's count to 0.                                                                                                    |
| `message.read_locally`                            | The same, for the current user, without a request. `Channel.markReadLocally()` dispatches it.                                                                                                                                                    |
| `notification.mark_unread`                        | The current user's entry takes its read time, unread count, first unread message and last read message from the event.                                                                                                                           |
| `message.deleted`                                 | The current user's count goes down by 1 when the client holds the message, it isn't already a deleted message, it was posted after the user last read the channel, and it counted as unread (next section). Both soft and hard deletes lower it. |
| `member.removed`                                  | The removed member's entry is dropped.                                                                                                                                                                                                           |
| `channel.truncated`                               | The current user's count only includes the messages after the truncation, or goes to 0.                                                                                                                                                          |

A message the current user sends, a reply that stays in its thread, and `message.read` or `notification.mark_read` for a thread don't change the channel's read state.

`message.read_locally` carries the newest loaded message as `last_read_message_id` only while the newest messages are loaded. When only an older interval is loaded, for example after a jump, the event carries none, and the read covers everything up to the event's time.

## What counts as unread in a channel

When a new message arrives in a channel, it raises the current user's count unless:

- the current user sent it;
- it's silent (`silent: true`) or shadowed by moderation;
- it's a thread reply that isn't shown in the channel;
- the current user muted its author;
- the current user muted the channel;
- the user is looking at the newest messages, as [Message paginators](https://getstream.io/chat/docs/sdk/react-native/state/loading-data/message-paginators/#live-view-and-unread) explains;
- read events are off in the channel, unless the app counts unread messages locally (see [Turning read events off](#turning-read-events-off)).

The SDK's message lists report that the user is looking at the newest messages while the app is in the foreground and the newest message is on screen.

`Channel.countUnread(lastRead)` and `Channel.countUnreadMentions()` follow the same rules, except the one about looking at the newest messages.

## Marking read and unread

Marking read or unread usually sends a request, and the read state changes when the server's event comes back, not when the call returns. Marking read locally changes it at once.

- **`Channel.markReadViaReporter()`** marks the channel read the way the SDK does on its own, through `client.messageDeliveryReporter`. It calls your `markReadRequest` handler if you registered one, resets the count locally when read events are off and local counting is on, and calls `Channel.markRead()` otherwise. Use it when your UI marks a channel read.
- **`Channel.markRead()`** always sends the request, even when you registered a handler. With `{ message_id }`, it marks the channel read up to that message. Use it inside a `markReadRequest` handler. It throws unless the channel is loaded, or restored from the offline database, and read events are on.
- **`Channel.markReadLocally()`** resets the current user's count without a request, for channels whose unread messages are only counted locally.
- **`Channel.markUnread({ message_id })`** marks the channel unread from that message. It takes `message_timestamp` instead of `message_id` too. Like `Channel.markRead()`, it throws when the channel isn't loaded or read events are off.

```ts
await channel.markUnread({ message_id: messageId });
// read[ownUserId].first_unread_message_id is set when notification.mark_unread arrives
```

To change how the SDK marks a channel read, register a `markReadRequest` handler. `Channel.markReadViaReporter()` and the SDK's own marking use it. The handler receives the channel and the options, and returns the response, or `null` when it sent nothing. Unlike the message request handlers, it gets no default request, so call `Channel.markRead(options)` in it to mark read the usual way:

```ts
client.config.set({
  channel: {
    requestHandlers: {
      markReadRequest: async ({ channel, options }) => {
        // Livestream channels are never marked read.
        if (channel.type === "livestream") return null;
        return await channel.markRead(options);
      },
    },
  },
});
```

[Message operations](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/message-operations/) explains request handlers. [What's optimistic and what isn't](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/whats-optimistic-and-what-isnt/) lists when each read action shows.

## When the SDK marks a channel read

`stream-chat` doesn't mark a channel read by itself. The SDK's components do, at these moments:

- **`<Channel>` mounts.** When the current user had unread messages in the channel as it mounted, `<Channel>` marks it read once the channel has loaded. The list keeps showing where the unread messages start. Pass `markReadOnMount={false}` to leave the read state alone.
- **A message arrives while the user is at the newest messages.** The channel's message list marks the channel read when a new message arrives while the app is in the foreground and the newest message is on screen, unless the user marked the channel unread.
- **The user goes to the newest messages.** Pressing the scroll-to-bottom button in a channel, or closing the unread messages notification, marks the channel read.
- **The connection recovers.** When `connection.recovered` arrives while connection recovery is on and the channel's list is at the newest messages, `<Channel>` marks it read. A list that shows an older interval, with the newest messages not loaded, is left alone. With recovery off, nothing reloads, so `<Channel>` doesn't mark the channel read on the event. [Connection](https://getstream.io/chat/docs/sdk/react-native/state/connection/) explains the event.

Each time, the SDK marks the channel through `client.messageDeliveryReporter`, which uses your `markReadRequest` handler and sends at most one request a second per channel. When the channel's `readEvents.enabled` is off, the SDK skips the reporter and calls `Channel.markReadLocally()` directly, but only with `localUnreadCountEnabled` on (see below), and otherwise does nothing. The `markReadRequest` handler isn't called then.

A thread list doesn't mark the channel read. `<Thread>` activates its thread, and an active thread marks itself read, as [Read state in threads](#read-state-in-threads) explains.

`useMarkRead(channel)` returns the function the SDK uses, for a component of your own that should mark the channel read the same way. By default it also clears the list's unread boundary, so the unread separator and the unread notification go away. Pass `{ updateChannelUnreadState: false }` to keep them:

```tsx
import { Pressable, Text } from "react-native";
import { useChannel, useMarkRead } from "stream-chat-react-native";

export const MarkReadButton = () => {
  const channel = useChannel();
  const markRead = useMarkRead(channel);

  return (
    <Pressable accessibilityRole="button" onPress={() => markRead()}>
      <Text>Mark as read</Text>
    </Pressable>
  );
};
```

## Turning read events off

To turn read events off, turn off the channel type's `read_events` setting on the server. The user can then no longer mark the channel read, and nothing counts unread messages.

`readEvents.enabled: false` in `client.config` does the same for this client. It stops the client from marking channels read and from counting their unread messages, and `Channel.markRead()` and `Channel.markUnread()` throw. The count the server sends when a channel loads is still in `channel.state.read`. Use it for a client that shouldn't mark anything read, such as a moderation tool:

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

`channel.config.readEvents.enabled` is `false` when the channel type's `read_events` is off on the server, or `readEvents.enabled` is `false` in `client.config`. `channel.readEventsEnabled` also needs the current user's `read-events` capability in the channel, and it's the value that decides whether the client counts unread messages and marks the channel read.

A livestream channel type usually has `read_events` off on the server, so nothing counts unread messages. To count them anyway, turn on `localUnreadCountEnabled`. It has no effect while read events are on. The client then counts new messages itself, and `Channel.markReadLocally()` resets the count. While read events are off, `Channel.markReadViaReporter()` and the SDK's own marking call it for you:

```ts
client.config.set({
  channel: { readEvents: { localUnreadCountEnabled: true } },
});

// The user has seen the newest messages.
channel.markReadLocally();
```

With offline support, the reset is saved in the offline database too.

A channel has no `updateConfig()`, so this setting applies to every channel. It changes only channels whose read events are off. Channels with read events on keep the server's count, so it's safe to set for the whole app.

With it on, in a channel without read events:

| What                | How it works                                                                                                                                                                                                                                                                                                        |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| What counts         | New messages from other users that arrive while the user isn't looking at the newest messages, including while the app is in the background. The other rules in [What counts as unread in a channel](#what-counts-as-unread-in-a-channel) still apply: a muted channel counts nothing, and muted users don't count. |
| Where the count is  | In the current user's entry of `channel.state.read`, like a server count.                                                                                                                                                                                                                                           |
| Resetting the count | `Channel.markReadLocally()` resets it without a request. `Channel.markReadViaReporter()` calls it for you, unless you registered a `markReadRequest` handler, which then runs instead.                                                                                                                              |
| Who knows the count | Only this client. The server doesn't know it, so the user's other devices don't show it.                                                                                                                                                                                                                            |

The message lists reset the count the same way when the user reaches the newest messages. They call `Channel.markReadLocally()` directly, so the limit of one mark-read a second per channel doesn't apply. For channels with read events on, `client.messageDelivery.markAsReadThrottleTimeoutMs` under the `client` key of `client.config` changes that interval, from the next mark-read.

## Read state in threads

A thread keeps its own read state, in the `read` field of `thread.state`, one entry per participant. Its fields are named differently from a channel's:

| Field                  | What it holds                                       |
| ---------------------- | --------------------------------------------------- |
| `lastReadAt`           | When the user last read the thread, in nanoseconds. |
| `unreadMessageCount`   | How many replies the user hasn't read.              |
| `lastReadMessageId`    | The last reply the user read.                       |
| `firstUnreadMessageId` | The reply the user marked the thread unread from.   |
| `user`                 | The user.                                           |

`thread.ownUnreadCount` is the current user's count.

```ts
const ownUserId = client.userId;

const unsubscribe = thread.state.subscribeWithSelector(
  ({ read }) => ({
    unread: ownUserId ? (read[ownUserId]?.unreadMessageCount ?? 0) : 0,
  }),
  ({ unread }) => {
    console.log(`${unread} unread replies`);
  },
);
```

What changes it:

- a new reply: the author's entry goes to 0 unread, and every other entry gets 1 more, except the current user's while the thread is active;
- `message.read` for the thread: that user's entry goes to 0 unread;
- `notification.mark_unread` for the thread: the current user's entry takes the event's values.

A thread counts every new reply from someone else as unread, except while the thread is active. The channel's other rules (silent, shadowed, muted author or channel, read events off) don't apply to threads.

`Thread.markRead()` marks the thread read. It sends nothing when the current user has nothing unread, unless you pass `{ force: true }`. An active thread marks itself read whenever it has unread replies, at most once a second, through the thread's `markReadRequest` handler if one is registered. To mark a thread unread, call `Channel.markUnread({ thread_id: thread.id })`.

To change how the client marks a thread read, register a `markReadRequest` handler under `thread`. It receives `{ thread, options }`, where `options` already carries the thread's id, and returns the response, or `null` when it sent nothing:

```ts
import { AppState } from "react-native";
import type { CustomThreadMarkReadRequestFn, StreamChat } from "stream-chat";

const markReadRequest: CustomThreadMarkReadRequestFn = async ({
  thread,
  options,
}) => {
  // Keep the thread unread while the app is in the background.
  if (AppState.currentState !== "active") return null;

  return thread.channel.markRead(options);
};

export const setUpThreadMarkRead = (client: StreamChat) => {
  client.config.set({ thread: { requestHandlers: { markReadRequest } } });
};
```

Thread replies don't count toward the channel's unread count, and reading a thread doesn't change the channel's read state. The number of threads with unread replies is `unreadThreadCount` in `client.threads.state`, explained in [What channels and threads hold](https://getstream.io/chat/docs/sdk/react-native/state/channels-and-threads/what-channels-and-threads-hold/#the-users-threads).

## Used in

- [Read state, unread and receipts](https://getstream.io/chat/docs/sdk/react-native/features/messaging/read-state-unread-and-receipts/)
- [Channel lists](https://getstream.io/chat/docs/sdk/react-native/features/channels/channel-lists/)
- [Replies and threads](https://getstream.io/chat/docs/sdk/react-native/features/messaging/replies-and-threads/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/state/channels-and-threads/read-state-and-unread-counts/](https://getstream.io/chat/docs/sdk/react-native/state/channels-and-threads/read-state-and-unread-counts/).