From 3a72b5a63203a0246696ec8c144cc4835555da10 Mon Sep 17 00:00:00 2001 From: not-meet Date: Wed, 18 Feb 2026 12:21:37 +0530 Subject: [PATCH] feat: add smart list auto-continuation to message composer --- .../markups/src/blocks/OrderedListBlock.js | 3 +- .../markups/src/blocks/UnOrderedListBlock.js | 3 +- packages/markups/src/blocks/blocks.styles.js | 5 +++ packages/react/src/lib/insertListPrefix.js | 20 +++++++++ packages/react/src/lib/textFormat.js | 2 + .../react/src/views/ChatInput/ChatInput.js | 45 ++++++++++++++++++- .../ChatInput/ChatInputFormattingToolbar.js | 17 ++++++- .../src/components/Icon/icons/ListBullets.js | 14 ++++++ .../src/components/Icon/icons/ListNumbers.js | 14 ++++++ .../src/components/Icon/icons/index.js | 4 ++ 10 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 packages/react/src/lib/insertListPrefix.js create mode 100644 packages/ui-elements/src/components/Icon/icons/ListBullets.js create mode 100644 packages/ui-elements/src/components/Icon/icons/ListNumbers.js diff --git a/packages/markups/src/blocks/OrderedListBlock.js b/packages/markups/src/blocks/OrderedListBlock.js index 0c1a3c62f3..da404f9298 100644 --- a/packages/markups/src/blocks/OrderedListBlock.js +++ b/packages/markups/src/blocks/OrderedListBlock.js @@ -1,9 +1,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import InlineElements from '../elements/InlineElements'; +import { listStyles } from './blocks.styles'; const OrderedListBlock = ({ items }) => ( -
    +
      {items.map((item, index) => (
    1. diff --git a/packages/markups/src/blocks/UnOrderedListBlock.js b/packages/markups/src/blocks/UnOrderedListBlock.js index a0f5f9ecf2..4ac0130beb 100644 --- a/packages/markups/src/blocks/UnOrderedListBlock.js +++ b/packages/markups/src/blocks/UnOrderedListBlock.js @@ -1,9 +1,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import InlineElements from '../elements/InlineElements'; +import { listStyles } from './blocks.styles'; const UnOrderedListBlock = ({ items }) => ( -
        +
          {items.map((item, index) => (
        • diff --git a/packages/markups/src/blocks/blocks.styles.js b/packages/markups/src/blocks/blocks.styles.js index 63765585de..87d2c85dbb 100644 --- a/packages/markups/src/blocks/blocks.styles.js +++ b/packages/markups/src/blocks/blocks.styles.js @@ -9,3 +9,8 @@ export const TaskListBlockStyles = { gap: 0.5em; `, }; + +export const listStyles = css` + padding-left: 1.5em; + margin: 0; +`; diff --git a/packages/react/src/lib/insertListPrefix.js b/packages/react/src/lib/insertListPrefix.js new file mode 100644 index 0000000000..68f549aed4 --- /dev/null +++ b/packages/react/src/lib/insertListPrefix.js @@ -0,0 +1,20 @@ +const insertListPrefix = (messageRef, listPrefix) => { + const input = messageRef.current; + if (!input) return; + + const { selectionStart, value } = input; + const before = value.slice(0, selectionStart); + const after = value.slice(selectionStart); + + const needsNewline = before.length > 0 && !before.endsWith('\n'); + const prefix = listPrefix(1); + const insert = (needsNewline ? '\n' : '') + prefix; + + input.value = before + insert + after; + const cursorPos = selectionStart + insert.length; + input.selectionStart = cursorPos; + input.selectionEnd = cursorPos; + input.focus(); +}; + +export default insertListPrefix; diff --git a/packages/react/src/lib/textFormat.js b/packages/react/src/lib/textFormat.js index 10ceb7f33c..3185c8ff1e 100644 --- a/packages/react/src/lib/textFormat.js +++ b/packages/react/src/lib/textFormat.js @@ -4,4 +4,6 @@ export const formatter = [ { name: 'strike', pattern: '~{{text}}~' }, { name: 'code', pattern: '`{{text}}`' }, { name: 'multiline', pattern: '```\n{{text}}\n``` ' }, + { name: 'list-numbers', type: 'list', listPrefix: (n) => `${n}. ` }, + { name: 'list-bullets', type: 'list', listPrefix: () => '- ' }, ]; diff --git a/packages/react/src/views/ChatInput/ChatInput.js b/packages/react/src/views/ChatInput/ChatInput.js index 99c1c8b4f7..f661699f99 100644 --- a/packages/react/src/views/ChatInput/ChatInput.js +++ b/packages/react/src/views/ChatInput/ChatInput.js @@ -396,10 +396,51 @@ const ChatInput = ({ scrollToBottom }) => { formatSelection(messageRef, '*{{text}}*'); break; } - case (e.ctrlKey || e.metaKey || e.shiftKey) && e.code === 'Enter': + case (e.ctrlKey || e.metaKey || e.shiftKey) && e.code === 'Enter': { e.preventDefault(); - handleNewLine(e); + const input = messageRef.current; + const { value, selectionStart } = input; + const textBefore = value.slice(0, selectionStart); + const currentLine = textBefore.split('\n').pop(); + // Match numbered list: "1. ", "2. ", etc. + const olMatch = currentLine.match(/^(\d+)\.\s/); + // Match bullet list: "- " + const ulMatch = currentLine.match(/^-\s/); + + if (olMatch && currentLine.trim() === `${olMatch[1]}.`) { + const lineStart = selectionStart - currentLine.length; + const rest = value.slice(selectionStart); + input.value = value.slice(0, lineStart) + rest; + input.selectionStart = lineStart; + input.selectionEnd = lineStart; + handleNewLine(e, false); + } else if (ulMatch && currentLine.trim() === '-') { + const lineStart = selectionStart - currentLine.length; + const rest = value.slice(selectionStart); + input.value = value.slice(0, lineStart) + rest; + input.selectionStart = lineStart; + input.selectionEnd = lineStart; + handleNewLine(e, false); + } else if (olMatch) { + const nextNum = parseInt(olMatch[1], 10) + 1; + const prefix = `\n${nextNum}. `; + const after = value.slice(selectionStart); + input.value = textBefore + prefix + after; + input.selectionStart = selectionStart + prefix.length; + input.selectionEnd = selectionStart + prefix.length; + handleNewLine(e, false); + } else if (ulMatch) { + const prefix = '\n- '; + const after = value.slice(selectionStart); + input.value = textBefore + prefix + after; + input.selectionStart = selectionStart + prefix.length; + input.selectionEnd = selectionStart + prefix.length; + handleNewLine(e, false); + } else { + handleNewLine(e); + } break; + } case e.code === 'Escape': if (editMessage.msg || editMessage.attachments) { e.preventDefault(); diff --git a/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js b/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js index 634b2255af..4caf6e5730 100644 --- a/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js +++ b/packages/react/src/views/ChatInput/ChatInputFormattingToolbar.js @@ -15,13 +15,22 @@ import AudioMessageRecorder from './AudioMessageRecorder'; import VideoMessageRecorder from './VideoMessageRecoder'; import { getChatInputFormattingToolbarStyles } from './ChatInput.styles'; import formatSelection from '../../lib/formatSelection'; +import insertListPrefix from '../../lib/insertListPrefix'; const ChatInputFormattingToolbar = ({ messageRef, inputRef, optionConfig = { surfaceItems: ['emoji', 'formatter', 'audio', 'video', 'file'], - formatters: ['bold', 'italic', 'strike', 'code', 'multiline'], + formatters: [ + 'bold', + 'italic', + 'strike', + 'code', + 'multiline', + 'list-numbers', + 'list-bullets', + ], }, }) => { const { classNames, styleOverrides, configOverrides } = useComponentOverrides( @@ -95,7 +104,11 @@ const ChatInputFormattingToolbar = ({ disabled={isRecordingMessage} ghost onClick={() => { - formatSelection(messageRef, item.pattern); + if (item.type === 'list') { + insertListPrefix(messageRef, item.listPrefix); + } else { + formatSelection(messageRef, item.pattern); + } }} > ( + + + +); + +export default ListBullets; diff --git a/packages/ui-elements/src/components/Icon/icons/ListNumbers.js b/packages/ui-elements/src/components/Icon/icons/ListNumbers.js new file mode 100644 index 0000000000..80ea33f4c5 --- /dev/null +++ b/packages/ui-elements/src/components/Icon/icons/ListNumbers.js @@ -0,0 +1,14 @@ +import React from 'react'; + +const ListNumbers = (props) => ( + + + +); + +export default ListNumbers; diff --git a/packages/ui-elements/src/components/Icon/icons/index.js b/packages/ui-elements/src/components/Icon/icons/index.js index 77cd6a4511..b1ce492774 100644 --- a/packages/ui-elements/src/components/Icon/icons/index.js +++ b/packages/ui-elements/src/components/Icon/icons/index.js @@ -61,6 +61,8 @@ import Arc from './Arc'; import Avatar from './Avatar'; import FormatText from './FormatText'; import Cog from './Cog'; +import ListNumbers from './ListNumbers'; +import ListBullets from './ListBullets'; const icons = { file: File, @@ -126,6 +128,8 @@ const icons = { avatar: Avatar, 'format-text': FormatText, cog: Cog, + 'list-numbers': ListNumbers, + 'list-bullets': ListBullets, }; export default icons;