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

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 goes away when the server confirms it. The user's latest message then shows whether it was sent, delivered and read.
  • Failed messages. A message whose request failed stays in the list with an error badge. Its menu offers Resend.
  • Editing. Edit Message loads the message into the composer, with an "Edit Message" preview above the input and a checkmark in place of the send button. The ✕ on the preview cancels the edit and puts back whatever the user was typing before. An edited message shows "Edited", with the time of the edit in a tooltip.
  • Deleting. Delete message asks for confirmation, then shows "Message deleted" in place of the message.
  • Bounced messages. A message that moderation bounced shows in an error style. Clicking it opens a prompt to delete it, edit it, or send it anyway.

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 Enter:

  1. The MessageComposer component calls the state layer's MessageComposer.send() on the composer it drives: channel.messageComposer, or thread.messageComposer inside a thread. See Which composer it drives.
  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() never throws. It resolves to one of three results:

  • 'sent': the message went out.
  • 'nothing-to-send': there was nothing to send, or a middleware stopped the send. The composer keeps its contents.
  • 'failed': the request failed. The composer is already empty, and the message is in the list as failed.

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

Resending

Resend calls handleRetry from the message context, which calls MessageOperations.retry({ localMessage }) on the channel or the 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 message that failed with a 403 error can't be resent.

Editing

Edit Message loads the message into the composer the user types in, the channel's or the thread's, with MessageComposer.initState({ composition: message }). The composer now holds the message as editedMessage.

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() never throws and resolves to the same three results.

The ✕ on EditedMessagePreview ends the edit and restores what the composer held before it started. Saving the edit leaves the composer empty. See Editing for the composer's side.

Deleting

Delete message opens a confirmation. Confirming calls handleDelete from the message context, which calls MessageOperations.delete({ localMessage }) on the channel or the thread. The message shows as deleted at once. If the server rejects the request, it comes back. The default action shows a "Message deleted" notification either way.

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.

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, and the message becomes clickable.

Clicking it opens MessageBounceModal with MessageBouncePrompt, which offers three choices from useMessageBounceContext():

  • Delete removes the message from the channel's message list, without a request. A bounced reply stays in its thread.
  • Edit Message loads it into the composer, as the Edit action does.
  • Send Anyway resends it with MessageOperations.retry().

What renders each part

Part Component Reads
The input and the send button MessageComposer the channel's, thread's or edit composer
The preview above the input while editing EditedMessagePreview composer.state.editedMessage
Sending, sent, delivered and read icons MessageStatus the message's status and the read state
"Edited" MessageEditedIndicator message.message_text_updated_at
A deleted message MessageDeletedBubble, or MessageDeleted to replace the whole message deleted_at, type: 'deleted' or deleted_for_me
The bounce prompt MessageBouncePrompt useMessageBounceContext()
Edit, Delete and Resend MessageActions, with the default message action set the user's capabilities and the message state

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 Giphy messages can't be edited. A message that is still sending has no actions.

A failed message has a short menu:

  • Resend, when the user has send-message and the error wasn't a 403.
  • Edit Message and Delete message, when the request never reached the server.

A bounced message has no menu at all: its choices are in the bounce prompt.

The server checks send-message, but the default composer doesn't hide itself when the user lacks it. To show something else instead, check the capability:

import {
  MessageComposer,
  useChannel,
  useChannelCapabilities,
} from "stream-chat-react";

export const ComposerIfAllowed = () => {
  const channel = useChannel();
  const capabilities = useChannelCapabilities({ cid: channel.cid });

  if (!capabilities.has("send-message")) {
    return (
      <p className="composer-read-only">You can't post in this channel.</p>
    );
  }

  return <MessageComposer />;
};

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 Customizing requests.

Changing the actions

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

Replacing the bounce prompt

MessageBouncePrompt is an override key. A replacement reads the bounced message and the three handlers from useMessageBounceContext(), and closes the modal with useModalContext():

import type { MessageBouncePromptProps } from "stream-chat-react";
import {
  useMessageBounceContext,
  useModalContext,
  WithComponents,
} from "stream-chat-react";

const BouncePrompt = (_props: MessageBouncePromptProps) => {
  const { handleDelete, handleEdit, message } = useMessageBounceContext();
  const { close } = useModalContext();

  return (
    <div className="bounce-prompt">
      <p>“{message.text}” wasn't sent: it breaks the community guidelines.</p>
      <button
        onClick={(event) => {
          handleEdit(event);
          close();
        }}
        type="button"
      >
        Edit
      </button>
      <button
        onClick={(event) => {
          handleDelete(event);
          close();
        }}
        type="button"
      >
        Delete
      </button>
    </div>
  );
};

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

MessageDeletedBubble, MessageDeleted, EditedMessagePreview, MessageEditedIndicator and MessageStatus are override keys too. See Overriding components.

Editing a message in place

By default, an edit happens in the composer at the bottom of the channel or thread. To edit a message where it sits, give it a composer of its own: an edit composer, whose composition context is the message. Supply it to a MessageComposer with MessageComposerControllerProvider:

import { useEffect, useState } from "react";
import type { LocalMessage, MessageComposerState } from "stream-chat";
import { MessageComposer as MessageComposerController } from "stream-chat";
import {
  MessageComposer,
  MessageComposerControllerProvider,
  useChatContext,
  useStateStore,
} from "stream-chat-react";

const editingSelector = (state: MessageComposerState) => ({
  isEditing: state.editedMessage !== null,
});

export const InlineEditor = ({
  message,
  onDone,
}: {
  message: LocalMessage;
  onDone: () => void;
}) => {
  const { client } = useChatContext();
  const [composer] = useState(
    () =>
      new MessageComposerController({
        client,
        composition: message,
        compositionContext: message,
      }),
  );
  const { isEditing } = useStateStore(composer.state, editingSelector);

  // Saving or cancelling the edit empties the composer, which ends the edit.
  useEffect(() => {
    if (!isEditing) onDone();
  }, [isEditing, onDone]);

  if (!isEditing) return null;

  return (
    <MessageComposerControllerProvider messageComposerController={composer}>
      <MessageComposer />
    </MessageComposerControllerProvider>
  );
};

Render InlineEditor in place of the message text from a custom MessageUI, and open it from a custom Edit action. The composer the user was typing in stays untouched. See Message composer UI for how a supplied composer is picked.

Reference