# Composer

> **Info:** This is documentation for **Stream Chat React Native SDK v9**, which is no longer actively maintained. For up-to-date documentation, see the [latest version (v10)](https://getstream.io/chat/docs/sdk/react-native/).

`ComposerView` is where the user writes to your agent. It's a text field that grows with its lines, photos from the library and the camera with previews, dictation through the platform's speech recognition, a sheet of chat options such as "Think harder", and a send button that turns into a stop button while the agent answers. It rides the keyboard up, and hands you what the user wrote when they send it:

```tsx
import { View } from "react-native";
import { AIConversation } from "stream-chat-react-native/ai-components/conversation";
import {
  type AIConversationController,
  ComposerView,
} from "stream-chat-react-native/ai-components";

export const Assistant = ({
  conversation,
}: {
  conversation: AIConversationController;
}) => (
  <View style={{ flex: 1 }}>
    <AIConversation conversation={conversation} />
    <ComposerView conversation={conversation} />
  </View>
);
```

>
> **Info:** `ComposerView` is available from version 9.10.0 of `stream-chat-react-native` and `stream-chat-expo`, in the `stream-chat-react-native/ai-components` and `stream-chat-expo/ai-components` entry points, and its chat options, `header` and `trailing` from 9.13.0, and `conversation` from 9.14.0. See the [Quickstart](https://getstream.io/chat/docs/sdk/react-native/v9/ai-components/#step-2-install-the-sdks) to install it. Render it inside `StreamTheme`, like the other AI components.
>

## Send what the user writes

Give the composer either a `conversation`, an [`AIConversationController`](https://getstream.io/chat/docs/sdk/react-native/v9/ai-components/conversation-state/#a-conversation-controller), or an `onSendMessage`. With a controller, the composer sends to it, shows the stop button while the agent answers, and shows an "Editing message" chip while the user edits a message. It takes the place of `onSendMessage`, `isGenerating`, `stopGenerating` and `state`.

| Name                 | Type                                                                                                                                                | Required | Description                                                                                                                                                                                                                    |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `conversation`       | `AIConversationController`                                                                                                                          | no       | The conversation the user writes in. Required without `onSendMessage`.                                                                                                                                                         |
| `onSendMessage`      | `(opts: { text: string; attachments?: MediaPickerState['assets']; chatOption?: AIChatOption; custom?: Record<string, unknown>; }) => Promise<void>` | no       | Called when the send button is pressed. Required without `conversation`. It gets the `text`, the `attachments` the user picked, the picked [chat option](#chat-options) and any `custom` data in the composer's state.         |
| `chatOptions`        | `AIChatOption[]`                                                                                                                                    | no       | Ways to ask, such as "Think harder", listed in the sheet. See [Chat options](#chat-options).                                                                                                                                   |
| `header`             | `ReactNode`                                                                                                                                         | no       | Shown above the composer, moving with it, such as [`AISuggestions`](https://getstream.io/chat/docs/sdk/react-native/v9/ai-components/conversation-view/#suggestions-to-start-with) for an empty conversation, or an error with a way to try again. |
| `trailing`           | `ReactNode`                                                                                                                                         | no       | Shown after the message box, on the same line, such as a button into voice mode.                                                                                                                                               |
| `keyboardOffset`     | `number`                                                                                                                                            | no       | How far above the bottom of the screen the composer sits, such as above a tab bar. The composer rides the keyboard up by as much as the keyboard rises past that. Defaults to the bottom safe area.                            |
| `bottomSheetOptions` | `BottomSheetOption[]`                                                                                                                               | no       | Extra options in the sheet. See [Add your own options](#add-your-own-options).                                                                                                                                                 |
| `bottomSheetInsets`  | `{ top: number; bottom: number; left: number; right: number }`                                                                                      | no       | Insets the sheet extends beyond, such as the safe area.                                                                                                                                                                        |
| `SpeechToTextButton` | `ComponentType \| null`                                                                                                                             | no       | The button that dictates into the message box while it is empty. Defaults to [`SpeechToTextButton`](#dictation). `null` shows none.                                                                                            |
| `isGenerating`       | `boolean`                                                                                                                                           | no       | Whether the agent is answering. The composer shows the stop button in place of the send button while it is. Not needed with `conversation`.                                                                                    |
| `stopGenerating`     | `() => Promise<void>`                                                                                                                               | no       | Called when the stop button is pressed. Not needed with `conversation`.                                                                                                                                                        |
| `mediaPickerService` | `AbstractMediaPickerService`                                                                                                                        | no       | A `MediaPickerService` of your own, for finer control over the picked photos. `new MediaPickerService()` finds the library you installed.                                                                                      |
| `state`              | `StateStore<ComposerState>`                                                                                                                         | no       | A store of the composer's state of your own, for finer control over it. `createNewComposerStore()` makes one. See [Control it from your code](#control-it-from-your-code).                                                     |

Without a controller, send through your own function and tell the composer when the agent is answering:

```tsx
<ComposerView
  isGenerating={isAnswering}
  onSendMessage={async ({ attachments, text }) => ask(text, attachments)}
  stopGenerating={stop}
/>
```

The send button shows once the text field has text. Until then, the microphone button takes its place. With Stream Chat, the composer can send through a channel's own composer, as [SDK integration](https://getstream.io/chat/docs/sdk/react-native/v9/ai-components/sdk-integration/#composerview) shows.

## The keyboard

`ComposerView` rides the keyboard with [`react-native-keyboard-controller`](https://kirillzyusko.github.io/react-native-keyboard-controller/), when it's installed and its `KeyboardProvider` is above the composer. Without the library, the composer doesn't move with the keyboard, so keep it in view yourself, for example with a `KeyboardAvoidingView`.

`AIConversation` handles the keyboard the same way, and both take a `keyboardOffset`, so give both the same value when you change it. [The keyboard](https://getstream.io/chat/docs/sdk/react-native/v9/ai-components/conversation-view/#the-keyboard) describes how they work together.

## Attach photos

The composer's sheet opens from the attach button before the text field. It lists the camera and the photo library, then your own options. Photos need one of two libraries, which the composer finds by itself:

- **`react-native-image-picker`**, for React Native CLI apps:

  ```bash
  yarn add react-native-image-picker
  ```

  To take photos, add the camera permission to `Info.plist` and `AndroidManifest.xml`:

  ```txt
  <key>NSCameraUsageDescription</key>
  <string>$(PRODUCT_NAME) would like to use your camera to share an image in a message.</string>
  ```

  ```xml
  <uses-permission android:name="android.permission.CAMERA" />
  ```

- **`expo-image-picker`**, for Expo apps:

  ```bash
  npx expo install expo-image-picker
  ```

  Add its config plugin with the `photosPermission` and `cameraPermission` texts, as [Expo's documentation](https://docs.expo.dev/versions/latest/sdk/imagepicker/#example-appjson-with-config-plugin) describes.

Without either library, the sheet has no camera or photos. Up to three photos can be picked at once. The previews show above the text field, each with a button that removes it, and the picked photos reach `onSendMessage` as `attachments`, or the controller's transport as `request.attachments`: files on the device, for you to upload or to send to the agent. The [Personal assistant](https://getstream.io/chat/docs/sdk/react-native/v9/ai-components/use-cases/personal-assistant/#send-photos) use case sends them to the agent as data URIs.

## Chat options

`chatOptions` adds ways to ask, such as **Think harder**, to the composer's sheet, under the camera and photos. The one picked shows as a chip above the input, which the user can remove, and it's passed to `onSendMessage` as `chatOption` with every message until they do. With a controller, it reaches the transport as the request's `chatOption`:

```tsx
import {
  type AIChatOption,
  ComposerView,
} from "stream-chat-react-native/ai-components";

const thinkHarder: AIChatOption = {
  id: "think_harder",
  title: "Think harder",
  description: "Takes longer, for harder questions",
  shortTitle: "Think",
};

<ComposerView
  chatOptions={[thinkHarder]}
  onSendMessage={async ({ chatOption, text }) =>
    ask(text, {
      reasoningEffort: chatOption?.id === thinkHarder.id ? "high" : undefined,
    })
  }
/>;
```

An `AIChatOption` has an `id`, a `title`, and an optional `description`, `icon`, `shortTitle` for the chip, and `customData` of your own. Picking the option that's already picked removes it. `ChatOptionsList` and `SelectedChatOptionChip` draw the menu and the chip, for a composer of your own.

## Add your own options

`bottomSheetOptions` adds actions of your own to the sheet. Each has a `title`, an `action`, and optionally a `subtitle` and an `Icon`. The sheet draws the `Icon` without props, so give it its size and color:

```tsx
import { Alert } from "react-native";
import { Flag, Picture } from "stream-chat-react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

const options = [
  {
    title: "Create Image",
    subtitle: "Visualize anything",
    action: () => Alert.alert("Pressed on Create Image !"),
    Icon: () => <Picture size={20} stroke="#1a1b25" />,
  },
  {
    title: "Thinking",
    subtitle: "Think longer for better answers",
    action: () => Alert.alert("Pressed on Thinking !"),
    Icon: () => <Flag size={20} stroke="#1a1b25" />,
  },
];

const insets = useSafeAreaInsets();

<ComposerView
  bottomSheetInsets={insets}
  bottomSheetOptions={options}
  conversation={conversation}
/>;
```

Pass the safe area insets as `bottomSheetInsets`, so the sheet can extend beyond them.

## Control it from your code

The composer keeps what the user writes in a store, a `StateStore<ComposerState>`. Inside the composer, these hooks read it:

| Hook                      | Returns                                                           |
| ------------------------- | ----------------------------------------------------------------- |
| `useComposerText()`       | `text`, what the user has written.                                |
| `useComposerHasText()`    | `hasText`, whether there is any.                                  |
| `useComposerChatOption()` | `chatOption`, the [chat option](#chat-options) picked, or `null`. |
| `useComposerIsEditing()`  | `isEditing`, whether the user is editing a message they sent.     |

To change the state from outside, give the composer a store of your own, and write to it:

```tsx
import {
  ComposerView,
  createNewComposerStore,
} from "stream-chat-react-native/ai-components";

const composer = createNewComposerStore();

// A button that fills in a prompt.
composer.partialNext({ text: "Summarize my week" });

<ComposerView onSendMessage={send} state={composer} />;
```

`ComposerState` has the `text`, the `chatOption`, `isEditing` and `custom`, data of your own that reaches `onSendMessage`, or the transport's `request.custom`. A controller makes a store of its own, which `conversation.composer` gives you, or takes yours as its `composer` option.

## Stop the reply

With a controller, the stop button shows by itself while the agent answers, and calls the transport's `stop`. Without one, show it with `isGenerating` and `stopGenerating`. With a Stream Chat channel, read the channel's AI state with `useAIState`, and stop the reply with `channel.stopAIResponse()`, which sends `ai_indicator.stop` to the channel:

```tsx
import { useCallback } from "react";
import {
  AIStates,
  useAIState,
  useChannelContext,
} from "stream-chat-react-native";
import {
  ComposerView,
  type ComposerViewProps,
} from "stream-chat-react-native/ai-components";

const activeStates = [
  AIStates.Thinking,
  AIStates.ExternalSources,
  AIStates.Generating,
];

export const AssistantComposer = ({
  send,
}: {
  send: ComposerViewProps["onSendMessage"];
}) => {
  const { channel } = useChannelContext();
  const { aiState } = useAIState(channel);
  const stop = useCallback(() => channel.stopAIResponse(), [channel]);

  return (
    <ComposerView
      isGenerating={activeStates.includes(aiState)}
      onSendMessage={send}
      stopGenerating={stop}
    />
  );
};
```

Render it inside `Channel`. The button doesn't hide itself when pressed: it goes once the backend's `ai_indicator.clear` reaches the channel and moves `aiState` out of the active states. Include `AIStates.ExternalSources`, as above, since the agent only calls tools in that state, and the stop button would go while a tool runs.

The default `MessageComposer` of the Chat SDK has a [stop button](https://getstream.io/chat/docs/sdk/react-native/v9/ui-components/message-composer/stop-message-streaming-button/) of its own. It shows only while `aiState` is `AI_STATE_THINKING` or `AI_STATE_GENERATING`, not during `AI_STATE_EXTERNAL_SOURCES`.

[Stop the reply](https://getstream.io/chat/docs/sdk/react-native/v9/ai-components/conversation-state/#stop-the-reply) covers what the backend does with the event.

## Dictation

The microphone button in the text field dictates into it with the platform's speech recognizer: Apple's Speech framework on iOS, and Android's `SpeechRecognizer`. It shows while the field is empty, and when tapped it asks for microphone access, records, and writes the transcript into the composer's text. It renders nothing when speech-to-text isn't part of the build, so it never reaches a missing module. `isDictationAvailable()` tells you at runtime whether it is.

`SpeechToTextButton` takes an `options` prop, which sets how the speech is recognized:

| Option             | Description                                                                       | Default               |
| ------------------ | --------------------------------------------------------------------------------- | --------------------- |
| `language`         | The language to recognize, such as `de-DE`.                                       | The device's language |
| `interimResults`   | Writes the words so far while the user speaks, rather than only the final result. | `true`                |
| `silenceTimeoutMs` | How many milliseconds of silence end the dictation.                               | `2500`                |

The composer's own button takes the defaults. To give it options, or to put the button elsewhere, render `SpeechToTextButton` yourself, and pass your component as the composer's `SpeechToTextButton`. `null` leaves the composer's out:

```tsx
import {
  ComposerView,
  SpeechToTextButton,
} from "stream-chat-react-native/ai-components";

const options = {
  language: "de-DE", // recognize German
  interimResults: false, // only the final result
  silenceTimeoutMs: 3500, // wait 3.5 seconds of silence
};

// Declared outside the screen, so the composer gets the same component on every render.
const GermanDictationButton = () => <SpeechToTextButton options={options} />;

<ComposerView
  SpeechToTextButton={GermanDictationButton}
  conversation={conversation}
/>;
```

Dictation needs setup in the native project, which differs by platform.

### iOS

Speech-to-text is opt-in on iOS. App Store Connect requires a speech recognition usage description from every app whose binary references the Speech framework, so the SDK only compiles the speech-to-text module into apps that ask for it. That keeps apps that don't use it from being rejected for a permission they never request.

To enable it, add this line to your `Podfile`, before `use_native_modules!`, and run `pod install`:

```ruby
$StreamChatEnableDictation = true
```

Running `STREAM_CHAT_ENABLE_DICTATION=1 pod install` works as well. Then add the usage descriptions to `Info.plist`:

```txt
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) would like to access your microphone to capture your voice.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>$(PRODUCT_NAME) would like to access speech recognition to transcribe your voice.</string>
```

Choose any permission strings you want. Without the opt-in, the microphone button is hidden, and a development-only warning explains how to enable it.

### Android

Add the permission to `android/app/src/main/AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.RECORD_AUDIO" />
```

The module is always part of the Android build, and the first dictation asks for the permission at runtime.

### Expo

Add the `stream-chat-expo` config plugin to your `app.json` or `app.config.[js|ts]`. It enables the iOS speech-to-text module, adds the iOS usage descriptions and adds the Android permission:

```json
"plugins": [
  // ... rest of your plugins
  [
    "stream-chat-expo",
    {
      "dictationMicrophoneUsageDescription": "$(PRODUCT_NAME) would like to access your microphone to capture your voice.",
      "dictationSpeechRecognitionUsageDescription": "$(PRODUCT_NAME) would like to access speech recognition to transcribe your voice."
    }
  ]
],
```

Both descriptions are optional, and the plugin uses default texts when you leave them out. Pass `"enableDictation": false` to leave speech-to-text out of the app. The plugin applies on `npx expo prebuild`, so run it again after changing the options. If you maintain the native projects yourself instead of generating them with prebuild, follow the iOS and Android steps above.

### Dictate somewhere else

The `useDictation` hook, which the button is built on, dictates into a place of your own, such as a voice mode that sends what it heard. It takes the same options as the button, and a `source` that says who is dictating, so the composer, which only takes dictation of its own, leaves yours alone:

```tsx
import {
  useDictation,
  useDictationState,
  useDictationTranscript,
} from "stream-chat-react-native/ai-components";

const { cancel, start, stop, toggle } = useDictation(undefined, {
  source: "voice-mode",
});
const { error, isRecording, state } = useDictationState();
const { transcript } = useDictationTranscript();
```

`useDictationState` follows the recognizer, `'idle'`, `'starting'`, `'listening'` or `'stopping'`, and the error of the last attempt, and `useDictationTranscript` the words so far. To move a [voice visualization](https://getstream.io/chat/docs/sdk/react-native/v9/ai-components/voice-visualizations/#follow-the-microphone) with the microphone, use `useDictationAudioLevel`. The [Voice agent](https://getstream.io/chat/docs/sdk/react-native/v9/ai-components/use-cases/voice-agent/) use case builds a hands-free conversation from them.

## Styling and texts

The composer takes its colors from the AI components' theme, which `StreamTheme` provides. The theme's `composer` key styles the composer, its input and its buttons, such as `container`, `inputPill`, `textInput`, `sendIcon` and `trailing`, `sheet` styles the bottom sheet, and `chatOptions` styles the chat options and their chip, which the "Editing message" chip shares:

```tsx
<StreamTheme
  style={{
    composer: { inputPill: { borderRadius: 12 }, textInput: { fontSize: 17 } },
  }}
>
  {children}
</StreamTheme>
```

Its texts, such as the placeholder, "Editing message" or the names of the buttons for screen readers, go through the Chat SDK's translations inside `Chat`, so you can change or translate them with `Streami18n`. Outside `Chat`, they're in English.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v9/ai-components/composer/](https://getstream.io/chat/docs/sdk/react-native/v9/ai-components/composer/).