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

Flagging, muting, blocking and bans

Four tools keep conversations safe, and they differ in who uses them and whom they affect:

  • Flagging reports a message or a user to the app's moderators.
  • Muting silences a user, or a channel, for the person who mutes.
  • Blocking cuts off a user's direct messages to the person who blocks.
  • Banning stops a user from posting, in one channel or in the whole app. Moderators ban; a shadow ban hides the user's messages without telling them.

This page covers what users see of each one, what it calls in the state layer, and how to change the actions.

What users get

The user long-presses a message from Bob. Besides the other message actions, the menu has:

  • Flag Message, which asks "Do you want to send a copy of this message to a moderator for further investigation?" and, once they confirm, says "Message has been successfully flagged";
  • Mute User, which mutes Bob and says "Bob has been muted". Once Bob is muted, the action reads Unmute User;
  • Ban User, when the user may ban members, which bans Bob. Once he's banned, it reads Unban User;
  • Block User, which asks them to confirm and blocks Bob. Once he's blocked, it reads Unblock User.

The same tools appear in other places:

  • Channel list rows have a swipe button that mutes the channel. In a direct message channel, the row's bottom sheet has Mute User and Block User for the other member. See Channel lists.
  • Channel details have a mute switch for the channel. In a direct message channel they also have a Mute User switch and Block User. The sheet that opens on a member has Mute User and Block User. See Channel details and members.

Moderation also shows in the message list. When moderation removes a message, its author sees a small centered notice in its place, with the text the server sent, such as "Message was blocked by moderation policies". A message that moderation bounces back to its author offers to send it anyway, edit it or delete it; see Bounced messages. The messages of a shadow-banned user don't show for anyone else.

How it works across the layers

State: what each tool calls

Every tool is a call to the server. None of them changes local state before the server answers: the state that follows each one changes with the response or with the server's event.

Tool What it calls What changes on the client
Flag a message client.moderation.flagMessage({ message_id, reason?, ...options }) Nothing. The flag goes to the moderation queue.
Flag a user client.moderation.flagUser({ user_id, reason?, ...options }) Nothing
Mute a user client.moderation.mute({ target_ids, timeout? }), client.moderation.unmute({ target_ids }) client.mutedUsersStore, when notification.mutes_updated arrives
Mute a channel Channel.mute(), Channel.unmute() channel.state.muteStatus, when notification.channel_mutes_updated arrives
Block a user client.blockUser(userId), client.unblockUser(userId) client.blockedUsers, when the response arrives
Ban in a channel Channel.banUser(userId, options), Channel.unbanUser(userId) The member's banned or shadow_banned in channel.state.members, when user.banned arrives
Ban in the whole app client.moderation.ban({ target_user_id, ... }), client.unbanUser(userId) No client state of its own

The sections below explain each tool.

Flagging

A flag sends a message or a user to the moderation queue, where moderators review it in the Stream Dashboard. The reporting user's view doesn't change. Pass a reason to help moderators sort the queue:

await client.moderation.flagMessage({ message_id: message.id, reason: "spam" });
await client.moderation.flagUser({ user_id: "bob", reason: "harassment" });

Flag Message calls client.moderation.flagMessage() with no reason. Nothing in the SDK flags a user; call client.moderation.flagUser() from your own UI.

Muting users

A muted user's messages still arrive and still show in the message list: the SDK doesn't hide them. They don't count as unread, and the server sends no push notifications for them. timeout mutes for that many minutes:

// Mute Bob for one day.
await client.moderation.mute({ target_ids: ["bob"], timeout: 24 * 60 });

await client.moderation.unmute({ target_ids: ["bob"] });

The user's mute list is client.mutedUsersStore, a store whose mutedUsers holds one entry per muted user, with the user as target. client.userMuteStatus(userId) reads it once, and the useMutedUsers() hook follows it in a component:

src/MutedUsersList.tsx
import { Button, FlatList, StyleSheet, Text, View } from "react-native";
import { useChatContext, useMutedUsers } from "stream-chat-react-native";

export const MutedUsersList = () => {
  const { client } = useChatContext();
  const mutedUsers = useMutedUsers();

  return (
    <FlatList
      data={mutedUsers}
      keyExtractor={(mute) => mute.target?.id ?? ""}
      renderItem={({ item: { target } }) => (
        <View style={styles.row}>
          <Text>{target?.name ?? target?.id}</Text>
          <Button
            onPress={() => {
              if (target) {
                void client.moderation.unmute({ target_ids: [target.id] });
              }
            }}
            title="Unmute"
          />
        </View>
      )}
    />
  );
};

const styles = StyleSheet.create({
  row: {
    alignItems: "center",
    flexDirection: "row",
    justifyContent: "space-between",
    padding: 16,
  },
});

When the server's notification.mutes_updated arrives, the store changes and the list follows.

Muting channels

A muted channel's new messages don't count as unread, and its row in the channel list shows a mute icon and no unread badge. Channel.mute({ expiration }) mutes for that many milliseconds. What channels and threads hold explains muteStatus.

Blocking

Blocking is personal: it changes what the blocking user gets, not what anyone else sees. The server hides the direct message channels between the two users from the one who blocked, and stops events and push notifications from the blocked user in them. Group channels aren't affected, and the blocked user's messages there still show. Block in the Chat API docs lists every effect.

await client.blockUser("bob");
await client.unblockUser("bob");

// The blocked users, with each user's details.
const { blocks } = await client.getBlockedUsers();
console.log(blocks.map(({ blocked_user }) => blocked_user.name));

The ids of the blocked users are in the client.blockedUsers store, as userIds. The client fills it when it connects, and client.blockUser(), client.unblockUser() and client.getBlockedUsers() update it after the server answers.

Bans and shadow bans

A ban stops a user from posting. Channel.banUser(userId, options) bans them from one channel, and needs the ban-channel-members capability. Its options are optional fields, so pass {} for none:

  • reason, an explanation for the ban;
  • timeout, the ban's length in minutes;
  • shadow: true, for a shadow ban.
// Ban Bob from this channel for an hour.
await channel.banUser("bob", { reason: "Spam", timeout: 60 });

// Shadow ban: Bob can still post, and isn't told about the ban.
await channel.banUser("bob", { shadow: true });

await channel.unbanUser("bob");

client.moderation.ban({ target_user_id }) bans a user from the whole app, and client.unbanUser(userId) lifts the ban. Apps usually ban app-wide from the server. Ban in the Moderation docs covers bans, shadow bans and their options.

The SDK doesn't check bans itself: the server rejects a banned user's messages, and the message list shows them as failed. Messages from a shadow-banned user reach other users marked shadowed. The client leaves them out of the message list and doesn't count them as unread.

UI: the actions and the removed message

The message actions come from the messageActions function on Channel and Thread, by default the SDK's own. Each action reads the message it was built for and calls the state layer:

Action actionType What it calls Shown when
Flag Message flagMessage Asks to confirm, then client.moderation.flagMessage({ message_id: message.id }) Not the user's own message, and flag-message
Mute User, Unmute User muteUser client.moderation.mute({ target_ids }) or client.moderation.unmute({ target_ids }) for the author Not the user's own message
Ban User, Unban User banUser client.moderation.ban({ target_user_id }) or client.unbanUser() for the author Not the user's own message, and ban-channel-members
Block User, Unblock User blockUser Asks to confirm a block, then client.blockUser() or client.unblockUser() for the author Not the user's own message

Ban User calls client.moderation.ban() without a channel, so it bans the author from the whole app, and the server checks that the user may do that. It reads Unban User when the message's user.banned is set.

Flag Message and Mute User report their result as a notification in the message list. Ban User and Block User report nothing. Message actions lists every message action.

The channel list rows and channel details have their own actions, which call the same methods. Their tables are on Channel lists and Channel details and members. The user actions behind them are also a hook: useUserActions(user) returns muteUser, unmuteUser, blockUser and unblockUser for any user, each publishing a notification. See Running channel and user actions.

Message renders MessageBlocked instead of the message when it's an error message whose moderation action is remove. The default shows the message's text in a small centered notice. It opens no menu: tapping and long-pressing it do nothing. isBlockedMessage(message) is the same check, exported for your own components.

Channel types can also offer the /ban, /unban, /mute and /unmute commands in the composer; see Commands and Giphy.

Turning it on or off

These tools are always available. Who sees each action depends on the user's capabilities in the channel, which the server derives from their roles and the channel type's permissions:

  • Flag Message needs flag-message.
  • Ban User needs ban-channel-members.
  • Mute User and Block User need no capability, in the message menu, in the channel list and in channel details, because they're personal. The channel list's mute button and the channel details switch check no capability either.

The channel type's settings and the moderation policies on the server decide the rest, such as which messages moderation removes. See Moderation for Chat and Permissions reference.

Each rule of a moderation policy has an action, chosen on the Policies page of the Dashboard. The table shows what each action does, and what the message's author sees:

Action What the server does What the author sees
Flag Publishes the message, and adds it to the review queue. The message, as usual.
Remove Doesn't publish the message, and adds it to the review queue. MessageBlocked, a small centered notice, in place of the message.
Bounce Returns the message to its author without publishing or storing it. The message in an error style. Tapping it offers to send it anyway, edit it or delete it. See Bounced messages.
Bounce and flag Bounces the message. If the author sends it again and it still breaks the rule, publishes and flags it. The bounced message, then the message, as usual.
Bounce and remove Bounces the message. If the author sends it again and it still breaks the rule, removes it. The bounced message, then MessageBlocked.

A bounced message isn't stored, so it doesn't reach the review queue, which holds only flagged and removed messages. Bounce message action and Policies in the Moderation docs cover the actions.

To hide an action from users who have the capability, pass overrideOwnCapabilities to Channel and Thread. This hides Flag Message and Ban User:

<Channel
  channel={channel}
  overrideOwnCapabilities={{ banChannelMembers: false, flagMessage: false }}
>

To remove Mute User or Block User, filter them out of the list in your own messageActions, as Message actions does for Block User.

Customizing

Flagging with a reason

This menu replaces Flag Message with Report spam, which flags the message with a reason and without the confirmation. The action keeps the flagMessage type, so the default rules still show it only to users with flag-message:

src/ChannelScreen.tsx
import { useCallback } from "react";
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageList,
  messageActions as defaultMessageActions,
  useChatContext,
} from "stream-chat-react-native";
import type { MessageActionsProp } from "stream-chat-react-native";

export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => {
  const { client } = useChatContext();

  const messageActions = useCallback<MessageActionsProp>(
    (params) => {
      const report = async () => {
        try {
          await client.moderation.flagMessage({
            message_id: params.message.id,
            reason: "spam",
          });
          client.notifications.addSuccess({
            message: "Reported as spam",
            origin: { emitter: "ReportSpam" },
          });
        } catch {
          client.notifications.addError({
            message: "Couldn't report the message",
            origin: { emitter: "ReportSpam" },
          });
        }
      };

      return defaultMessageActions(params).map((action) =>
        action.actionType === "flagMessage"
          ? { ...action, action: () => void report(), title: "Report spam" }
          : action,
      );
    },
    [client],
  );

  return (
    <Channel channel={channel} messageActions={messageActions}>
      <MessageList />
      <MessageComposer />
    </Channel>
  );
};

The notifications carry no target, so they show in the notification list that mounted last, the message list's. Actions explains the action objects, and the actions in the channel list and in channel details.

The removed message

Replace MessageBlocked through WithComponents. It receives the message as message:

src/RemovedMessage.tsx
import type { PropsWithChildren } from "react";
import { StyleSheet, Text } from "react-native";
import { WithComponents } from "stream-chat-react-native";
import type { MessageBlockedProps } from "stream-chat-react-native";

const RemovedMessage = ({ message }: MessageBlockedProps) => (
  <Text style={styles.text}>
    {message.user?.name ?? "This message"} was removed by moderation.
  </Text>
);

// Declared outside the component, so WithComponents gets the same object on every render.
const overrides = { MessageBlocked: RemovedMessage };

export const WithRemovedMessages = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

const styles = StyleSheet.create({
  text: { fontStyle: "italic", padding: 8, textAlign: "center" },
});

Showing who is blocked

client.blockedUsers is a store, so a component that subscribes to it updates when the user blocks or unblocks someone:

src/BlockedBadge.tsx
import { Text } from "react-native";
import type { BlockedUsersState } from "stream-chat";
import { useChatContext, useStateStore } from "stream-chat-react-native";

const blockedSelector = ({ userIds }: BlockedUsersState) => ({ userIds });

export const BlockedBadge = ({ userId }: { userId: string }) => {
  const { client } = useChatContext();
  const { userIds } = useStateStore(client.blockedUsers, blockedSelector);

  return userIds.includes(userId) ? <Text>Blocked</Text> : null;
};

Reference

  • Message UI components: MessageBlocked and MessageBounce.
  • ComponentsContext: the MessageBlocked, MessageActionList and MessageActionListItem override keys.
  • Channel: messageActions, overrideOwnCapabilities, and handleFlag, handleMute, handleBan and handleBlockUser.
  • Message hooks: useMessageActions and useMessageActionHandlers.
  • Flag, mute and ban in the Moderation docs: the server side of each tool.
  • Moderation for Chat in the Chat API docs: blocking and how it affects chat.