# MessageComposer API

This section covers the `MessageComposer` API and its sub-managers.

## Best Practices

- Use the appropriate manager (text, attachments, link previews, polls) instead of mixing concerns.
- Avoid direct state mutation; use manager APIs to keep state consistent.
- Keep suggestion logic lightweight to preserve typing performance.
- Validate attachments and custom data before sending.
- Use link preview manager settings to control network and UX impact.

`MessageComposer` provides the core API for composing messages (text, attachments, polls, custom data). It coordinates specialized submanagers, each focused on one aspect of composition.

## MessageComposer Class Architecture

`MessageComposer` follows a coordinator pattern and delegates responsibilities to submanagers:

```mermaid
graph TD
    MC[MessageComposer] --> TC[TextComposer]
    MC --> PC[PollComposer]
    MC --> AM[AttachmentManager]
    MC --> LPM[LinkPreviewsManager]
    MC --> CDM[CustomDataManager]
```

### Submanager Responsibilities

1. **TextComposer**
   - Handles text input and editing
   - Manages text state and validation
   - Processes text-related events

2. **PollComposer**
   - Manages poll creation and editing
   - Handles poll state and validation

3. **AttachmentManager**
   - Manages attachments other than link previews
   - Handles uploads and downloads

4. **LinkPreviewsManager**
   - Manages link previews
   - Handles URL detection and preview generation

5. **LocationComposer**
   - Collects information for sharing user location within a message

6. **CustomDataManager**
   - Manages custom message data
   - Manages custom composer data (bucket to store any temporary data that will not be submitted with the message)

### Message Composition Scenarios

`MessageComposer` supports multiple composition scenarios with distinct initialization and behavior:

#### 1. Message Editing

When editing an existing message, MessageComposer initializes with the message's current state:

```ts
const composer = new MessageComposer({
  composition: existingMessage, // LocalMessage
  compositionContext: channel,
  client,
});
```

Key characteristics:

- Initializes with message's current state (text, attachments, etc.)
- Tracks editing timestamps via `editingAuditState`
- Prevents draft creation (drafts are disabled)
- Maintains original message ID
- Updates are applied to the existing message

#### 2. Drafts

Drafts can be enabled via the `MessageComposer` configuration:

```ts
const composer = new MessageComposer({
  composition: draftResponse, // DraftResponse
  compositionContext: channel,
  client,
  config: { drafts: { enabled: true } },
});

composer.updateConfig({ drafts: { enabled: false } });
```

The composer exposes the draft management API:

```ts
await composer.createDraft();

await composer.deleteDraft();
```

No parameters are needed; the composer extracts data from its state and draft composition middleware.

The composer takes care of local state updates based on `draft.updated` and `draft.deleted` events automatically.

#### 3. New Message Context

When creating a new message, MessageComposer starts with an empty state:

```ts
const composer = new MessageComposer({
  compositionContext: channel,
  client,
});
```

Key characteristics:

- Starts with empty state
- Generates new message ID
- No draft tracking by default
- Clean slate for all submanagers

### MessageComposer API

#### MessageComposer Constructor

```ts
constructor({
  composition,
  config,
  compositionContext,
  client,
}: MessageComposerOptions)
```

Where `MessageComposerOptions` is:

```ts
type MessageComposerOptions = {
  client: StreamChat;
  // Channel | Thread | LocalMessageWithLegacyThreadId
  compositionContext: CompositionContext;
  // state seed - state is initialized with the draft or an edited message
  composition?: DraftResponse | MessageResponse | LocalMessage;
  config?: DeepPartial<MessageComposerConfig>;
};
```

#### MessageComposer State Management

```ts
// Initialize or reset composer state
initState({ composition }: { composition?: DraftResponse | MessageResponse | LocalMessage } = {}): void

// Init state from channel response
initStateFromChannelResponse(channelApiResponse: ChannelAPIResponse): void

// Generate a new message id
refreshId(): void

// Clear all composer state
clear(): void

// Restore state from edited message or clear
restore(): void

// Update composer configuration
updateConfig(config: DeepPartial<MessageComposerConfig>): void

// Override or clear the edited message baseline used by restore()
setEditedMessage(editedMessage: LocalMessage | null | undefined): void

// Set quoted message
setQuotedMessage(quotedMessage: LocalMessage | null): void

// Toggle the boolean flag showReplyInChannel
// to show a thread reply in a channel's message list too
toggleShowReplyInChannel(): void
```

#### Message Composition

```ts
// Compose a message
compose(): Promise<MessageComposerMiddlewareValue['state'] | undefined>

// Compose a draft message
composeDraft(): Promise<MessageComposerMiddlewareValue['state'] | undefined>

// Create a draft message
createDraft(): Promise<void>

// Delete a draft message
deleteDraft(): Promise<void>

// Create a poll
createPoll(): Promise<void>

// Gets a draft and init the composer state
getDraft(): Promise<DraftResponse | null>
```

#### Context Information Getters

```ts
// Get context type (channel, thread, legacy_thread, or message)
get contextType(): string

// Get composer tag
get tag(): string

// Get thread ID
get threadId(): string | null
```

#### MessageComposer State Getters

```ts
// Get message ID
get id(): string

// Get draft ID
get draftId(): string | null

// Get edited message baseline used by restore()
get editedMessage(): LocalMessage | undefined

// Get last change timestamp
get lastChange(): LastComposerChange

// Get quoted message
get quotedMessage(): LocalMessage | null

// Get poll ID
get pollId(): string | null

/**
 * Get the boolean flag determining, whether the message will sent to the channel's message list.
 * During the composition, the `show_in_channel: true` will be added to the thread reply's payload.
  */
get showReplyInChannel(): boolean
```

#### Composition State Getters

```ts
// Check if composer has sendable data
get hasSendableData(): boolean

// Check if text, attachments, poll, and location are all empty
get contentIsEmpty(): boolean

// Check if composition is empty
get compositionIsEmpty(): boolean

// Check if last change was local
get lastChangeOriginIsLocal(): boolean
```

#### Static Methods

```ts
// Generate a unique ID
static generateId(): string
```


## Text Composition With Suggestions

`TextComposer` handles text composition and tracks mentioned users. It supports autocomplete and suggestions based on typed text. Its state includes:

| Field          | Description                                                                                                              |
| -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| text           | Message text                                                                                                             |
| selection      | Cursor position (start/end of selection)                                                                                 |
| command        | The currently selected slash command, or `null` if no command is active                                                  |
| mentionedUsers | An array of user objects mentioned in the text. Before the message submission, the array is filtered for stale mentions. |
| suggestions    | Active suggestions (mentions, commands, emojis) based on the trigger in the text                                         |

### Configuration

TextComposer allows adjusting individual configuration parameters:

```ts
// Enable/disable text composition
textComposer.enabled = true;

// Set default text value
textComposer.defaultValue = "Hello";

// Configure text length limits
textComposer.maxLengthOnEdit = 1000;
textComposer.maxLengthOnSend = 500;

// Toggle typing events
textComposer.publishTypingEvents = true;
```

### Text State Management

```ts
// Get current state values
const text = textComposer.text;
const selection = textComposer.selection;
const command = textComposer.command;
const mentionedUsers = textComposer.mentionedUsers;
const suggestions = textComposer.suggestions;
const isEmpty = textComposer.textIsEmpty;

// Update text state
textComposer.setText("New message");
textComposer.setSelection({ start: 0, end: 5 });

// Insert text at selection
textComposer.insertText({
  text: "Hello",
  selection: { start: 0, end: 0 },
});

// Wrap selection with text
textComposer.wrapSelection({
  head: "**",
  tail: "**",
  selection: { start: 0, end: 5 },
});

// Initialize state
textComposer.initState({ message: existingMessage });
```

If you don’t pass a `selection` to `insertText` or `wrapSelection`, update selection with `textComposer.setSelection(selection)` so those methods use the correct `textComposer.selection` value.

### Command State

The selected slash command is stored separately from the text:

```ts
// Set selected command
textComposer.setCommand(command);

// Clear selected command
textComposer.clearCommand();
```

### User Mentions

For a user mention to be updated, the user object has to always contain an `id`.

```ts
// Add mentioned user
textComposer.upsertMentionedUser(user);

// Remove mentioned user
textComposer.removeMentionedUser(userId);

// Get mentioned user
const user = textComposer.getMentionedUser(userId);

// Set all mentioned users
textComposer.setMentionedUsers(users);
```

### Suggestions

Suggestions are set during text change processing in the `TextComposer` middleware chain. You can also set or clear them directly:

```ts
const searchSource = new CustomSearchSource();

// Set suggestions
textComposer.setSuggestions({
  query: "mar",
  searchSource,
  trigger: "@",
});

// Close suggestions
textComposer.closeSuggestions();
```

### Event Handling

Register text changes with `TextComposer.handleChange()`:

```ts
// Handle text change
textComposer.handleChange({
  text: "New text",
  selection: { start: 0, end: 0 },
});
```

Handle suggestion selection with `TextComposer.handleSelect()`:

```ts
// Handle suggestion selection
textComposer.handleSelect(suggestion);
```


## Attachment Management

`AttachmentManager` handles file attachments during composition, including upload state.

#### Basic Attachment Management

```ts
// Get current attachments
const attachments = attachmentManager.attachments;

// Add or update attachments
attachmentManager.upsertAttachments([newAttachment]);

// Remove attachments
attachmentManager.removeAttachments([attachmentId]);
```

### Uploading Attachments

When using `uploadAttachment`, ensure the attachment object is correctly formed:

```ts
// Convert file to proper attachment object
const localAttachment =
  await attachmentManager.fileToLocalUploadAttachment(file);

// Upload the attachment
const uploadedAttachment =
  await attachmentManager.uploadAttachment(localAttachment);
```

Uploaded attachments include `localMetadata`, which is removed before sending the message. `localMetadata` contains:

- `file` - the file reference
- `uploadState` - `'uploading'`, `'finished'`, `'failed'`, `'blocked'`, `'pending'`
- `uploadPermissionCheck` - contains the upload permission check result to the CDN and explains the reason if blocked

When editing a draft or existing message, uploaded attachments are marked `uploadState: "finished"` and have no file reference or `uploadPermissionCheck`.

### Custom Upload Configuration

**File Filtering**

Use `fileUploadFilter` to add custom filtering logic beyond `acceptedFiles`:

```ts
attachmentManager.fileUploadFilter = (file) => {
  // Custom filter logic
  return file.size < 10 * 1024 * 1024; // Example: 10MB limit
};
```

**Custom Upload Destination**

Use `doUploadRequest` to customize where files are uploaded:

```ts
attachmentManager.setCustomUploadFn(async (file) => {
  // Upload to custom CDN
  const result = await customCDN.upload(file);
  return { file: result.url };
});
```

For custom CDN uploads, you may need to override `getUploadConfigCheck`:

```ts
class CustomAttachmentManager extends AttachmentManager {
  getUploadConfigCheck = async (file) => {
    // Skip default upload checks for custom CDN
    return { uploadBlocked: false };
  };
}
```

### Attachment Identity Functions

`AttachmentManager` uses attachment identity functions internally. Use them for type safety:

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

if (isLocalImageAttachment(attachment)) {
  // Type-safe access to image-specific properties
  console.log(attachment.original_height);
}
```

Attachments are considered “local” only if they have `localMetadata`.

| Function                          | Description                                                                                    |
| --------------------------------- | ---------------------------------------------------------------------------------------------- |
| `isLocalAttachment`               | Checks if attachment has local metadata                                                        |
| `isLocalUploadAttachment`         | Checks if attachment has upload state metadata                                                 |
| `isFileAttachment`                | Checks if attachment is a file attachment                                                      |
| `isLocalFileAttachment`           | Checks if attachment is a local file attachment                                                |
| `isImageAttachment`               | Checks if attachment is an image attachment                                                    |
| `isLocalImageAttachment`          | Checks if attachment is a local image attachment                                               |
| `isAudioAttachment`               | Checks if attachment is an audio attachment                                                    |
| `isLocalAudioAttachment`          | Checks if attachment is a local audio attachment                                               |
| `isVoiceRecordingAttachment`      | Checks if attachment is a voice recording attachment                                           |
| `isLocalVoiceRecordingAttachment` | Checks if attachment is a local voice recording attachment                                     |
| `isVideoAttachment`               | Checks if attachment is a video attachment                                                     |
| `isLocalVideoAttachment`          | Checks if attachment is a local video attachment                                               |
| `isUploadedAttachment`            | Checks if attachment is an uploaded attachment (audio, file, image, video, or voice recording) |
| `isScrapedContent`                | Checks if attachment is scraped content (has og_scrape_url or title_link)                      |
| `isGiphyAttachment`               | Checks if attachment is a Giphy attachment                                                     |


## Link Preview Management

`LinkPreviewsManager` handles link preview generation during composition. It finds, enriches, and manages previews, then converts them to attachments before send.

### State

State:

```ts
type LinkPreviewsManagerState = {
  previews: Map<string, LinkPreview>; // URL -> Preview mapping
};
```

Each preview has a status:

- `LOADING`: Preview is being fetched
- `LOADED`: Preview successfully loaded
- `FAILED`: Preview loading failed
- `DISMISSED`: Preview was dismissed by user
- `PENDING`: Preview is waiting to be processed

### LinkPreviewsManager API

#### Finding and Enriching URLs

Enrichment is debounced by 1.5s by default.

```ts
// Find and enrich URLs in text (debounced)
linkPreviewsManager.findAndEnrichUrls(text);

// Cancel ongoing enrichment
linkPreviewsManager.cancelURLEnrichment();
```

#### Managing Previews

```ts
// Clear non-dismissed previews
linkPreviewsManager.clearPreviews();

// Update a specific preview
linkPreviewsManager.updatePreview(url, {
  title: "New Title",
  description: "New Description",
  status: LinkPreviewStatus.LOADED,
});

// Dismiss a preview
linkPreviewsManager.dismissPreview(url);
```

Dismissed previews are not re-enriched when the same URL appears again in the text. The `clearPreviews` method preserves dismissed previews while removing others.

#### Static Helpers

`LinkPreviewsManager` exposes static helpers for preview state:

```ts
// Check preview status
LinkPreviewsManager.previewIsLoading(preview);
LinkPreviewsManager.previewIsLoaded(preview);
LinkPreviewsManager.previewIsDismissed(preview);
LinkPreviewsManager.previewIsFailed(preview);
LinkPreviewsManager.previewIsPending(preview);
```

The `getPreviewData` static method extracts the preview data that will be converted to a scraped attachment when the message is sent to the server.

```ts
// Get preview data without status
LinkPreviewsManager.getPreviewData(preview);
```


## Poll Composition

`PollComposer` handles poll composition and creation. It builds poll data to attach to a message.

### PollComposer State

Poll state includes data and validation errors:

```ts
type PollComposerState = {
  data: {
    id: string;
    name: string;
    description: string;
    options: Array<{ id: string; text: string }>;
    max_votes_allowed: string;
    enforce_unique_vote: boolean;
    allow_answers: boolean;
    allow_user_suggested_options: boolean;
    voting_visibility: VotingVisibility;
    user_id: string;
  };
  errors: Record<string, string>;
};
```

Validation prevents failed creation requests on the server.

### PollComposer State Management

Initialize or reset state:

```ts
// Reset to initial state
pollComposer.initState();
```

Poll composition updates multiple fields. You can react to field updates and blur events. `updateFields` accepts partial objects:

```ts
// Update poll fields
pollComposer.updateFields({
  name: "Favorite Color?",
  description: "Choose your favorite color",
  options: [
    { id: "1", text: "Red" },
    { id: "2", text: "Blue" },
  ],
  max_votes_allowed: "1",
  enforce_unique_vote: true,
});
```

```ts
// Handle field blur validation
pollComposer.handleFieldBlur("name");
```

Validation runs on each update or blur. Customize via PollComposer middleware (see the PollComposer middleware guide).

### Poll Creation

```ts
messageComposer.createPoll();
```

Behind the scenes, the poll is composed and created on the server. Watch for failed creation requests by subscribing to `StreamChat.notifications.state` (see the client notifications service guide).

### State Access

```ts
// Check if poll can be created
const canCreate = pollComposer.canCreatePoll;

// Access poll fields
const name = pollComposer.name;
const description = pollComposer.description;
const options = pollComposer.options;
const maxVotes = pollComposer.max_votes_allowed;
const enforceUniqueVote = pollComposer.enforce_unique_vote;
const allowAnswers = pollComposer.allow_answers;
const allowUserOptions = pollComposer.allow_user_suggested_options;
const votingVisibility = pollComposer.voting_visibility;
```

### Validation

`canCreatePoll` checks the minimum requirements for creation:

- At least one non-empty option exists
- Poll name is not empty
- `max_votes_allowed` is either empty or a valid number between 2 and 10
- No validation errors are present


## Custom Data Management

`CustomDataManager` handles custom data for messages and the composer. It manages two types:

1. Message custom data - data that will be sent with the message
2. Composer custom data - data that stays in the composer and is not sent with the message

### State

The custom data state consists of two separate objects:

```ts
type CustomDataManagerState = {
  message: CustomMessageData; // Data sent with the message
  custom: CustomMessageComposerData; // Data for custom integration needs, not sent with the message
};
```

The `custom` property stores integration data that should not be sent with the message (for example, UI state or temporary data).

### State Management

```ts
// Get current state values
const messageData = customDataManager.customMessageData;
const composerData = customDataManager.customComposerData;

// Update message custom data
customDataManager.setMessageData({
  customField: "value",
});

// Update composer custom data
customDataManager.setCustomData({
  composerField: "value",
});

// Initialize state
customDataManager.initState({ message: existingMessage });
```

### Custom Data Comparison

`isMessageDataEqual` determines whether message custom data changed. By default, it JSON-stringifies and compares:

```ts
isMessageDataEqual = (
  nextState: CustomDataManagerState,
  previousState?: CustomDataManagerState,
) =>
  JSON.stringify(nextState.message) === JSON.stringify(previousState?.message);
```

You can override this method to implement custom comparison logic. For example:

```ts
class CustomDataManager {
  isMessageDataEqual = (
    nextState: CustomDataManagerState,
    previousState?: CustomDataManagerState,
  ) => {
    // Custom comparison logic
    return nextState.message.customField === previousState?.message.customField;
  };
}
```

This is particularly useful when:

- Only specific fields need to be compared
- Complex data structures require special comparison logic
- Certain fields should be ignored in the comparison



---

This page was last updated at 2026-04-17T17:33:46.020Z.

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