# Upgrade to v10

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

This page lists what changed between v9 and v10 of Stream Chat React Native, and what to change in your app. Each change links to the v10 page that explains how the new version works. The samples import from `stream-chat-react-native`. In an Expo app, import the same names from `stream-chat-expo`.

>
> **Note:** This page starts from v9. Upgrade an app on v8 to v9 first with the "Upgrading from v8" guide, which you open by picking v9 in the version selector, and then follow this page.
>

v10 needs `stream-chat` 10, version 10.0.0-rc.26 or later. Upgrade both packages together, as [Installation](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/installation/) describes. Many of the changes below come from `stream-chat` 10. This page covers the ones a React Native app meets. The complete list of client changes is [stream-chat v9 to v10 breaking changes](https://github.com/GetStream/stream-chat-js/blob/release-v10/ai-docs/v10-breaking-changes.md) in the `stream-chat` repository.

## Quick migration checklist

1. Upgrade React Native to 0.79 or later and Expo to SDK 53 or later, then install the SDK and `stream-chat` from the `rc` tag. See [Dependencies](#dependencies).
2. Move the options you passed to `useCreateChatClient` or `new StreamChat()` under `client`. See [Creating the client](#creating-the-client).
3. Update how you read data. Your custom fields are under `custom`, server timestamps are nanosecond numbers, and sorts are arrays. See [Data shapes](#data-shapes).
4. Rewrite every `ChannelList`. Create a `ChannelPaginator`, register it on `client.channelManager`, and pass it as the `paginator` prop. See [Channel lists](#channel-lists).
5. Render `Thread` next to `Channel` instead of inside it, pass it a `Thread` object, and render the thread's list and composer as its children. See [Threads](#threads).
6. Move the `Channel` props that customized requests, throttling, uploads and pruning to `client.config`. See [Channel props that moved](#channel-props-that-moved).
7. Replace `TypingContext`, `PaginatedMessageListContext` and the removed context fields with reads from the channel's stores. See [Reading channel state](#reading-channel-state).
8. Call message operations through `useMessageOperations()` instead of `useMessagesContext()`. See [Messages](#messages).
9. Replace `isOnline` and `connectionRecovering` from `useChatContext()`, and any `connection.changed` listener. See [Connection status](#connection-status).
10. Rename your translation keys, and supply any language other than English yourself. See [Localization](#localization).
11. Expect the offline database to be rebuilt on the first launch after the upgrade. See [Offline storage](#offline-storage).

## What's new

- [How the SDK is built](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/how-the-sdk-is-built/) follows one sent message through the state layer in `stream-chat` and the UI layer in the SDK.
- [Configuration](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/configuration/) covers `client.config`, the one place to configure the client and the objects it creates.
- [Channel list paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/channel-list-paginators/) and [Channel manager and ChannelPaginator](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/channel-manager-and-channelpaginator/) describe channel lists as objects, with `client.channelManager` keeping several lists current.
- [Message paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/message-paginators/) and [Message paginator](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/message-paginator/) cover message intervals, jumping to a message, and unread tracking.
- [Message operations](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/message-operations/) covers the optimistic send, retry, update and delete, and the request handlers that customize them.
- [Connection](https://getstream.io/chat/docs/sdk/react-native/v10/state/connection/) describes the device's network and the WebSocket as two separate reactive services.
- [Reading state in components](https://getstream.io/chat/docs/sdk/react-native/v10/ui/reading-state-in-components/) explains `useStateStore`, which hook reads which store, and the selector rules.
- [Thread UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/thread-ui/) and [Navigation](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/navigation/) show threads on their own screens.

## Dependencies

| Package                  | v9                  | v10                                           |
| ------------------------ | ------------------- | --------------------------------------------- |
| `stream-chat`            | `^9`                | `^10.0.0-rc.26`                               |
| `react-native` (peer)    | `>=0.76`            | `>=0.79`                                      |
| `expo` (peer, Expo only) | `>=52`              | `>=53`                                        |
| `dayjs`                  | `1.11.13`, pinned   | `^1.11.23`                                    |
| `i18next`                | a direct dependency | none. It comes through `@stream-io/i18n`      |
| `@stream-io/i18n`        | none                | a new dependency, for the translation runtime |

Upgrade React Native or Expo first. `stream-chat` and `@stream-io/i18n` resolve their files through package `exports`, which Metro reads by default from the version that React Native 0.79 uses.

`stream-chat` 10 requires Node 22.18 or later in its `engines` field. Node is not your app's runtime, but it runs your install and your CI. Yarn 1 refuses to install on an older Node, and npm and pnpm print a warning.

Your app imports from `stream-chat` too, so add it at the version the SDK uses. Exactly one copy must resolve. The same goes for `dayjs`. If your app declares it, use a range that matches `^1.11.23`, or a second copy installs and the `dayjs` locales you import never reach the dates the SDK formats. Don't declare `i18next`.

`StateStore` and `Unsubscribe` are no longer exported from `stream-chat`. If you build your own stores, import them from `@stream-io/state-store`, add that package to your app, and keep one copy of it installed. A second copy makes your stores unassignable to `useStateStore`.

```diff
- import { StateStore, type Unsubscribe } from "stream-chat";
+ import { StateStore, type Unsubscribe } from "@stream-io/state-store";
```

## Localization

If you use the SDK in English and never customized translations, there is nothing to do here. The v10 behavior is on [Localization](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/localization/). This section is the migration.

### English is the only bundled language

The `ar`, `es`, `fr`, `he`, `hi`, `it`, `ja`, `ko`, `nl`, `pt-BR`, `ru` and `tr` dictionaries are removed, together with their `dayjs` calendar formats. So are the exports `enTranslations`, `esTranslations`, `frTranslations`, `heTranslations`, `hiTranslations`, `itTranslations`, `jaTranslations`, `koTranslations`, `nlTranslations`, `ptBRTranslations`, `ruTranslations` and `trTranslations`. Metro doesn't drop unused code, so every app bundled all twelve languages, even one that only rendered English.

If you rendered one of them, supply it yourself. The last published files are in the SDK's git history:

```bash
git show v9.10.1:package/src/i18n/nl.json > nl.json
```

Its keys are the old ones, so rename them before you register it, as the next section describes. You also need the `dayjs` locale and its calendar formats, which [Localization](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/localization/) shows.

### Translation keys are dotted identifiers

A key used to be the English copy itself. It's now a stable dotted identifier, and the SDK passes the English copy at each call as `i18next`'s `defaultValue`. The v9 key `a11y/Send message` became `messageInput.sendMessage.accessibilityLabel`. The `a11y/` prefix is gone, and what the text is for is the last segment of the key.

>
> **Warning:** Renaming your keys isn't optional, and a missed one fails quietly. An old key never matches, so your translation stops applying and the SDK's English copy renders instead, with no error.
>

The table that maps every v9 key to its v10 key, 389 rows, is [`ai-docs/i18n-v10-key-map.json`](https://github.com/GetStream/stream-chat-react-native/blob/V10/ai-docs/i18n-v10-key-map.json) in the SDK repository. A few rows:

| v9 key                   | v10 key                                       |
| ------------------------ | --------------------------------------------- |
| `Cancel`                 | `common.cancel.label`                         |
| `Reply`                  | `message.reply.label`                         |
| `Send a message`         | `autoCompleteInput.placeholder`               |
| `a11y/Send message`      | `messageInput.sendMessage.accessibilityLabel` |
| `{{count}} new messages` | `messageList.newMessages.text`, a plural key  |

A German dictionary in v9:

```ts
i18n.registerTranslation("de", {
  Cancel: "Abbrechen",
  "a11y/Send message": "Nachricht senden",
  "{{count}} new messages": "{{count}} neue Nachrichten",
});
```

The same dictionary in v10:

```ts
import { Streami18n } from "stream-chat-react-native";
import type { TranslationDictionary } from "stream-chat-react-native";

const de: TranslationDictionary = {
  "common.cancel.label": "Abbrechen",
  "messageInput.sendMessage.accessibilityLabel": "Nachricht senden",
  "messageList.newMessages.text_one": "{{count}} neue Nachricht",
  "messageList.newMessages.text_other": "{{count}} neue Nachrichten",
};

export const i18n = new Streami18n();
i18n.registerTranslation("de", de);
```

Typing the dictionary as `TranslationDictionary` turns a missed key into a compile error. `Streami18n.registerTranslation()` and `translationsForLanguage` take that type, so a dictionary written inline is checked without the annotation.

Five count strings that took `{{count}}` but weren't plural in v9 are plural keys now: `messageList.newMessages.text`, `messageList.scrollToBottom.withCount.accessibilityLabel`, `channelPreview.unreadCount.accessibilityLabel`, `message.reactionList.more.accessibilityLabel` and `avatar.channel.group.accessibilityLabel`. Provide their `_one` and `_other` forms, and the other plural forms your language needs. A translation of the bare key no longer applies.

One v9 key became two. `a11y/Double tap to open` was the screen reader hint of both an image gallery item and a channel list row. In v10 they're `attachment.gallery.doubleTapToOpen.accessibilityLabel` and `channelPreview.doubleTapToOpen.accessibilityLabel`, so a translation of one no longer changes the other. Translate both. The key map lists the second one under `alsoKeys`.

### Your own keys

`t()` from `useTranslationContext()` is typed `StreamTFunction`, and accepts only the SDK's keys. To translate your app's own copy on the same `Streami18n` instance, register it in a `LooseTranslationDictionary`, and wrap the key in `asDynamicKey()` when you read it:

```tsx
import { Text } from "react-native";
import {
  asDynamicKey,
  Streami18n,
  useTranslationContext,
} from "stream-chat-react-native";
import type { LooseTranslationDictionary } from "stream-chat-react-native";

const appKeys: LooseTranslationDictionary = {
  "myApp.settings.title": "Einstellungen",
};

export const i18n = new Streami18n({ language: "de" });
i18n.registerTranslation("de", appKeys);

export const SettingsTitle = () => {
  const { t } = useTranslationContext();
  return <Text>{t(asDynamicKey("myApp.settings.title"), "Settings")}</Text>;
};
```

Nothing checks the SDK's keys inside a `LooseTranslationDictionary`, so keep those in a `TranslationDictionary`.

### Keys whose value is not copy

The `timestamp/*` and `duration/*` keys became `timestamp.*` and `duration.*`. Their values are `dayjs` and `i18next` expressions. The SDK ships them as defaults that every dictionary you register is merged over, so you list one only to change it. Two of them, `timestamp.ChannelPreviewStatus` and `timestamp.ThreadListItem`, put English day words such as "Yesterday" in their own `calendarFormats` argument. To translate those words, override the two keys. [Localization](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/localization/) has the expressions.

Four formatter keys that v9 shipped but never rendered are gone from the catalog, with no replacement key: `duration/Location end at`, `duration/Remind Me`, `timestamp/ReminderNotification` and `timestamp/MessageEditedTimestamp`. If your app called them, register the expressions under keys of your own, for every language you register, English included:

```ts
import { Streami18n } from "stream-chat-react-native";
import type { LooseTranslationDictionary } from "stream-chat-react-native";

const appFormatterExpressions: LooseTranslationDictionary = {
  "duration.locationEndAt":
    "{{ milliseconds | durationFormatter(withSuffix: false) }}",
  "duration.remindMe":
    "{{ milliseconds | durationFormatter(withSuffix: true) }}",
  "timestamp.reminderNotification":
    "{{ timestamp | timestampFormatter(calendar: true) }}",
};

export const i18n = new Streami18n();
i18n.registerTranslation("en", appFormatterExpressions);
```

The `relativeCompactDateFormatter` formatter is removed. In an expression you override, write `{{ timestamp | timestampFormatter(relativeCompact: true) }}` instead. An unknown formatter doesn't throw, so a leftover one renders the timestamp wrong.

Convert a server timestamp with `convertTimestampToDate()` from `stream-chat` before you pass it to `t("timestamp.X", { timestamp })`. The interpolation values aren't type-checked, and a raw nanosecond timestamp renders as "Invalid Date". See [Timestamps are nanosecond numbers](#timestamps-are-nanosecond-numbers).

### Notifications and poll errors

The SDK translates a notification by its `notification.type`, not by its English `message`. A dictionary keyed by the English notification text no longer applies.

The errors in the poll composer's state are `PollComposerValidationError` objects instead of English strings. If you render them yourself, choose your copy by `error.code` and fall back to `error.message`.

### The translation runtime

`Streami18n` is built on the `@stream-io/i18n` package, shared with the React SDK. You still import it from `stream-chat-react-native`. Code that calls the instance directly needs these changes:

| v9                                              | v10                                                                            |
| ----------------------------------------------- | ------------------------------------------------------------------------------ |
| `new Streami18n(options, i18nextConfig)`        | `new Streami18n({ ...options, i18nextConfigOverrides: i18nextConfig })`        |
| `await i18n.getTranslators()`                   | `await i18n.init()`, which resolves to the same value                          |
| `const t = await i18n.setLanguage("de")`        | `await i18n.setLanguage("de")` resolves to nothing. Read `t` from `i18n.state` |
| `i18n.addOnLanguageChangeListener(listener)`    | `i18n.state.subscribeWithSelector(({ t }) => ({ t }), ({ t }) => listener(t))` |
| `i18n.addOnTFunctionOverrideListener(listener)` | subscribe to `i18n.state`                                                      |
| `i18n.geti18Instance()`                         | `i18n.i18nInstance`                                                            |
| `i18n.getAvailableLanguages()`                  | `i18n.registeredLanguages`, a read-only `Set`                                  |
| `i18n.getTranslations()`                        | removed. Call `i18n.t(key)` to read a translation                              |
| `i18n.isCustomDateTimeParser`, `i18n.Dayjs`     | removed                                                                        |

`Streami18n.registerTranslation()` merges the dictionary into the language's existing one, so repeated calls add up, and a partial dictionary keeps the bundled formatter keys. Its third argument is a `DayjsLocaleConfig`.

The date helpers were deep imports in v9 and are exported from the package root in v10. `getDateString({ date })` takes `messageCreatedAt` instead of `date`, and returns `null` instead of `undefined` when there is nothing to render. `getDateStringForA11y` is now `getCalendarDateStringForA11y`. The name `getDateStringForA11y` still exists, but it formats the date in full, as the React SDK does.

Every component and hook that takes or returns `t` types it as `StreamTFunction`. `useTranslationContext().userLanguage` is a `string`, and `DEFAULT_USER_LANGUAGE` is `"en"`. `stream-chat`'s translation API takes a closed set of languages, so narrow `userLanguage` before you pass it there. The values of `SCROLL_TO_BOTTOM_ACCESSIBILITY_LABEL_KEY` and `SCROLL_TO_BOTTOM_WITH_COUNT_ACCESSIBILITY_LABEL_KEY` are the dotted keys. `isDayOrMoment` comes from `@stream-io/i18n`, and narrows to `DateTimeLike`. `TDateTimeParserInput` and `TDateTimeParserOutput` are typed by `@stream-io/i18n` too.

## Creating the client

`stream-chat` 10 builds the client from one configuration tree, the same tree `client.config.set()` takes. The options v9 took at the top level now go under `client`. `useCreateChatClient` passes its `options` to the client, so they move too.

In v9:

```tsx
const client = useCreateChatClient({
  apiKey,
  options: { baseURL: "https://chat.example.com", timeout: 6000 },
  tokenOrProvider: userToken,
  userData: { id: userId },
});
```

In v10:

```tsx
import { Text } from "react-native";
import { Chat, useCreateChatClient } from "stream-chat-react-native";

const apiKey = "YOUR_API_KEY";
const userId = "YOUR_USER_ID";
const userToken = "YOUR_USER_TOKEN";

export const App = () => {
  const client = useCreateChatClient({
    apiKey,
    options: {
      client: {
        axiosRequestConfig: { timeout: 6000 },
        baseURL: "https://chat.example.com",
      },
    },
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  if (!client) return <Text>Connecting...</Text>;

  return <Chat client={client}>{/* your app */}</Chat>;
};
```

A client you create yourself takes the same tree, `new StreamChat(apiKey, { client: { baseURL } })`, and so does `StreamChat.getInstance()`. The `secret` argument is gone.

Where the common v9 options went:

| v9 option                                                    | v10 setting                                                           |
| ------------------------------------------------------------ | --------------------------------------------------------------------- |
| `timeout`, `headers` and the other top-level `axios` options | `client.axiosRequestConfig`                                           |
| `baseURL`                                                    | `client.baseURL`                                                      |
| `persistUserOnConnectionFailure`                             | `client.persistUserOnConnectionFailure`                               |
| `enableWSFallback`                                           | `client.wsConnection.enableWSFallback`                                |
| `recoverStateOnReconnect`                                    | `client.connectionRecovery.enabled`                                   |
| `isLocalUnreadCountEnabled`                                  | `channel.readEvents.localUnreadCountEnabled`                          |
| `logger`                                                     | removed. Configure logging with `chatLoggerSystem` from `stream-chat` |
| `device`                                                     | removed. Call `client.createDevice()` after connecting                |

`userData` is typed `ClientUser`, the type `client.connectUser()` takes. Your own user fields go under `custom`. A `UserResponse` no longer fits there, because it requires fields the server sets, such as `created_at`.

`Chat` writes `persistUserOnConnectionFailure` from its `enableOfflineSupport` prop when it mounts, and overwrites a value you set. To keep it `true` without offline support, set it again after `Chat` mounts. `Chat` no longer turns off the client's own reconnect recovery, as v9 did, and no longer sets `client.preventThreadCleanup`. See [Connection status](#connection-status).

[Configuration](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/configuration/) explains the tree, which settings can change after the client is created, and setup functions.

## Data shapes

`stream-chat` 10 types its data from the API's specification. Three changes reach almost every app.

### Custom fields are under custom

Your own fields on a message, a channel, a user, an attachment or another object are no longer spread at the top level. They're in the object's `custom` field:

| v9                                        | v10                                                                |
| ----------------------------------------- | ------------------------------------------------------------------ |
| `message.priority`                        | `message.custom?.priority`                                         |
| `message.ai_generated`                    | `message.custom?.ai_generated`                                     |
| `channel.data?.name`                      | `channel.data?.custom?.name`                                       |
| `channel.data?.image`                     | `channel.data?.custom?.image`                                      |
| `attachment.mime_type`, `file_size`       | `attachment.custom?.mime_type`, `attachment.custom?.file_size`     |
| `Channel.sendMessage({ text, priority })` | `Channel.sendMessage({ message: { text, custom: { priority } } })` |
| `client.channel(type, id, { name })`      | `client.channel(type, id, { custom: { name } })`                   |

`customDataManager.setMessageData()` sends its fields under `custom` too.

In filters, custom fields are `"custom.<field>"` keys, and each key takes one operator. To type your fields, extend the `Custom*Data` interfaces of `stream-chat`, as [Data conventions](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/data-conventions/) shows.

`message.moderation_details` is replaced by `message.moderation`. `isBlockedMessage` and `isBouncedMessage` read only `message.moderation`, so if your backend still sends the older moderation payload, they return `false`.

### Timestamps are nanosecond numbers

Every date the server sends, such as `created_at`, is a `TimestampNS`, a `number` that holds Unix time in nanoseconds. In v9 messages carried `Date` objects and other data carried ISO strings.

- Compare and sort timestamps as numbers.
- Convert where a timestamp meets a `Date`, a date library or `Date.now()`, with `convertTimestampToDate()`, `nsToDate()` or `nsToMs()` from `stream-chat`. To create one, use `nowNs()`, `msToNs()`, `dateToNs()` or `asTimestampNS()`.
- `new Date(message.created_at)` is marked deprecated, and its result has none of the methods of a `Date`, so code that uses it as a date, such as `new Date(message.created_at).getTime()`, doesn't compile. `dayjs(message.created_at)` compiles, because `TimestampNS` is a number, but reads nanoseconds as milliseconds. The SDK's `getDateString` turns the result into `null`, so a missed conversion shows as a blank timestamp.

Dates you send in a request, such as `remind_at`, are still `Date` objects. The components that render a date, such as `MessageFooter`, still take a `Date` or a string.

### Sorts are arrays

Every sort is an array of `{ field, direction }` objects, typed `SortParamRequest[]`. The object form and the per-endpoint sort types, such as `ChannelSort`, are removed:

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

// v9: { pinned_at: -1, last_message_at: -1 }
export const sort: SortParamRequest[] = [
  { direction: -1, field: "pinned_at" },
  { direction: -1, field: "last_message_at" },
];
```

The first field sorts first. `ChannelListEventListenerOptions.sort` and the `sort` of `UseFetchReactionParams` take the same type.

### Other client changes

These come up less often. The [complete list](https://github.com/GetStream/stream-chat-js/blob/release-v10/ai-docs/v10-breaking-changes.md) has each one in detail.

- `client.queryChannels()` is a plain request that returns the response and stores no channels. The v9 behavior is `client.queryChannelsAndHydrate({ filter_conditions, sort, ...options })`, which returns `Channel[]`.
- Methods take request objects, with path parameters first, such as `Channel.sendMessage({ message })`, `client.search({ payload })`, `client.queryUsers({ payload })`, `Channel.queryMembers({ payload })`, and `Channel.sendEvent({ event: { type, custom } })`.
- `Channel.update({ name })` is `Channel.updatePartial({ set: { name } })`, or `Channel.update({ data: { custom: { name } } })`. Members are objects, as in `members: [{ user_id }]`.
- `client.addDevice()`, `client.getDevices()` and `client.removeDevice()` are `client.createDevice({ id, push_provider, push_provider_name })`, `client.listDevices()` and `client.deleteDevice({ id })`. `client.setLocalDevice()` and the `device` option are removed.
- Moderation calls moved to `client.moderation` and take request objects. `client.muteUser(id)` is `client.moderation.mute({ target_ids: [id] })`, `client.banUser(id)` is `client.moderation.ban({ target_user_id: id })`, `client.flagMessage(id)` is `client.moderation.flagMessage({ message_id: id })`, and `client.flagUser(id)` is `client.moderation.flagUser({ user_id: id })`.
- `Channel.addMembers(members)` is `Channel.addMembers({ members })`. `client.pinMessage(message, expiration)` is `client.pinMessage(message, { pin_expires, pinned_at })`: the first argument is anything with an `id`, and the expiration moves into an options object. `client.unpinMessage(message)` is unchanged.
- `client.userID` and `client.clientID` are deprecated. Use `client.userId` and `client.clientId`.
- The paginators in `stream-chat` 10, such as `channel.messagePaginator` and `ChannelPaginator`, have no `next()`, `prev()`, `nextDebounced()`, `prevDebounced()`, `hasNext` or `hasPrev`. Use `paginator.toTail()`, `paginator.toHead()`, `paginator.toTailDebounced()`, `paginator.toHeadDebounced()`, `hasMoreTail` and `hasMoreHead`. A paginator's state has no `hasNext` and `hasPrev` either, so a selector reading them gets `undefined`. Select `hasMoreTail` and `hasMoreHead`. A cursor's `next` and `prev` are `tailward` and `headward`. [Head and tail](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/how-pagination-works/#head-and-tail) explains both ends.
- `client.reminders.upsertReminder({ message_id }, { remind_at })` takes a `Date`. `Channel.stopLiveLocationSharing({ message_id })` takes the message id. Call `LiveLocationManager.dispose()` on a live location manager you created when you're done with it.
- `client.uploadFile()` and `client.uploadImage()` take `{ file: { uri, name, type } }`, and replace `Channel.sendFile()` and `Channel.sendImage()` too.
- `Channel.lastMessage()` is removed. See [The channel's latest message](#the-channels-latest-message).
- `client.activeChannels`, `client.getChannelById()` and `client.getChannelByMembers()` are removed. Use `client.channelManager.get(cid)`, `client.channelManager.values()` and `client.channel()`. Channels stay in the client until `client.channelManager.releaseUnusedChannels()` releases the ones nothing uses. `client.hydrateActiveChannels()` is `client.hydrateChannels()`.
- `Channel.getConfig()` is the `channel.serverConfig` getter, and `client.configs[cid]` is `client.channelServerConfigs[cid]`. What the client enforces is `channel.config`, the server's settings combined with yours.
- `client.setMessageComposerSetupFunction(fn)` is deprecated. Use `client.config.setSetupFunction("messageComposer", fn)`.
- `Event` is a union of event types. Fields such as `event.me` exist only on the event types that carry them, so narrow with `EventPayload<"...">`, or let `client.on("health.check", ...)` narrow it.
- Many types were renamed, among them `Mute` to `UserMuteResponse`, `ReminderResponse` to `ReminderResponseData`, `SharedLocationResponse` to `SharedLocationResponseData`, `MessageResponseBase` to `MessageResponse`, `MessageRequest` to `SendMessageRequest`, and `DeliveredMessageConfirmation` to `DeliveredMessagePayload`. `DraftFilters`, `DraftSort` and `Pager` are replaced by `QueryDraftsRequest`. Search results are `SearchResultMessage`, with an optional `message`. Some names stayed with a different meaning, such as `User`, `Reaction`, `PollResponse` and `ChannelConfig`, so check what an annotation resolves to.
- API errors are `StreamAPIError`, which replaces `ErrorFromResponse`.
- Logging goes through `chatLoggerSystem` from `stream-chat`, and is on by default, to the console at `info` level. Turn it down with `chatLoggerSystem.configureLoggers({ default: { level: "warn" } })`.

## Connection status

In v9, "online" meant that the WebSocket was up. `stream-chat` 10 reports two things separately, whether the device has a network, in `client.networkConnection`, and whether the socket is up, in `client.wsConnection`. The v10 behavior is on [Connection](https://getstream.io/chat/docs/sdk/react-native/v10/state/connection/).

`useChatContext()` no longer returns `isOnline` or `connectionRecovering`. Read the status with these hooks:

| Hook                             | What it reports                                                                                                                    |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `useNetworkConnectionState()`    | The device's network. Its `isOnline` is `undefined` until the network is first reported, so test it with `=== false`               |
| `useWSConnectionState()`         | The socket's raw state, such as `isHealthy` and `lastHealthyAt`                                                                    |
| `useSettledWSConnectionHealth()` | Whether the socket counts as healthy for UI. A drop counts once it lasts `offlineNotificationDisplayDelayMs`, 5 seconds by default |

In v9:

```tsx
const { isOnline } = useChatContext();
if (!isOnline) return <OfflineBanner />;
```

In v10:

```tsx
import { Text } from "react-native";
import {
  useNetworkConnectionState,
  useSettledWSConnectionHealth,
} from "stream-chat-react-native";

export const ConnectionBanner = () => {
  const isDeviceOffline = useNetworkConnectionState()?.isOnline === false;
  const isSocketHealthy = useSettledWSConnectionHealth();

  if (isDeviceOffline) return <Text>Waiting for network...</Text>;
  if (!isSocketHealthy) return <Text>Reconnecting...</Text>;
  return null;
};
```

Change the delay with `client.config.set({ client: { wsConnection: { offlineNotificationDisplayDelayMs: 3000 } } })`.

Other changes around the connection:

- `useIsOnline(client)` is renamed `useConnectionLifecycle(client)`, and returns nothing. It installs the network reporter and closes the socket while the app is in the background. `Chat` already calls it.
- `Chat` registers `netInfoStatusReporter`, built on `@react-native-community/netinfo`, when it mounts, unless you registered a `statusReporter` on the client yourself. A client you build yourself can take the reporter at construction, with `new StreamChat(apiKey, { client: { networkConnection: { statusReporter: netInfoStatusReporter } } })`.
- The `connection.changed` event is no longer sent. A listener for it compiles and never runs. Subscribe to `client.wsConnection.state` or `client.networkConnection.state` instead.
- `NetworkDownIndicator` shows "Waiting for network..." when the device is offline, and "Reconnecting..." when the socket has been down longer than the delay. It no longer shows an error when messages fail to load. `ChannelListHeaderNetworkDownIndicator` and `NotificationAnnouncer` follow the same two signals.
- `MessageComposer`'s and `OutputButtons`' `isOnline` prop is a `boolean`, the settled socket health, instead of `boolean | null`.

### Recovery after a reconnect

The client recovers after a reconnect by itself. It reloads each registered channel list, every active channel and every active thread, and then sends `connection.recovered`. A channel list reloads every page it has loaded, up to 150 channels, and drops the channels no page returned, such as ones deleted or hidden while the app was offline. This runs on every reconnect, including the one after the app returns from the background, which v9 missed. `Channel`, `Thread` and `ChannelList` no longer resync on their own. Delete any listener of yours that re-queries a list or re-watches a channel after a reconnect, because it now doubles the work.

- `recoverStateOnReconnect` is removed. To turn recovery off, set `client.config.set({ client: { connectionRecovery: { enabled: false } } })`. v9's `Chat` set `client.recoverStateOnReconnect = false` for you, and v10's doesn't.
- `client.recoverState()` is `client.connectionRecovery.recover()`.
- A listener for `connection.recovered` now also runs after the app returns from the background. If it's expensive, check it.
- Recovery reloads only active channels and threads. `Channel` and `Thread` activate theirs while they're mounted. A screen of your own that shows a channel or a thread without them must call `Channel.activate()` or `Thread.activate()`, and call the function it returns when the screen goes away. There is no `Channel.deactivate()` or `Thread.deactivate()`.

A request that watches a channel or asks for presence, such as `Channel.watch()`, `queryChannels` and `queryUsers` with `presence`, waits for the socket's connection id. v9 sent it without one, and the server registered nothing. With no socket open or opening, it throws, so connect the user first. See [Troubleshooting](https://getstream.io/chat/docs/sdk/react-native/v10/troubleshooting/).

## Channels

### Channel props that moved

Props that customized requests and the message list moved to `client.config`, or to the component that uses them:

| v9 prop on `Channel`                                                                  | v10                                                                                                             |
| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `doSendMessageRequest`, `preSendMessageRequest`                                       | `client.config.set({ channel: { requestHandlers: { sendMessageRequest, retrySendMessageRequest } } })`          |
| `doUpdateMessageRequest`                                                              | `client.config.set({ channel: { requestHandlers: { updateMessageRequest } } })`                                 |
| `doMarkReadRequest`                                                                   | `client.config.set({ channel: { requestHandlers: { markReadRequest } } })`                                      |
| `doFileUploadRequest`                                                                 | `client.config.set({ messageComposer: { attachments: { doUploadRequest, customCdn: true } } })`                 |
| `stateUpdateThrottleInterval`, `newMessageStateUpdateThrottleInterval`                | `client.config.set({ channel: { messagePaginator: { stateThrottleMs } } })`                                     |
| `maximumMessageLimit`                                                                 | `client.config.set({ channel: { messagePaginator: { maxLoadedItems } } })`                                      |
| `allowSendBeforeAttachmentsUpload`                                                    | `client.config.set({ messageComposer: { attachments: { pendingUploadsEnabled } } })`. See [Composer](#composer) |
| `enableOfflineSupport`                                                                | removed. Set it on `Chat`                                                                                       |
| `isMessageAIGenerated`                                                                | removed. Set it on `Chat`, which always overrode it                                                             |
| `messageId`                                                                           | removed. See [Opening a channel at a message](#opening-a-channel-at-a-message)                                  |
| `thread`, `threadList`, `allowThreadMessagesInChannel`, `shouldSyncChannel`           | removed. Render `Thread` next to `Channel`. See [Threads](#threads)                                             |
| `messages`, `loadingMore`, `loadingMoreRecent`, `threadMessages`, `setThreadMessages` | removed. The lists read the message paginators                                                                  |
| `isOnline`                                                                            | removed. It was never read                                                                                      |

The request handlers change shape as well as place. They take one params object instead of positional arguments, plus the default request as a second argument, and resolve to `{ message }`. They're registered once per client, not per mounted `Channel`, so to treat channels differently, branch inside the handler on `params.localMessage.cid`. Register them before the client reaches `Chat`, not from a component's effect.

In v9:

```tsx
<Channel
  channel={channel}
  doSendMessageRequest={(channelId, message, options) =>
    mySend(channelId, message, options)
  }
  doUpdateMessageRequest={(channelId, message, options) =>
    myUpdate(channelId, message, options)
  }
/>
```

In v10:

```ts
import { StreamChat } from "stream-chat";
import type {
  DefaultOperationRequest,
  OperationParams,
  OperationRequestFn,
} from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

// Generic over "send" and "retry", so the same function fits both handlers.
const sendMessageRequest = async <K extends "send" | "retry">(
  params: OperationParams<K>,
  defaultRequest: DefaultOperationRequest<K>,
) => {
  // Inspect or change the message here, then send it the default way.
  console.log("Sending to", params.localMessage.cid);
  return defaultRequest(params);
};

const updateMessageRequest: OperationRequestFn<"update"> = async (
  params,
  defaultRequest,
) => defaultRequest(params);

client.config.set({
  channel: {
    requestHandlers: {
      retrySendMessageRequest: sendMessageRequest,
      sendMessageRequest,
      updateMessageRequest,
    },
  },
});
```

- `sendMessageRequest` runs for the first send, and for a retry when no `retrySendMessageRequest` is registered. A registered `retrySendMessageRequest` takes over retries.
- `sendMessageRequest` replaces `preSendMessageRequest` too, but runs later. It runs after the message appears in the list and after its attachments finish uploading. If it throws, the message is marked failed, where `preSendMessageRequest` left it pending.
- A handler that resolves without a `message` sends nothing more, as in v9. The `message.new` event updates the message.
- Thread replies go through the same `channel` handlers. The `thread` key takes only `markReadRequest`.
- `customCdn: true` says your function sends files to storage Stream doesn't host, so the channel type's `uploads` flag and the `upload-file` capability stop gating the composer's uploads. Leave it out if your function uploads to Stream. The attach button still shows only to users with `upload-file`.

### What Channel does on mount

`Channel` still watches its channel when it mounts, loads the first page of messages, and marks the channel read. Several details changed:

- The first page has 25 messages, as in v9, where it was the server's default. `stream-chat` 10's own default is 100, so `Channel` sets 25 unless you register a page size. To change it, register a page size with `client.config.set({ channel: { messagePaginator: { pageSize: 50 } } })`. Setting `channel.messagePaginator.pageSize` after mount doesn't last.
- `Channel` no longer listens to the channel's events, resyncs after a reconnect, or renders nothing when the channel is deleted. See [A deleted channel keeps rendering](#a-deleted-channel-keeps-rendering).
- A failed `Channel.watch()` no longer shows the full-screen error. `Channel` renders `LoadingErrorIndicator` when the channel's message paginator has an error and no messages to show.
- `initializeOnMount={false}` skips `Channel.watch()`, but still sends a query that doesn't watch when the paginator has no messages. To send no request at all, load the paginator before you mount `Channel`.
- A channel created from members only, without an id, that your code has already marked `initialized`, for example to show a conversation before it exists, isn't queried on mount. Its first message creates it. A members-only channel that isn't marked `initialized` is watched on mount, which creates it.
- When `channel.supersededBy` is set, `Channel` renders that channel instead of the one you passed. Inside `Channel`, read the channel from `useChannelContext()` or `useChannel()`, not from your own prop.
- `client.options.isLocalUnreadCountEnabled` is no longer read. Use `client.config.set({ channel: { readEvents: { localUnreadCountEnabled: true } } })`.
- Sending a message no longer removes `type: "error"` messages from the list. Remove one with `useMessageOperations().removeMessage(message)`.

### watch() resolves with the channel

`Channel.watch()` loads and watches a channel only when it needs to. On a channel that's already watched and loaded, such as one from a channel list, it sends no request, unless you pass options the current watch wasn't made with, such as `presence: true`. A call made while the same watch is in flight joins it instead of sending another. It resolves with the `Channel` to use from then on: this one, or the one that superseded it. v9's `Channel.watch()` sent a query on every call and resolved with the server response.

- Code that read the response, such as `const { messages } = await channel.watch()`, reads the channel instead: its messages from `channel.messagePaginator`, and its members and read state from `channel.state`. See [Reading channel state](#reading-channel-state).
- Calling `Channel.watch()` again no longer refreshes the channel. Call `Channel.reload()`. To load other messages, use `MessagePaginator.toTail()`, `MessagePaginator.toHead()` or `MessagePaginator.jumpToMessage()`.
- Keep using the channel `Channel.watch()` resolves with. A channel created from its members gets its real `cid` from its first watch, and the object you hold may have been superseded. See [Superseded channels](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/entity-lifecycle/#superseded-channels).
- Earlier v10 release candidates had `Channel.ensureWatched()`. It's removed, and `Channel.watch()` does what it did.

### Opening a channel at a message

The `messageId` prop is removed. Jump to the message through the channel's message paginator, from a layout effect, so the jump starts before the list picks its first scroll position. Turn off `initialScrollToFirstUnreadMessage` while you do, or the two jumps race:

```tsx
import { useLayoutEffect } from "react";
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  DEFAULT_HIGHLIGHT_DURATION,
  MessageComposer,
  MessageList,
} from "stream-chat-react-native";

type Props = { channel: StreamChannel; messageId?: string };

export const ChannelScreen = ({ channel, messageId }: Props) => {
  useLayoutEffect(() => {
    if (!messageId) return;
    channel.messagePaginator.jumpToMessage(messageId, {
      focusReason: "jump-to-message",
      focusSignalTtlMs: DEFAULT_HIGHLIGHT_DURATION,
    });
  }, [channel, messageId]);

  return (
    <Channel channel={channel} initialScrollToFirstUnreadMessage={!messageId}>
      <MessageList />
      <MessageComposer />
    </Channel>
  );
};
```

### A deleted channel keeps rendering

v9's `Channel` rendered nothing once its channel was deleted. v10's keeps rendering it, as the React SDK does, and what to show is up to your app. A channel object is gone for good when the channel is deleted, when the user is removed from it, or when the client disconnects. Its `pendingDisposal` state says so:

```tsx
import type { PropsWithChildren } from "react";
import type {
  ChannelLifecycleState,
  Channel as StreamChannel,
} from "stream-chat";
import { Channel, useStateStore } from "stream-chat-react-native";

const pendingDisposalSelector = (state: ChannelLifecycleState) => ({
  pendingDisposal: state.pendingDisposal,
});

export const ChannelIfPresent = ({
  channel,
  children,
}: PropsWithChildren<{ channel: StreamChannel }>) => {
  const { pendingDisposal } = useStateStore(
    channel.state,
    pendingDisposalSelector,
  );

  // Or render your own "this channel is gone" view.
  if (pendingDisposal) return null;

  return <Channel channel={channel}>{children}</Channel>;
};
```

`channel.disconnected` is renamed `channel.pendingDisposal`, with no alias. A disposed channel object never comes back. To open the same conversation again, get a new object with `client.channel(type, id)`.

### Reading channel state

In `stream-chat` 9, `channel.state` was a plain object, and the SDK copied parts of it into contexts. In `stream-chat` 10, `channel.state` is a store, and the SDK no longer copies it. Read it with `useStateStore` and a selector declared outside the component. Changing a field of `channel.state` or `channel.data` in place notifies no one. To change `channel.data`, assign a new object. The v10 behavior is on [Reading state in components](https://getstream.io/chat/docs/sdk/react-native/v10/ui/reading-state-in-components/).

`TypingContext` and `PaginatedMessageListContext` are removed, with their providers, their `use...Context` hooks, their value types, and `useCreateTypingContext` and `useCreatePaginatedMessageListContext`. So is `DebugContext`, which fed the Flipper plugin, with nothing in its place.

In v9:

```tsx
const { typing } = useTypingContext();
const { messages, hasMore, loadMore } = usePaginatedMessageListContext();
const { members, watcherCount } = useChannelContext();
```

In v10:

```tsx
import { Button, Text, View } from "react-native";
import type { ChannelStateData, MessagePaginatorState } from "stream-chat";
import {
  useChannel,
  useMessagePaginator,
  useStateStore,
} from "stream-chat-react-native";

const channelSelector = (state: ChannelStateData) => ({
  memberCount: state.memberCount,
  typing: state.typing,
  watcherCount: state.watcherCount,
});

const messagesSelector = (state: MessagePaginatorState) => ({
  hasMore: state.hasMoreTail,
  messages: state.items,
});

export const ChannelSummary = () => {
  const channel = useChannel();
  const paginator = useMessagePaginator();
  const { memberCount, typing, watcherCount } = useStateStore(
    channel.state,
    channelSelector,
  );
  const { hasMore, messages } = useStateStore(
    paginator.state,
    messagesSelector,
  );

  return (
    <View>
      <Text>
        {messages?.length ?? 0} messages, {memberCount} members, {watcherCount}{" "}
        online, {Object.keys(typing).length} typing
      </Text>
      {hasMore ? (
        <Button onPress={() => paginator.toTail()} title="Load older" />
      ) : null}
    </View>
  );
};
```

`useChannel()` returns the channel of the surrounding `Channel`, or the thread's channel inside a `Thread`. `useMessagePaginator()` returns the thread's message paginator inside a `Thread`, and the channel's elsewhere. In a message list, the tail is where older messages load, and the head is the newest message.

Where the removed context fields went:

| v9                                                                                        | v10                                                                                                                                                                                               |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `typing` from `useTypingContext()`                                                        | `typing` in `channel.state`                                                                                                                                                                       |
| `messages` from `usePaginatedMessageListContext()`                                        | `items` in `useMessagePaginator().state`                                                                                                                                                          |
| `hasMore`, `hasMoreNewer`, `loadingMore`, `loadingMoreRecent`                             | `hasMoreTail` (older), `hasMoreHead` (newer) and `isLoading` in the same state                                                                                                                    |
| `setLoadingMore`, `setLoadingMoreRecent`                                                  | removed. The paginator sets `isLoading` itself                                                                                                                                                    |
| `viewabilityChangedCallback`                                                              | `viewabilityChangedCallback` from `useMessageList({ paginator })`. It calls `MessagePaginator.setPruningSuspended()`, which holds off pruning while the user is near the messages it would remove |
| `loadMore()`, `loadMoreRecent()`, `loadLatestMessages()`                                  | `paginator.toTail()`, `paginator.toHead()` and `paginator.jumpToTheLatestMessage()`                                                                                                               |
| `members`, `read`, `watchers`, `watcherCount` from `useChannelContext()`                  | the same keys in `channel.state`                                                                                                                                                                  |
| `members` from `useMessageContext()`                                                      | `members` in `channel.state`                                                                                                                                                                      |
| `markRead` from `useChannelContext()`                                                     | `useMarkRead(channel)`, or `Channel.markReadViaReporter()`. See [Read state](#read-state-and-unread-counts)                                                                                       |
| `loadChannelAroundMessage({ messageId })`                                                 | `paginator.jumpToMessage(messageId, { focusReason: "jump-to-message" })`                                                                                                                          |
| `loadChannelAtFirstUnreadMessage()`                                                       | `paginator.jumpToTheFirstUnreadMessage()`                                                                                                                                                         |
| `reloadChannel()`                                                                         | `paginator.jumpToTheLatestMessage()`                                                                                                                                                              |
| `loading`                                                                                 | `isLoading` in `paginator.state`                                                                                                                                                                  |
| `error`                                                                                   | `lastQueryError` in `paginator.state`                                                                                                                                                             |
| `targetedMessage`, `setTargetedMessage`, `highlightedMessageId`, and `useTargetedMessage` | `paginator.jumpToMessage(id, ...)` to set, `useIsTargetedMessage(id)` to read                                                                                                                     |
| `channelUnreadStateStore`, `setChannelUnreadState`                                        | `paginator.unreadStateSnapshot`                                                                                                                                                                   |
| `isChannelActive`                                                                         | `!useThreadContext().thread`                                                                                                                                                                      |
| `maximumMessageLimit`                                                                     | `maxLoadedItems` in the paginator's configuration                                                                                                                                                 |
| `uploadAbortControllerRef`                                                                | removed. Remove an attachment from the composer to cancel its upload                                                                                                                              |
| `scrollToFirstUnreadThreshold`                                                            | removed. It was always `0`                                                                                                                                                                        |
| `useSelectedChannelState({ channel, selector })`                                          | `useStateStore(channel.state, selector)`                                                                                                                                                          |

`useChannelContext().error` was the error of the last failed query. Read `lastQueryError` from the paginator of the list you render, which `useMessagePaginator()` returns. `NetworkDownIndicator` no longer shows it, so render your own banner from it if you want one.

Most keys of `channel.state` keep their v9 names. `watcher_count` is `watcherCount`, and `member_count` is `memberCount`. The state also holds values that lived elsewhere, such as `data`, `membership`, `muteStatus`, `ownCapabilities`, `initialized`, `pendingDisposal`, `watchStatus` and `aiState`. The getters, such as `channel.state.members`, still work, but they return the value at the moment you read them, and don't tell you when it changes, so they re-render nothing. [What channels and threads hold](https://getstream.io/chat/docs/sdk/react-native/v10/state/channels-and-threads/what-channels-and-threads-hold/) lists the fields.

The messages moved off `channel.state` too:

| v9                                       | v10                                               |
| ---------------------------------------- | ------------------------------------------------- |
| `channel.state.messages`                 | `items` in `channel.messagePaginator.state`       |
| `channel.state.findMessage(id)`          | `channel.messagePaginator.getItem(id)`            |
| `channel.state.loadMessageIntoState(id)` | `channel.messagePaginator.jumpToMessage(id)`      |
| `channel.state.isUpToDate`               | `channel.messagePaginator.isActiveIntervalAtHead` |
| `channel.state.pinnedMessages`           | `channel.pinnedMessagesPaginator`                 |
| `channel.state.threads[id]`              | `client.threads.get(id)?.messagePaginator`        |
| `channel.state.mutedUsers`               | `useMutedUsers()`                                 |
| `Channel.query({ messages: { id_lt } })` | `channel.messagePaginator.toTail()`               |

`Channel.query()` returns a page and no longer puts it in `channel.messagePaginator`. Its second argument, the message set to add the messages to, is removed, and the second argument is now the request options, such as an abort `signal`. Open a channel with `Channel.watch()`, and load more messages with `MessagePaginator.toTail()`, `MessagePaginator.toHead()` or `MessagePaginator.jumpToMessage()`.

### The channel's latest message

`Channel.lastMessage()` and `channel.state.latestMessages` are removed. The channel's newest message is in its message paginator's `aggregateState`, whichever interval of messages is on screen. `channel.messagePaginator.latestMessage` reads it once, and is `null` when there is none.

```tsx
import { Text } from "react-native";
import type { MessagePaginatorAggregateState } from "stream-chat";
import { useChannel, useStateStore } from "stream-chat-react-native";

const latestMessageSelector = (state: MessagePaginatorAggregateState) => ({
  latestMessage: state.latestMessage,
});

export const LatestMessageText = () => {
  const channel = useChannel();
  const { latestMessage } = useStateStore(
    channel.messagePaginator.aggregateState,
    latestMessageSelector,
  );
  return <Text>{latestMessage?.text}</Text>;
};
```

### ChatContext holds the client and two functions

`useChatContext()` returns `client`, `getAppSettings` and `isMessageAIGenerated`. The other fields are removed:

| v9 `useChatContext()`              | v10                                                                                       |
| ---------------------------------- | ----------------------------------------------------------------------------------------- |
| `appSettings`                      | `await getAppSettings()`. The first call fetches the settings, and later calls reuse them |
| `mutedUsers`                       | `useMutedUsers()`                                                                         |
| `channel`, `setActiveChannel`      | keep the open channel in your navigation state, and pass it to `Channel`                  |
| `enableOfflineSupport`             | `client.offlineDb !== undefined`                                                          |
| `isOnline`, `connectionRecovering` | the connection hooks. See [Connection status](#connection-status)                         |

`useAppSettings` and `useClientMutedUsers` are removed. Use `getAppSettings()` and `useMutedUsers()`. `useMutedUsers()` takes no argument, and returns `UserMuteResponse[]`. `useMutedChannels()` takes no argument either. For one channel's mute state, use `useIsChannelMuted(channel)`, which returns a `ChannelMuteStatus` with nanosecond timestamps.

## Channel lists

`ChannelList` no longer creates its query. Its data comes from a `ChannelPaginator` that your app creates and registers on `client.channelManager`, and the list renders what that paginator holds. The v10 behavior is on [Channel list paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/channel-list-paginators/).

In v9:

```tsx
<ChannelList
  filters={{ members: { $in: [userId] }, type: "messaging" }}
  onSelect={onSelect}
  options={{ presence: true, state: true, watch: true }}
  sort={{ last_message_at: -1 }}
/>
```

In v10:

```tsx
import { useEffect, useMemo } from "react";
import { ChannelPaginator } from "stream-chat";
import type { Channel, ChannelFilters, SortParamRequest } from "stream-chat";
import { ChannelList, useChatContext } from "stream-chat-react-native";

const sort: SortParamRequest[] = [{ direction: -1, field: "last_message_at" }];

type Props = { onSelect: (channel: Channel) => void; userId: string };

export const MyChannelList = ({ onSelect, userId }: Props) => {
  const { client } = useChatContext();

  const paginator = useMemo(() => {
    const filters: ChannelFilters = {
      members: { $in: [userId] },
      type: "messaging",
    };
    return new ChannelPaginator({
      client,
      filters,
      id: "channels:default",
      paginatorOptions: { pageSize: 20 },
      requestOptions: { presence: true, state: true, watch: true },
      sort,
    });
  }, [client, userId]);

  useEffect(() => {
    client.channelManager.insertPaginator({ paginator });
    return () => {
      client.channelManager.removePaginator(paginator);
      paginator.dispose();
    };
  }, [client, paginator]);

  return <ChannelList onSelect={onSelect} paginator={paginator} />;
};
```

What to know about the paginator:

- Register it. `client.channelManager` routes events into the lists it holds. A paginator that's only passed as a prop shows its first page, and then never adds, moves or removes a channel.
- The list never removes or disposes the paginator. Your app does, when nothing shows it any more, for example when the user logs out. `client.disconnectUser()` empties the registered paginators but keeps them registered.
- The list loads a first page only when the paginator has none, which happens on the first mount, after `client.disconnectUser()` empties it, and when you pass a different paginator. A list that mounts again with the same paginator shows its channels without a query. To keep the channels while the list's screen is unmounted, create the paginator above the navigator, with the user.
- Give each paginator a unique `id`. The manager keys its lists by it.
- The query options split in two. Options of the request, such as `presence`, `state`, `watch` and `message_limit`, go in `requestOptions`. The page size goes in `paginatorOptions.pageSize`. `offset` and `limit` aren't accepted, because the paginator owns them.

Where the removed props went:

| v9 prop on `ChannelList`                                                                                                                                                                                              | v10                                                                                                                                                   |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `filters`, `sort`, `options`                                                                                                                                                                                          | the `filters`, `sort` and `requestOptions` of the `ChannelPaginator`. `options.limit` is `paginatorOptions.pageSize`                                  |
| `lockChannelOrder`                                                                                                                                                                                                    | `paginatorOptions.lockItemOrder`                                                                                                                      |
| `queryChannelsOverride`                                                                                                                                                                                               | `paginatorOptions.doRequest`. See below                                                                                                               |
| `onNewMessage`, `onNewMessageNotification`, `onAddedToChannel`, `onRemovedFromChannel`, `onChannelUpdated`, `onChannelDeleted`, `onChannelHidden`, `onChannelVisible`, `onChannelTruncated`, `onChannelMemberUpdated` | the paginator's `filters` and `sort` decide membership and order. For anything else, `client.channelManager.addEventHandler(...)` or `client.on(...)` |

A prop change no longer queries again. To change the query, set `staticFilters` or `sort` on the paginator and call `paginator.reload()`, or pass a new paginator with a new `id`. `useChannelUpdated` is removed, because the channel manager handles `channel.updated`.

`queryChannelsOverride` returned `Channel[]`. Its replacement, `doRequest`, receives the request the paginator would send and resolves to `{ items }`. Call `client.queryChannelsAndHydrate()` inside it, so the client stores the channels:

```ts
import { ChannelPaginator } from "stream-chat";
import type { StreamChat } from "stream-chat";

export const createChannelPaginator = (client: StreamChat) =>
  new ChannelPaginator({
    client,
    id: "channels:custom-query",
    paginatorOptions: {
      doRequest: async ({ stateOptions, ...request }) => ({
        items: await client.queryChannelsAndHydrate(request, stateOptions),
      }),
    },
  });
```

### The list follows its filters and sort

The list is a projection of its paginator's `filters` and `sort`, which every query and every event re-applies:

- A channel moves by its sort key when an event arrives. Nothing moves a channel to the top by default, so a pinned-first sort keeps pinned channels first. To move a channel to the top for a while, call `paginator.boost(cid)`.
- A channel that stops matching the filters after an event leaves the list.
- An empty `sort`, and a paginator created without one, mean the backend's default order, by the latest message.
- After a reconnect, the client reloads every loaded page of each registered list, up to 150 channels, and removes the channels no page returned. `ChannelList` has no reconnect listener of its own. Pulling down on the list still reloads only the first page.
- The channel preview's typing indicator ignores typing inside threads.

### ChannelsContext holds the paginator

`useChannelsContext()` no longer carries the list's state. It holds the list's configuration, the `paginator`, and the `loadNextPage`, `refreshList` and `reloadList` functions:

| v9 `useChannelsContext()`                   | v10                                                                  |
| ------------------------------------------- | -------------------------------------------------------------------- |
| `channels`, `hasNextPage`, `error`          | `items`, `hasMoreTail` and `lastQueryError` in `paginator.state`     |
| `loadingChannels`, `channelListInitialized` | `items === undefined` in `paginator.state`                           |
| `loadingNextPage`, `refreshing`             | props of `ChannelListView`                                           |
| `forceUpdate`                               | removed. It was always `0`                                           |
| `loadNextPage(queryType?, retryCount?)`     | `loadNextPage()`, which does nothing until the first page has loaded |

```tsx
import { Text } from "react-native";
import type { ChannelPaginatorState } from "stream-chat";
import { useChannelsContext, useStateStore } from "stream-chat-react-native";

const channelsSelector = (state: ChannelPaginatorState) => ({
  channels: state.items,
});

export const LoadedChannelCount = () => {
  const { paginator } = useChannelsContext();
  const { channels } = useStateStore(paginator.state, channelsSelector);
  return <Text>{channels?.length ?? 0} channels</Text>;
};
```

`ChannelListView` takes the list's state as required props: `channels`, `channelListInitialized`, `hasNextPage`, `loadingChannels`, `loadingNextPage` and `refreshing`. Its `loadNextPage` is `() => Promise<void>`, and its `forceUpdate` is removed. `ChannelPreview` lost `forceUpdate` too, and `useChannelPreviewData(channel, client)` lost its third argument. Its `lastMessage` is a `LocalMessage` or `undefined`.

A channel row re-renders only when its own channel changes. A custom `ChannelPreview` that reads `channel.data` or `channel.state` without subscribing no longer picks up changes through other rows' updates. Subscribe with `useStateStore`, or with hooks such as `useChannelName(channel)`.

## Threads

### Thread is a sibling of Channel

A thread is a `Thread` object from `stream-chat`. `Thread` takes it as a required `thread` prop, renders next to `Channel` instead of inside it, and renders the children you pass. Opening and closing a thread is up to your app, typically a screen of its own. The v10 behavior is on [Thread UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/thread-ui/).

In v9:

```tsx
<Channel channel={channel} thread={thread} threadList>
  <Thread />
</Channel>
```

In v10:

```tsx
import type { Thread as StreamThread } from "stream-chat";
import { MessageComposer, MessageList, Thread } from "stream-chat-react-native";

type Props = { onClose: () => void; thread: StreamThread };

export const ThreadScreen = ({ onClose, thread }: Props) => (
  <Thread onThreadDismount={onClose} thread={thread}>
    <MessageList />
    <MessageComposer />
  </Thread>
);
```

`MessageList` inside a `Thread` renders the thread's replies, with the parent message above them. `Thread` takes the same configuration props as `Channel`, such as `keyboardVerticalOffset`, `messageActions` and `audioRecordingEnabled`, so pass it what you pass `Channel`. `allowThreadMessagesInChannel` moved from `Channel` to `Thread`.

`onThreadSelect` on `MessageList`, `MessageFlashList` and `ThreadList` receives the `Thread`:

```diff
- <MessageList onThreadSelect={(parentMessage) => setThread(parentMessage)} />
+ <MessageList onThreadSelect={(thread) => setThread(thread)} />

- <ThreadList onThreadSelect={({ thread, threadInstance }, channel) => open(threadInstance, channel)} />
+ <ThreadList onThreadSelect={(thread) => open(thread, thread.channel)} />
```

Where you have only the parent message, for example from a push notification or a deep link, get the thread with `client.threads.ensure({ channel, parentMessage })`. It returns the `Thread` the client holds for that message, or creates one. `client.threads.get(id)` returns a thread the client holds. The `ThreadType` type is removed.

Where the removed props went:

| v9 prop on `Thread`                                                                   | v10                                                  |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `thread`, the parent message                                                          | `thread`, the `Thread` object, now required          |
| `threadInstance`                                                                      | `thread`                                             |
| `additionalMessageListProps`, `additionalMessageFlashListProps`, `shouldUseFlashList` | render the list you want as a child, with its props  |
| `additionalMessageComposerProps`, `autoFocus`, `disabled`                             | render `MessageComposer` as a child, with its props  |
| `closeThreadOnDismount`                                                               | `onThreadDismount`                                   |
| `parentMessagePreventPress`                                                           | a prop of `ThreadFooterComponent`, `true` by default |
| `client`, `closeThread`, `loadMoreThread`, `reloadThread`                             | removed                                              |

The `ThreadMessageComposer` and `MessageList` slots are removed from `WithComponents`. Render the composer and the list you want as children of `Thread`. They're the only slots v10 removed.

### ThreadContext holds the Thread

`useThreadContext()` returns `{ thread, allowThreadMessagesInChannel }`. `thread` used to be the parent message. It's now the `Thread`, or `undefined` outside a `Thread`. `useThreadContext()` no longer throws outside a `Thread`, and only `Thread` provides it, so `!useThreadContext().thread` tells a component that it isn't rendering a thread.

`thread.id` is the parent message's id, as before. Message fields such as `thread.text` are in `thread.state`, as `parentMessage`. The same applies to the `thread` of `useMessageComposerContext()`.

| v9 `useThreadContext()`                                         | v10                                                                                  |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `thread`, the parent message                                    | `parentMessage` in `thread.state`                                                    |
| `threadMessages`                                                | `items` in `thread.messagePaginator.state`                                           |
| `loadMoreThread()`, `loadMoreRecentThread()`                    | `thread.messagePaginator.toTail()` and `thread.messagePaginator.toHead()`            |
| `threadHasMore`, `threadLoadingMore`, `threadLoadingMoreRecent` | `hasMoreTail` and `isLoading` in `thread.messagePaginator.state`                     |
| `reloadThread()`                                                | `Thread.reload()`                                                                    |
| `threadInstance`                                                | `thread`                                                                             |
| `openThread`, `closeThread`, `setThreadLoadingMore`             | removed. Your navigation opens and closes threads                                    |
| `parentMessagePreventPress`                                     | a prop of `ThreadFooterComponent`                                                    |
| `onAlsoSentToChannelHeaderPress`                                | moved to `useMessagesContext()`. Its payload has `thread` instead of `parentMessage` |

`threadList`, the flag that told a component it rendered a thread, is removed from `useChannelContext()`, `useMessageContext()` and the props of `Message`, `MessageContent`, `MessageReplies`, `MessageList`, `MessageFlashList` and `MessageComposer`. Read `useThreadContext().thread` instead. `MessageItemView`, `SentToChannelHeader` and `ShowThreadMessageInChannelButton` take a `thread` prop instead. `MessageReplies` no longer hides itself inside a thread. `MessageItemView` hides it there, so a custom layout that renders `MessageReplies` directly checks `useThreadContext().thread` itself.

`ThreadFooterComponent` reads the thread from the context, and its only prop is `parentMessagePreventPress`.

The thread state in `stream-chat` moved too. `thread.state.replies`, `Thread.loadNextPage()` and `Thread.loadPrevPage()` are `thread.messagePaginator`. The thread list's `client.threads.state` and `client.threads.loadNextPage()` are `client.threads.paginator`, and `threadsById` is `client.threads.get(id)`.

### What Thread does

- `Thread` doesn't need a `Channel` above it. It uses `thread.channel`, and watches that channel once if nothing has queried it.
- Opening a thread, or pressing the scroll-to-bottom button in one, no longer marks the channel read. The thread marks itself read while it's open, and never marks a thread with no replies.
- When the first page of replies fails to load, `Thread` renders `LoadingErrorIndicator`, with a retry, instead of the list and the composer.

## Message lists

`MessageList` and `MessageFlashList` read their own paginator, the channel's or, inside a `Thread`, the thread's. The props that `Channel` used to fill in are removed:

- `loadMore`, `loadMoreRecent`, `hasMore`, `loadingMore`, `loadingMoreRecent`, `threadHasMore`, `loadMoreThread`, `loadMoreRecentThread`, `loadChannelAroundMessage`, `loading`, `reloadChannel` and `threadInstance`.
- `targetedMessage`, `setTargetedMessage`, and `highlightedMessageId` on `MessageFlashList`.
- `channelUnreadStateStore` and `setChannelUnreadState`.
- `maximumMessageLimit`, `scrollToFirstUnreadThreshold` and `threadList`.
- `allowSendBeforeAttachmentsUpload`, which is `pendingUploadsEnabled` now.
- `shouldShowUnreadUnderlay`, which was never read. Set it on `Channel`.
- `FlatList` and `inverted` on `MessageFlashList`, which were never read.

`thread` is still a prop, typed `Thread | undefined` instead of the parent message. Inside a `Thread` you can leave it out. `onThreadSelect` receives a `Thread`. To change how a page is fetched, configure the paginator through `client.config`. `useMessageList()` takes `{ paginator }` instead of `{ threadList }`.

The component at the older end of the list, `FooterComponent` on `MessageList` and `HeaderComponent` on `MessageFlashList`, receives `loadingMore` as a prop. It used to read it from `PaginatedMessageListContext`:

```tsx
import { ActivityIndicator } from "react-native";
import { MessageList } from "stream-chat-react-native";

const OlderMessagesSpinner = ({ loadingMore }: { loadingMore?: boolean }) =>
  loadingMore ? <ActivityIndicator /> : null;

export const MyMessageList = () => (
  <MessageList FooterComponent={OlderMessagesSpinner} />
);
```

The component at the newer end gets no props, and nothing gives it a loading flag for newer messages. Read `isLoading` from the paginator there. `InlineLoadingMoreIndicator` and `InlineLoadingMoreRecentIndicator` take `loadingMore` and `loadingMoreRecent` as props, and render nothing without them.

### Jumping and highlighting

`useTargetedMessage` is removed. Jump with the paginator, which loads the message if it isn't loaded and highlights it for `focusSignalTtlMs`. Read whether a row is the highlighted one with `useIsTargetedMessage(messageId)`:

```tsx
import { Button } from "react-native";
import {
  DEFAULT_HIGHLIGHT_DURATION,
  useMessagePaginator,
} from "stream-chat-react-native";

export const JumpToMessageButton = ({ messageId }: { messageId: string }) => {
  const paginator = useMessagePaginator();

  return (
    <Button
      onPress={() =>
        paginator.jumpToMessage(messageId, {
          focusReason: "jump-to-message",
          focusSignalTtlMs: DEFAULT_HIGHLIGHT_DURATION,
        })
      }
      title="Show message"
    />
  );
};
```

### Read state and unread counts

The SDK marks a channel read on its own, when the user is at the newest message. Most apps can delete their own `Channel.markRead()` calls. Where you still need one:

- `useMarkRead(channel)` returns a `markRead()` function, the replacement for the `markRead` that `useChannelContext()` returned.
- `Channel.markReadViaReporter()` marks the channel read outside React. It also calls your `markReadRequest` handler.
- `Channel.markRead()` is the direct request in `stream-chat` 10. It skips your handler, and throws when read events are turned off.

The unread state moved to `channel.messagePaginator.unreadStateSnapshot`, a store of `UnreadSnapshotState`. The `ChannelUnreadState` type is removed. `ScrollToBottomButton` lost `unreadCount`, and `UnreadMessagesNotification` lost `channelUnreadStateStore` and `unreadCount`. Both count unread messages themselves. `UnreadMessagesNotification` marks the channel read through a new `markRead` prop, so a custom component that wraps it must pass `markRead` on, or dismissing it stops marking the channel read.

`TypingIndicatorContainer` takes `threadId` instead of `thread`. See [Read state, unread and receipts](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/read-state-unread-and-receipts/).

### Pruning the list

`maximumMessageLimit` is now `maxLoadedItems` in the message paginator's configuration. It caps the number of loaded messages, as before:

```ts
import { StreamChat } from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

// Every channel's message list. Use `thread` for thread replies, or `messagePaginator` for both.
client.config.set({ channel: { messagePaginator: { maxLoadedItems: 200 } } });
```

## Messages

### Message operations

`sendReaction`, `deleteReaction`, `deleteMessage`, `removeMessage`, `retrySendMessage` and `updateMessage` are removed from `useMessagesContext()`. `useMessageOperations()` returns them, with the same names:

```tsx
import { Button } from "react-native";
import type { LocalMessage } from "stream-chat";
import { useMessageOperations } from "stream-chat-react-native";

export const MessageTools = ({ message }: { message: LocalMessage }) => {
  const { deleteMessage, sendReaction } = useMessageOperations();

  return (
    <>
      <Button onPress={() => sendReaction("like", message.id)} title="Like" />
      <Button
        onPress={() => deleteMessage(message, { hard: true })}
        title="Delete"
      />
    </>
  );
};
```

- `deleteMessage`'s options are `{ hard, delete_for_me }` instead of `{ hardDelete, deleteForMe }`.
- `updateMessage` takes one argument, the message.
- A custom `Message` component still receives the six functions as props.
- Outside React, `channel.messageOperations` and `thread.messageOperations` run the same optimistic updates. `stream-chat` 10 has no `*WithLocalUpdate` methods, and `Channel.sendMessage()` and `Channel.sendReaction()` are plain requests that update nothing locally.

The behavior changed with the move. See [Message operations](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/message-operations/).

- Reactions are always optimistic. v9 applied them optimistically only with offline support. A reaction that fails for good is rolled back. Remove any optimistic reaction logic of your own, or it applies twice.
- Edits and deletes show at once. A delete that fails for good is reverted. A hard delete removes the message from the list.
- Deleting a message whose edit failed deletes it on the server. v9 removed every failed message locally only.
- A failed send rejects the promise, where v9 swallowed the error. Code of yours that sends must handle the rejection. The built-in composer shows a notification.

### Message actions

`useMessageActions()` and `useMessageActionHandlers()` take no arguments. They read the contexts of the message they're called in, so call them inside a message, such as in a custom `Message` or `MessageActionList`. `MessageActionsHookProps` is removed.

```diff
- const actions = useMessageActions({ channel, client, message, t /* ... */ });
- const handlers = useMessageActionHandlers({ channel, client, message /* ... */ });
+ const actions = useMessageActions();
+ const handlers = useMessageActionHandlers();
```

`useMessageActionHandlers()` returns the message's `MessageActionHandlerCallbacks`, which `useMessageContext()` also holds as `messageActionHandlers`.

`Message` no longer declares `handleBan`, `handleBlockUser`, `handleCopy`, `handleDelete`, `handleDeleteForMe`, `handleEdit`, `handleFlag`, `handleMarkUnread`, `handleMute`, `handlePinMessage`, `handleQuotedReply`, `handleRetry`, `messageActions` and `supportedReactions`. Passed to a `Message`, they never reached the action menu. Set them on `Channel` or `Thread`. To change the actions of one message, give `messageActions` a function. It now runs when a message's menu opens, not on every render of every row. The `updateMessage` it receives takes one argument. See [Actions](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/actions/).

`MessageActionList` lost `dismissOverlay`, which it never read. A custom list that needs it reads `useMessageContext().dismissOverlay`.

### Read and delivery receipts

`readBy` and `deliveredToCount` are removed from `useMessageContext()` and from the props of `Message` and `MessageStatus`. `MessageStatus` takes the booleans `read` and `delivered`, and works them out itself when you pass neither:

```tsx
import { Text } from "react-native";
import {
  useIsMessageDeliveredToOthers,
  useIsMessageReadByOthers,
  useMessageContext,
} from "stream-chat-react-native";

export const ReceiptText = () => {
  const { message } = useMessageContext();
  const isRead = useIsMessageReadByOthers({ message });
  const isDelivered = useIsMessageDeliveredToOthers({ message });

  return <Text>{isRead ? "Read" : isDelivered ? "Delivered" : "Sent"}</Text>;
};
```

For the counts, use the length of `useMessageReadData({ message })` or `useMessageDeliveredData({ message })`. All four hooks need a channel above them.

### Other message changes

- `isMessageAIGenerated` is a prop of `Chat` only, and `useChatContext()` holds it. It's removed from `Channel`, `useMessagesContext()`, `useMessageContext()`, and the props of `Message` and `MessageContent`. The AI flag is `message.custom?.ai_generated`:

  ```diff
  - <Channel channel={channel} isMessageAIGenerated={(message) => !!message.ai_generated}>
  + <Chat client={client} isMessageAIGenerated={(message) => !!message.custom?.ai_generated}>
  ```

- `Message` lost `members`, `openThread` and `chatContext`, and `useMessageContext()` lost `members`. `onThreadSelect` on `Message` and in `useMessageContext()` receives a `Thread`. `MessageFooter` and `MessageItemView` lost `members`. `MessageFooter` shows the sender's name on other users' messages whenever the channel's `memberCount` isn't exactly two, where v9 counted the loaded members and needed more than two.
- `useMessagesContext()` lost `targetedMessage`, `initialScrollToFirstUnreadMessage` and `quotedMessage`. Read the quoted message from the composer's state.
- `useMessageContext()` has a new `isVeryLastMessage`, which marks the newest message of the list it renders in.
- The `attachment` of `Giphy`, `GiphyImage`, `URLPreview` and `URLPreviewCompact` is `stream-chat`'s `Attachment`, with your fields under `custom`. `giphyVersion` is typed `keyof Images`.
- `AIStates` is an object of literal values, so `[AIStates.Thinking, AIStates.Generating].includes(aiState)` no longer compiles. Compare with `===`.
- In `ImageGalleryStateStore`'s `assets`, `created_at` is a `Date` or `undefined`, `channelId` a `string`, and `user` a `UserResponse`.

## Composer

### Sending goes through the message composer

`useMessageInputContext().sendMessage()` submits the channel's message composer. It sends a new message, or saves the edit while a message is being edited. `editMessage` is removed from the context. A custom `MessageInputProvider` value can no longer carry its own `sendMessage` or `editMessage`. Register `sendMessageRequest` and `updateMessageRequest` handlers, as [Channel props that moved](#channel-props-that-moved) shows.

The SDK used to rewrite `/mute @Jane Doe` and the other moderation commands into user ids. It no longer does, because the server reads the target from the message's mentioned users, and `patchMessageTextCommand` is removed. To change a message before it's sent, add a composition middleware in a setup function, `client.config.setSetupFunction("messageComposer", fn)`.

A failed send or edit shows as a `client.notifications` notification of the type `messageSendFailed` or `messageUpdateFailed`, from `CORE_NOTIFICATION_TYPE`.

### Pending uploads

`allowSendBeforeAttachmentsUpload` is now `stream-chat`'s composer setting `messageComposer.attachments.pendingUploadsEnabled`. It's removed from `Channel` and `useMessageInputContext()`. Read it with `usePendingUploadsEnabled()`. `MessageList` and `MessageFlashList` take it as a `pendingUploadsEnabled` prop. `useMessageInputContext()` lost `doFileUploadRequest`, which is `messageComposer.attachments.doUploadRequest` now.

With `enableOfflineSupport`, `Chat` turns it on when nothing else has set it, as v9's prop default did. It does so once, and doesn't turn it off again when offline support goes off. To set it for every channel, register it before the client reaches `Chat`. To set it for some channels, use a setup function:

```ts
import { StreamChat } from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

// Every channel.
client.config.set({
  messageComposer: { attachments: { pendingUploadsEnabled: false } },
});

// Livestream channels only. One setup function per key: add this to yours if you have one.
client.config.setSetupFunction("messageComposer", ({ composer }) => {
  if (composer.channel.type !== "livestream") return;
  composer.updateConfig({ attachments: { pendingUploadsEnabled: true } });
});
```

See [Sending with pending uploads](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/sending-with-pending-uploads/).

### Attachments that are still uploading

v9 wrote a local file's URI into `image_url` or `asset_url` while the file uploaded, and kept the upload's id and file in `custom.localId` and `custom.originalFile`. In v10 such an attachment has no URL until its upload finishes. It carries `localMetadata` instead, with `id`, `file`, `previewUri` and `uploadState`. A custom component that renders attachments uses the helpers:

```ts
import { getAttachmentPreviewUrl, isLocalUploadAttachment } from "stream-chat";
import type { Attachment } from "stream-chat";
import { getPlayableVideoUrl } from "stream-chat-react-native";

export const describeAttachment = (attachment: Attachment) => ({
  // The upload's id while it uploads.
  localId: isLocalUploadAttachment(attachment)
    ? attachment.localMetadata.id
    : undefined,
  // The uploaded URL, or the local preview while it uploads. For images, files and audio.
  previewUrl: getAttachmentPreviewUrl(
    attachment,
    attachment.asset_url,
    attachment.image_url,
  ),
  // What a video player plays. For videos, the preview is a thumbnail image.
  videoUrl: getPlayableVideoUrl(attachment),
});
```

`createAttachmentsCompositionMiddleware`, `createDraftAttachmentsCompositionMiddleware` and `localAttachmentToAttachment` are removed. The composer installs `stream-chat`'s `createAttachmentsCompositionMiddleware` by default. A draft keeps only the attachments that finished uploading. `DefaultAttachmentData` is empty, and `AudioUpload` and `LocalAudioAttachmentType` lost `localId`, `originalFile`, `latitude` and `longitude`. In `AudioAttachmentType`, `AudioUpload` and `LocalAudioAttachmentType`, `waveform_data`, `mime_type`, `file_size` and `duration` moved under `custom`.

`isLocalUrl()` checks the URL's scheme, and returns `false` for `undefined`. `makeImageCompatibleUrl()` returns `undefined` for `undefined`. Both threw on `undefined` in v9.

### Other composer changes

- The composer loads link previews by default where the channel type enriches URLs. To turn them off, set `client.config.set({ messageComposer: { linkPreviews: { enabled: false } } })`.
- The poll button follows `messageComposer.config.polls.enabled`, which is `true` until the server's settings arrive. Hide it with `client.config.set({ messageComposer: { polls: { enabled: false } } })`.
- The input's `maxLength` follows `messageComposer.config.text.maxLengthOnSend`, so a smaller value you register caps the input too.
- `AutoCompleteInput`'s and `InputView`'s `t` is a `StreamTFunction`.
- In `CommandSuggestionItem`, `args`, `description`, `name` and `set` are required strings, and `created_at` and `updated_at` are nanosecond timestamps.
- A composer's `contextType` is `'channel'`, `'thread'` or `'message'`. `'legacy_thread'` is removed, with `LocalMessageWithLegacyThreadId` and its `legacyThreadId`. A thread's composer is always its `Thread`'s `messageComposer`, with the `contextType` `'thread'`, so a setup function or middleware that checked for `'legacy_thread'` checks for `'thread'`.

## Offline storage

The offline database's version, `SqliteClient.dbVersion`, is 18 in v10, up from 16. The first launch after the upgrade deletes and rebuilds the database. Channels and messages load again from the server. Actions the user queued while offline in v9, such as unsent messages, are lost with it. Date columns that hold server data store nanosecond integers instead of ISO strings. See [Offline support](https://getstream.io/chat/docs/sdk/react-native/v10/state/offline-support/offline-support/).

The rest affects only a custom `AbstractOfflineDB`, a subclass of `OfflineDB`, or code that calls `OfflineStoreApis` or reads the database:

- `getChannelsForQuery` takes `{ userId, options }`, with the filters in `options.filter_conditions` and the sort in `options.sort`. It returns `{ channels, predefinedFilter }` instead of an array. `upsertCidsForQuery` takes a `predefinedFilter` to store with the query.
- `OfflineDB` lost `handleOfflineFailedUpdateMessagePendingTask` and `mergeFailedMessageUpdateIntoPendingSendMessage`. Move overrides of them to `handleUpdateMessagePendingTask`. `hardDeleteMessages`, `upsertMessageBehindChannelGuard` and `queriesBehindChannelGuard` are new. Its event handlers and upserts take `stream-chat` 10 types.
- `client.offlineDb.syncManager.syncStatus` is `isSynced`.
- `OfflineStoreApis` functions take `userId` instead of `currentUserId`. `deleteReactionsForMessage` is removed. `getChannelsForFilterSort` returns `{ channels, predefinedFilter }`, sorts are `SortParamRequest[]`, and `getAppSettings` returns a `GetApplicationResponse`. `deleteMessagesForChannel` takes `truncated_at` as a nanosecond timestamp.
- `insertReaction` and `updateReaction` skip a reaction whose message isn't stored, where v9's write failed.
- `channelExists` answers correctly. In v9 it was always `true`.
- `SqliteClient.logger` is a `stream-chat` `Sink`, called as before.
- A timestamp you read from the tables yourself is already in nanoseconds. Brand it with `asTimestampNS()`, not `msToNs()`.

## Other changes

These come up less often:

- `getLastReceivedMessage(messages)` is removed. It did what `messages.find((m) => m.status !== "failed")` does. `reduceMessagesToString`, `findInMessagesById` and `findInMessagesByDate` are removed, and replaced by `channel.messagePaginator.getItem(id)` and `channel.messagePaginator.jumpToMessage(id)`.
- `useQueryReminders()` returns `ReminderResponseData[]`, with nanosecond timestamps.
- `usePollState`, `usePollStateStore`, `usePollAnswersPagination` and `usePollOptionVotesPagination` use the renamed poll types. `addComment()` resolves to a `PollVoteResponse`, and `endVote()` to a `PollResponse` or `undefined`. In the poll components, `PollOption` is `PollOptionResponseData`, `PollAnswer` and `PollVote` are `PollVoteResponseData`, and `CreatePollOption.error` is a `PollComposerValidationError`.
- `useMessagePaginator()` returns a `MessagePaginator`, never `undefined`.
- `useCreateMessagesContext`, `useCreateThreadContext`, `useCreateMessageInputContext` and `useCreateMessageContext` take the new, smaller context shapes.
- `LoadingErrorIndicator`'s `error` is an `Error`. `error={true}` no longer compiles.
- `useInAppNotificationsState`, deprecated since v9, still reads a store of its own that no SDK component shows. [Hooks left out of this reference](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/state-and-entity-hooks/#hooks-left-out-of-this-reference) maps it to the notification hooks.
- `ProgressControl` lost `width`, which it never read.
- `defaultTranslatorFunction` returns the English copy instead of the key.
- `LastMessageType` is still `LocalMessage | MessageResponse`, but both types changed with `stream-chat` 10.
- Imports from `stream-chat-react-native-core/src/...` or `lib/...` of modules that were never exported from the package root may fail. `ChannelsStateProvider`, `useChannelsStateContext`, `useChannelState`, `ActiveChannelsRefContext`, `useChannelDataState`, `ChannelUnreadStateStore`, `MessagePreviousAndNextMessageStore`, the channel list's pin and archive sort helpers, `useCreateMessageComposer` and the `utils/i18n/*` modules are deleted. The functions of `utils/i18n/*` are exported from the package root.
- `filterTypingUsers`, a deep import, takes `threadId` instead of `thread`.
- No other `WithComponents` slot or icon was added, removed or renamed, and the theme's shape is the same. The props and contexts that the slots receive did change, as the sections above describe, so check each custom component you register against them.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/release-guides/upgrade-to-v10/](https://getstream.io/chat/docs/sdk/react-native/v10/release-guides/upgrade-to-v10/).