feat(MessageComposer)!: let integrators supply the composer MessageComposer edits - #3299
Conversation
Enter always called `messageComposer.send()`, while the send button chose between `update()` and `send()` itself. Submitting an edit with Enter therefore posted a new message under the edited message's id, which the server rejects as already existing. Both now go through `useMessageComposerSubmitFn`, which picks `update()` while a message is being edited and `send()` otherwise, deciding when submitting rather than when rendering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…poser edits Ports #3249 onto v15. `MessageComposerControllerProvider` supplies the composer for every `MessageComposer` below it, ahead of the thread's and the channel's own - for a composition the integrator owns, such as editing a message inline. `useMessageComposerController` consults it first. `preventClearingOnUnmount` keeps that composer's state when `MessageComposer` unmounts instead of clearing it. A draft save that fails on unmount is caught rather than left unhandled. Two departures from #3249: the context lives in `src/context/`, since importing it from `MessageComposer.tsx` into the hook made a cycle, and the unmount path drops its `config.drafts.enabled` check, which `createDraft()` already performs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An "Edit inline" message action, switched on per message list in Settings > Message Actions, swaps the message for a `MessageComposer` in place. The edit goes into a composer the message owns, supplied through `MessageComposerControllerProvider`, so the channel's own composer - and anything already typed into it - is left alone. The ✕ on the edit preview cancels. Ports the demo from #3249. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A composer supplied through `MessageComposerControllerProvider` belongs to whoever supplied it, so `MessageComposer` no longer clears it when it unmounts. The owner decides when the composition ends: submitting it, or cancelling it with the ✕ on the edit preview. Its draft is still saved on unmount; `createDraft()` skips edits and composers with drafts disabled. The thread's and the channel's own composers are cleared on unmount as before, after their draft is saved. This replaces the `preventClearingOnUnmount` opt-out, which every inline editor needed, including under React StrictMode, whose dev remount cleared an edit loaded before `MessageComposer` mounted. BREAKING CHANGE: the `preventClearingOnUnmount` prop is removed from `MessageComposer`. Drop it. If you relied on the unmount clearing a composer supplied through `MessageComposerControllerProvider`, call `clear()` on it yourself once your UI is done with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Size Change: +1.47 kB (+0.17%) Total Size: 847 kB 📦 View Changed
ℹ️ View Unchanged
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## release-v15 #3299 +/- ##
==============================================
Coverage ? 85.42%
==============================================
Files ? 531
Lines ? 15561
Branches ? 4908
==============================================
Hits ? 13293
Misses ? 2268
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| if (!inlineEditEnabled && editing) editingComposer?.clear(); | ||
| }, [editing, editingComposer, inlineEditEnabled]); | ||
|
|
||
| const MessageActionsWithInlineEdit = useMemo(() => { |
There was a problem hiding this comment.
Why this isn't a top level component? This way, instead of a re-render, we get a full remount in case one of the memo deps change.
There was a problem hiding this comment.
Adjusted to follow the established pattern of declaring message actions components in b5bdc73
The "Edit inline" action and the `MessageActions` wrapper that added it were component types created inside `useMemo`, so a change to one of the memo's dependencies gave React a new type and remounted the actions menu instead of re-rendering it. `InlineEditMessageAction` is now a module-level component, and `ConfigurableMessageActions` adds it next to the built-in edit, alongside the demo's other custom actions, while the setting is on. It starts editing its row through `InlineEditContext`, which `InlineEditableMessage` provides. The `WithComponents` override is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🎯 Goal
Lets an app give
MessageComposerits own composer, for example to edit a message inline in the message list without touching the channel's composer or whatever the user has already typed into it.Ports #3249 from master, with one change to how it behaves: instead of the
preventClearingOnUnmountprop, a composer you supply is simply never cleared whenMessageComposerunmounts. It also fixes a bug found while porting: submitting an edit with Enter sent a new message instead of saving the edit.🛠 Implementation details
Supplying a composer.
MessageComposerControllerProvidertakes amessageComposerControllerand makes everyMessageComposerand composer hook below it use that composer.useMessageComposerControllerlooks for it first, then falls back to the thread's composer, then the channel's.useMessageComposerControllerContextreturns the supplied one, orundefinedif there isn't one. The context lives insrc/context/; importing it fromMessageComposer.tsxinto the hook created an import cycle.Who clears it. A supplied composer belongs to whoever supplied it. When
MessageComposerunmounts it saves the composer's draft but never clears it, so the owner decides when the composition ends: by submitting, or with the ✕ on the edit preview. The thread's and the channel's own composers are still cleared on unmount, after their draft is saved, as before.Master does this with an opt-in prop,
preventClearingOnUnmount. Here it isn't optional, because every inline editor needed it. Without it, React StrictMode's dev-only unmount/remount cleared an edit that had been loaded into the composer beforeMessageComposermounted, so the editor never opened.Enter now saves edits. The send button chose between
update()andsend(), but Enter always calledsend(). Submitting an edit with Enter posted a new message with the edited message's id, and the server rejected it because that id already exists. Both now calluseMessageComposerSubmitFn, a new exported hook that checks at submit time whether a message is being edited.Demo. The vite example gets an "Edit inline" message action, switched on per message list in Settings > Message Actions, that swaps the message for a
MessageComposerin place. Its composer is kept inclient.messageComposerCache, so an unfinished edit survives the row unmounting (leaving the channel and coming back). The ✕ on the edit preview discards it.Tests.
useMessageComposerController: which composer it picks, and that it switches when a different one is supplied.MessageInput: saving an edit with Enter and with the send button.MessageInput, on unmount: the channel's composer is saved and cleared, even when saving the draft fails; a supplied composer gets its draft saved but isn't cleared; and an edit in a supplied composer survives a StrictMode remount.BREAKING CHANGE: the
preventClearingOnUnmountprop is removed fromMessageComposer. Drop it. If you relied on unmounting to clear a composer supplied throughMessageComposerControllerProvider, callclear()on it yourself when your UI is done with it.🎨 UI Changes
None in the SDK. The demo gains the inline editor described above.