Skip to content

Conversation

@ishanmitra
Copy link

@ishanmitra ishanmitra commented May 12, 2025

Proposed changes

This PR introduces an experimental real-time message composer powered by a <div contenteditable> instead of the classic <textarea>. The goal is to enable richer in-place text formatting and interaction using modern DOM APIs, while maintaining compatibility with the current composer functionality.

Key changes include:

  • Replaced the classic <textarea> input with a <div contenteditable> in MessageComposerInputNew.
  • Implemented selection and cursor management using the Selection API to support real-time formatting features.
  • Fixed the Enter key behavior to send messages as expected.
  • Adapted input handling logic (e.g., onInput, innerText.trim()) to support correct state management (e.g., Send button enable/disable).
  • Restored basic formatting actions (bold, italic, strikethrough).
  • Added a feature flag featurePreviewComposer to toggle between the new input and the current one safely.
  • Added an in-UI experimental label (MessageComposerHint) to indicate this is a preview feature.

image
image

Steps to test

Set the featurePreviewComposer flag to true (or false) in apps/meteor/client/views/room/composer/ComposerMessage.tsx

Known Issues & Next Steps

  • Slash commands / and emoji autocomplete open as expected, but pressing Enter inserts the selection and also sends the message, which is unintended.
  • Placeholder text is not yet implemented for <div contenteditable>.

Additional work is needed to:

  • Prevent sending when the slash command menu or emoji popups are active.
  • Implement placeholder text and fix component UI breakage when the message is over 6 lines tall.

- 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.
- 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.
- 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.
- 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.
@ishanmitra ishanmitra requested a review from a team as a code owner May 12, 2025 22:36
@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented May 12, 2025

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
Copy link

changeset-bot bot commented May 12, 2025

⚠️ No Changeset found

Latest commit: e730794

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

@MartinSchoeler MartinSchoeler self-assigned this May 12, 2025
@MartinSchoeler MartinSchoeler requested review from MartinSchoeler and removed request for a team May 12, 2025 22:38
Copy link
Contributor

@AyushKumar123456789 AyushKumar123456789 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is just the initial version of code, but there some anomaly that I found that might can help, when you make a text bold and then you remove the astrisk around it , still show the text in bold in text editor and also there is some issue with the autogrow (I know its commented) but when the input size increase there is some error with the toolbox.

- 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.
@ishanmitra
Copy link
Author

I know this is just the initial version of code, but there some anomaly that I found that might can help, when you make a text bold and then you remove the astrisk around it , still show the text in bold in text editor and also there is some issue with the autogrow (I know its commented) but when the input size increase there is some error with the toolbox.

I tested the hotkey which is Ctrl+B and these are my observations:

When the selection is empty, the asterisks are added. And any text within that asterisk will naturally get bolded after submitting the text. However, if you bold a text with the same shortcut, it shows an actual bold text because that is the behaviour of the div contenteditable which I did not anticipate. I believe that some kind of a preventDefault will help solve this issue as we want only the markdown format to update the style.

So a bold text on submission does not remain bold since Rocket.Chat composer cannot see the asterisk formatters. It only sees plain text.

This is a really good find. Thank you for pointing that out. The next commit will be focusing on eliminating this issue.

import '@rocket.chat/icons/dist/rocketchat.css';
import {
MessageComposer,
MessageComposerAction,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the MessageComposer we are creating is a different component, not a variation, I would suggest creating a new .stories.tsx file for it, let's try to modify existing files as little as possible (this way we will have less conflicts merging develop into this branch)

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`
- Moved `getSelectionRange` and `setSelectionRange` from `createRichTextComposerAPI.ts` to a new `selectionRange.ts` file.
- Improves separation of concerns and allows reuse across other modules.
- 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
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
- 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
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
- Blocked browser insertion of <b> and <i> tags
- Delegated formatting to custom shortcut handler
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
- Fixed cursor moving to end of contentEditable input and regaining focus after editing reset
- Updated `createRichTextComposerAPI` to accept `setMdLines` and `setCursorHistory` state setters
- Added `beforeinput` listener to update markdown lines and cursor history via `resolveBeforeInput`
- 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
- 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.
- Implemented useEffect on `mdLines` to reflect the latest content during room change or message editing state
@ishanmitra ishanmitra marked this pull request as draft August 11, 2025 14:43
@ishanmitra ishanmitra marked this pull request as ready for review August 11, 2025 14:46
- 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`
- Add `Options` type and `parse` from `message-parser`
- Pass `parseOptions` through `createRichTextComposerAPI` to `resolveBeforeInput`
- Implement `parseMessage` helper and log parsed AST for debugging
- 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
- Extended `handleFormattingShortcut` to accept `setMdLines`, `setCursorHistory`, and `parseOptions`
@MartinSchoeler MartinSchoeler force-pushed the feat/real-time-composer branch from f08e548 to c8dd16d Compare August 14, 2025 14:24
@MartinSchoeler MartinSchoeler force-pushed the feat/real-time-composer branch from 9cac38d to 322744b Compare August 14, 2025 15:05
- 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
const tag = node.nodeName.toLowerCase();

// Skip inline tags that don't cause linebreaks
if (['b', 'i', 'u', 'span', 'strong', 'em', 'small', 'abbr', 'sub', 'sup', 'mark', 'code', 'del'].includes(tag) && node !== input) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use an enum instead of hardcoding the array, it is better for reusability and typechecking

const tag = (node as HTMLElement).tagName.toLowerCase();

const isInline =
['b', 'i', 'u', 'span', 'strong', 'em', 'small', 'abbr', 'sub', 'sup', 'mark', 'code', 'del'].includes(tag) && node !== input;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, use an enum

}

// This prevents focus from being stolen between the two RichText Composers
if ((target as HTMLElement).tagName === 'SPAN' && (target as HTMLElement).isContentEditable) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use this hook https://github.com/RocketChat/Rocket.Chat/blob/develop/packages/ui-client/src/hooks/useFeaturePreview.ts
To verify that the target will be contenteditable
just pass the parameter realtimeMessageComposer to it and from the result, you will know if the composer is content editable or not

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! I will test and resolve them! I am just in the brink of finishing up some of the code and I would be more than happy to resolve and make the code more readable and logically more stable.

},
"dependencies": {
"@types/stream-buffers": "^3.0.7",
"hono": "^4.7.9",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this change is unrelated to the PR

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it happened due to some package installation way back in May. I do remember this change happening, and committing it to the branch and PR. Although this was the commit it is associated with: a638791

- 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
- 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
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.
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
- Implemented `resolveComposerBox` when navigating threads with Up/Down arrow keys
- 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
- 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
- Emoji shortcodes convert into Unicode emoticons.
- Custom uploaded emojis render as <span> with inline <img> and shortcode fallback.
@ishanmitra ishanmitra force-pushed the feat/real-time-composer branch from ec5512b to 8103d3f Compare August 15, 2025 14:22
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 />`
- 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
- 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
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants