Skip to content

feat: Real Time Message Composer - #41020

Draft
MartinSchoeler wants to merge 33 commits into
developfrom
poc/real-time-composer
Draft

feat: Real Time Message Composer#41020
MartinSchoeler wants to merge 33 commits into
developfrom
poc/real-time-composer

Conversation

@MartinSchoeler

@MartinSchoeler MartinSchoeler commented Jun 19, 2026

Copy link
Copy Markdown
Member

Feature branch for the Real Time Message Composer implementation project

https://rocketchat.atlassian.net/jira/software/c/projects/RTC

@dionisio-bot

dionisio-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is missing the required milestone or project

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot

changeset-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f99ea5c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@CLAassistant

CLAassistant commented Jun 19, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ ishanmitra
✅ MartinSchoeler
❌ cursoragent
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MartinSchoeler
MartinSchoeler force-pushed the poc/real-time-composer branch from 9d46406 to d13646e Compare July 1, 2026 20:46
@MartinSchoeler MartinSchoeler changed the title chore: poc Real Time Composer + gazzodown-alt feat: Real Time Message Composer Jul 1, 2026
@MartinSchoeler
MartinSchoeler force-pushed the poc/real-time-composer branch from cc3d467 to b59db10 Compare July 9, 2026 19:12
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.87892% with 347 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.43%. Comparing base (53b519c) to head (f99ea5c).
⚠️ Report is 6 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             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     
Flag Coverage Δ
e2e 58.17% <5.43%> (-0.85%) ⬇️
e2e-api 46.18% <ø> (+0.25%) ⬆️
unit 71.39% <84.78%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@MartinSchoeler
MartinSchoeler force-pushed the poc/real-time-composer branch 4 times, most recently from 6eda512 to 7021e91 Compare July 15, 2026 14:25
@MartinSchoeler

Copy link
Copy Markdown
Member Author

#41412 (comment)

@MartinSchoeler
MartinSchoeler force-pushed the poc/real-time-composer branch 4 times, most recently from be5156e to f1785c7 Compare July 27, 2026 19:08
@MartinSchoeler
MartinSchoeler force-pushed the poc/real-time-composer branch from 418d2fe to 2e5601c Compare August 26, 2026 16:39
ishanmitra and others added 12 commits August 27, 2026 14:08
- 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>
@MartinSchoeler
MartinSchoeler force-pushed the poc/real-time-composer branch from e146031 to b7f90a9 Compare August 27, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants