From 2f9b89dad095f9f26c4b068c4a487e05295eef34 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sun, 16 Jul 2023 17:52:52 +0200 Subject: [PATCH 01/18] WIP migrate to fc --- src/components/Composer/index.js | 573 ++++++++++++++----------------- 1 file changed, 257 insertions(+), 316 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index af350ae48336..992185671e26 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState, useRef, useEffect, useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; @@ -121,372 +121,313 @@ const IMAGE_EXTENSIONS = { 'image/webp': 'webp', }; -/** - * Enable Markdown parsing. - * On web we like to have the Text Input field always focused so the user can easily type a new chat - */ -class Composer extends React.Component { - constructor(props) { - super(props); - - const initialValue = props.defaultValue ? `${props.defaultValue}` : `${props.value || ''}`; - - this.state = { - numberOfLines: props.numberOfLines, - selection: { - start: initialValue.length, - end: initialValue.length, - }, - valueBeforeCaret: '', - }; - - this.paste = this.paste.bind(this); - this.handleKeyPress = this.handleKeyPress.bind(this); - this.handlePaste = this.handlePaste.bind(this); - this.handlePastedHTML = this.handlePastedHTML.bind(this); - this.handleWheel = this.handleWheel.bind(this); - this.shouldCallUpdateNumberOfLines = this.shouldCallUpdateNumberOfLines.bind(this); - this.addCursorPositionToSelectionChange = this.addCursorPositionToSelectionChange.bind(this); - this.textRef = React.createRef(null); - this.unsubscribeBlur = () => null; - this.unsubscribeFocus = () => null; - } - - componentDidMount() { - this.updateNumberOfLines(); - - // This callback prop is used by the parent component using the constructor to - // get a ref to the inner textInput element e.g. if we do - // this.textInput = el} /> this will not - // return a ref to the component, but rather the HTML element by default - if (this.props.forwardedRef && _.isFunction(this.props.forwardedRef)) { - this.props.forwardedRef(this.textInput); - } - - // There is no onPaste or onDrag for TextInput in react-native so we will add event - // listeners here and unbind when the component unmounts - if (this.textInput) { - this.textInput.addEventListener('wheel', this.handleWheel); +// Get characters from the cursor to the next space or new line +const getNextChars = (str, cursorPos) => { + // Get the substring starting from the cursor position + const substr = str.substring(cursorPos); - // we need to handle listeners on navigation focus/blur as Composer is not unmounting - // when navigating away to different report - this.unsubscribeFocus = this.props.navigation.addListener('focus', () => document.addEventListener('paste', this.handlePaste)); - this.unsubscribeBlur = this.props.navigation.addListener('blur', () => document.removeEventListener('paste', this.handlePaste)); + // Find the index of the next space or new line character + const spaceIndex = substr.search(/[ \n]/); - // We need to add paste listener manually as well as navigation focus event is not triggered on component mount - document.addEventListener('paste', this.handlePaste); - } + if (spaceIndex === -1) { + return substr; } - componentDidUpdate(prevProps) { - if (!prevProps.shouldClear && this.props.shouldClear) { - this.textInput.clear(); - // eslint-disable-next-line react/no-did-update-set-state - this.setState({numberOfLines: 1}); - this.props.onClear(); - } - - if ( - prevProps.value !== this.props.value || - prevProps.defaultValue !== this.props.defaultValue || - prevProps.isComposerFullSize !== this.props.isComposerFullSize || - prevProps.windowWidth !== this.props.windowWidth || - prevProps.numberOfLines !== this.props.numberOfLines - ) { - this.updateNumberOfLines(); - } - - if (prevProps.selection !== this.props.selection) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({selection: this.props.selection}); - } - } + // If there is a space or new line, return the substring up to the space or new line + return substr.substring(0, spaceIndex); +}; - componentWillUnmount() { - if (!this.textInput) { +/** + * Enable Markdown parsing. + * On web we like to have the Text Input field always focused so the user can easily type a new chat + */ +function Composer({onKeyPress, style, ...props}) { + const textRef = useRef(null); + const textInput = useRef(null); + + const setTextRef = useCallback( + (el) => { + textRef.current = el; + }, + [textRef], + ); + + const initialValue = props.defaultValue ? `${props.defaultValue}` : `${props.value || ''}`; + + const [numberOfLines, setNumberOfLines] = useState({ + numberOfLines: props.numberOfLines, + }); + const [selection, setSelection] = useState({ + start: initialValue.length, + end: initialValue.length, + }); + const [caretContent, setCaretContent] = useState(''); + const [valueBeforeCaret, setValueBeforeCaret] = useState(''); + const [textInputWidth, setTextInputWidth] = useState(''); + const [cursorPosition, setCursorPosition] = useState({ + positionX: 0, + positionY: 0, + }); + const [propStyles, setPropStyle] = useState(StyleSheet.flatten([style, {outline: 'none'}])); + + useEffect(() => { + if (!props.shouldClear) { return; } - - document.removeEventListener('paste', this.handlePaste); - this.unsubscribeFocus(); - this.unsubscribeBlur(); - this.textInput.removeEventListener('wheel', this.handleWheel); - } - - // Get characters from the cursor to the next space or new line - getNextChars(str, cursorPos) { - // Get the substring starting from the cursor position - const substr = str.substring(cursorPos); - - // Find the index of the next space or new line character - const spaceIndex = substr.search(/[ \n]/); - - if (spaceIndex === -1) { - return substr; - } - - // If there is a space or new line, return the substring up to the space or new line - return substr.substring(0, spaceIndex); - } + textInput.current.clear(); + setNumberOfLines(1); + props.onClear(); + }, [props.shouldClear]); /** * Adds the cursor position to the selection change event. * * @param {Event} event */ - addCursorPositionToSelectionChange(event) { - if (this.props.shouldCalculateCaretPosition) { - const newValueBeforeCaret = event.target.value.slice(0, event.nativeEvent.selection.start); - - this.setState( - { - valueBeforeCaret: newValueBeforeCaret, - caretContent: this.getNextChars(this.props.value, event.nativeEvent.selection.start), - }, - - () => { - const customEvent = { - nativeEvent: { - selection: { - start: event.nativeEvent.selection.start, - end: event.nativeEvent.selection.end, - positionX: this.textRef.current.offsetLeft - CONST.SPACE_CHARACTER_WIDTH, - positionY: this.textRef.current.offsetTop, - }, - }, - }; - this.props.onSelectionChange(customEvent); - }, - ); - return; + const addCursorPositionToSelectionChange = (event) => { + if (props.shouldCalculateCaretPosition) { + setValueBeforeCaret(event.target.value.slice(0, event.nativeEvent.selection.start)); + setCaretContent(getNextChars(props.value, event.nativeEvent.selection.start)); } + props.onSelectionChange(event); + }; - this.props.onSelectionChange(event); - } - - // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed - handleKeyPress(e) { - if (!this.props.onKeyPress || isEnterWhileComposition(e)) { - return; - } - this.props.onKeyPress(e); - } + useEffect(() => { + setSelection({ + start: valueBeforeCaret.length, + end: valueBeforeCaret.length + caretContent.length, + }); + setCursorPosition({ + positionX: textRef.current.offsetLeft - CONST.SPACE_CHARACTER_WIDTH, + positionY: textRef.current.offsetTop, + }); + }, [valueBeforeCaret, caretContent]); - /** - * Set pasted text to clipboard - * @param {String} text - */ - paste(text) { + // /** + // * Set pasted text to clipboard + // * @param {String} text + // */ + const paste = useCallback((text) => { try { - this.textInput.focus(); document.execCommand('insertText', false, text); - this.updateNumberOfLines(); + // updateNumberOfLines(); // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view. - this.textInput.blur(); - this.textInput.focus(); + textInput.blur(); + textInput.focus(); // eslint-disable-next-line no-empty } catch (e) {} - } + }, []); /** * Manually place the pasted HTML into Composer * * @param {String} html - pasted HTML */ - handlePastedHTML(html) { - const parser = new ExpensiMark(); - this.paste(parser.htmlToMarkdown(html)); - } - - /** - * Check the paste event for an attachment, parse the data and call onPasteFile from props with the selected file, - * Otherwise, convert pasted HTML to Markdown and set it on the composer. - * - * @param {ClipboardEvent} event - */ - handlePaste(event) { - if (!this.props.checkComposerVisibility() && !this.state.isFocused) { - return; - } - - event.preventDefault(); - - const {files, types} = event.clipboardData; - const TEXT_HTML = 'text/html'; - - // If paste contains files, then trigger file management - if (files.length > 0) { - // Prevent the default so we do not post the file name into the text box - this.props.onPasteFile(event.clipboardData.files[0]); - return; - } - - // If paste contains HTML - if (types.includes(TEXT_HTML)) { - const pastedHTML = event.clipboardData.getData(TEXT_HTML); - - const domparser = new DOMParser(); - const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images; - - // If HTML has img tag, then fetch images from it. - if (embeddedImages.length > 0 && embeddedImages[0].src) { - // If HTML has emoji, then treat this as plain text. - if (embeddedImages[0].dataset && embeddedImages[0].dataset.stringifyType === 'emoji') { - const plainText = event.clipboardData.getData('text/plain'); - this.paste(plainText); + const handlePastedHTML = useCallback( + (html) => { + const parser = new ExpensiMark(); + paste(parser.htmlToMarkdown(html)); + }, + [paste], + ); + + const handlePaste = useCallback( + (event) => { + event.preventDefault(); + + const {files, types} = event.clipboardData; + const TEXT_HTML = 'text/html'; + + // If paste contains files, then trigger file management + if (files.length > 0) { + // Prevent the default so we do not post the file name into the text box + props.onPasteFile(event.clipboardData.files[0]); + return; + } + // If paste contains HTML + if (types.includes(TEXT_HTML)) { + const pastedHTML = event.clipboardData.getData(TEXT_HTML); + + const domparser = new DOMParser(); + const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images; + + // If HTML has img tag, then fetch images from it. + if (embeddedImages.length > 0 && embeddedImages[0].src) { + // If HTML has emoji, then treat this as plain text. + if (embeddedImages[0].dataset && embeddedImages[0].dataset.stringifyType === 'emoji') { + const plainText = event.clipboardData.getData('text/plain'); + paste(plainText); + return; + } + fetch(embeddedImages[0].src) + .then((response) => { + if (!response.ok) { + throw Error(response.statusText); + } + return response.blob(); + }) + .then((x) => { + const extension = IMAGE_EXTENSIONS[x.type]; + if (!extension) { + throw new Error(props.translate('composer.noExtensionFoundForMimeType')); + } + + return new File([x], `pasted_image.${extension}`, {}); + }) + .then(props.onPasteFile) + .catch(() => { + const errorDesc = props.translate('composer.problemGettingImageYouPasted'); + Growl.error(errorDesc); + + /* + * Since we intercepted the user-triggered paste event to check for attachments, + * we need to manually set the value and call the `onChangeText` handler. + * Synthetically-triggered paste events do not affect the document's contents. + * See https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event for more details. + */ + handlePastedHTML(pastedHTML); + }); return; } - fetch(embeddedImages[0].src) - .then((response) => { - if (!response.ok) { - throw Error(response.statusText); - } - return response.blob(); - }) - .then((x) => { - const extension = IMAGE_EXTENSIONS[x.type]; - if (!extension) { - throw new Error(this.props.translate('composer.noExtensionFoundForMimeType')); - } - - return new File([x], `pasted_image.${extension}`, {}); - }) - .then(this.props.onPasteFile) - .catch(() => { - const errorDesc = this.props.translate('composer.problemGettingImageYouPasted'); - Growl.error(errorDesc); - - /* - * Since we intercepted the user-triggered paste event to check for attachments, - * we need to manually set the value and call the `onChangeText` handler. - * Synthetically-triggered paste events do not affect the document's contents. - * See https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event for more details. - */ - this.handlePastedHTML(pastedHTML); - }); + + handlePastedHTML(pastedHTML); return; } + const plainText = event.clipboardData.getData('text/plain'); - this.handlePastedHTML(pastedHTML); - return; - } - - const plainText = event.clipboardData.getData('text/plain'); - - this.paste(plainText); - } + paste(plainText); + }, + [props.onPasteFile, props.translate, paste, handlePastedHTML], + ); /** * Manually scrolls the text input, then prevents the event from being passed up to the parent. * @param {Object} event native Event */ - handleWheel(event) { + const handleWheel = useCallback((event) => { if (event.target !== document.activeElement) { return; } - this.textInput.scrollTop += event.deltaY; + textInput.current.scrollTop += event.deltaY; event.preventDefault(); event.stopPropagation(); - } - - /** - * We want to call updateNumberOfLines only when the parent doesn't provide value in props - * as updateNumberOfLines is already being called when value changes in componentDidUpdate - */ - shouldCallUpdateNumberOfLines() { - if (!_.isEmpty(this.props.value)) { + }, []); + const updateNumberOfLines = useCallback(() => { + const computedStyle = window.getComputedStyle(textInput.current); + const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20; + const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); + const computedNumberOfLines = ComposerUtils.getNumberOfLines(props.maxLines, lineHeight, paddingTopAndBottom, textInput.current.scrollHeight); + const generalNumberOfLines = computedNumberOfLines === 0 ? props.numberOfLines : computedNumberOfLines; + + updateIsFullComposerAvailable({isFullComposerAvailable: props.isFullComposerAvailable, setIsFullComposerAvailable: props.setIsFullComposerAvailable}, generalNumberOfLines); + + setNumberOfLines(generalNumberOfLines); + props.onNumberOfLinesChange(generalNumberOfLines); + }, [props.maxLines, props.numberOfLines, props.isComposerFullSize, props.value]); + + useEffect(() => { + if (textInput.current === null) { return; } + setNumberOfLines(1); + props.setIsFullComposerAvailable(false); + updateNumberOfLines(); + }, [props.maxLines, props.numberOfLines, props.isComposerFullSize, props.value]); + + const handleKeyPress = useCallback( + (e) => { + // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed + if (!onKeyPress || isEnterWhileComposition(e)) { + return; + } + onKeyPress(e); + }, + [onKeyPress], + ); + + // /** + // * Set the TextInput Ref + // * + // * @param {Element} el + // */ + const setTextInputRef = useCallback((el) => { + textInput.current = el; + + if (_.isFunction(props.forwardedRef)) { + props.forwardedRef(textInput.current); + } - this.updateNumberOfLines(); - } - - /** - * Check the current scrollHeight of the textarea (minus any padding) and - * divide by line height to get the total number of rows for the textarea. - */ - updateNumberOfLines() { - // Hide the composer expand button so we can get an accurate reading of - // the height of the text input - this.props.setIsFullComposerAvailable(false); - - // We have to reset the rows back to the minimum before updating so that the scrollHeight is not - // affected by the previous row setting. If we don't, rows will be added but not removed on backspace/delete. - this.setState({numberOfLines: 1}, () => { - const computedStyle = window.getComputedStyle(this.textInput); - const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20; - const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); - const computedNumberOfLines = ComposerUtils.getNumberOfLines(this.props.maxLines, lineHeight, paddingTopAndBottom, this.textInput.scrollHeight); - const numberOfLines = computedNumberOfLines === 0 ? this.props.numberOfLines : computedNumberOfLines; - updateIsFullComposerAvailable(this.props, numberOfLines); - this.setState({ - numberOfLines, - width: computedStyle.width, - }); - this.props.onNumberOfLinesChange(numberOfLines); - }); - } - - render() { - const propStyles = StyleSheet.flatten(this.props.style); - propStyles.outline = 'none'; - const propsWithoutStyles = _.omit(this.props, 'style'); - - // This code creates a hidden text component that helps track the caret position in the visible input. - const renderElementForCaretPosition = ( - { + // we need to handle listeners on navigation focus/blur as Composer is not unmounting + // when navigating away to different report + const unsubscribeFocus = props.navigation.addListener('focus', () => document.addEventListener('paste', handlePaste)); + const unsubscribeBlur = props.navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste)); + + () => { + unsubscribeFocus(); + unsubscribeBlur(); + document.removeEventListener('paste', this.handlePaste); + textInput.removeEventListener('wheel', this.handleWheel); + }; + }, []); + + const renderElementForCaretPosition = ( + + + {`${valueBeforeCaret} `} - {`${this.state.valueBeforeCaret} `} - - {`${this.state.caretContent}`} - + {`${caretContent}`} - - ); - - // We're disabling autoCorrect for iOS Safari until Safari fixes this issue. See https://github.com/Expensify/App/issues/8592 - return ( - <> - (this.textInput = el)} - selection={this.state.selection} - onChange={this.shouldCallUpdateNumberOfLines} - style={[ - propStyles, - - // We are hiding the scrollbar to prevent it from reducing the text input width, - // so we can get the correct scroll height while calculating the number of lines. - this.state.numberOfLines < this.props.maxLines ? styles.overflowHidden : {}, - ]} - /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...propsWithoutStyles} - onSelectionChange={this.addCursorPositionToSelectionChange} - numberOfLines={this.state.numberOfLines} - disabled={this.props.isDisabled} - onKeyPress={this.handleKeyPress} - /> - {this.props.shouldCalculateCaretPosition && renderElementForCaretPosition} - - ); - } + + + ); + + return ( + <> + + {props.shouldCalculateCaretPosition && renderElementForCaretPosition} + + ); } Composer.propTypes = propTypes; From 22be8212ca4df320dc30f62dfd578672d72a2765 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 19 Jul 2023 21:17:02 +0200 Subject: [PATCH 02/18] WIP migration composer with numberOfLines --- src/components/Composer/index.js | 79 ++++++++++++++++---------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 992185671e26..4bdaea5d9b6e 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -1,5 +1,6 @@ -import React, {useState, useRef, useEffect, useCallback, useMemo} from 'react'; -import {StyleSheet, View} from 'react-native'; +import React, {useState, useRef, useEffect, useCallback, useLayoutEffect} from 'react'; +import { flushSync } from 'react-dom'; +import {StyleSheet, View, LayoutAnimation} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; @@ -144,19 +145,8 @@ const getNextChars = (str, cursorPos) => { function Composer({onKeyPress, style, ...props}) { const textRef = useRef(null); const textInput = useRef(null); - - const setTextRef = useCallback( - (el) => { - textRef.current = el; - }, - [textRef], - ); - const initialValue = props.defaultValue ? `${props.defaultValue}` : `${props.value || ''}`; - - const [numberOfLines, setNumberOfLines] = useState({ - numberOfLines: props.numberOfLines, - }); + const [numberOfLines, setNumberOfLines] = useState(props.numberOfLines); const [selection, setSelection] = useState({ start: initialValue.length, end: initialValue.length, @@ -210,7 +200,7 @@ function Composer({onKeyPress, style, ...props}) { const paste = useCallback((text) => { try { document.execCommand('insertText', false, text); - // updateNumberOfLines(); + updateNumberOfLines(); // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view. textInput.blur(); @@ -314,27 +304,32 @@ function Composer({onKeyPress, style, ...props}) { event.preventDefault(); event.stopPropagation(); }, []); - const updateNumberOfLines = useCallback(() => { - const computedStyle = window.getComputedStyle(textInput.current); - const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20; - const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); - const computedNumberOfLines = ComposerUtils.getNumberOfLines(props.maxLines, lineHeight, paddingTopAndBottom, textInput.current.scrollHeight); - const generalNumberOfLines = computedNumberOfLines === 0 ? props.numberOfLines : computedNumberOfLines; - - updateIsFullComposerAvailable({isFullComposerAvailable: props.isFullComposerAvailable, setIsFullComposerAvailable: props.setIsFullComposerAvailable}, generalNumberOfLines); - setNumberOfLines(generalNumberOfLines); - props.onNumberOfLinesChange(generalNumberOfLines); - }, [props.maxLines, props.numberOfLines, props.isComposerFullSize, props.value]); - useEffect(() => { - if (textInput.current === null) { - return; - } - setNumberOfLines(1); - props.setIsFullComposerAvailable(false); + // const updateNumberOfLines = useCallback(() => { + const updateNumberOfLines = useCallback(() => { + if (textInput.current === null) { + return; + } + // flushSync(() => { + setTimeout(() => { + setNumberOfLines(1); + + const computedStyle = window.getComputedStyle(textInput.current); + const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20; + const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); + const computedNumberOfLines = ComposerUtils.getNumberOfLines(props.maxLines, lineHeight, paddingTopAndBottom, textInput.current.scrollHeight); + const generalNumberOfLines = computedNumberOfLines === 0 ? props.numberOfLines : computedNumberOfLines; + + props.onNumberOfLinesChange(generalNumberOfLines); + updateIsFullComposerAvailable(props, generalNumberOfLines); + setNumberOfLines(generalNumberOfLines); + }, 0); + },[props.value, props.maxLines, props.isFullComposerAvailable, props.numberOfLines, props.isComposerFullSize]); + + useLayoutEffect(() => { updateNumberOfLines(); - }, [props.maxLines, props.numberOfLines, props.isComposerFullSize, props.value]); + }, [props.isComposerFullSize]); const handleKeyPress = useCallback( (e) => { @@ -347,11 +342,11 @@ function Composer({onKeyPress, style, ...props}) { [onKeyPress], ); - // /** - // * Set the TextInput Ref - // * - // * @param {Element} el - // */ + /** + * Set the TextInput Ref + * + * @param {Element} el + */ const setTextInputRef = useCallback((el) => { textInput.current = el; @@ -368,9 +363,13 @@ function Composer({onKeyPress, style, ...props}) { useEffect(() => { // we need to handle listeners on navigation focus/blur as Composer is not unmounting // when navigating away to different report + // const textareaElement = textInput.current; const unsubscribeFocus = props.navigation.addListener('focus', () => document.addEventListener('paste', handlePaste)); const unsubscribeBlur = props.navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste)); + console.log('updateIsFullComposerAvailable.UseEffect', props.isFullComposerAvailable); + // updateIsFullComposerAvailable({isFullComposerAvailable: props.isFullComposerAvailable, setIsFullComposerAvailable: props.setIsFullComposerAvailable}, numberOfLines); + () => { unsubscribeFocus(); unsubscribeBlur(); @@ -411,8 +410,9 @@ function Composer({onKeyPress, style, ...props}) { placeholderTextColor={themeColors.placeholderText} ref={setTextInputRef} selection={selection} + onChange={updateNumberOfLines} style={[ - StyleSheet.flatten([style, {outline: 'none'}, {backgroundColor: props.isComposerFullSize ? 'red' : 'purple'}]), + StyleSheet.flatten([style, {outline: 'none'}]), // We are hiding the scrollbar to prevent it from reducing the text input width, // so we can get the correct scroll height while calculating the number of lines. @@ -446,3 +446,4 @@ export default compose( /> )), ); + From 4f415ed7539a3eac1a45d9bc7111e175bd3fa608 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 21 Jul 2023 17:32:01 +0200 Subject: [PATCH 03/18] fix positionX and positionX --- src/components/Composer/index.js | 113 +++++++++++++++---------------- 1 file changed, 54 insertions(+), 59 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 4bdaea5d9b6e..389c0ade4b1b 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -1,6 +1,5 @@ -import React, {useState, useRef, useEffect, useCallback, useLayoutEffect} from 'react'; -import { flushSync } from 'react-dom'; -import {StyleSheet, View, LayoutAnimation} from 'react-native'; +import React, {useState, useRef, useEffect, useCallback} from 'react'; +import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; @@ -138,10 +137,8 @@ const getNextChars = (str, cursorPos) => { return substr.substring(0, spaceIndex); }; -/** - * Enable Markdown parsing. - * On web we like to have the Text Input field always focused so the user can easily type a new chat - */ +// Enable Markdown parsing. +// On web we like to have the Text Input field always focused so the user can easily type a new chat function Composer({onKeyPress, style, ...props}) { const textRef = useRef(null); const textInput = useRef(null); @@ -154,11 +151,6 @@ function Composer({onKeyPress, style, ...props}) { const [caretContent, setCaretContent] = useState(''); const [valueBeforeCaret, setValueBeforeCaret] = useState(''); const [textInputWidth, setTextInputWidth] = useState(''); - const [cursorPosition, setCursorPosition] = useState({ - positionX: 0, - positionY: 0, - }); - const [propStyles, setPropStyle] = useState(StyleSheet.flatten([style, {outline: 'none'}])); useEffect(() => { if (!props.shouldClear) { @@ -169,29 +161,32 @@ function Composer({onKeyPress, style, ...props}) { props.onClear(); }, [props.shouldClear]); + useEffect(() => { + setSelection(props.selection); + }, [props.selection]); + /** * Adds the cursor position to the selection change event. * * @param {Event} event */ const addCursorPositionToSelectionChange = (event) => { - if (props.shouldCalculateCaretPosition) { + flushSync(() => { setValueBeforeCaret(event.target.value.slice(0, event.nativeEvent.selection.start)); setCaretContent(getNextChars(props.value, event.nativeEvent.selection.start)); - } - props.onSelectionChange(event); - }; - - useEffect(() => { - setSelection({ - start: valueBeforeCaret.length, - end: valueBeforeCaret.length + caretContent.length, }); - setCursorPosition({ - positionX: textRef.current.offsetLeft - CONST.SPACE_CHARACTER_WIDTH, - positionY: textRef.current.offsetTop, + + props.onSelectionChange({ + nativeEvent: { + selection: { + start: event.nativeEvent.selection.start, + end: event.nativeEvent.selection.end, + positionX: textRef.current.offsetLeft - CONST.SPACE_CHARACTER_WIDTH, + positionY: textRef.current.offsetTop, + }, + }, }); - }, [valueBeforeCaret, caretContent]); + }; // /** // * Set pasted text to clipboard @@ -200,8 +195,6 @@ function Composer({onKeyPress, style, ...props}) { const paste = useCallback((text) => { try { document.execCommand('insertText', false, text); - updateNumberOfLines(); - // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view. textInput.blur(); textInput.focus(); @@ -305,31 +298,38 @@ function Composer({onKeyPress, style, ...props}) { event.stopPropagation(); }, []); - - // const updateNumberOfLines = useCallback(() => { const updateNumberOfLines = useCallback(() => { - if (textInput.current === null) { - return; - } - // flushSync(() => { - setTimeout(() => { - setNumberOfLines(1); - - const computedStyle = window.getComputedStyle(textInput.current); - const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20; - const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); - const computedNumberOfLines = ComposerUtils.getNumberOfLines(props.maxLines, lineHeight, paddingTopAndBottom, textInput.current.scrollHeight); - const generalNumberOfLines = computedNumberOfLines === 0 ? props.numberOfLines : computedNumberOfLines; - - props.onNumberOfLinesChange(generalNumberOfLines); - updateIsFullComposerAvailable(props, generalNumberOfLines); - setNumberOfLines(generalNumberOfLines); - }, 0); - },[props.value, props.maxLines, props.isFullComposerAvailable, props.numberOfLines, props.isComposerFullSize]); - - useLayoutEffect(() => { + if (textInput.current === null) { + return; + } + const computedStyle = window.getComputedStyle(textInput.current); + const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20; + const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); + setTextInputWidth(computedStyle.width); + + const computedNumberOfLines = ComposerUtils.getNumberOfLines(props.maxLines, lineHeight, paddingTopAndBottom, textInput.current.scrollHeight); + const generalNumberOfLines = computedNumberOfLines === 0 ? props.numberOfLines : computedNumberOfLines; + + props.onNumberOfLinesChange(generalNumberOfLines); + updateIsFullComposerAvailable(props, generalNumberOfLines); + setNumberOfLines(generalNumberOfLines); + }, [props.value, props.maxLines, props.isFullComposerAvailable, props.numberOfLines, props.isComposerFullSize]); + + const triggerUpdateNumberOfLines = useCallback(() => { + if (textInput.current === null) { + return; + } updateNumberOfLines(); - }, [props.isComposerFullSize]); + + setTimeout(() => { + setNumberOfLines(1); + updateNumberOfLines(); + }, 0); + }, [props.value, props.maxLines, props.isFullComposerAvailable, props.numberOfLines, props.isComposerFullSize]); + + useEffect(() => { + triggerUpdateNumberOfLines(); + }, [props.isComposerFullSize, triggerUpdateNumberOfLines]); const handleKeyPress = useCallback( (e) => { @@ -363,18 +363,14 @@ function Composer({onKeyPress, style, ...props}) { useEffect(() => { // we need to handle listeners on navigation focus/blur as Composer is not unmounting // when navigating away to different report - // const textareaElement = textInput.current; const unsubscribeFocus = props.navigation.addListener('focus', () => document.addEventListener('paste', handlePaste)); const unsubscribeBlur = props.navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste)); - console.log('updateIsFullComposerAvailable.UseEffect', props.isFullComposerAvailable); - // updateIsFullComposerAvailable({isFullComposerAvailable: props.isFullComposerAvailable, setIsFullComposerAvailable: props.setIsFullComposerAvailable}, numberOfLines); - - () => { + return () => { unsubscribeFocus(); unsubscribeBlur(); - document.removeEventListener('paste', this.handlePaste); - textInput.removeEventListener('wheel', this.handleWheel); + document.removeEventListener('paste', handlePaste); + textInput.removeEventListener('wheel', handleWheel); }; }, []); @@ -410,7 +406,7 @@ function Composer({onKeyPress, style, ...props}) { placeholderTextColor={themeColors.placeholderText} ref={setTextInputRef} selection={selection} - onChange={updateNumberOfLines} + onChange={triggerUpdateNumberOfLines} style={[ StyleSheet.flatten([style, {outline: 'none'}]), @@ -446,4 +442,3 @@ export default compose( /> )), ); - From 0b38fd9c45ebb16b273832d29dc76ea04bf0b3c7 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 26 Jul 2023 14:10:18 +0200 Subject: [PATCH 04/18] updateIsFullComposerAvailable --- src/components/Composer/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 0c2ac43c2647..298c29c42b58 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -340,7 +340,7 @@ function Composer({onKeyPress, style, ...props}) { if (textInput.current === null) { return; } - updateNumberOfLines(); + updateIsFullComposerAvailable(props, props.numberOfLines); setTimeout(() => { setNumberOfLines(1); From 09b5740c684e08d8176217012bbbc294dc519de5 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 27 Jul 2023 14:32:16 +0200 Subject: [PATCH 05/18] fix jumping --- src/components/Composer/index.js | 194 +++++++++++++++---------------- 1 file changed, 91 insertions(+), 103 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 298c29c42b58..3fca708ec489 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -1,9 +1,9 @@ -import React, {useState, useRef, useEffect, useCallback} from 'react'; +import React, {useState, useRef, useEffect, useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import { flushSync } from 'react-dom'; +import {flushSync} from 'react-dom'; import RNTextInput from '../RNTextInput'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; import Growl from '../../libs/Growl'; @@ -217,90 +217,90 @@ function Composer({onKeyPress, style, ...props}) { [paste], ); -/** - * Check the paste event for an attachment, parse the data and call onPasteFile from props with the selected file, - * Otherwise, convert pasted HTML to Markdown and set it on the composer. - * - * @param {ClipboardEvent} event - */ + /** + * Check the paste event for an attachment, parse the data and call onPasteFile from props with the selected file, + * Otherwise, convert pasted HTML to Markdown and set it on the composer. + * + * @param {ClipboardEvent} event + */ const handlePaste = useCallback( (event) => { - const isVisible = props.checkComposerVisibility(); - const isFocused = textInput.current.isFocused(); - - if (!(isVisible || isFocused)) { - return; - } - - if (textInput.current !== event.target) { - return; - } - - event.preventDefault(); - - const {files, types} = event.clipboardData; - const TEXT_HTML = 'text/html'; - - // If paste contains files, then trigger file management - if (files.length > 0) { - // Prevent the default so we do not post the file name into the text box - props.onPasteFile(event.clipboardData.files[0]); - return; - } - - // If paste contains HTML - if (types.includes(TEXT_HTML)) { - const pastedHTML = event.clipboardData.getData(TEXT_HTML); - - const domparser = new DOMParser(); - const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images; - - // If HTML has img tag, then fetch images from it. - if (embeddedImages.length > 0 && embeddedImages[0].src) { - // If HTML has emoji, then treat this as plain text. - if (embeddedImages[0].dataset && embeddedImages[0].dataset.stringifyType === 'emoji') { - const plainText = event.clipboardData.getData('text/plain'); - paste(plainText); - return; - } - fetch(embeddedImages[0].src) - .then((response) => { - if (!response.ok) { - throw Error(response.statusText); - } - return response.blob(); - }) - .then((x) => { - const extension = IMAGE_EXTENSIONS[x.type]; - if (!extension) { - throw new Error(props.translate('composer.noExtensionFoundForMimeType')); - } - - return new File([x], `pasted_image.${extension}`, {}); - }) - .then(props.onPasteFile) - .catch(() => { - const errorDesc = props.translate('composer.problemGettingImageYouPasted'); - Growl.error(errorDesc); - - /* - * Since we intercepted the user-triggered paste event to check for attachments, - * we need to manually set the value and call the `onChangeText` handler. - * Synthetically-triggered paste events do not affect the document's contents. - * See https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event for more details. - */ - handlePastedHTML(pastedHTML); - }); - return; - } - - handlePastedHTML(pastedHTML); - return; - } - - const plainText = event.clipboardData.getData('text/plain'); - - paste(plainText); + const isVisible = props.checkComposerVisibility(); + const isFocused = textInput.current.isFocused(); + + if (!(isVisible || isFocused)) { + return; + } + + if (textInput.current !== event.target) { + return; + } + + event.preventDefault(); + + const {files, types} = event.clipboardData; + const TEXT_HTML = 'text/html'; + + // If paste contains files, then trigger file management + if (files.length > 0) { + // Prevent the default so we do not post the file name into the text box + props.onPasteFile(event.clipboardData.files[0]); + return; + } + + // If paste contains HTML + if (types.includes(TEXT_HTML)) { + const pastedHTML = event.clipboardData.getData(TEXT_HTML); + + const domparser = new DOMParser(); + const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images; + + // If HTML has img tag, then fetch images from it. + if (embeddedImages.length > 0 && embeddedImages[0].src) { + // If HTML has emoji, then treat this as plain text. + if (embeddedImages[0].dataset && embeddedImages[0].dataset.stringifyType === 'emoji') { + const plainText = event.clipboardData.getData('text/plain'); + paste(plainText); + return; + } + fetch(embeddedImages[0].src) + .then((response) => { + if (!response.ok) { + throw Error(response.statusText); + } + return response.blob(); + }) + .then((x) => { + const extension = IMAGE_EXTENSIONS[x.type]; + if (!extension) { + throw new Error(props.translate('composer.noExtensionFoundForMimeType')); + } + + return new File([x], `pasted_image.${extension}`, {}); + }) + .then(props.onPasteFile) + .catch(() => { + const errorDesc = props.translate('composer.problemGettingImageYouPasted'); + Growl.error(errorDesc); + + /* + * Since we intercepted the user-triggered paste event to check for attachments, + * we need to manually set the value and call the `onChangeText` handler. + * Synthetically-triggered paste events do not affect the document's contents. + * See https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event for more details. + */ + handlePastedHTML(pastedHTML); + }); + return; + } + + handlePastedHTML(pastedHTML); + return; + } + + const plainText = event.clipboardData.getData('text/plain'); + + paste(plainText); }, [props.onPasteFile, props.translate, paste, handlePastedHTML], ); @@ -319,10 +319,14 @@ function Composer({onKeyPress, style, ...props}) { event.stopPropagation(); }, []); - const updateNumberOfLines = useCallback(() => { + useEffect(() => { if (textInput.current === null) { return; } + updateIsFullComposerAvailable(props, props.numberOfLines); + + // we reset the height to 0 to get the correct scrollHeight + textInput.current.style.height = 0; const computedStyle = window.getComputedStyle(textInput.current); const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20; const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); @@ -334,23 +338,8 @@ function Composer({onKeyPress, style, ...props}) { props.onNumberOfLinesChange(generalNumberOfLines); updateIsFullComposerAvailable(props, generalNumberOfLines); setNumberOfLines(generalNumberOfLines); - }, [props.value, props.maxLines, props.isFullComposerAvailable, props.numberOfLines, props.isComposerFullSize]); - - const triggerUpdateNumberOfLines = useCallback(() => { - if (textInput.current === null) { - return; - } - updateIsFullComposerAvailable(props, props.numberOfLines); - - setTimeout(() => { - setNumberOfLines(1); - updateNumberOfLines(); - }, 0); - }, [props.value, props.maxLines, props.isFullComposerAvailable, props.numberOfLines, props.isComposerFullSize]); - - useEffect(() => { - triggerUpdateNumberOfLines(); - }, [props.isComposerFullSize, triggerUpdateNumberOfLines]); + textInput.current.style.height = 'auto'; + }, [props.value, props.maxLines, props.numberOfLines, props.isComposerFullSize]); const handleKeyPress = useCallback( (e) => { @@ -427,7 +416,6 @@ function Composer({onKeyPress, style, ...props}) { placeholderTextColor={themeColors.placeholderText} ref={setTextInputRef} selection={selection} - onChange={triggerUpdateNumberOfLines} style={[ StyleSheet.flatten([style, {outline: 'none'}]), From fcba1d3fcbc472592ff54ed5ee7dd39dfec7a77b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 28 Jul 2023 14:01:15 +0200 Subject: [PATCH 06/18] remove optional chaining --- src/components/Composer/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 3fca708ec489..8fe96c4f9308 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -1,4 +1,4 @@ -import React, {useState, useRef, useEffect, useCallback, useMemo} from 'react'; +import React, {useState, useRef, useEffect, useCallback} from 'react'; import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; @@ -380,7 +380,7 @@ function Composer({onKeyPress, style, ...props}) { unsubscribeFocus(); unsubscribeBlur(); document.removeEventListener('paste', handlePaste); - textInput.current?.removeEventListener('wheel', handleWheel); + textInput.current.removeEventListener('wheel', handleWheel); }; }, []); From 0956e6d69127ea4cb5c5ce6962895cdbe5457344 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 28 Jul 2023 15:19:00 +0200 Subject: [PATCH 07/18] memo input style --- src/components/Composer/index.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 8fe96c4f9308..5bf68985581c 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -1,4 +1,4 @@ -import React, {useState, useRef, useEffect, useCallback} from 'react'; +import React, {useState, useRef, useEffect, useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; @@ -190,10 +190,10 @@ function Composer({onKeyPress, style, ...props}) { }); }; - // /** - // * Set pasted text to clipboard - // * @param {String} text - // */ + /** + * Set pasted text to clipboard + * @param {String} text + */ const paste = useCallback((text) => { try { document.execCommand('insertText', false, text); @@ -380,7 +380,8 @@ function Composer({onKeyPress, style, ...props}) { unsubscribeFocus(); unsubscribeBlur(); document.removeEventListener('paste', handlePaste); - textInput.current.removeEventListener('wheel', handleWheel); + // eslint-disable-next-line es/no-optional-chaining + textInput.current?.removeEventListener('wheel', handleWheel); }; }, []); @@ -408,6 +409,15 @@ function Composer({onKeyPress, style, ...props}) { ); + const inputStyle = useMemo(() => [ + // We are hiding the scrollbar to prevent it from reducing the text input width, + // so we can get the correct scroll height while calculating the number of lines. + numberOfLines < props.maxLines ? styles.overflowHidden : {}, + StyleUtils.getComposeTextAreaPadding(props.numberOfLines), + + StyleSheet.flatten([style, {outline: 'none'}]) + ], [props.style, props.maxLines, props.numberOfLines, props.isComposerFullSize]); + return ( <> Date: Fri, 28 Jul 2023 16:05:55 +0200 Subject: [PATCH 08/18] use destructing --- src/components/Composer/index.js | 100 ++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 36 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 5bf68985581c..519537d4ae35 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -141,11 +141,33 @@ const getNextChars = (str, cursorPos) => { // Enable Markdown parsing. // On web we like to have the Text Input field always focused so the user can easily type a new chat -function Composer({onKeyPress, style, ...props}) { +function Composer({ + onKeyPress, + style, + shouldClear, + onClear, + checkComposerVisibility, + onPasteFile, + translate, + isComposerFullSize, + maxLines, + value, + onNumberOfLinesChange, + isFullComposerAvailable, + setIsFullComposerAvailable, + shouldCalculateCaretPosition, + numberOfLines: numberOfLinesProp, + isDisabled, + forwardedRef, + navigation, + defaultValue, + onSelectionChange: onSelectionChangeProp, + ...props +}) { const textRef = useRef(null); const textInput = useRef(null); - const initialValue = props.defaultValue ? `${props.defaultValue}` : `${props.value || ''}`; - const [numberOfLines, setNumberOfLines] = useState(props.numberOfLines); + const initialValue = defaultValue ? `${defaultValue}` : `${value || ''}`; + const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp); const [selection, setSelection] = useState({ start: initialValue.length, end: initialValue.length, @@ -155,13 +177,13 @@ function Composer({onKeyPress, style, ...props}) { const [textInputWidth, setTextInputWidth] = useState(''); useEffect(() => { - if (!props.shouldClear) { + if (!shouldClear) { return; } textInput.current.clear(); setNumberOfLines(1); - props.onClear(); - }, [props.shouldClear]); + onClear(); + }, [shouldClear, onClear]); useEffect(() => { setSelection(props.selection); @@ -175,10 +197,10 @@ function Composer({onKeyPress, style, ...props}) { const addCursorPositionToSelectionChange = (event) => { flushSync(() => { setValueBeforeCaret(event.target.value.slice(0, event.nativeEvent.selection.start)); - setCaretContent(getNextChars(props.value, event.nativeEvent.selection.start)); + setCaretContent(getNextChars(value, event.nativeEvent.selection.start)); }); - props.onSelectionChange({ + onSelectionChangeProp({ nativeEvent: { selection: { start: event.nativeEvent.selection.start, @@ -225,7 +247,7 @@ function Composer({onKeyPress, style, ...props}) { */ const handlePaste = useCallback( (event) => { - const isVisible = props.checkComposerVisibility(); + const isVisible = checkComposerVisibility(); const isFocused = textInput.current.isFocused(); if (!(isVisible || isFocused)) { @@ -244,7 +266,7 @@ function Composer({onKeyPress, style, ...props}) { // If paste contains files, then trigger file management if (files.length > 0) { // Prevent the default so we do not post the file name into the text box - props.onPasteFile(event.clipboardData.files[0]); + onPasteFile(event.clipboardData.files[0]); return; } @@ -273,14 +295,14 @@ function Composer({onKeyPress, style, ...props}) { .then((x) => { const extension = IMAGE_EXTENSIONS[x.type]; if (!extension) { - throw new Error(props.translate('composer.noExtensionFoundForMimeType')); + throw new Error(translate('composer.noExtensionFoundForMimeType')); } return new File([x], `pasted_image.${extension}`, {}); }) - .then(props.onPasteFile) + .then(onPasteFile) .catch(() => { - const errorDesc = props.translate('composer.problemGettingImageYouPasted'); + const errorDesc = translate('composer.problemGettingImageYouPasted'); Growl.error(errorDesc); /* @@ -302,7 +324,7 @@ function Composer({onKeyPress, style, ...props}) { paste(plainText); }, - [props.onPasteFile, props.translate, paste, handlePastedHTML], + [onPasteFile, translate, paste, handlePastedHTML, checkComposerVisibility], ); /** @@ -323,7 +345,7 @@ function Composer({onKeyPress, style, ...props}) { if (textInput.current === null) { return; } - updateIsFullComposerAvailable(props, props.numberOfLines); + updateIsFullComposerAvailable({isFullComposerAvailable, setIsFullComposerAvailable}, numberOfLinesProp); // we reset the height to 0 to get the correct scrollHeight textInput.current.style.height = 0; @@ -332,14 +354,14 @@ function Composer({onKeyPress, style, ...props}) { const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); setTextInputWidth(computedStyle.width); - const computedNumberOfLines = ComposerUtils.getNumberOfLines(props.maxLines, lineHeight, paddingTopAndBottom, textInput.current.scrollHeight); - const generalNumberOfLines = computedNumberOfLines === 0 ? props.numberOfLines : computedNumberOfLines; + const computedNumberOfLines = ComposerUtils.getNumberOfLines(maxLines, lineHeight, paddingTopAndBottom, textInput.current.scrollHeight); + const generalNumberOfLines = computedNumberOfLines === 0 ? numberOfLinesProp : computedNumberOfLines; - props.onNumberOfLinesChange(generalNumberOfLines); - updateIsFullComposerAvailable(props, generalNumberOfLines); + onNumberOfLinesChange(generalNumberOfLines); + updateIsFullComposerAvailable({isFullComposerAvailable, setIsFullComposerAvailable}, generalNumberOfLines); setNumberOfLines(generalNumberOfLines); textInput.current.style.height = 'auto'; - }, [props.value, props.maxLines, props.numberOfLines, props.isComposerFullSize]); + }, [value, maxLines, numberOfLinesProp, isComposerFullSize, onNumberOfLinesChange, isFullComposerAvailable, setIsFullComposerAvailable]); const handleKeyPress = useCallback( (e) => { @@ -360,8 +382,8 @@ function Composer({onKeyPress, style, ...props}) { const setTextInputRef = useCallback((el) => { textInput.current = el; - if (_.isFunction(props.forwardedRef)) { - props.forwardedRef(textInput.current); + if (_.isFunction(forwardedRef)) { + forwardedRef(textInput.current); } if (textInput.current) { @@ -373,8 +395,8 @@ function Composer({onKeyPress, style, ...props}) { useEffect(() => { // we need to handle listeners on navigation focus/blur as Composer is not unmounting // when navigating away to different report - const unsubscribeFocus = props.navigation.addListener('focus', () => document.addEventListener('paste', handlePaste)); - const unsubscribeBlur = props.navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste)); + const unsubscribeFocus = navigation.addListener('focus', () => document.addEventListener('paste', handlePaste)); + const unsubscribeBlur = navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste)); return () => { unsubscribeFocus(); @@ -396,7 +418,7 @@ function Composer({onKeyPress, style, ...props}) { > {`${valueBeforeCaret} `} ); - const inputStyle = useMemo(() => [ - // We are hiding the scrollbar to prevent it from reducing the text input width, - // so we can get the correct scroll height while calculating the number of lines. - numberOfLines < props.maxLines ? styles.overflowHidden : {}, - StyleUtils.getComposeTextAreaPadding(props.numberOfLines), - - StyleSheet.flatten([style, {outline: 'none'}]) - ], [props.style, props.maxLines, props.numberOfLines, props.isComposerFullSize]); + const inputStyleMemo = useMemo( + () => [ + // We are hiding the scrollbar to prevent it from reducing the text input width, + // so we can get the correct scroll height while calculating the number of lines. + numberOfLines < maxLines ? styles.overflowHidden : {}, + StyleUtils.getComposeTextAreaPadding(numberOfLinesProp), + + StyleSheet.flatten([style, {outline: 'none'}]), + ], + [style, maxLines, numberOfLinesProp, numberOfLines], + ); return ( <> @@ -426,15 +451,18 @@ function Composer({onKeyPress, style, ...props}) { placeholderTextColor={themeColors.placeholderText} ref={setTextInputRef} selection={selection} - style={inputStyle} + style={inputStyleMemo} + value={value} + forwardedRef={forwardedRef} + defaultValue={defaultValue} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} numberOfLines={numberOfLines} - disabled={props.isDisabled} + disabled={isDisabled} onKeyPress={handleKeyPress} /> - {props.shouldCalculateCaretPosition && renderElementForCaretPosition} + {shouldCalculateCaretPosition && renderElementForCaretPosition} ); } From 5a90581fe2763055512f6a58bf47f563fc6d124b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 4 Aug 2023 10:56:27 +0200 Subject: [PATCH 09/18] fix lint --- src/components/Composer/index.js | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 519537d4ae35..a44c377d9f42 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -161,7 +161,7 @@ function Composer({ forwardedRef, navigation, defaultValue, - onSelectionChange: onSelectionChangeProp, + onSelectionChange, ...props }) { const textRef = useRef(null); @@ -200,7 +200,7 @@ function Composer({ setCaretContent(getNextChars(value, event.nativeEvent.selection.start)); }); - onSelectionChangeProp({ + onSelectionChange({ nativeEvent: { selection: { start: event.nativeEvent.selection.start, @@ -379,18 +379,21 @@ function Composer({ * * @param {Element} el */ - const setTextInputRef = useCallback((el) => { - textInput.current = el; + const setTextInputRef = useCallback( + (el) => { + textInput.current = el; - if (_.isFunction(forwardedRef)) { - forwardedRef(textInput.current); - } + if (_.isFunction(forwardedRef)) { + forwardedRef(textInput.current); + } - if (textInput.current) { - textInput.current.addEventListener('paste', handlePaste); - textInput.current.addEventListener('wheel', handleWheel); - } - }, []); + if (textInput.current) { + textInput.current.addEventListener('paste', handlePaste); + textInput.current.addEventListener('wheel', handleWheel); + } + }, + [forwardedRef, handlePaste, handleWheel], + ); useEffect(() => { // we need to handle listeners on navigation focus/blur as Composer is not unmounting @@ -439,7 +442,7 @@ function Composer({ StyleUtils.getComposeTextAreaPadding(numberOfLinesProp), StyleSheet.flatten([style, {outline: 'none'}]), - ], + ], [style, maxLines, numberOfLinesProp, numberOfLines], ); From 561687c2fca0c70253c5839edad1fc43ecd1acbe Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 4 Aug 2023 11:05:49 +0200 Subject: [PATCH 10/18] again lint --- src/components/Composer/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index a44c377d9f42..25dd9dc98826 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -408,6 +408,7 @@ function Composer({ // eslint-disable-next-line es/no-optional-chaining textInput.current?.removeEventListener('wheel', handleWheel); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const renderElementForCaretPosition = ( From fb0ef9981c7d9272d9b2e9bd794c18874ca36be9 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sun, 6 Aug 2023 20:15:14 +0200 Subject: [PATCH 11/18] remove defaultProps --- src/components/Composer/index.js | 159 ++++++++++++++----------------- 1 file changed, 71 insertions(+), 88 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 25dd9dc98826..58a9b04ff564 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -1,3 +1,4 @@ +/* eslint-disable react/require-default-props */ import React, {useState, useRef, useEffect, useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; @@ -88,31 +89,6 @@ const propTypes = { ...windowDimensionsPropTypes, }; -const defaultProps = { - defaultValue: undefined, - value: undefined, - numberOfLines: undefined, - onNumberOfLinesChange: () => {}, - maxLines: -1, - onPasteFile: () => {}, - shouldClear: false, - onClear: () => {}, - style: null, - isDisabled: false, - autoFocus: false, - forwardedRef: null, - onSelectionChange: () => {}, - selection: { - start: 0, - end: 0, - }, - isFullComposerAvailable: false, - setIsFullComposerAvailable: () => {}, - isComposerFullSize: false, - shouldCalculateCaretPosition: false, - checkComposerVisibility: () => false, -}; - const IMAGE_EXTENSIONS = { 'image/bmp': 'bmp', 'image/gif': 'gif', @@ -142,26 +118,28 @@ const getNextChars = (str, cursorPos) => { // Enable Markdown parsing. // On web we like to have the Text Input field always focused so the user can easily type a new chat function Composer({ + value = undefined, + defaultValue = undefined, + maxLines = -1, onKeyPress, - style, - shouldClear, - onClear, - checkComposerVisibility, - onPasteFile, + style = null, + shouldClear = false, + autoFocus = false, translate, - isComposerFullSize, - maxLines, - value, - onNumberOfLinesChange, - isFullComposerAvailable, - setIsFullComposerAvailable, - shouldCalculateCaretPosition, - numberOfLines: numberOfLinesProp, - isDisabled, - forwardedRef, + isComposerFullSize = false, + isFullComposerAvailable = false, + shouldCalculateCaretPosition = false, + numberOfLines: numberOfLinesProp = undefined, + isDisabled = false, + forwardedRef = null, navigation, - defaultValue, - onSelectionChange, + onClear = () => {}, + onPasteFile = () => {}, + onSelectionChange = () => {}, + onNumberOfLinesChange = () => {}, + setIsFullComposerAvailable = () => {}, + checkComposerVisibility = () => false, + selection: selectionProp = {start: 0, end: 0}, ...props }) { const textRef = useRef(null); @@ -186,8 +164,13 @@ function Composer({ }, [shouldClear, onClear]); useEffect(() => { - setSelection(props.selection); - }, [props.selection]); + setSelection((prevSelection) => { + if (selectionProp.start === prevSelection.start && selectionProp.end === prevSelection.end) { + return; + } + return selectionProp; + }); + }, [selectionProp]); /** * Adds the cursor position to the selection change event. @@ -195,21 +178,25 @@ function Composer({ * @param {Event} event */ const addCursorPositionToSelectionChange = (event) => { - flushSync(() => { - setValueBeforeCaret(event.target.value.slice(0, event.nativeEvent.selection.start)); - setCaretContent(getNextChars(value, event.nativeEvent.selection.start)); - }); + if (shouldCalculateCaretPosition) { + flushSync(() => { + setValueBeforeCaret(event.target.value.slice(0, event.nativeEvent.selection.start)); + setCaretContent(getNextChars(value, event.nativeEvent.selection.start)); + }); + } + const selectionValue = { + start: event.nativeEvent.selection.start, + end: event.nativeEvent.selection.end, + positionX: textRef.current.offsetLeft - CONST.SPACE_CHARACTER_WIDTH, + positionY: textRef.current.offsetTop, + }; onSelectionChange({ nativeEvent: { - selection: { - start: event.nativeEvent.selection.start, - end: event.nativeEvent.selection.end, - positionX: textRef.current.offsetLeft - CONST.SPACE_CHARACTER_WIDTH, - positionY: textRef.current.offsetTop, - }, + selection: selectionValue, }, }); + setSelection(selectionValue); }; /** @@ -345,7 +332,6 @@ function Composer({ if (textInput.current === null) { return; } - updateIsFullComposerAvailable({isFullComposerAvailable, setIsFullComposerAvailable}, numberOfLinesProp); // we reset the height to 0 to get the correct scrollHeight textInput.current.style.height = 0; @@ -363,44 +349,21 @@ function Composer({ textInput.current.style.height = 'auto'; }, [value, maxLines, numberOfLinesProp, isComposerFullSize, onNumberOfLinesChange, isFullComposerAvailable, setIsFullComposerAvailable]); - const handleKeyPress = useCallback( - (e) => { - // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed - if (!onKeyPress || isEnterWhileComposition(e)) { - return; - } - onKeyPress(e); - }, - [onKeyPress], - ); - - /** - * Set the TextInput Ref - * - * @param {Element} el - */ - const setTextInputRef = useCallback( - (el) => { - textInput.current = el; - - if (_.isFunction(forwardedRef)) { - forwardedRef(textInput.current); - } - - if (textInput.current) { - textInput.current.addEventListener('paste', handlePaste); - textInput.current.addEventListener('wheel', handleWheel); - } - }, - [forwardedRef, handlePaste, handleWheel], - ); - useEffect(() => { // we need to handle listeners on navigation focus/blur as Composer is not unmounting // when navigating away to different report const unsubscribeFocus = navigation.addListener('focus', () => document.addEventListener('paste', handlePaste)); const unsubscribeBlur = navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste)); + if (_.isFunction(forwardedRef)) { + forwardedRef(textInput.current); + } + + if (textInput.current) { + textInput.current.addEventListener('paste', handlePaste); + textInput.current.addEventListener('wheel', handleWheel); + } + return () => { unsubscribeFocus(); unsubscribeBlur(); @@ -411,6 +374,26 @@ function Composer({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const handleKeyPress = useCallback( + (e) => { + // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed + if (!onKeyPress || isEnterWhileComposition(e)) { + return; + } + onKeyPress(e); + }, + [onKeyPress], + ); + + // /** + // * Set the TextInput Ref + // * + // * @param {Element} el + // */ + const setTextInputRef = useCallback((el) => { + textInput.current = el; + }, []); + const renderElementForCaretPosition = ( Date: Mon, 7 Aug 2023 16:53:36 +0200 Subject: [PATCH 12/18] create updateNumberOfLines --- src/components/Composer/index.js | 48 +++++++++++--------------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 16ce6f1f8f20..cdf0c66705f0 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -74,9 +74,6 @@ const propTypes = { /** Allow the full composer to be opened */ setIsFullComposerAvailable: PropTypes.func, - /** Whether the composer is full size */ - isComposerFullSize: PropTypes.bool, - /** Should we calculate the caret position */ shouldCalculateCaretPosition: PropTypes.bool, @@ -88,16 +85,6 @@ const propTypes = { ...windowDimensionsPropTypes, }; -const IMAGE_EXTENSIONS = { - 'image/bmp': 'bmp', - 'image/gif': 'gif', - 'image/jpeg': 'jpg', - 'image/png': 'png', - 'image/svg+xml': 'svg', - 'image/tiff': 'tiff', - 'image/webp': 'webp', -}; - // Get characters from the cursor to the next space or new line const getNextChars = (str, cursorPos) => { // Get the substring starting from the cursor position @@ -125,7 +112,6 @@ function Composer({ shouldClear = false, autoFocus = false, translate, - isComposerFullSize = false, isFullComposerAvailable = false, shouldCalculateCaretPosition = false, numberOfLines: numberOfLinesProp = undefined, @@ -230,10 +216,13 @@ function Composer({ * * @param {ClipboardEvent} event */ - const handlePastePlainText = (event) => { - const plainText = event.clipboardData.getData('text/plain'); - paste(plainText); - }; + const handlePastePlainText = useCallback( + (event) => { + const plainText = event.clipboardData.getData('text/plain'); + paste(plainText); + }, + [paste], + ); /** * Check the paste event for an attachment, parse the data and call onPasteFile from props with the selected file, @@ -286,7 +275,7 @@ function Composer({ } handlePastePlainText(event); }, - [onPasteFile, translate, paste, handlePastedHTML, checkComposerVisibility], + [onPasteFile, handlePastedHTML, checkComposerVisibility, handlePastePlainText], ); /** @@ -303,11 +292,10 @@ function Composer({ event.stopPropagation(); }, []); - useEffect(() => { + const updateNumberOfLines = useCallback(() => { if (textInput.current === null) { return; } - // we reset the height to 0 to get the correct scrollHeight textInput.current.style.height = 0; const computedStyle = window.getComputedStyle(textInput.current); @@ -322,7 +310,12 @@ function Composer({ updateIsFullComposerAvailable({isFullComposerAvailable, setIsFullComposerAvailable}, generalNumberOfLines); setNumberOfLines(generalNumberOfLines); textInput.current.style.height = 'auto'; - }, [value, maxLines, numberOfLinesProp, isComposerFullSize, onNumberOfLinesChange, isFullComposerAvailable, setIsFullComposerAvailable]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value, maxLines, numberOfLinesProp, onNumberOfLinesChange, isFullComposerAvailable, setIsFullComposerAvailable]); + + useEffect(() => { + updateNumberOfLines(); + }, [updateNumberOfLines]); useEffect(() => { // we need to handle listeners on navigation focus/blur as Composer is not unmounting @@ -360,15 +353,6 @@ function Composer({ [onKeyPress], ); - // /** - // * Set the TextInput Ref - // * - // * @param {Element} el - // */ - const setTextInputRef = useCallback((el) => { - textInput.current = el; - }, []); - const renderElementForCaretPosition = ( (textInput.current = el)} selection={selection} style={inputStyleMemo} value={value} From 336ca5674faa3be1a5d46b4fc2a882d7bfac7339 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 8 Aug 2023 12:32:46 +0200 Subject: [PATCH 13/18] add JSdoc and fix emoji inserting --- src/components/Composer/index.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index cdf0c66705f0..3fdf9df396ba 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -85,7 +85,14 @@ const propTypes = { ...windowDimensionsPropTypes, }; -// Get characters from the cursor to the next space or new line +/** + * Retrieves the characters from the specified cursor position up to the next space or new line. + * + * @param {string} str - The input string. + * @param {number} cursorPos - The position of the cursor within the input string. + * @returns {string} - The substring from the cursor position up to the next space or new line. + * If no space or new line is found, returns the substring from the cursor position to the end of the input string. + */ const getNextChars = (str, cursorPos) => { // Get the substring starting from the cursor position const substr = str.substring(cursorPos); @@ -150,7 +157,7 @@ function Composer({ useEffect(() => { setSelection((prevSelection) => { - if (selectionProp.start === prevSelection.start && selectionProp.end === prevSelection.end) { + if (!!prevSelection && selectionProp.start === prevSelection.start && selectionProp.end === prevSelection.end) { return; } return selectionProp; @@ -164,6 +171,7 @@ function Composer({ */ const addCursorPositionToSelectionChange = (event) => { if (shouldCalculateCaretPosition) { + // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state flushSync(() => { setValueBeforeCaret(event.target.value.slice(0, event.nativeEvent.selection.start)); setCaretContent(getNextChars(value, event.nativeEvent.selection.start)); @@ -292,6 +300,10 @@ function Composer({ event.stopPropagation(); }, []); + /** + * Check the current scrollHeight of the textarea (minus any padding) and + * divide by line height to get the total number of rows for the textarea. + */ const updateNumberOfLines = useCallback(() => { if (textInput.current === null) { return; @@ -337,7 +349,10 @@ function Composer({ unsubscribeBlur(); document.removeEventListener('paste', handlePaste); // eslint-disable-next-line es/no-optional-chaining - textInput.current?.removeEventListener('wheel', handleWheel); + if(!textInput.current) { + return; + } + textInput.current.removeEventListener('wheel', handleWheel); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From 1b7fc24bd1a60ffc4044d9fa338be03e2fb38601 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 8 Aug 2023 12:53:40 +0200 Subject: [PATCH 14/18] lint --- src/components/Composer/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 3fdf9df396ba..8c5b78d4054c 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -90,7 +90,7 @@ const propTypes = { * * @param {string} str - The input string. * @param {number} cursorPos - The position of the cursor within the input string. - * @returns {string} - The substring from the cursor position up to the next space or new line. + * @returns {string} - The substring from the cursor position up to the next space or new line. * If no space or new line is found, returns the substring from the cursor position to the end of the input string. */ const getNextChars = (str, cursorPos) => { @@ -171,7 +171,7 @@ function Composer({ */ const addCursorPositionToSelectionChange = (event) => { if (shouldCalculateCaretPosition) { - // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state + // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state flushSync(() => { setValueBeforeCaret(event.target.value.slice(0, event.nativeEvent.selection.start)); setCaretContent(getNextChars(value, event.nativeEvent.selection.start)); @@ -349,8 +349,8 @@ function Composer({ unsubscribeBlur(); document.removeEventListener('paste', handlePaste); // eslint-disable-next-line es/no-optional-chaining - if(!textInput.current) { - return; + if (!textInput.current) { + return; } textInput.current.removeEventListener('wheel', handleWheel); }; From ae0a2a3e30af52c8ac5ea7048182d050e04f9b68 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 8 Aug 2023 17:09:31 +0200 Subject: [PATCH 15/18] add defaultProps --- src/components/Composer/index.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 8c5b78d4054c..bbaf000288c4 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -1,4 +1,3 @@ -/* eslint-disable react/require-default-props */ import React, {useState, useRef, useEffect, useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; @@ -85,6 +84,30 @@ const propTypes = { ...windowDimensionsPropTypes, }; +const defaultProps = { + defaultValue: undefined, + value: undefined, + numberOfLines: undefined, + onNumberOfLinesChange: () => {}, + maxLines: -1, + onPasteFile: () => {}, + shouldClear: false, + onClear: () => {}, + style: null, + isDisabled: false, + autoFocus: false, + forwardedRef: null, + onSelectionChange: () => {}, + selection: { + start: 0, + end: 0, + }, + isFullComposerAvailable: false, + setIsFullComposerAvailable: () => {}, + shouldCalculateCaretPosition: false, + checkComposerVisibility: () => false, +}; + /** * Retrieves the characters from the specified cursor position up to the next space or new line. * @@ -430,6 +453,7 @@ function Composer({ } Composer.propTypes = propTypes; +Composer.defaultProps = defaultProps; export default compose( withLocalize, From e2105471dc379f435823d0e02fe000746757067b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 9 Aug 2023 10:33:32 +0200 Subject: [PATCH 16/18] undo defining destructed props --- src/components/Composer/index.js | 36 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index bbaf000288c4..184a612aa085 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -134,27 +134,27 @@ const getNextChars = (str, cursorPos) => { // Enable Markdown parsing. // On web we like to have the Text Input field always focused so the user can easily type a new chat function Composer({ - value = undefined, - defaultValue = undefined, - maxLines = -1, + value , + defaultValue , + maxLines , onKeyPress, - style = null, - shouldClear = false, - autoFocus = false, + style , + shouldClear , + autoFocus , translate, - isFullComposerAvailable = false, - shouldCalculateCaretPosition = false, - numberOfLines: numberOfLinesProp = undefined, - isDisabled = false, - forwardedRef = null, + isFullComposerAvailable , + shouldCalculateCaretPosition , + numberOfLines: numberOfLinesProp , + isDisabled , + forwardedRef , navigation, - onClear = () => {}, - onPasteFile = () => {}, - onSelectionChange = () => {}, - onNumberOfLinesChange = () => {}, - setIsFullComposerAvailable = () => {}, - checkComposerVisibility = () => false, - selection: selectionProp = {start: 0, end: 0}, + onClear , + onPasteFile , + onSelectionChange , + onNumberOfLinesChange , + setIsFullComposerAvailable , + checkComposerVisibility , + selection: selectionProp , ...props }) { const textRef = useRef(null); From 2def59b4ba718814e129a69e7faa36ef43f6e033 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 9 Aug 2023 10:47:03 +0200 Subject: [PATCH 17/18] lint --- src/components/Composer/index.js | 36 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 184a612aa085..78d5a1565ceb 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -134,27 +134,27 @@ const getNextChars = (str, cursorPos) => { // Enable Markdown parsing. // On web we like to have the Text Input field always focused so the user can easily type a new chat function Composer({ - value , - defaultValue , - maxLines , + value, + defaultValue, + maxLines, onKeyPress, - style , - shouldClear , - autoFocus , + style, + shouldClear, + autoFocus, translate, - isFullComposerAvailable , - shouldCalculateCaretPosition , - numberOfLines: numberOfLinesProp , - isDisabled , - forwardedRef , + isFullComposerAvailable, + shouldCalculateCaretPosition, + numberOfLines: numberOfLinesProp, + isDisabled, + forwardedRef, navigation, - onClear , - onPasteFile , - onSelectionChange , - onNumberOfLinesChange , - setIsFullComposerAvailable , - checkComposerVisibility , - selection: selectionProp , + onClear, + onPasteFile, + onSelectionChange, + onNumberOfLinesChange, + setIsFullComposerAvailable, + checkComposerVisibility, + selection: selectionProp, ...props }) { const textRef = useRef(null); From a88b92bead4dc7d6d81ec9a793590de476c8b7ca Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 9 Aug 2023 16:12:35 +0200 Subject: [PATCH 18/18] fix padding --- src/components/Composer/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 78d5a1565ceb..a31f367e7675 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -420,9 +420,9 @@ function Composer({ // We are hiding the scrollbar to prevent it from reducing the text input width, // so we can get the correct scroll height while calculating the number of lines. numberOfLines < maxLines ? styles.overflowHidden : {}, - StyleUtils.getComposeTextAreaPadding(numberOfLinesProp), StyleSheet.flatten([style, {outline: 'none'}]), + StyleUtils.getComposeTextAreaPadding(numberOfLinesProp), ], [style, maxLines, numberOfLinesProp, numberOfLines], );