MessageComposer Middleware

Message Composer Middleware

MessageComposer middleware purpose is to allow for full control over the data processing in different composition scenarios.

Some managers (TextComposer, PollComposer, MessageComposer) use middleware to process state changes and compose data. These managers have one or more middleware executors that execute handler chains.

The middleware execution is performed via MiddlewareExecutor. It allows:

  • Registering middleware with specific handlers
  • Controlling handler execution order
  • Deciding when to terminate the middleware chaing execution

Middleware Execution

Middleware is an object with an id and a handlers object containing specific event handlers:

type Middleware<TValue, THandlers extends string> = {
  id: string;
  handlers: {
    [K in THandlers]: MiddlewareHandler<TValue>;
  };
};

The MiddlewareExecutor registers the middleware:

// Define middleware with handlers for different events
const textValidationMiddleware = {
  id: "textValidation",
  handlers: {
    // Handler for text changes
    onChange: async ({ state, next, discard }) => {
      if (state.text.length > 100) {
        return discard();
      }
      return next(state);
    },

    // Handler for suggestion selection
    onSuggestionItemSelect: async ({ state, next, complete }) => {
      // Process suggestion...
      return next(state);
    },
  },
};

// Register middleware with executor
middlewareExecutor.use(textValidationMiddleware);

The executor executes a chain of handlers by name. For example, when onChange is called, it runs all handlers registered for the onChange event. The state type varies based on specific MiddlewareExecutor. And each handler is given the control functions (next, discard, etc.).

// Executor runs the onChange handler chain
middlewareExecutor.execute({eventName: 'onChange', initialValue: { text: 'Hello', selection: {start: 1, end:} } });

Each handler in the chain receives the current state and control functions to manage the execution flow:

  • next(state): Continue the execution with the next handler in the chain and with the given state
  • complete(state): Stop the execution with and commit the given state
  • discard(): Stop execution and discard changes
  • forward(): Skip processing and continue with the next handler

Middleware Management

Middleware can be registered in several ways:

// Add middleware
middlewareExecutor.use({
  id: "customMiddleware",
  handlers: {
    onChange: async ({ state, next, complete }) => {
      // Process state
      if (state.text === "done") {
        return complete(state);
      }
      return next(state);
    },
  },
});

// Insert middleware at specific position
middlewareExecutor.insert({
  middleware: [customMiddleware],
  position: { after: "existingMiddlewareId" },
});

// Replace existing middleware
middlewareExecutor.replace([newMiddleware]);

// Set middleware order
middlewareExecutor.setOrder([
  "middleware-1-id",
  "middleware-2-id",
  "middleware-3-id",
]);

Middleware can be removed as well:

// array of middleware IDs
middlewareExecutor.remove([
  "middleware-1-id",
  "middleware-2-id",
  "middleware-3-id",
]);

// or a single middleware ID
middlewareExecutor.remove("middleware-1-id");

Custom Middleware Execution

To create a custom execution chain with custom state (all the handlers work with the same executor state) we can extend MiddlewareExecutor class to build a custom executor. There we can customize how the chain is executed or how the middleware is added, sorted or removed.

Message Composer Middleware Overview

Each manager defines its own handler events and state types. In general there are two types of executors:

  1. Executors for state change - handlers mutate the original middleware state value into a new value
  2. Composition executors - handlers generate a new value

TextComposer

The executor for state change recognized the following handlers:

  • onChange: Handles text changes
  • onSuggestionItemSelect: Handles suggestion selection

As integrators, we do not need to access messageComposer.textComposer.middlewareExecutor or the above handlers. The above handlers are executed within text composer’s methods internally.

// internally relies on onChange handler
messageComposer.textComposer.handleChange({
  text,
  selection,
});

// internally relies on onSuggestionItemSelect handler
messageComposer.textComposer.handleSelect(suggestionItem);

AttachmentManager

Executor for upload preparation recognizes the following handlers:

  • prepare: actions taken on attachment before a file is uploaded

Executor for post-upload actions recognizes the following handlers:

  • postProcess: actions taken after the file upload success or failure

PollComposer

The executor for state change recognizes the following handlers:

  • handleFieldChange: Processes poll form field updates
  • handleFieldBlur: Handles poll form field blur events

The executor for composition recognized the following handlers:

  • compose: Composes poll data before the poll is created server-side

MessageComposer

The executor for message composition recognizes the following handlers

  • compose: Composes final message data

The executor for draft composition recognizes the following handlers

  • compose: Composes final message draft data

Text Composition Middleware

TextComposer state changes based on text change or suggestion selection are processed via middleware execution. The responsible executor is available as TextComposer.middlewareExecutor.

The executor registers the default middleware with the following middleware factories:

FactoryReturned middleware ID
createTextComposerPreValidationMiddleware'stream-io/text-composer/pre-validation-middleware'
createMentionsMiddleware'stream-io/text-composer/mentions-middleware'
createCommandsMiddleware'stream-io/text-composer/commands-middleware'

The mentions and commands middleware determines:

  • when to trigger suggestions offering by recognizing a trigger character sequence in the input text
  • how to retrieve the suggestions items (client-side or server-side pagination). This is done via MentionsSearchSource for mentions middleware and CommandSearchSource for commands middleware.

The pre-validation middleware on the other side makes sure that the TextComposer’s configuration parameter maxLengthOnEdit is reflected in the maximum text size.

TextComposer Middleware State

TextComposer middleware executor expects that every registered handler function would adhere to the middleware state structure. The TextComposer middleware state is a union of the TextComposer state fields and field called change.

export type TextComposerMiddlewareExecutorState<T extends Suggestion = Suggestion> =
  TextComposerState<T> & {
  change?: {
    selectedSuggestion?: T;
  };
};

The field change holds temporary information that is to be adapted for incorporation into the TextComposer state after the middleware chain execution. Currently, it holds information about which item has been selected from a command or mentions suggestion list.

TextComposer Middleware Customization

There are various level of detail in which text composition can be customized. We will demonstrate these use cases from the most general to the most specific.

Change Trigger or Minimum Trigger Characters

textComposer.middlewareExecutor.replace([
  createMentionsMiddleware(textComposer.channel, {
    minChars: 3,
    trigger: "__",
  }),
  createCommandsMiddleware(textComposer.channel, { minChars: 3, trigger: "§" }),
]);

Pagination Parameters

The mentions middleware and commands middleware handles data pagination and query loading state management out of the box. Both implement the BaseSearchSource API and allow for customization of debounce interval and page size.

const commandSearchSource = new CommandSearchSource(channel, {
  debounceMs,
  pageSize,
});
const mentionSearchSource = new MentionsSearchSource(channel, {
  debounceMs,
  pageSize,
});

Mentions Middleware

IDHandlers
'stream-io/text-composer/mentions-middleware'onChange, onSuggestionItemSelect

Mentions Middleware Handlers

OnChange

The handler has the following responsibilities:

  • identify the mention trigger character sequence (@ by default) at the cursor position
  • retrieve the suggestion items in case the trigger has been identified
  • pass the suggestion items into the TextComposer’s middleware state as suggestions object

After the middleware handler chain execution the suggestions would appear in the TextComposer state and thus in the UI.

onSuggestionItemSelect

The handler has the following responsibilities:

  • add the selected item into the mentionedUsers array of TextComposer state
  • inject the whole mention text representation into the TextComposer’s text state (from @M to @Martin and adjust the cursor position
  • reset suggestions back to undefined which should lead to suggestion UI disappearing

Custom Mentions Retrieval

The suggestion items for mentions are retrieved inside the handlers via MentionsSearchSource. The createMentionsMiddleware counts with a default implementation but we can also provide custom search source:

Mentions transliteration

Transliteration is the process of writing words from one language using the closest corresponding letters of another writing system while preserving their original pronunciation. The transliteration MentionsSearchSource task. By default, the search source does not perform transliteration. We need to provide it with transliterate function that will do the heavy lifting. See MentionsSearchSource Customization section for more details.

MentionsSearchSource Configuration

MentionsSearchSource allows to configure the following:

  • debounceMs - states the debounce interval in milliseconds for pagination requests
  • pageSize - states the mention suggestion page size during pagination
  • mentionAllAppUsers - forces the MentionsSearchSource instance to always trigger users query instead of just channel members query
  • textComposerText - allows to keep the whole text composer text, not only the search query extracted from the trigger
  • transliterate - allows to transliterate the mention names
import { MentionsSearchSource } from 'stream-chat';
import { default: transliterate } from '@stream-io/transliterate';

const mentionSearchSource = new MentionsSearchSource(channel, {
  debounceMs,
  mentionAllAppUsers,
  pageSize,
  textComposerText,
  transliterate,
});

Further we can customize how the data is retrieved, filtered, transformed and maintained.

Custom MentionsSearchSource

The custom searchSource implementation should adhere to the MentionsSearchSource interface for compatibility. We can provide our custom mentions search source as follows:

const searchSource = new CustomMentionsSearchSource();

textComposer.middlewareExecutor.replace([
  createMentionsMiddleware(channel, {searchSource})
]);

The custom instance can customize the following behavior:

Query Parameters Customization

Query parameters are filters, sort and options. These can be customized by overriding MentionsSearchSource.prepareQueryUsersParams and MentionsSearchSource.prepareQueryMembersParams methods.

import { MentionsSearchSource } from "stream-chat";
import type {
  UserFilters,
  UserSort,
  UserOptions,
  MemberFilters,
  MemberSort,
  UserOptions,
} from "stream-chat";

class CustomMentionsSearchSource extends MentionsSearchSource {
  // ...
  prepareQueryUsersParams = (
    searchQuery: string,
  ): { filters: UserFilters; sort: UserSort; options: UserOptions } => {
    //... generate filters, sort, options
    return { filers, sort, options };
  };
  prepareQueryMembersParams = (
    searchQuery: string,
  ): { filters: MemberFilters; sort: MemberSort; options: UserOptions } => {
    //... generate filters, sort, options
    return { filers, sort, options };
  };
}

Or we can overwrite the parameters if they do not depend on the search query value (normally does not apply to filters):

mentionsSearchSource.userSort = { name: -1 };
mentionsSearchSource.memberSort = { name: -1 };
mentionsSearchSource.searchOptions = { include_deactivated_users: true };

Custom Pagination Logic

We can customize how the searched items are retrieved by overriding the MentionsSearchSource method query. If the pagination supports cursor, the next value (cursor) should also be returned. We expect it to be a string:

import { MentionsSearchSource } from 'stream-chat';

class CustomMentionsSearchSource extends MentionsSearchSource {
  // ...
  query = async (searchQuery: string) => {
    let result;
    // custom logic...

    return {
      items: result.items,
      next: result.cursor,
    };
  }
}

Custom Results Filtering

Apply custom filtering logic to the query results before they are committed to the search source state.

import {MentionsSearchSource} from 'stream-chat';
import type {UserSuggestion} from 'stream-chat';

const mentionsFilter = (item: UserSuggestion) => {
  //... custom filtering logic
}

class CustomMentionsSearchSource extends MentionsSearchSource {
  // ...
  filterQueryResults = (items: UserSuggestion[]) => {
    return items.filter(mentionsFilter)
  }
}

State Retention Between Searches

With a new search query the search state is reset to the initial form except for items, which are kept from the last search result until the next query results are committed to the state. This is meant to prevent UI flickering between empty state and the first page results on every text change. This behavior can be, however, changed by overriding getStateBeforeFirstQuery method:

import {MentionsSearchSource} from 'stream-chat';

class CustomMentionsSearchSource extends MentionsSearchSource {
  // ...
  getStateBeforeFirstQuery = (newSearchString: string) => {
    return {
      ...super.getStateBeforeFirstQuery(newSearchString),
      items: [],
    };
  }
}

Commands Middleware

IDHandlers
'stream-io/text-composer/commands-middleware'onChange, onSuggestionItemSelect

Commands Middleware Handlers

OnChange

The handler has the following responsibilities:

  • identify the command trigger character sequence (/ by default) at the cursor position
  • retrieve the suggestion items in case the trigger has been identified
  • pass the suggestion items into the TextComposer’s middleware state as suggestions object

After the middleware handler chain execution the suggestions would appear in the TextComposer state and thus in the UI.

onSuggestionItemSelect

The handler has the following responsibilities:

  • set the selected item to command field of TextComposer state
  • inject the whole command text representation into the TextComposer’s text state (from /g to /giphy and adjust the cursor position
  • reset suggestions back to undefined which should lead to suggestion UI disappearing

Custom Command Retrieval

By default, the command data is retrieved via CommandSearchSource instance. But it is also possible to provide custom command search source instance initiated from a class that extends CommandSearchSource.

CommandSearchSource Configuration

CommandSearchSource allows to configure the following:

  • debounceMs - states the debounce interval in milliseconds for pagination requests
  • pageSize - states the mention suggestion page size during pagination

Further we can customize how the data is retrieved, filtered, transformed and maintained.

Custom CommandSearchSource

If there is a need to redefine how the suggestion items are retrieved, we can provide a custom search source:

const searchSource = new CustomCommandSearchSource();

textComposer.middlewareExecutor.replace([
  createCommandsMiddleware(textComposer.channel, { searchSource }),
]);

The custom instance can customize the following behavior:

Custom Pagination Logic

We can customize how the searched items are retrieved by overriding the CommandSearchSource method query. If the pagination supports cursor, the next value (cursor) should also be returned. We expect it to be a string:

import { CommandSearchSource } from 'stream-chat';

class CustomCommandSearchSource extends CommandSearchSource {
  // ...
  query = async (searchQuery: string) => {
    let result;
    // custom logic...

    return {
      items: result.items,
      next: result.cursor,
    };
  }
}

Custom Results Filtering

Apply custom filtering logic to the query results before they are committed to the search source state.

import {CommandSearchSource} from 'stream-chat';
import type {CommandSuggestion} from 'stream-chat';

const commandFilter = (item: CommandSuggestion) => {
  //... custom filtering logic
}

class CustomMentionsSearchSource extends CommandSearchSource {
  // ...
  filterQueryResults = (items: CommandSuggestion[]) => {
    return items.filter(commandFilter)
  }
}

State Retention Between Searches

With a new search query the search state is reset to the initial form except for items, which are kept from the last search result until the next query results are committed to the state. This is meant to prevent UI flickering between empty state and the first page results on every text change. This behavior can be, however, changed by overriding getStateBeforeFirstQuery method:

import { CommandSearchSource } from 'stream-chat';

class CustomCommandSearchSource extends CommandSearchSource {
  // ...
  getStateBeforeFirstQuery = (newSearchString: string) => {
    return {
      ...super.getStateBeforeFirstQuery(newSearchString),
      items: [],
    };
  }
}

Add Custom Suggestion Type

To add a new suggestion type and associate it with a trigger, we need to:

  1. create a brand-new middleware and
  2. insert it at a convenient position in the execution flow
import { ChannelSearchSource } from 'stream-chat';
import type {Channel, MessageComposer, TextComposerMiddlewareExecutorState} from 'stream-chat';

type ChannelWithId = Channel & {id: string};

type ChannelMentionsMiddleware = Middleware<
  TextComposerMiddlewareExecutorState<ChannelWithId>,
  'onChange' | 'onSuggestionItemSelect'
>;
// Custom middleware for emoji suggestions
const createTextComposerChannelMiddleware: TextComposerMiddleware = (composer: MessageComposer) =>  {
  const trigger = '#';
  const channelSearchSource = new ChannelSearchSource(composer.client)
  return {
    id: 'text-composer/channels-middleware',
    handlers: {
      onChange: async ({ state, forward, next }) => {
        const { text } = state;
        if (!text) return forward();
        const newState = {};

        // identifying the trigger, removing existin suggestions ...

        return next(newState);
      },
    onSuggestionItemSelect: async ({ state, forward, next }) => {
      const { selectedSuggestion: channel } = state.change;

      return next({
        ...state,
        text: insertTextAtSelection(state.text, channel, state.selection),
        suggestions: undefined
      });
    }
  }};
};

// Register the middleware
messageComposer.textComposer.middlewareExecutor.insert({
  middleware: [createTextComposerChannelMiddleware(messageComposer)],
  position: { before: 'stream-io/text-composer/mentions-middleware' }
});

Poll Composition Middleware

PollComposer state changes and composition are processed via middleware execution. There are two middleware chain executors:

PollComposer.stateMiddlewareExecutor

Reflects the poll creation form fields validation and updates

Registers the default middleware with the following middleware factories:

FactoryReturned middleware IDRecognized handler names
createPollComposerStateMiddleware'stream-io/poll-composer-state-processing'handleFieldChange, handleFieldBlur

PollComposer.compositionMiddlewareExecutor

Validates the final poll composition before creation

Registers the default middleware with the following middleware factories:

FactoryReturned middleware IDRecognized handler names
createPollCompositionValidationMiddleware'stream-io/poll-composer-composition'compose

PollComposer State Middleware Customization

The main task of this middleware chain is to transform and validate the data of the poll being composed. Therefore, the customization will focus on specifying

  • processors - functions that manipulate the state field data
  • validators - functions that validate the field values in the current poll composition state

These are invoked when one of the fields change or a field is blurred (handlers handleFieldChangeand handleFieldBlur respectively).

The custom processor and validator functions are supplied to the middleware factory function:

const stateMiddleware = createPollComposerStateMiddleware({
  processors: customProcessors,
  validators: customValidators,
}: PollComposerStateMiddlewareFactoryOptions);

These are objects formed as:

export type PollComposerStateMiddlewareFactoryOptions = {
  processors?: {
    handleFieldChange?: Partial<
      Record<keyof PollComposerState['data'], PollCompositionStateProcessor>
    >;
    handleFieldBlur?: Partial<
      Record<keyof PollComposerState['data'], PollCompositionStateProcessor>
    >;
  };
  validators?: {
    handleFieldChange?: Partial<
      Record<keyof PollComposerState['data'], PollStateChangeValidator>
    >;
    handleFieldBlur?: Partial<
      Record<keyof PollComposerState['data'], PollStateChangeValidator>
    >;
  };
};

An example of custom validators:

const customValidators = {
  handleFieldChange: {
    name: ({ value }) => {
      if (value.length < 3)
        return { name: "Name must be at least 3 characters" };
      return { name: undefined };
    },
  },
};

A validator is responsible for validating a single field value and returns an object with a key corresponding to the field name and a value as:

  • string representing error message (in case the field value is found incorrect) or
  • undefined if the field value is considered correct

In the example above the field name is being validated inside the handleFieldChange middleware handler.

An example of custom processors:

const customProcessors = {
  handleFieldChange: {
    options: ({ value, data }) => {
      // Custom option processing logic
      return {
        options: value.map((option) => ({
          ...option,
          text: option.text.toUpperCase(),
        })),
      };
    },
  },
};

Also, processor functions return an object where the key corresponds the field name, but the value is the actual desired field value to be committed. In the example above we adjust text of every poll option to upper case.

The State Change Middleware Value

Every middleware handler run by stateMiddlewareExecutor receives state value with the following structure:

  • previousState: The state (PollComposerState) before the current change
  • nextState: The next state (PollComposerState) to be committed after successful middleware execution
  • targetFields: The fields being updated in the current change

The PollComposerState type includes:

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

Poll Composition Middleware

The composition middleware validates the final poll state before sending the poll data to the server for creation in DB (existing errors, at least one option, etc.). It verifies there are no errors and that the required fields have data. There are no customization parameters for the default middleware factory. We could replace the default middleware using known pattern however:

// appends the custom middleware
messageComposer.pollComposer.compositionMiddlewareExecutor.use({
  id: 'custom-id-you-choose',
  handlers: {
    compose: ({
      discard,
      forward,
    }: MiddlewareHandlerParams<PollComposerCompositionMiddlewareValueState>) => {
      if (customValidation()) return forward();
      return discard();
    },
  }
});

Message Composition Middleware

The custom message data can be injected via custom middleware handlers or by setting the data directly:

MessageComposer uses middleware executors to handle message composition and draft composition. The executors are available as:

  • MessageComposer.compositionMiddlewareExecutor - for message composition
  • MessageComposer.draftCompositionMiddlewareExecutor - for draft composition

Default Middleware

MessageComposerMiddlewareState Structure

The MessageComposerMiddlewareState is the core state object that middleware operates on. It has three main fields:

export type MessageComposerMiddlewareState = {
  message: Message | UpdatedMessage;
  localMessage: LocalMessage;
  sendOptions: SendMessageOptions;
};

Fields Explanation

  1. localMessage: MessageResponse

    • Used for local channel state updates
    • Contains the message data that will be shown in the UI immediately
    • Can include temporary data or UI-specific properties
    • Example: Adding temporary IDs, local timestamps, or UI-specific flags
  2. message: MessageResponse

    • Contains the data that will be sent to the backend
    • Should match the expected backend message structure - Message type
    • Used for actual message creation/update
    • Example: Final message content, attachments, metadata
  3. sentOptions: SendMessageOptions | UpdateMessageOptions

    • Contains options for sending/updating the message
    • Must match either SendMessageOptions or UpdateMessageOptions type

Composition Middleware Executor

The composition middleware executor registers the following middleware in order:

OrderFactoryReturned middleware IDHandlerRole
1.createTextComposerCompositionMiddleware'stream-io/message-composer-middleware/text-composition'composeAdds text to the composition. Adjusts mentioned users array based on the current text
2.createAttachmentsCompositionMiddleware'stream-io/message-composer-middleware/attachments'composeAdd the AttachmentManager.attachments into the attachments array of both localMessage and message. Discards the composition process if there are any unfinished uploads in progress.
3.createLinkPreviewsCompositionMiddleware'stream-io/message-composer-middleware/link-previews'composeIncludes loaded link previews in the attachments array. Determines whether link enrichment should be skipped server-side.
4.createSharedLocationCompositionMiddleware'stream-io/message-composer-middleware/shared-location'composeEnriches localMessage and message with shared_location.
5.createMessageComposerStateCompositionMiddleware'stream-io/message-composer-middleware/own-state'composeEnriches localMessage and message with relevant MessageComposer state values (poll_id, quoted_message_id, show_in_channel).
6.createCustomDataCompositionMiddleware'stream-io/message-composer-middleware/custom-data'composeEnriches the composition with CustomDataManager.customMessageData.
7.createCompositionValidationMiddleware'stream-io/message-composer-middleware/data-validation'composeEnforces text config parameter maxLengthOnSend. Discards composition if it can be considered empty or the data has not been changed locally (meaning the update took place based on message.updated and draft.updated WS events).
8.createCompositionDataCleanupMiddleware'stream-io/message-composer-middleware/data-cleanup'composeConverts the message payload from Message to UpdatedMessage. Converts the sendOptions from SendMessageOptions to UpdateMessageOptions.

Draft Composition Middleware Executor

The draft composition middleware executor (messageComposer.draftCompositionMiddlewareExecutor) registers:

OrderFactoryReturned middleware IDHandlerRole
1.createDraftTextComposerCompositionMiddleware'stream-io/message-composer-middleware/draft-text-composition'composeAdds text to the composition. Adjusts mentioned users array based on the current tex
2.createDraftAttachmentsCompositionMiddleware'stream-io/message-composer-middleware/draft-attachments'composeAdds the AttachmentManager.attachments into the attachments array of both localMessage and message. Does not discard the composition process if there are any uploads in progress - just includes the successful uploads
3.createDraftLinkPreviewsCompositionMiddleware'stream-io/message-composer-middleware/draft-link-previews'composeIncludes loaded link previews in the attachment array
4.createDraftMessageComposerStateCompositionMiddleware'stream-io/message-composer-middleware/draft-own-state'composeEnriches localMessage and message with relevant MessageComposer state values (poll_id, quoted_message_id, show_in_channel).
5.createDraftCustomDataCompositionMiddleware'stream-io/message-composer-middleware/draft-custom-data'composeEnriches the composition with CustomDataManager.customMessageData.
6.createDraftCompositionDataCleanupMiddleware'stream-io/message-composer-middleware/draft-data-validation'composeVerifies whether draft can be created and discards the process if the draft cannot be created (is empty).

Message Composition Customization

Custom Message Data Transformation

We can add custom transformation logic by adding a new composition middleware ( contains compose handler):

import type {
  MessageCompositionMiddleware,
  MessageComposerMiddlewareValueState,
  MiddlewareHandlerParams,
} from "stream-chat";

const createCustomCompositionMiddleware = (
  composer: MessageComposer,
): MessageCompositionMiddleware => ({
  id: "message-composer/message-composer/custom-message-data",
  handlers: {
    compose: async ({
      state,
      next,
      forward,
    }: MiddlewareHandlerParams<MessageComposerMiddlewareValueState>) => {
      if (!composer.textComposer) return forward();

      const { mentionedUsers, text } = composer.textComposer;

      // Create new state objects
      const newLocalMessage = {
        ...state.localMessage,
        custom_ui_flag: true,
      };

      const newMessage = {
        ...state.message,
        custom_field: "value",
        mentioned_users: mentionedUsers.map((u) => u.id),
        text: text.toUpperCase(),
      };

      // Pass updated state to next middleware
      return next({
        ...state,
        localMessage: newLocalMessage,
        message: newMessage,
      });
    },
  },
});

Insert the middleware into the correct middleware executor chain at a correct position:

messageComposer.compositionMiddlewareExecutor.insert({
  middleware: [createCustomCompositionMiddleware(messageComposer)],
  position: {after: 'stream-io/message-composer-middleware/data-cleanup'}
});

Draft Composition Customization

Similarly, we can customize draft composition:

import type {
  MessageDraftCompositionMiddleware,
  MessageDraftComposerMiddlewareValueState,
  MiddlewareHandlerParams,
} from "stream-chat";

const createCustomDraftMiddleware = (
  composer: MessageComposer,
): MessageDraftCompositionMiddleware => ({
  id: "message-composer/message-composer/custom-draft",
  handlers: {
    compose: async ({
      state,
      next,
      forward,
    }: MiddlewareHandlerParams<MessageDraftComposerMiddlewareValueState>) => {
      if (!composer.textComposer) return forward();

      const { text } = composer.textComposer;

      // Create new draft state
      const newDraft = {
        ...state.draft,
        custom_field: "draft_value",
        text: text.toLowerCase(),
      };

      // Pass updated state to next middleware
      return next({
        ...state,
        draft: newDraft,
      });
    },
  },
});

Insert the middleware into the correct middleware executor chain at a correct position:

messageComposer.draftCompositionMiddlewareExecutor.insert({
  middleware: [createCustomDraftMiddleware(messageComposer)],
  position: {after: 'stream-io/message-composer-middleware/draft-data-validation'}
});

Middleware Management

Registering Custom Middleware

When adding custom middleware, you can insert it at specific positions using the middleware IDs:

// Add middleware after text composition
messageComposer.compositionMiddlewareExecutor.insert({
  middleware: [customCompositionMiddleware],
  position: { after: "stream-io/message-composer-middleware/text-composition" },
});

// Add middleware before draft composition
messageComposer.draftCompositionMiddlewareExecutor.insert({
  middleware: [customDraftMiddleware],
  position: {
    before: "stream-io/message-composer-middleware/draft-text-composition",
  },
});

// Replace existing middleware
messageComposer.compositionMiddlewareExecutor.replace([newMiddleware]);

// Set middleware order
messageComposer.compositionMiddlewareExecutor.setOrder([
  "stream-io/message-composer-middleware/text-composition",
  "custom-composition",
  "stream-io/message-composer-middleware/attachments",
  "stream-io/message-composer-middleware/link-previews",
  "stream-io/message-composer-middleware/custom-data",
  "stream-io/message-composer-middleware/data-validation",
  "stream-io/message-composer-middleware/data-cleanup",
]);

Emoji Suggestions Middleware

Triggering emoji suggestion while typing is handled by TextComposer middleware. The middleware is not enabled by default. To enable it we have to import it from stream-chat-react-native plugin and register as follows:

import { createTextComposerEmojiMiddleware } from "stream-chat-react-native";
import type { TextComposerMiddleware } from "stream-chat";

import { init, SearchIndex } from "emoji-mart";
import data from "@emoji-mart/data";

init({ data });

const Component = () => {
  useEffect(() => {
    if (!chatClient) return;

    chatClient.setMessageComposerSetupFunction(({ composer }) => {
      composer.textComposer.middlewareExecutor.insert({
        middleware: [
          createTextComposerEmojiMiddleware({
            emojiSearchIndex: SearchIndex,
          }) as TextComposerMiddleware,
        ],
        position: { before: "stream-io/text-composer/mentions-middleware" },
        unique: true,
      });
    });
  }, [chatClient]);
};

We can customize the middleware in the following ways:

Change Trigger or Minimum Trigger Characters

textComposer.middlewareExecutor.use([
  createTextComposerEmojiMiddleware(SearchIndex, {
    minChars: 3,
    trigger: "__",
  }),
]);

Provide Custom Search Source

If there is a need to redefine how the suggestion items are retrieved, we can provide a custom search source:

const searchSource = new CustomSearchSource();

textComposer.middlewareExecutor.use([
  createTextComposerEmojiMiddleware(searchSource),
]);

Command UI Middleware

The command state is set in the TextComposer through the createCommandsMiddleware. This middleware handles command-related functionalities, including command suggestions and command execution.

The command state can be used to display a custom command UI in the message input. The command state is an object that contains the command response.

The stream-chat provides following middlewares for command processing in the Message Composer to show it in the UI:

  • createActiveCommandGuardMiddleware(stream-io/text-composer/active-command-guard) - This middleware ensures that if the command state is set, the input will complete the execution with the current state.
  • createCommandStringExtractionMiddleware(stream-io/text-composer/command-string-extraction) - This extracts the command string from the input text. Eg: /ban user will be trimmed to user.
  • createCommandInjectionMiddleware(stream-io/message-composer-middleware/command-string-injection) - This injects the command string into the message composer state. Eg: user will be sent as /ban user when the message is composed.
  • createDraftCommandInjectionMiddleware(stream-io/message-composer-middleware/draft-command-string-injection) - This injects the command string into the draft state. Eg: user will be sent as /ban user when the draft is saved.

© Getstream.io, Inc. All Rights Reserved.