diff --git a/package.json b/package.json index b93783b317..6f6e7753ed 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "emoji-mart": "^5.4.0", "react": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0", "react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.14.0", - "stream-chat": "^9.17.0" + "stream-chat": "^9.19.0" }, "peerDependenciesMeta": { "@breezystack/lamejs": { @@ -236,7 +236,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "semantic-release": "^24.2.3", - "stream-chat": "^9.17.0", + "stream-chat": "^9.19.0", "ts-jest": "^29.2.5", "typescript": "^5.4.5", "typescript-eslint": "^8.17.0" diff --git a/src/components/TextareaComposer/TextareaComposer.tsx b/src/components/TextareaComposer/TextareaComposer.tsx index 828e9302b0..e550b4a098 100644 --- a/src/components/TextareaComposer/TextareaComposer.tsx +++ b/src/components/TextareaComposer/TextareaComposer.tsx @@ -5,7 +5,7 @@ import type { TextareaHTMLAttributes, UIEventHandler, } from 'react'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import Textarea from 'react-textarea-autosize'; import { useMessageComposer } from '../MessageInput'; import type { @@ -200,7 +200,6 @@ export const TextareaComposer = ({ event.preventDefault(); } handleSubmit(); - textareaRef.current.selectionEnd = 0; } }, [ @@ -225,7 +224,7 @@ export const TextareaComposer = ({ [onScroll, textComposer], ); - const setSelectionDebounced = useCallback( + const setSelection = useCallback( (e: SyntheticEvent) => { onSelect?.(e); textComposer.setSelection({ @@ -236,17 +235,6 @@ export const TextareaComposer = ({ [onSelect, textComposer], ); - useEffect(() => { - // FIXME: find the real reason for cursor being set to the end on each change - // This is a workaround to prevent the cursor from jumping - // to the end of the textarea when the user is typing - // at the position that is not at the end of the textarea value. - if (textareaRef.current && !isComposing) { - textareaRef.current.selectionStart = selection.start; - textareaRef.current.selectionEnd = selection.end; - } - }, [text, textareaRef, selection.start, selection.end, isComposing]); - useEffect(() => { if (textComposer.suggestions) { setFocusedItemIndex(0); @@ -259,18 +247,22 @@ export const TextareaComposer = ({ textareaRef.current.focus(); }, [attachments, focus, quotedMessage, textareaRef]); - useEffect(() => { + useLayoutEffect(() => { /** - * The textarea value has to be overridden outside the render cycle so that the events like compositionend can be triggered. - * If we have overridden the value during the component rendering, the compositionend event would not be triggered, and - * it would not be possible to type composed characters (รด). - * On the other hand, just removing the value override via prop (value={text}) would not allow us to change the text based on - * middleware results (e.g. replace characters with emojis) + * It is important to perform set text and after that the range + * to prevent cursor reset to the end of the textarea if doing it in separate effects. */ const textarea = textareaRef.current; - if (!textarea) return; - textarea.value = text; - }, [textareaRef, text]); + if (!textarea || isComposing) return; + + const length = textarea.value.length; + const start = Math.max(0, Math.min(selection.start, length)); + const end = Math.max(start, Math.min(selection.end, length)); + + if (textarea.selectionStart === start && textarea.selectionEnd === end) return; + + textarea.setSelectionRange(start, end, 'forward'); + }, [text, selection.start, selection.end, isComposing, textareaRef]); return (
{ textareaRef.current = ref; }} + value={text} /> {/* todo: X document the layout change for the accessibility purpose (tabIndex) */} {!isComposing && ( diff --git a/yarn.lock b/yarn.lock index 1393f6ecbe..5432a3cd54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12042,10 +12042,10 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -stream-chat@^9.17.0: - version "9.17.0" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.17.0.tgz#540cf1ea03b08a394d6140696aae8528e9ba9ce2" - integrity sha512-ys6K73wIVWs5+qsfPJ9wumEUtgbMXYVbH1dhmAZ1oYtQ01dY/avsvt25PYDakVjKeyrnT+y8T/xEzfeF/WDJsg== +stream-chat@^9.19.0: + version "9.19.0" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.19.0.tgz#8a2055be0f7c073ee8ca10cbc40af7d36648c476" + integrity sha512-ooRLubHPWxVr8Ws3fZvR30BFhVNM1xcrEgRnGGBxNINYYH/Wq+uc6AWONYIeu+n8crwRp3NSrtNfGWpWhLSy7Q== dependencies: "@types/jsonwebtoken" "^9.0.8" "@types/ws" "^8.5.14"