const STORAGE_KEY = "@chat/drafts";
const getDrafts = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
const removeDraft = (key: string) => {
const drafts = getDrafts();
if (drafts[key]) {
delete drafts[key];
localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts));
}
};
const updateDraft = (key: string, value: string) => {
const drafts = getDrafts();
if (!value) {
delete drafts[key];
} else {
drafts[key] = value;
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts));
};
Storing message drafts
In this recipe, we would like to demonstrate how you can start storing unsent user’s messages as drafts. The whole implementation turns around the use of MessageInput
’s prop getDefaultValue
and custom change event handler. We will store the messages in localStorage.
Building the draft storage logic
Below, we have a simple logic to store all the message text drafts in a localStorage object under the key @chat/drafts
.
On top of this logic we build a hook that exposes the change handler functions for both thread and main MessageInput
components as well as functions for MessageInput
’s getDefaultValue
prop. We also have to override the MessageInput
’s default submit handler, because we want to remove the draft from storage when a message is sent.
import { ChangeEvent, useCallback } from "react";
import {
MessageToSend,
useChannelActionContext,
useChannelStateContext,
} from "stream-chat-react";
import type { Message } from "stream-chat";
const STORAGE_KEY = "@chat/drafts";
const getDrafts = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
const removeDraft = (key: string) => {
const drafts = getDrafts();
if (drafts[key]) {
delete drafts[key];
localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts));
}
};
const updateDraft = (key: string, value: string) => {
const drafts = getDrafts();
if (!value) {
delete drafts[key];
} else {
drafts[key] = value;
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts));
};
const useDraftAPI = () => {
const { channel, thread } = useChannelStateContext();
const { sendMessage } = useChannelActionContext();
const handleInputChange = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
updateDraft(channel.cid, e.target.value);
},
[channel.cid],
);
const handleThreadInputChange = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
if (!thread) return;
updateDraft(`${channel.cid}:${thread.id}`, e.target.value);
},
[channel.cid, thread],
);
const getMainInputDraft = useCallback(() => {
const drafts = getDrafts();
return drafts[channel.cid] || "";
}, [channel.cid]);
const getThreadInputDraft = useCallback(() => {
if (!thread) return;
const drafts = getDrafts();
return drafts[`${channel.cid}:${thread.id}`] || "";
}, [channel.cid, thread]);
const overrideSubmitHandler = useCallback(
async (
message: MessageToSend,
channelCid: string,
customMessageData?: Partial<Message>,
) => {
await sendMessage(message, customMessageData);
const key = message.parent
? `${channelCid}:${message.parent.id}`
: channelCid;
removeDraft(key);
},
[sendMessage],
);
return {
getMainInputDraft,
getThreadInputDraft,
handleInputChange,
handleThreadInputChange,
overrideSubmitHandler,
};
};
Plugging it in
Now it is time to access the API in our React component. The component has to be a descendant of Channel
component, because useDraftAPI
accesses the ChannelStateContext
and ChannelActionContext
through corresponding consumers. In our example we call this component ChannelWindow
.
import {
ChannelFilters,
ChannelOptions,
ChannelSort,
StreamChat,
} from "stream-chat";
import { useDraftAPI } from "./useDraftAPI";
import type { StreamChatGenerics } from "./types";
const ChannelWindow = () => {
const {
getMainInputDraft,
getThreadInputDraft,
handleInputChange,
handleThreadInputChange,
overrideSubmitHandler,
} = useDraftAPI();
return (
<>
<Window>
<TruncateButton />
<ChannelHeader />
<MessageList />
<MessageInput
additionalTextareaProps={{ onChange: handleInputChange }}
getDefaultValue={getMainInputDraft}
overrideSubmitHandler={overrideSubmitHandler}
focus
/>
</Window>
<Thread
additionalMessageInputProps={{
additionalTextareaProps: { onChange: handleThreadInputChange },
getDefaultValue: getThreadInputDraft,
overrideSubmitHandler,
}}
/>
</>
);
};
// In your application you will probably initiate the client in a React effect.
const chatClient = StreamChat.getInstance<StreamChatGenerics>("<YOUR_API_KEY>");
// User your own filters, options, sort if needed
const filters: ChannelFilters = {
type: "messaging",
members: { $in: ["<YOUR_USER_ID>"] },
};
const options: ChannelOptions = { state: true, presence: true, limit: 10 };
const sort: ChannelSort = { last_message_at: -1, updated_at: -1 };
const App = () => {
return (
<Chat client={chatClient}>
<ChannelList
filters={filters}
sort={sort}
options={options}
showChannelSearch
/>
<Channel>
<ChannelWindow />
</Channel>
</Chat>
);
};
Now once you start typing, you should be able to see the drafts in the localStorage
under the key @chat/drafts
. Despite changing channels or threads, the unsent message text should be kept in the textarea.