Commands and Giphy

Commands are messages that start with a slash, such as /giphy cats or /mute @bob, which the server runs instead of posting as text. This page covers how users pick a command in the composer, how a picked command is sent, and the Giphy preview.

What users get

The user types / at the start of the input. A list titled "Instant Commands" shows the commands of the channel's type, each with its name and its arguments, such as /giphy [text] and /ban [@username] [text]. The attachment picker has the same list behind its commands button. When they pick giphy, the composer shows a GIPHY chip before the input and empties itself. If they remove the chip with its close button, the files and other content they had added come back. The text they typed to find the command doesn't. Had they picked it from the attachment picker, their text would come back too. While the chip shows, the attach button is hidden and the send button shows a checkmark.

They type cats and send. A Giphy preview appears in the list, marked "Only visible to you", with three buttons: Send posts the GIF to the channel, Shuffle shows another one, and Cancel removes the preview.

Some commands need more before they can be sent: /ban, /mute, /unmute and /unban need a mentioned user, and /ban a reason too. Sending one too early shows a warning, and nothing is sent. No command can be used while a message is being edited, and moderation commands can't be used in a reply that quotes a message. A disabled command shows greyed out in the list.

How it works across the layers

  • State. The channel type's commands are in channel.config.availableCommands. The text composer's commands middleware suggests them after / and keeps the picked one in textComposer.command. The server runs a command when a message's text starts with it.
  • UI. AutoCompleteSuggestionList shows the suggestions, the attachment picker's commands button shows the same list, and InputView shows the chip next to the input. In the message list, Giphy renders the preview with its buttons.
  • Layout. Not involved.

Picking a command

  1. The user types /gi. The commands middleware matches gi against availableCommands, locally and without a request, and opens the suggestions.
  2. They pick giphy. textComposer.command is set, and the command.activate effect saves what the composer held and empties it, keeping only the command. From the attachment picker, the SDK calls TextComposer.setCommand(command) and focuses the input.
  3. They type cats. While a command is active, mentions still work, so /mute @bob can be completed.
  4. On send, the composer validates the command (see Commands) and sends the message. Removing the chip instead calls TextComposer.clearCommand(), which brings back what was saved.

In step 4 the command name goes back in front of the text, so the message is sent as /giphy cats. The four command middleware that stream-chat installs in every composer do that, see Commands in the text instead of a chip. Commands explains them.

The Giphy preview

  1. The server answers /giphy cats with an ephemeral message: type: "ephemeral", command: "giphy", and an attachment of type giphy with three actions. Only the user receives it, and it takes the place of their sent message in the list.
  2. Attachment renders it with Giphy, which shows the GIF and a button for each action. A message whose attachment has actions shows no text, and can't be long-pressed for the message menu.
  3. Each button calls the message's handleAction(name, value), which calls Channel.sendAction(message.id, { [name]: value }) with the action's value: send, shuffle or cancel.
  4. When the response carries a message, it replaces the preview: another GIF after Shuffle, the posted message after Send. After Cancel it carries none, and the preview is removed from the list.

The GIF shows the giphyVersion rendition, fixed_height by default. Set it on Channel and Thread for the message list, and on OverlayProvider for the image gallery.

Turning it on or off

Which commands exist

The server decides. Each channel type has a list of commands, set in the Stream Dashboard or through the server API. The client can't add to it. channel.config.availableCommands is the server's list as it is. With an empty list, no suggestions open and the attachment picker has no commands button.

Commands in the text instead of a chip

By default, the composer shows the active command as a chip beside the text, and sends it in front of the text:

The user enters The composer shows It sends
/giphy cats, typed in full cats, beside the chip /giphy cats
giphy picked from the suggestions or the attachment picker, then cats cats, beside the chip /giphy cats

A draft saved while a command is active keeps the command the same way. A composer of your own that shows the command in the text, with no chip, removes the four command middleware by their ids. They are stream-io/text-composer/active-command-guard, stream-io/text-composer/command-string-extraction, stream-io/message-composer-middleware/command-string-injection and stream-io/message-composer-middleware/draft-command-string-injection. Commands shows how.

Hide commands in the UI

hasCommands={false} on Channel and Thread hides the attachment picker's commands button. The suggestions after / come from the commands middleware, so remove it in your composer setup function:

import type { StreamChat } from "stream-chat";

export const hideCommandSuggestions = (client: StreamChat) =>
  client.config.setSetupFunction("messageComposer", ({ composer }) => {
    composer.textComposer.middlewareExecutor.remove(
      "stream-io/text-composer/commands-middleware",
    );
  });

The server still runs a command a user types in full, so to turn a command off for good, remove it from the channel type.

Customizing

When a command can be sent

messageComposer.commands.sendValidator decides whether the active command is ready, for example to require a search term after /giphy. Commands has the example.

The command chip

The chip has no override key of its own. InputView renders it next to the text input, AutoCompleteInput, and the also-send-to-channel checkbox of a thread under them. This InputView shows the active command as a line of text above the input, which clears it when tapped:

import { Pressable, StyleSheet, Text, View } from "react-native";
import type { TextComposerState } from "stream-chat";
import {
  AutoCompleteInput,
  ShowThreadMessageInChannelButton,
  useMessageComposer,
  useStateStore,
} from "stream-chat-react-native";
import type {
  ComponentOverrides,
  InputViewProps,
} from "stream-chat-react-native";

const commandSelector = ({ command }: TextComposerState) => ({ command });

const InputViewWithCommandLine = (props: InputViewProps) => {
  const { textComposer } = useMessageComposer();
  const { command } = useStateStore(textComposer.state, commandSelector);

  return (
    <View style={styles.container}>
      {command ? (
        <Pressable
          accessibilityLabel={`Remove /${command.name}`}
          onPress={() => textComposer.clearCommand()}
        >
          <Text style={styles.command}>/{command.name}</Text>
        </Pressable>
      ) : null}
      <AutoCompleteInput {...props} />
      <ShowThreadMessageInChannelButton />
    </View>
  );
};

// Pass to `WithComponents`, declared once at module scope.
export const overrides: ComponentOverrides = {
  InputView: InputViewWithCommandLine,
};

const styles = StyleSheet.create({
  command: { fontWeight: "600", paddingHorizontal: 8 },
  container: { flex: 1 },
});

The Commands UI cookbook builds a fuller command UI.

Command suggestions

AutoCompleteSuggestionList, AutoCompleteSuggestionHeader and AutoCompleteSuggestionItem replace the suggestion list, its header and its items. CommandSuggestionItem is the SDK's command row, if you want to reuse it. The header's "Instant Commands" isn't a translation key, so replace AutoCompleteSuggestionHeader to change it. The Custom suggestions UI cookbook shows the list and its items.

Replacing the Giphy preview

Giphy is an override key, and receives the attachment. A Giphy of your own reads handleAction from useMessageContext() and calls it with an action's name and value to run it, the way the SDK's buttons do. Without actions, the attachment is a posted GIF.

Reference