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

Sending, editing and deleting

What users get

A message appears in the list the moment the user sends it, marked as sending, and settles when the server answers. Users can edit and delete their own messages, and resend one that failed.

  • Sending. The message shows at once with a clock icon, which turns into a check when the server confirms it. Two checks mean it was delivered, and two checks in the accent color mean someone read it.
  • Failed messages. A message whose request failed stays in the list with an error icon. Tapping it opens the message menu, which offers Resend.
  • Editing. Edit Message in the message menu shows an "Edit Message" preview above the input and a checkmark in place of the send button. The close button on the preview cancels the edit. Whatever the user was typing before comes back to the input when the edit ends, because the edit happens in a composer of its own. An edited message shows "Edited" in its footer.
  • Deleting. Delete Message asks for confirmation in a native alert, then shows "Message deleted" in place of the message and a "Message deleted" notification.
  • Bounced messages. A message that moderation bounced shows in an error style. Tapping it opens an alert to send it anyway, edit it, or delete it.

The message menu opens on a long press. See Message actions.

The composer while editing a message, with an Edit Message preview above the input and a checkmark in place of the send button on iOS
The composer while editing a message, with an Edit Message preview above the input and a checkmark in place of the send button on Android

How it works across the layers

The work happens in the state layer: the composer builds the message, and the message operations of the channel or thread change the message list and send the request. The components ask for each step and render the result.

Sending

The user types "Hello" in the channel "Design" and presses the send button:

  1. The MessageComposer component calls sendMessage from MessageInputContext, which calls the state layer's MessageComposer.send() on the composer that useMessageComposer() returns: channel.messageComposer, or thread.messageComposer inside a Thread.
  2. The composer builds the message from its text, attachments, mentions and quoted message, and clears itself before the request goes out. The user can start typing the next message right away.
  3. channel.messageOperations.send() adds "Hello" to the message list with status sending. It sends the request through the sendMessageRequest handler when one is registered, and through the default request otherwise.
  4. When the server answers, its copy replaces the local one with status received. Every list that shows the message updates.
  5. If the request fails, the message stays in the list with status failed, and the composer publishes an error notification.

MessageComposer.send() doesn't throw when the request fails. It resolves to one of three results (a composition middleware that throws still rejects it):

  • 'sent': the message went out.
  • 'nothing-to-send': there was nothing to send, or a middleware stopped the send. MessageComposer puts the text back into the input.
  • 'failed': the request failed. The composer is already empty, and the message is in the list as failed.

When the user lacks the send-links capability and the text contains a link, sendMessage shows a "Links are disabled" alert and sends nothing.

The statuses and the request handlers are explained on Message operations, and what the composer does on Message composer.

Resending

Resend calls retrySendMessage from useMessageOperations(), which calls MessageOperations.retry({ localMessage }) on the open thread, or on the channel outside a thread. The message goes back to sending.

A failed send is kept for 5 minutes (up to 100 messages), so a retry sends exactly what failed. After that, the retry rebuilds the request from the message.

A failed edit also offers Resend. It sends the message again rather than repeating the edit, so to retry an edit, edit the message again.

Editing

Edit Message calls setEditingState(message) from useMessageComposerAPIContext(). The provider that Channel and Thread render creates an edit composer for the message, or reuses the one in client.messageComposerCache, and starts it from the message. While the edit lasts, useMessageComposer() returns the edit composer, so the input, the preview and the buttons work on it. The channel's or thread's own composer keeps what the user had typed.

Pressing the checkmark calls MessageComposer.update() instead of MessageComposer.send(). MessageOperations.update() shows the new text at once and marks the message edited. If the request fails, the edit is kept and the message is marked failed. Like MessageComposer.send(), MessageComposer.update() doesn't throw when the request fails, and resolves to the same three results. A bounced message is the exception: saving its edit sends it again as a new message, with MessageComposer.send().

Saving the edit empties the edit composer, which ends the edit. The close button on the preview calls clearEditingState(), which ends it without saving. Either way, the input shows the channel's or thread's composer again. See Message composer for the composer's side.

Deleting

Delete Message opens a confirmation alert. Confirming calls deleteMessage(message) from useMessageOperations(), which calls MessageOperations.delete({ localMessage, options }) on the thread when the message is a reply loaded there, and on the channel otherwise. The message shows as deleted at once. If the server rejects the request, the message comes back and an error notification shows.

A message whose send failed exists only on the device, so deleting it removes it from the list without a request.

MessageOperations.delete() takes two options:

  • hard: true removes the message from the list instead of marking it deleted.
  • delete_for_me: true deletes it for the current user only.

The default menu deletes for everyone. The action set also has a deleteForMeMessage action, which the default list leaves out; see Message actions.

What each operation does before and after the server answers is on What's optimistic and what isn't.

Bounced messages

When moderation bounces a message, the server returns it with type error and the moderation action bounce. The message list shows it in an error style. Tapping or long-pressing it renders MessageBounce, which opens an alert with three choices:

  • Send Anyway resends it with MessageOperations.retry().
  • Edit Message starts an edit, as the Edit action does.
  • Delete Message removes the message from the message lists and the offline database, without a request.

What renders each part

Part Component Reads
The input and the send button MessageComposer, SendButton the channel's, thread's or edit composer
The preview above the input while editing Reply, with mode='edit' composer.editedMessage
Sending, sent, delivered and read icons MessageStatus, in MessageFooter the message's status and channel.messageReceiptsTracker
"Edited" MessageFooter message.message_text_updated_at
The error icon MessageError message.type === 'error' or status failed
A deleted message MessageDeleted message.type === 'deleted'
The bounce alert MessageBounce the message from useMessageContext()
Edit, Delete and Resend MessageActionList, with the actions from messageActions the user's capabilities and the message state

MessageFooter shows the timestamp, the status and "Edited" on the last message of each group, and on any message that hasn't reached the server yet. The status shows on the user's own messages.

Turning it on or off

Sending, editing and deleting are always available. The user's capabilities in the channel decide who can use them:

Action Capability
Send a message send-message
Edit a message update-own-message or update-any-message
Delete a message delete-own-message or delete-any-message

The message actions follow these capabilities: Edit Message and Delete Message appear only when the user may use them. Polls and messages with a Giphy or Imgur attachment can't be edited.

A failed message has a short menu:

  • Resend, on the user's own message;
  • Edit Message and Delete Message, when the user has the capability.

A bounced message has no menu: its choices are in the bounce alert.

Without send-message, for example in a frozen channel, MessageComposer renders SendMessageDisallowedIndicator ("You can't send messages in this channel") instead of the input, unless a message is being edited. To show something else, replace SendMessageDisallowedIndicator through WithComponents.

To take actions away from users who have the capability, pass overrideOwnCapabilities to Channel and Thread, for example overrideOwnCapabilities={{ deleteOwnMessage: false }}. It changes what the UI offers, not what the server allows.

Customizing

Changing the request

Register a request handler in client.config to change how a message is sent, edited or deleted. A handler receives the default request, so it can wrap it. This one records every sent message:

client.config.set({
  channel: {
    requestHandlers: {
      sendMessageRequest: async (params, defaultRequest) => {
        const response = await defaultRequest(params);
        analytics.track("message_sent", { cid: params.localMessage.cid });
        return response;
      },
    },
  },
});

The handlers for editing and deleting are updateMessageRequest and deleteMessageRequest, and retrySendMessageRequest handles resends. Threads use their channel's handlers. See Message operations.

Changing the actions

Edit, Delete and Resend are items of the message action list. To remove, replace or add actions, see Message actions and Actions.

To run your own code when an action runs, pass handleEdit, handleDelete, handleDeleteForMe or handleRetry to Channel and Thread. They're called with the message before the default action, and don't replace it.

Replacing the bounce alert

MessageBounce is an override key. It renders while the bounce choices are open, and receives setIsBounceDialogOpen to close them. This one offers editing and deleting only:

import { useEffect } from "react";
import { Alert } from "react-native";
import type { MessageBounceProps } from "stream-chat-react-native";
import {
  useMessageComposerAPIContext,
  useMessageContext,
  useMessageOperations,
  WithComponents,
} from "stream-chat-react-native";

const BounceAlert = ({ setIsBounceDialogOpen }: MessageBounceProps) => {
  const { message } = useMessageContext();
  const { removeMessage } = useMessageOperations();
  const { setEditingState } = useMessageComposerAPIContext();

  useEffect(() => {
    Alert.alert(
      "Message not sent",
      "It breaks the community guidelines.",
      [
        {
          onPress: () => {
            setEditingState(message);
            setIsBounceDialogOpen(false);
          },
          text: "Edit",
        },
        {
          onPress: async () => {
            await removeMessage(message);
            setIsBounceDialogOpen(false);
          },
          style: "destructive",
          text: "Delete",
        },
      ],
      { cancelable: true, onDismiss: () => setIsBounceDialogOpen(false) },
    );
  }, [message, removeMessage, setEditingState, setIsBounceDialogOpen]);

  return null;
};

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

Call setIsBounceDialogOpen(false) on every way out, including a dismissed alert, so the next tap opens the choices again.

MessageDeleted, MessageError, MessageFooter, MessageStatus, Reply and SendButton are override keys too. See Overriding components.

Reference