Drafts
What users get
A message the user started but didn't send is kept as a draft on the server. The user types half a message in "Design" and leaves the channel. The next time the app loads "Design", on this device or another one, the text is back in the composer.
- Each channel has its own draft, and so does each thread.
- A draft keeps the text and mentions, uploaded attachments, link previews, the quoted message and a poll.
- Sending the message, or clearing the composer, deletes the draft.
- A draft saved on another device or tab appears in the composer while the channel is open.
How it works across the layers
Drafts belong to the composer, in the state layer. The UI only decides when to save one.
Saving
- The user leaves "Design".
MessageComposerunmounts and callsMessageComposer.createDraft(), then clears the composer. MessageComposer.createDraft()builds the draft through the composer's draft middleware and saves it withChannel.createDraft(): for the channel, or for a thread when the composer belongs to one.- Nothing is saved when the composer is empty, or when nothing changed since the draft was loaded.
Edits are never saved as drafts, and neither are messages in a channel that the server hasn't created yet.
Restoring
- Channels. The server returns a channel's draft with the channel, whenever the channel is queried or watched: when a channel list loads or reloads, and after a reconnect. The composer loads the draft into its state.
- Threads. A thread from the thread list carries its draft. When
MessageComposermounts for a thread whose composer is empty, it fetches the thread's draft withChannel.getDraft({ parent_id }). - Other devices. While drafts are on, the composer listens for
draft.updatedanddraft.deleted. It loads an updated draft, and clears itself when its draft is deleted elsewhere. It ignores both while the user is editing a message.
Deleting
When the composer becomes empty, MessageComposer.deleteDraft() deletes the saved draft. Sending empties the composer, so a sent message takes its draft with it.
Draft operations change local state first and send the request after; see What's optimistic and what isn't. The composer's side is on Drafts.
Limitations
- Opening a channel that is already loaded doesn't query it again. A user who leaves a channel and comes back in the same session finds the composer empty, because it was cleared when it unmounted, and the draft returns the next time the channel is queried. To load it on the spot, call
MessageComposer.getDraft()when the composer mounts. MessageComposersaves a draft when it unmounts. Closing the browser tab doesn't unmount it, so the text typed last is lost unless you save it yourself, as shown under Customizing.- Custom data sent with the draft isn't restored into the composer.
- A thread draft saves the "Also send in channel" choice, but restoring the draft leaves the checkbox off.
- The channel list doesn't show drafts.
Turning it on or off
Drafts are off by default. Turn them on with messageComposer.drafts.enabled:
client.config.set({ messageComposer: { drafts: { enabled: true } } });Or pass the same settings to useCreateChatClient as config. The setting is client only: there is no server switch. It reaches every composer, in channels and threads alike.
Customizing
Saving when the page is hidden
Save the draft when the user switches tabs or closes the browser. Render this component next to the MessageComposer it should save, inside Channel or Thread:
import { useEffect } from "react";
import { useMessageComposerController } from "stream-chat-react";
export const SaveDraftWhenHidden = () => {
const composer = useMessageComposerController();
useEffect(() => {
const saveDraft = () => {
if (document.visibilityState === "hidden") void composer.createDraft();
};
document.addEventListener("visibilitychange", saveDraft);
return () => document.removeEventListener("visibilitychange", saveDraft);
}, [composer]);
return null;
};MessageComposer.createDraft() does nothing while drafts are off, and when there is nothing new to save.
Listing a user's drafts
client.queryDrafts() returns the current user's drafts across channels, for a "Drafts" screen. Each draft has its channel's channel_cid, a parent_id for a thread draft, and the draft message:
const { drafts } = await client.queryDrafts({
limit: 20,
sort: [{ direction: -1, field: "created_at" }],
});
for (const draft of drafts) {
console.log(draft.channel_cid, draft.parent_id, draft.message.text);
}It's a one-off query: nothing keeps the result current. See Paginators vs one-off queries.
Changing what a draft holds
Drafts are built by their own middleware chain. To add or drop data, add middleware to it; see Composer middleware.
Reference
- State: Message composer, the MessageComposer API and the configuration tree.
- UI: Message composer UI.
- Props: MessageComposer.
- Hooks:
useMessageComposerController.