1- import { type ComponentProps , useEffect , useMemo , useState } from 'react' ;
1+ import { type ComponentProps , useCallback , useEffect , useMemo , useState } from 'react' ;
22import { MessageComposer as MessageComposerController } from 'stream-chat' ;
3- import type { MessageComposerState } from 'stream-chat' ;
3+ import type { LocalMessage , MessageComposerState } from 'stream-chat' ;
44import {
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 */
5050export 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