Info:
This is release candidate documentation for Stream Chat React Native SDK v10. For the latest stable version, see the latest version (v9).

Typing indicators

Typing indicators show who is writing a message right now. This page covers the typing events behind them, when they are sent, and how to change the indicator.

What users get

Bob starts typing in a channel. On the user's screen, his avatar and three animated dots appear at the bottom of the message list. With more people typing, up to three avatars show. In a thread, the indicator shows who is typing a reply in that thread.

The channel list shows typing too: the row of a channel with a name reads "Bob is typing", "Bob and Cleo are typing", or "3 people are typing", in place of the last message. The row of a channel without a name, such as a one-to-one conversation, reads "Typing".

Nobody sees their own typing.

The bottom of a message list with the typing indicator, the avatar of the person typing and three dots on iOS
The bottom of a message list with the typing indicator, the avatar of the person typing and three dots on Android

How it works across the layers

  • State. Channel.keystroke() and Channel.stopTyping() send typing.start and typing.stop events. When another member's event arrives, the channel stores it in channel.state.typing, keyed by user id. That map is the only place typing lives.
  • UI. The text composer calls Channel.keystroke() as the user types. MessageList and MessageFlashList render TypingIndicator inside TypingIndicatorContainer at the newest end of the list. Both read channel.state.typing, filtered to the list they're in: events without a parent_id in a channel, and the thread's own events inside a Thread. ChannelPreview renders ChannelPreviewTypingIndicator from the channel's own events.
  • Navigation. Not involved.

The SDK has no channel header. For a header of your own, useTypingString() returns "Bob is typing" or "Bob and 2 more are typing" for the list the component is in, see Customizing.

What channels and threads hold explains the rest of channel.state. Channel UI shows where the indicators sit.

Typing events

A typing event belongs to a channel. One sent from a thread carries the thread's id as parent_id, so the same event can show in the channel or in one of its threads.

Sending typing events

Info:

Example

The user types "Hello" in the composer:

  1. Their first keystroke calls Channel.keystroke(), which sends typing.start.
  2. Their next keystrokes call it again, but it sends nothing until two seconds have passed since the last typing.start.
  3. They stop typing. A second later, the client sends typing.stop by itself.
  4. If they send the message instead, message operations send typing.stop before the message.

In a thread, the composer calls Channel.keystroke(thread.id) and the events carry parent_id. The typing.stop the client sends by itself after a pause carries no parent_id; the channel removes the user's entry either way.

When the app goes to the background while the user is typing, Channel and Thread send typing.stop for the channel or thread they render.

A custom input that writes through messageComposer.textComposer sends typing events the same way. An input that doesn't use the composer calls the channel directly:

// On every change of the input: sends typing.start at most every two seconds
await channel.keystroke();

// When the user sends or abandons the message
await channel.stopTyping();

// The same for a thread
await channel.keystroke(thread.id);
await channel.stopTyping(thread.id);

Timing

  • typing.start goes out at most once every two seconds while the user keeps typing.
  • typing.stop goes out when the user hasn't typed for more than a second, and before a message is sent. The client checks every half second.
  • Another member's entry is dropped from channel.state.typing seven seconds after their last typing.start arrived, in case their typing.stop never comes. The client then raises a local typing.stop event.
  • The indicators follow the map: they show while someone is in it, and hide as soon as nobody is.

When typing events are suppressed

Channel.keystroke() and Channel.stopTyping() send nothing when:

  • typing events are off for the channel, by the client configuration or by the channel type, see Turning it on or off;
  • the user's privacy_settings.typing_indicators.enabled is false;
  • the WebSocket connection isn't healthy;
  • the channel has no id from the server yet.

The composer also skips Channel.keystroke() when the text is empty, and when its text.publishTypingEvents setting is false.

Turning it on or off

Typing events are on when both of these allow them:

  • the channel type's typing_events setting on the server, see Typing indicators in the JavaScript client docs;
  • channel.typingEvents.enabled in the client configuration, which defaults to true.

A server "no" wins. The resolved value is channel.config.typingEvents.enabled. When it's false, nothing is sent, so no indicator shows.

To turn typing events off in the client for every channel:

client.config.set({ channel: { typingEvents: { enabled: false } } });

To keep showing others' typing but stop sending it from some composers, change text.publishTypingEvents on those composers. This setup function turns it off for thread composers:

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  if (composer.threadId) {
    composer.updateConfig({ text: { publishTypingEvents: false } });
  }
});

There is one setup function per kind, so this one replaces any other messageComposer setup function, such as one that calls setupCommandUIMiddlewares or replaces the mention search. Do all composer setup in one function.

To hide the indicator in a message list and keep the events, pass disableTypingIndicator to Channel and Thread.

A user can also stop sharing their typing. With privacy_settings.typing_indicators.enabled: false on their user, the client sends no typing events and the server drops any that reach it:

import { useCreateChatClient } from "stream-chat-react-native";

const apiKey = "YOUR_API_KEY";
const userId = "YOUR_USER_ID";
const userToken = "YOUR_USER_TOKEN";

export const useClientWithoutTypingEvents = () =>
  useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: {
      id: userId,
      privacy_settings: { typing_indicators: { enabled: false } },
    },
  });

Configuration explains client.config and setup functions.

Customizing

The indicator in the list

Replace the list's indicator through the TypingIndicator override key. TypingIndicatorContainer renders it only while someone other than the user is typing in that list, so the replacement doesn't need to check. This one shows text instead of avatars:

import { Text } from "react-native";
import { useTypingString, WithComponents } from "stream-chat-react-native";

const TypingText = () => {
  const typingString = useTypingString();

  return <Text>{typingString}...</Text>;
};

export const App = () => (
  <WithComponents overrides={{ TypingIndicator: TypingText }}>
    {/* the rest of the app */}
  </WithComponents>
);

TypingIndicatorContainer is an override key too, for the space around the indicator. A HeaderComponent passed to MessageList replaces the list's newest-end header, including the typing indicator.

Typing in a header

useTypingString() works anywhere inside Channel or Thread. This header subtitle shows who is typing, and the channel's display name otherwise:

import { Text } from "react-native";
import {
  useChannel,
  useChannelPreviewDisplayName,
  useTypingString,
} from "stream-chat-react-native";

export const HeaderSubtitle = () => {
  const channel = useChannel();
  const displayName = useChannelPreviewDisplayName(channel);
  const typingString = useTypingString();

  return <Text numberOfLines={1}>{typingString || displayName}</Text>;
};

The Channel header UI cookbook builds a header around it.

The channel list row

ChannelPreviewTypingIndicator is an override key. It receives the channel and usersTyping, the users typing in the channel itself.

Texts

The texts come from translation keys: messageList.typing.withMoreUsers.label and channelPreview.typing.withUser.label for useTypingString(), and the other channelPreview.typing.* keys for the channel list. See Localization.

Reference