MessageComposer Class

MessageComposer Class

The MessageComposer serves as a central orchestrator for message creation and editing. It:

  • Manages the state of message composition
  • Handles different types of content (text, attachments, polls, etc.)
  • Provides draft functionality for saving message progress
  • Supports message editing and quoting
  • Integrates with the Stream Chat API for message operations

We can initiate the composer instance as follows:

new MessageComposer({
  client, // StreamChat client instance
  compositionContext, // it is necessary to provide a Channel or Thread instance or LocalMessage object
  composition, // optional initial state like draft message or edited message
  config, // optional custom configuration
});

Each Channel and Thread instance has its own messageComposer attribute assigned on instantiation. In case we need to edit a message, we pass the LocalMessage object to compositionContext and composition constructor parameters.

MessageComposer is built using a modular architecture with specialized managers for different aspects of message composition:

  • TextComposer: Handles text input, mentions, and text-related operations
  • AttachmentManager: Keeps message attachments state and manages file attachments uploads
  • LinkPreviewsManager: Processes and manages link previews in messages
  • PollComposer: Handles poll composition and creation
  • CustomDataManager: Allows for custom message and non-message related data

Each manager operates independently and keeps its own dedicated state. MessageComposer orchestrates their interactions.

The following are the general MessageComposer features:

  • State Management: Uses a reactive state management system to track changes
  • Middleware Support: Allows for custom processing of messages through middleware
  • Draft System: Supports saving and restoring message drafts
  • Rich Content: Handles various types of content including text, attachments, polls, and link previews
  • Event System: Provides event subscriptions for real-time updates
  • Configurable: Highly configurable through the MessageComposerConfig interface

StreamChat.setMessageComposerSetupFunction

One MessageComposer setup function per client, ideally you’d setup this function only once (or each time a specific dependency changes).

import { defaultTextComposerMiddlewares } from "stream-chat";

const chatClient = useCreateChatClient({
  apiKey,
  tokenOrProvider: userToken,
  userData: { id: userId, language: "en" },
});

const [emojisEnabled, setEmojisEnabled] = useState(false);

useEffect(() => {
  chatClient.setMessageComposerSetupFunction(({ composer }) => {
    if (composer.contextType === "channel") {
      composer.textComposer.upsertMiddleware(
        defaultTextComposerMiddlewares.map(composer.channel),
      );

      if (emojisEnabled) {
        composer.textComposer.upsertMiddleware([
          createEmojiMiddleware(SearchIndex),
        ]);
      }

      return () => {
        composer.textComposer.removeMiddleware("emoji");
      };
    }
  });
}, [chatClient, emojisEnabled]);

Message Composer Configuration

Configuration

The MessageComposer can be configured at different levels, from global client-wide settings to individual manager configurations.

Configuration Structure

The configuration is defined by the MessageComposerConfig interface:

type MessageComposerConfig = {
  drafts: {
    enabled: boolean; // Enables server-side draft functionality. Disabled by default.
  };
  attachments: {
    /**
     * Function to filter out files before being uploaded. The default forwards without filtering.
     */
    fileUploadFilter: (file: FileLike) => boolean;
    /**
     * Maximum file upload attachments per message. Default is 10.
     */
    maxNumberOfFilesPerMessage: number;
    /**
     * Allowed file types.
     * Expected are unique file type specifiers
     * (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept#unique_file_type_specifiers).
     * The default is an empty array - all files accepted.
     */
    acceptedFiles: string[];
    /**
     * Custom upload request (and possibly logic). For example upload to custom CDN.
     */
    doUploadRequest?: (
      file: FileLike,
    ) => Promise<{ file: string; thumb_url?: string }>;
  };
  linkPreviews: {
    /**
     * Enables link previews. Disabled by default.
     */
    enabled: boolean;
    /**
     * Debounce time for URL detection and enrichment. By default 1500ms.
     */
    debounceURLEnrichmentMs: number;
    /**
     * Custom URL detection function. The default is linkifyjs.find.
     */
    findURLFn: (text: string) => string[];
    /**
     * Custom logic to be invoked when a specific link preview is dismissed
     */
    onLinkPreviewDismissed?: (linkPreview: LinkPreview) => void;
  };
  text: {
    /**
     * Enables text input. Enabled by default.
     */
    enabled: boolean;
    /**
     * Enable sending typing events which are used to display typing indicators. Enabled by default.
     */
    publishTypingEvents: boolean;
    /**
     * Initial text value if draft or existing message is not available.
     */
    defaultValue?: string;
    /**
     * The message text will be trimmed to maxLengthOnEdit on text change or suggestion selection.
     */
    maxLengthOnEdit?: number;
    /**
     * The message text will be trimmed to maxLengthOnSend during the final message composition before being sent.
     */
    maxLengthOnSend?: number;
  };
};

Configuration Methods

1. Global Configuration

Use client.setMessageComposerSetupFunction to configure all MessageComposer instances:

client.setMessageComposerSetupFunction(({ composer }) => {
  composer.updateConfig({
    drafts: { enabled: true },
    attachments: {
      maxNumberOfFilesPerMessage: 5,
      acceptedFiles: ["image/*", "video/*"],
    },
  });
});

This configuration will be applied also to all new MessageComposer instances (for example created for editing a message).

2. Instance-Specific Configuration

Configure a specific MessageComposer instance using updateConfig:

messageComposer.updateConfig({
  text: {
    enabled: true,
    publishTypingEvents: false,
  },
});

3. Manager-Level Configuration

Each manager can be configured individually through its properties:

// Configure TextComposer
messageComposer.textComposer.defaultValue = "Dear Customer Service";

// Configure AttachmentManager
messageComposer.attachmentManager.acceptedFiles(["image/*", "video/*"]);

// Configure LinkPreviewsManager
messageComposer.linkPreviewsManager.enabled = true;

// General message composition parameters. Applies to drafts.
messageComposer.updateConfig({ drafts: enabled });

Configuring the message id generation

We can provide a custom message id generator function by overriding the MessageComposer.generateId static method:

messageComposer.generateId = customGenerator;

Message Composer Context Type

Each MessageComposer has a contextType getter which gets evaluated from the compositionContext value provided at construction time. A composer can have a context type of channel, thread, legacy_thread or message.

You might not need this information but it’s there when you need it; for example when you’re deciding what middlewares to apply to only channel-based or thread-based composers.

:::note Type legacy_thread is a simple message object extended with legacyThreadId property whose value equals to that message’s id. :::

Accessing the Corresponding MessageComposer instance

The useMessageComposer hook provides access to the appropriate MessageComposer instance based on the current context (channel, thread, message). It automatically handles different composition scenarios:

  • Composing a new message in a channel
  • Replying in a thread
  • Editing an existing message
  • Replying in a legacy thread that does not rely on Thread instance

The usage:

import { useMessageComposer } from "stream-chat-react";

const MyComponent = () => {
  const messageComposer = useMessageComposer();
  const handleSend = async () => {
    // Access composer features
    const { localMessage, message, sendOptions } =
      await messageComposer.compose();
    // Handle the composed message
    // ...
  };

  return <button onClick={handleSend}>Send</button>;
};

The hook automatically resolves the correct MessageComposer instance based on the following hierarchy:

  1. Editing a message: If a message is being edited, a new composer instance is created for that specific message
  2. Thread reply: If in a thread context, it uses the thread’s composer instance
  3. Legacy thread reply: If replying to a message in a context that does not rely on Thread instance that already contains its MessageComposer instance.
  4. Channel message: Falls back to the channel’s composer instance

It automatically does the following:

  • Registers the necessary subscriptions when the composer is mounted
  • Cleans up subscriptions when the component unmounts

The hook implements caching for composer instances in certain contexts:

  • Editing a message
  • Replying in a legacy thread

This ensures that the same composer instance is reused when returning to these contexts, preserving the composition state.

Message Composer Utility Hooks

useAttachmentManagerState

const {
  attachments,
  availableUploadSlots,
  blockedUploadsCount,
  failedUploadsCount,
  isUploadEnabled,
  pendingUploadsCount,
  successfulUploadsCount,
  uploadsInProgressCount,
} = useAttachmentManagerState();

Use this hook when you need to:

  • Access the attachment data
  • Track attachment upload progress and status
  • Show upload progress indicators
  • Enable/disable UI elements based on upload state
  • Display attachment counts and limits

useCanCreatePoll

const canCreatePoll = useCanCreatePoll();

Use this hook to:

  • Determine if a poll can be created with the current state
  • Enable/disable the send button

useMessageComposerHasSendableData

const hasSendableData = useMessageComposerHasSendableData();

Use this hook to:

  • Enable/disable the send button

© Getstream.io, Inc. All Rights Reserved.