Skip to content

Commit c60bb5e

Browse files
MartinCupelaclaude
andcommitted
chore(demo): keep an unfinished inline edit in the composer cache
The inline editor's composer now lives in `client.messageComposerCache` under `MessageComposer.constructTag(message)`, with the message as its composition context. It is created and stored when the user enters edit mode; a row nobody edits creates nothing. Every Edit Inline starts again from the message. Because `MessageComposer` never clears a supplied composer on unmount, an unfinished edit survives the row unmounting - leaving the channel, or scrolling the row away in a virtualized list - and the editor comes back on return. The ✕ on the edit preview discards it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4f540c2 commit c60bb5e

1 file changed

Lines changed: 41 additions & 21 deletions

File tree

‎examples/vite/src/InlineEditMessage/InlineEditMessage.tsx‎

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { type ComponentProps, useEffect, useMemo, useState } from 'react';
1+
import { type ComponentProps, useCallback, useEffect, useMemo, useState } from 'react';
22
import { MessageComposer as MessageComposerController } from 'stream-chat';
3-
import type { MessageComposerState } from 'stream-chat';
3+
import type { LocalMessage, MessageComposerState } from 'stream-chat';
44
import {
55
asDynamicKey,
66
ContextMenuButton,
@@ -12,7 +12,6 @@ import {
1212
MessageComposer,
1313
MessageComposerControllerProvider,
1414
type MessageUIComponentProps,
15-
useChannel,
1615
useChatContext,
1716
useComponentContext,
1817
useContextMenuContext,
@@ -43,36 +42,54 @@ const editingSelector = (state: MessageComposerState) => ({
4342
});
4443

4544
/**
46-
* Swaps a message for a `MessageComposer` in place, editing it through a composer this component
47-
* owns and supplies with `MessageComposerControllerProvider` - so the channel's own composer, and
48-
* whatever the user was typing into it, are left alone.
45+
* Swaps a message for a `MessageComposer` in place, editing it through a composer of its own
46+
* supplied with `MessageComposerControllerProvider` - so the channel's own composer, and whatever
47+
* the user was typing into it, are left alone. That composer is kept in the client's composer cache
48+
* under the message's tag, so an unfinished edit outlives this row.
4949
*/
5050
export const InlineEditableMessage = (props: MessageUIComponentProps) => {
5151
const { client } = useChatContext();
52-
const channel = useChannel();
52+
const { message } = useMessageContext();
5353
const surface: MessageActionSurface = useThreadContext() ? 'thread' : 'channel';
5454
const { customMessageActions } = useAppSettingsSelector(
5555
(state) => state.messageActions,
5656
);
5757
const inlineEditEnabled = customMessageActions[surface].inlineEdit;
5858
const { MessageActions: OuterMessageActions = MessageActions } = useComponentContext();
5959

60-
// Drafts off: an edit is not a draft, and the channel's draft must not leak into this composer.
61-
const [editingComposer] = useState(
62-
() =>
63-
new MessageComposerController({
64-
client,
65-
compositionContext: channel,
66-
config: { drafts: { enabled: false } },
67-
}),
60+
// Only looks - a row nobody edits creates nothing. A composer found here holding an edit is one
61+
// left unfinished while this row was unmounted, so the editor comes straight back.
62+
const [editingComposer, setEditingComposer] = useState(() =>
63+
client.messageComposerCache.peek(MessageComposerController.constructTag(message)),
6864
);
6965

70-
const { editing } = useStateStore(editingComposer.state, editingSelector);
66+
const { editing } = useStateStore(editingComposer?.state, editingSelector) ?? {
67+
editing: false,
68+
};
69+
70+
// Entering edit mode is where the composer is created if missing, and stored at once.
71+
const startEditing = useCallback(
72+
(messageToEdit: LocalMessage) => {
73+
const tag = MessageComposerController.constructTag(messageToEdit);
74+
const composer =
75+
client.messageComposerCache.peek(tag) ??
76+
// Drafts off: an edit is not a draft.
77+
new MessageComposerController({
78+
client,
79+
compositionContext: messageToEdit,
80+
config: { drafts: { enabled: false } },
81+
});
82+
client.messageComposerCache.add(tag, composer);
83+
composer.initState({ composition: messageToEdit });
84+
setEditingComposer(composer);
85+
},
86+
[client],
87+
);
7188

7289
// Turning the setting off mid-edit abandons the edit, rather than leaving the message stuck as a
7390
// composer with no way back.
7491
useEffect(() => {
75-
if (!inlineEditEnabled && editing) editingComposer.clear();
92+
if (!inlineEditEnabled && editing) editingComposer?.clear();
7693
}, [editing, editingComposer, inlineEditEnabled]);
7794

7895
const MessageActionsWithInlineEdit = useMemo(() => {
@@ -90,7 +107,7 @@ export const InlineEditableMessage = (props: MessageUIComponentProps) => {
90107
className='str-chat__message-actions-list-item-button'
91108
Icon={IconEdit}
92109
onClick={() => {
93-
editingComposer.initState({ composition: message });
110+
startEditing(message);
94111
closeMenu();
95112
}}
96113
>
@@ -121,15 +138,18 @@ export const InlineEditableMessage = (props: MessageUIComponentProps) => {
121138
};
122139
Component.displayName = 'MessageActionsWithInlineEdit';
123140
return Component;
124-
}, [OuterMessageActions, editingComposer]);
141+
}, [OuterMessageActions, startEditing]);
125142

126143
if (!inlineEditEnabled) return <DefaultMessageUI {...props} />;
127144

128-
if (editing) {
145+
if (editing && editingComposer) {
129146
return (
130147
<MessageComposerControllerProvider messageComposerController={editingComposer}>
131148
<div className='app__inline-edit-message'>
132-
{/* Cancelling is the ✕ on the composer's edit preview, which clears the composer. */}
149+
{/* The composer outlives this editor: leaving the channel, or scrolling the row away in a
150+
virtualized message list, unmounts the editor, and `MessageComposer` never clears a
151+
supplied composer on unmount - so the unfinished edit is still there on return.
152+
Cancelling is the ✕ on the edit preview, which clears it on purpose. */}
133153
<MessageComposer />
134154
</div>
135155
</MessageComposerControllerProvider>

0 commit comments

Comments
 (0)