Commands

A command is a message that starts with / and a command's name, such as /giphy cats or /ban @ann spam. The server runs the command when the message arrives. Each channel type has its own list of commands, which the client reads from channel.config.availableCommands.

Info:

Example

The user wants to send a GIF of cats:

  1. The user types / and picks Giphy from the suggestions.
  2. The composer keeps Giphy as the active command, apart from the text, and sets aside anything the user had typed. The UI can show the command as a chip.
  3. The user types "cats" and sends. The message goes out as /giphy cats, and the server answers with a GIF.

The active command

Command suggestions open when the user types / at the start of the text. They list the channel type's commands from channel.config.availableCommands, matched locally, without a request: the names that contain what was typed, those that start with it first. Suggestions explains how suggestions work in general.

The active command is in textComposer.command, or null when there is none.

Picking a command sets textComposer.command. The composer then saves its content and empties itself, keeping only the command, so the user types the command's arguments on their own. Typing a whole command and then a space, as in /giphy cats, sets it too, and moves the command's name out of the text, so the text keeps cats. A typed command leaves the rest of the composer's content as it is. TextComposer.clearCommand() drops the command and puts the saved attachments, poll and other content back. When TextComposer.clearCommand() drops the command, a command picked from the suggestions comes back with an empty text. A command set with TextComposer.setCommand() brings back the text the composer had when it was called.

Saving and restoring the content are the built-in command.activate and command.clear effects, which Effect handlers explains.

The command middlewares

The active command is kept apart from the text, and goes back in front of it when the message is sent. Four middlewares do that, and stream-chat installs them in every composer:

Factory Middleware id What it does
createActiveCommandGuardMiddleware() stream-io/text-composer/active-command-guard While a command is active, stops the text chain before command suggestions, so no other command is suggested.
createCommandStringExtractionMiddleware() stream-io/text-composer/command-string-extraction Takes a typed command's name out of the text: /ban @ann spam keeps @ann spam.
createCommandInjectionMiddleware(composer) stream-io/message-composer-middleware/command-string-injection Puts the command back in front of the text when the message is sent: cats goes out as /giphy cats.
createDraftCommandInjectionMiddleware(composer) stream-io/message-composer-middleware/draft-command-string-injection Does the same when a draft is saved.

The two injection middlewares leave text that already starts with the command as it is, so the command never goes out twice. Built-in chains shows where each one runs.

A UI without a command chip

The default composer shows the active command as a chip beside the text. A composer of your own that shows the command in the text instead removes all four middlewares, so the text keeps /giphy cats and is sent as typed. Remove them in the composer setup function:

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  composer.textComposer.middlewareExecutor.remove([
    "stream-io/text-composer/active-command-guard",
    "stream-io/text-composer/command-string-extraction",
  ]);
  composer.compositionMiddlewareExecutor.remove(
    "stream-io/message-composer-middleware/command-string-injection",
  );
  composer.draftCompositionMiddlewareExecutor.remove(
    "stream-io/message-composer-middleware/draft-command-string-injection",
  );
});

Remove all four, not only the two in the text chain. With the injection middlewares left in, the command stays active after the user deletes /giphy from the text, and cats still goes out as /giphy cats.

Without the injection middlewares, a command set with TextComposer.setCommand(), for example from a Commands menu, isn't added to the text. Such a UI writes the command's name and a space, such as /giphy followed by a space, into the text itself.

Sending a command

Some commands can't be used in some messages:

  • While a message is being edited, no command can be used.
  • While the message quotes another message, moderation commands can't be used.

MessageComposer.isCommandDisabled(command) tells whether a command is disabled. A disabled command can't be picked or sent. The composer publishes a notification instead, with severity warning and type validation:command:disabled.

Each command in channel.config.availableCommands has an args field: the help text for its arguments, which the command suggestions show. The built-in commands all take arguments, so the message needs text after the command's name, such as cats after /giphy.

A command your server defines with empty args takes no arguments, and the command alone is the message. For example, a /shrug command that your server answers with "¯\_(ツ)_/¯". While such a command is active, textComposer.activeCommandIsContent is true, so the composer counts the command as content, and the user can send it with no text. Custom commands shows how to define a command and its args on the server.

Some commands also need more than their name before they can be sent. /ban, /mute, /unmute and /unban need a mentioned user, and /ban needs a reason too. Sending such a command too early publishes a notification with severity warning and type validation:command:not-ready, and MessageComposer.send() returns 'nothing-to-send'. composer.isCommandSendable tells whether the active command is ready.

To change those rules, register a sendValidator. Return { command, ready: false, reason } to hold a command back, and the default validator's answer for the rest:

import type { CommandSendValidator } from "stream-chat";
import { defaultCommandSendabilityValidator } from "stream-chat";

const sendValidator: CommandSendValidator = (context) => {
  const { command, commandArgsText } = context;

  if (command.name === "giphy" && !commandArgsText) {
    return { command, ready: false, reason: "missing-query" };
  }

  return defaultCommandSendabilityValidator(context);
};

client.config.set({ messageComposer: { commands: { sendValidator } } });

CommandSearchSource loads the command suggestions, and also recognizes a command typed in full. To change which commands are offered, replace the commands middleware with one built on a search source of your own. Pass the composer too: without it, a disabled command isn't refused when it's picked or typed.

import type { TextComposerMiddleware } from "stream-chat";
import { CommandSearchSource, createCommandsMiddleware } from "stream-chat";

const HIDDEN_COMMANDS = ["ban", "unban"];

class WithoutBanSearchSource extends CommandSearchSource {
  // `query()` serves both the suggestions and a command typed in full.
  query(searchQuery: string) {
    const { items, next } = super.query(searchQuery);
    return {
      items: items.filter(({ name }) => !HIDDEN_COMMANDS.includes(name)),
      next,
    };
  }
}

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  const { channel, textComposer } = composer;

  // Same id as the built-in commands middleware, so it takes its place.
  textComposer.middlewareExecutor.replace([
    createCommandsMiddleware(channel, {
      composer,
      searchSource: new WithoutBanSearchSource(channel),
    }) as TextComposerMiddleware,
  ]);

  return () =>
    textComposer.middlewareExecutor.replace([
      createCommandsMiddleware(channel, { composer }) as TextComposerMiddleware,
    ]);
});

A hidden command typed in full goes out as plain text, and the server still runs it. To turn a command off for good, remove it from the channel type.

Pitfalls

  • A typed command leaves the text. In a composer without a command chip, /giphy disappears as soon as the user types it. Remove the command middlewares, as A UI without a command chip shows.
  • A command can't be added on the client. channel.config.availableCommands is the channel type's list as the server sends it.

Used in