feat: Real Time Message Composer - #41020
Conversation
|
Looks like this PR is not ready to merge, because of the following issues:
Please fix the issues and try again If you have any trouble, please check the PR guidelines |
|
|
|
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks 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 |
9d46406 to
d13646e
Compare
cc3d467 to
b59db10
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #41020 +/- ##
===========================================
- Coverage 69.44% 69.43% -0.02%
===========================================
Files 4286 4295 +9
Lines 170164 171316 +1152
Branches 30274 30570 +296
===========================================
+ Hits 118178 118952 +774
- Misses 46784 47155 +371
- Partials 5202 5209 +7
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
6eda512 to
7021e91
Compare
be5156e to
f1785c7
Compare
418d2fe to
2e5601c
Compare
- Replaced the `ComposerMessageInput`'s underlying `<textarea>` with a
`<div contenteditable>`.
- Currently, this implementation is non-functional beyond accepting
plain text input.
- Placeholder support is currently not working due to limitations with
`contenteditable`.
- Further work is needed to restore full functionality, including
keyboard event handling, Markdown formatting, and cursor management.
- Clipboard paste and file attachments continue to work as expected.
- Updated Storybook stories to include `MessageComposerInputNew` for
preview and testing.
- Refactored existing stories and exports for clarity and compatibility
with the new input component.
feat: enable sending in contenteditable div
- Cloned `CreateComposerAPI.ts` as `newCreateComposerAPI.ts` and updated
references from `HTMLTextAreaElement` to `HTMLDivElement`.
- Implemented `getSelectionRange` and `setSelectionRange` using the
Selection API to replace `input.selectionStart` and
`input.selectionEnd`.
- Fixed Enter key functionality, allowing messages to be sent as
expected.
- Restored Bold, Italics, and Strikethrough button functionality,
confirming successful Selection API migration.
Known Issues & Next Steps
- Enabled `/` key to trigger the slash command menu, but pressing Enter
currently inserts the command and sends the message immediately.
- Emoji autocomplete works, but pressing Enter finalizes the emoji and
sends the message.
- **The Send button remains visually disabled**, requiring an additional
fix to update its state.
- Next step: Prevent message sending when popups (slash command, emoji
autocomplete) are active.
fix: re-enable Send button state in MessageBoxNew
- Updated reducer function to handle FormEvent<HTMLDivElement> instead
of FormEvent<HTMLInputElement>.
- Changed event target from HTMLInputElement to HTMLDivElement for
compatibility with contenteditable.
- Used `innerText.trim()` instead of `value.trim()` to correctly
determine if the message input is empty.
- Replaced `onChange` with `onInput` in `MessageComposerInputNew` to
properly detect user input.
This ensures the Send button is correctly enabled or disabled based on
input presence.
Known Issues & Next Steps:
- Pressing `/` opens the slash command menu, but pressing Enter inserts
the command and sends the message immediately.
- Emoji autocomplete works, but Enter finalizes the emoji and sends the
message.
- Next step: Prevent message sending when popups (slash command or emoji
autocomplete) are active.
feat: add real-time composer preview feature flag
- Introduced `featurePreviewComposer` flag to toggle between the new
<div contenteditable> input and the classic <textarea> input.
- Conditionally render `MessageBoxNew` or `MessageBox` based on the
flag.
- Added `MessageComposerHint` in `MessageBoxNew` to label the feature as
experimental.
refactor: clean up MessageComposerInputNew layout
- Changed class name from `rc-message-box__textarea` to
`rc-message-box__divcontenteditable`.
- Rectified string values for `minHeight` and `maxHeight` with numeric
values.
- Corrected `minHeight` from `52px` to `20px` to match new layout
requirements.
chore: restore focus() after input and change call
chore: replace MessageComposerInputNewProps interface with type alias
chore: add TODO for composer race condition fix
refactor: clarify RichTextComposer component names
This refactor removes ambiguity and improves naming consistency across
the message composer components.
Renamed:
- `MessageComposerInputNew.tsx` → `RichTextComposerInput.tsx`
- `MessageBoxNew.tsx` → `RichTextMessageBox.tsx`
- `newCreateComposerAPI.ts` → `createRichTextComposerAPI.ts`
Updated references in:
- `ComposerMessage.tsx`
- `MessageComposer.stories.tsx`
- `MessageComposer/index.ts`
refactor: modularize logic into selectionRange.ts
- Moved `getSelectionRange` and `setSelectionRange` from
`createRichTextComposerAPI.ts` to a new `selectionRange.ts` file.
- Improves separation of concerns and allows reuse across other modules.
chore: add TODO for updating selectionRange API
refactor: use selectionRange API in RichTextMessageBox
- Replaced `input.value` and `input.selectionEnd` with `innerText` and
`getSelectionRange`
- Used `setSelectionRange` to handle caret movement
- Updated event targets and types from HTMLTextAreaElement to
HTMLDivElement
fix: prevent focus stealing between rich text composers
This is a major bugfix that resolves an erratic behavior during keydown
events
- Added check to skip auto-focus when target is a <div
contenteditable="true">
- Prevented focus tug-of-war between main and thread composers
- Improved stability of typing behavior in multi-composer layouts
- Resolved a critical issue where main and thread composers kept
stealing focus from each other
- Stabilized typing behavior in multi-composer scenarios
feat: mimic cursor restoration in RichTextMessageBox
- Implemented a `WeakMap` to store the last cursor position per
`contenteditable div` instance
- Saved cursor position on `blur` event and restored it on `focus` event
fix: preserve whitespaces in RichTextComposerInput
This is a major bugfix that resolves an erratic behavior during Editing
mode
- Editing mode failed to reset due to `.innerText` collapsing multiple
spaces
- Caused mismatch between original message and `RichTextComposer`
content
- Fixed by adding `whiteSpace: 'pre-wrap'` `to RichTextComposerInput`
- Ensures consistent text comparison and reliable edit cancellation
- Added comment in the `RichTextComposerInput` definition to highlight
the significance
chore: fix a typo
fix: disable Ctrl+B/I events in RichTextMessageBox
- Blocked browser insertion of <b> and <i> tags
- Delegated formatting to custom shortcut handler
fix: remove error from Formatter button clicking
This issue was caused due to the editor losing focus and cursor position
state when clicking a Formatter button.
- Added `setSelectionRange` and `focus` prior to `execCommand` call
- Fixed issue where the button would append instead of replacing the
selected text
fix: set RichTextEditor cursor to EOL after resetting edited message
- Fixed cursor moving to end of contentEditable input and regaining
focus after editing reset
fix: change RichText local storage to innerHTML
Draft messages were not properly stored and restored due to use of
innerText, which strips formatting.
- Switched to innerHTML to retain full content structure in
Accounts.storageLocation.
feat: implement placeholder in RichTextComposer
- Added TypingState reducer to manage `isTyping` and `hidePlaceholder`
flags
- Adjusted placeholder visibility based on input DOM structure
- Normalized content to treat `<div><br></div>` and empty input as
`<br>`
- Updated RichTextComposerInput to support and render `hidePlaceholder`
prop
- Ensured cursor is text-style inside `contenteditable` element
- Prevented saving of Composer drafts when content is only `<br>`
refactor: rename to `RichTextComposer` in storybook
- Replaced `_MessageComposerNew` story with `RichTextComposer` for
clarity
- Added `MessageComposerHint` to highlight experimental status
- Passed `placeholder` and new `hidePlaceholder` prop to
`RichTextComposerInput`
- Updated Storybook args for enhanced prop control
fix: change `RichTextComposerInput` ref type to `HTMLDivElement`
- Corrected the forwarded `ref` target to match the `contentEditable
div`
- Ensures proper typing and `ref` behavior for editable container
- Minor style cleanup for whiteSpace and cursor properties
feat: implement scrolling in RichTextComposerInput
- Applied `overflow-y: scroll` to the `contenteditable div` for
consistent scrollbar visibility
- Styled `::-webkit-scrollbar-thumb` to fix invisible scrollbar in
Chromium browsers
fix: improve RichTextComposerInput Selection API
- Updated `getSelectionRange` to walk DOM and compute offsets precisely
- Skipped inline elements and handled `<br>` inside empty `<div>` as
visual linebreak
- Added offset increment for block-level elements to reflect line
structure
- Updated `setSelectionRange` to match same offset logic for accurate
selection
- Resolved inconsistency between `selectionStart` and `selectionEnd` and
visual cursor placement
fix: sanitize input text for wrapSelection method
- Normalized multiple newlines in `innerText` during selection wrapping
- Added selectionchange listener to debug selection range
- Implemented listener cleanup on component release
fix: restore full text selection on refocus
- Updated `cursorMap` to store both `selectionStart` and `selectionEnd`
for accurate range restoration
- Replaced single-value tracking with object structure in `WeakMap`
- Improved `setSelectionRange` on focus to restore full selection, not
just caret
chore: add realtime composer to feature preview options
chore: fix Typescript and Implement FeaturePreview
fix: sanitize input text for substring method
- Normalized multiple newlines in `innerText` in `substring` method
- Fixed emoji autocomplete issue caused by incorrect text slicing
fix: sanitize RichTextComposer text before sending
- Normalized multiple newlines in `innerText` caused by
`<div><br></div>` blocks
fix: bypass innerText sanitizing for flat edited messages
- Added conditional to skip newline cleanup when editing existing
messages
- Preserved original flat text structure lacking DOM nodes
- Temporarily resolves formatting issues until `RichTextComposer` parser
is implemented
fix: resolve React warning inside RichTextComposer
Fixes a React warning: `hidePlaceholder` is not a recognized DOM
property.
Issue was caused by `Box` wrapper incorrectly defined as a `label`
instead of `div`.
fix: clean up unused DOM in RichTextMessageBox
- Removed unused `shadowRef` and its placeholder `div` object
- Dropped commented `useAutoGrow` hook and related variables
fix: patch Firefox handling in RichTextComposer
- Detects Firefox using the user agent string
- Forces `onSend` for flat text input when Firefox is detected
- Ensures consistent message editing behavior across browsers
fix: change the contenteditable from div to span
- Switched `is='div'` to `is='span'` for improved inline flexibility
- Applied `display: block` to preserve layout and scroll behavior
feat: add line number tracking to RichTextComposer
- Introduced `getLineFromCursorPosition` to compute start and end line
numbers from cursor selection
- Integrated the new utility in `createRichTextComposerAPI` to log
selection line ranges for debugging
refactor: rename to getCursorSelectionInfo
- Renamed `getLineFromCursorPosition` to `getCursorSelectionInfo` for
clarity
- Enhanced returned metadata to include full cursor selection details
- Included computation of line index, column offset, first & last
character positions
fix: update focus stealing check from DIV to SPAN
fix: resolve React DOM warning for hideplaceholder
- Renamed `hidePlaceholder` to `hideplaceholder` across all components
and stories
- Prevented invalid DOM prop warning by excluding `hideplaceholder` from
spread into native elements
- Updated relevant reducer and typing logic
fix: modify TreeWalker algorithm in selectionRange
- Created exception for NodeFilter skipping the contenteditable span
node
- Fixed issue where the selectionRange calculated incorrect cursor
position
feat: update TreeWalker to ignore code and del tag
- Added `code` and `del` tags as non-linebreaking elements in
selectionRange
feat: add messageStateHandler for state updation
- Added `messageStateHandler` to update message box text state during
`beforeinput` events
- Implemented `getTextLines` which returns an array of strings
representing each line of the message
feat: add beforeinput handling in RichTextComposer
- Updated `createRichTextComposerAPI` to accept `setMdLines` and
`setCursorHistory` state setters
- Added `beforeinput` listener to update markdown lines and cursor
history via `resolveBeforeInput`
feat: add cursor history and beforeInput resolver
- Added `CursorHistory` type for managing undo/redo stacks
- Implemented `resolveBeforeInput` to update `mdLines` and track cursor
position
- Integrated `getSelectionRange` and `getCursorSelectionInfo` for
selection state history logging
refactor: init states with non-undefined defaults
- Updated `useState` hooks to initialize `mdLines` and `cursorHistory`
with default values, removing `undefined` as a possible state.
- Adjusted type signatures in `createRichTextComposerAPI` and
`messageStateHandler` to use non-optional types.
- Minor logging text improvement for missing cursor position, which
displayed a verbose stack trace.
feat: implement mdLines update in RichTextComposer
- Implemented useEffect on `mdLines` to reflect the latest content
during room change or message editing state
fix: reset mdLines state after sending message
feat: add parseOptions to RichTextComposer API
- Retrieve `katex`, `customDomains`, and `showColors` from message list
context
- Build `parseOptions` with `useMemo` for stable reference
- Pass `parseOptions` to `createRichTextComposerAPI`
- Updated `useCallback` dependencies to include `parseOptions`
feat: add parseOptions to beforeInput handler
- Add `Options` type and `parse` from `message-parser`
- Pass `parseOptions` through `createRichTextComposerAPI` to
`resolveBeforeInput`
- Implement `parseMessage` helper and log parsed AST for debugging
refactor: rename resolverBeforeInput function
- Renamed `resolveBeforeInput` to `resolveComposerBox` for clearer
intent
- Updated all imports and event listeners to use `resolveComposerBox`
- Extended handler to support both `InputEvent` and `KeyboardEvent` with
safe `inputType` checks
- Integrated Enter key handling in `RichTextMessageBox` to trigger
composer state resolution
feat: update composer state formatting shortcuts
- Extended `handleFormattingShortcut` to accept `setMdLines`,
`setCursorHistory`, and `parseOptions`
feat: add resolver function on formatter click
- Updated `resolveComposerBox` to accept `React.FocusEvent` in addition
to keyboard and input events.
- Added `setMdLines` and `setCursorHistory` state to
`RichTextMessageBox` to support composer resolution.
- Implemented `resolveComposerBox` on keyboard formatting shortcuts and
UI button click
feat: add DOM node support to resolveComposerBox
- Updated `resolveComposerBox` to handle both event and programmatic
calls with `HTMLDivElement`
- Added `instanceof` check to differentiate between `HTMLElement` and
event inputs
- Added `useEffect` on `shouldPopupPreview` for text resolution after a
popup option is selected
feat: parse composer on text change or non-focus events
- Added `beforeText` state before for text state updation
- Added skip checks for parsing if text is unchanged after a focus event
- Removed unnecessary reparsing on repeated focus events causing
potential edit history bloating
fix: resolve paste issue in empty composer input
This is an edge case fix that uses `closest('[contenteditable="true"]')`
to ensure the correct contenteditable element is targeted.
It prevents an issues where pasting into an empty composer caused
`input.innerText` to be blank due to `<br>` removal.
- Added `parseAST` debug logging for parsed message output.
- Updated parsing to treat empty text as `'\n'` to avoid AST generation
errors.
- Renamed unused state setter params with `_` prefix to indicate they
are unused.
feat: protect and restore links during AST parsing
This is done to prevent the parser grammar from exploding the URLs
inside the Composer
The other solution to prevent this is to edit the grammar.pegjs itself
which can be complicated
- Added `protectLinks` to temporarily replace Markdown, Slack-style, and
bare domain links with placeholders before parsing
- Added `restoreLinks` to revert placeholders back to original links
after generating HTML
- Updated `resolveComposerBox` to use these functions, ensuring links
are not mangled during parsing
- Logs final HTML after restoring links for debugging
fix: resolve composer during keyboard navigation
- Implemented `resolveComposerBox` when navigating threads with Up/Down
arrow keys
feat: add messageParser for rich text rendering
- Introduced `renderInline` to convert inline AST tokens
- Added support for plain text, mentions, formatting (bold, italic,
strike, code)
- Implemented partial support for emojis, images, timestamps, and inline
LaTeX
- Removed support for link token due to URL explosion issue
- Added `parseAST` to map parsed blocks into HTML including paragraphs,
headings, lists, tasks, quotes, code blocks and line breaks
- Implemented partial support for KaTeX blocks
fix: escape HTML characters in RichTextComposerAPI
- Escaped user text before assigning to `innerHTML` in the
RichTextComposer
- Prevented DOM text from being reinterpreted as HTML
- Resolved CodeQL/code scanning warnings related to XSS
feat: add emoji render for both native and custom
- Emoji shortcodes convert into Unicode emoticons.
- Custom uploaded emojis render as <span> with inline <img> and
shortcode fallback.
fix: change parser to prevent chain explosion
This is still a work in progress
- Paragraphs: replaced `<p>` wrappers with raw text + newline
- Headings: preserve `#` prefix inside `<hX>` tags
- Unordered lists: prefix list items with `-`
- Ordered lists: include numeric prefix and `value` attribute
- Code blocks: render with Markdown-style fences (```lang ... ```)
- Line breaks: output raw `\n` instead of `<br />`
refactor: change composer render on input event
- Switched `resolveComposerBox` to run on `input` instead of
`beforeinput`
- Removed unused undo/redo and focus event handling logic
- Improved `protectLinks`:
- Added email detection
- Prevented mentions (`@something`) from being parsed as URLs
- Adjusted bare domain regex to exclude `@` prefixes
- Updated rendering pipeline:
- Writes `innerHTML` directly (works but clears undo history)
- Left comments for `execCommand('insertHTML')` as alternative that
preserves history
- Cursor position is restored after re-render
refactor: clean up RichTextComposerAPI events
- Removed unused types (`Dispatch`, `SetStateAction`, `CursorHistory`)
- Dropped debug helper `printSelection` and selection tracking
- Replaced `beforeinput` listener with unified `input` listener
- Updated `resolveComposerBox` signature to remove state dependencies
- Cleaned up event removal in `release` function
refactor: remove resolver from RichTextMessageBox
- Dropped unused types (`Dispatch`, `SetStateAction`, `CursorHistory`)
- Removed `setCursorHistory` state and related resolver calls
- Simplified `handleFormattingShortcut` to only wrap selection
- Cleaned up `setLastCursorPosition` and keyboard/newline handlers by
removing resolver logic
- Updated `createRichTextComposerAPI` usage to new signature without
cursor/history state
- Removed `useEffect` that resolved composer after popup option
selection
fix: Remove cursor reposition bug when typing emoji shortcodes (#37358)
…ndering This new package provides an alternative to @rocket.chat/gazzodown that is optimized for real-time rendering in WYSIWYG text fields. Key differences from gazzodown: - Renders paragraphs as <span> instead of <div> for inline layout - Represents line breaks with newline characters instead of <br> tags - No heavy Fuselage UI dependencies (MessageHighlight, CheckBox, etc.) - Minimal footprint suitable for re-rendering on every keystroke - Consumes the same AST from @rocket.chat/message-parser (grammar.pegjs) Exported components: - ComposerMarkup: Main renderer (drop-in for Markup) - ComposerMarkupContext: Context for emoji/mention resolution - Individual inline elements (Bold, Italic, Strike, Code, Emoji, etc.) Co-authored-by: Martin Schoeler <martin.schoeler@rocket.chat>
…ndering This new package provides an alternative to @rocket.chat/gazzodown that is optimized for real-time rendering in WYSIWYG text fields. Key differences from gazzodown: - Renders paragraphs as <span> instead of <div> for inline layout - Represents line breaks with newline characters instead of <br> tags - No heavy Fuselage UI dependencies (MessageHighlight, CheckBox, etc.) - Minimal footprint suitable for re-rendering on every keystroke - Consumes the same AST from @rocket.chat/message-parser (grammar.pegjs) Exported components: - ComposerMarkup: Main renderer (drop-in for Markup) - ComposerMarkupContext: Context for emoji/mention resolution - Individual inline elements (Bold, Italic, Strike, Code, Emoji, etc.) Co-authored-by: Martin Schoeler <martin.schoeler@rocket.chat>
… sync feats with develop (#41374)
e146031 to
b7f90a9
Compare
Feature branch for the Real Time Message Composer implementation project
https://rocketchat.atlassian.net/jira/software/c/projects/RTC