From 130df1b8c785950a61362b2ff79956aed2ac7c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 8 Oct 2022 13:32:37 +0200 Subject: [PATCH 001/110] wip: prevent exsessive state and rerenders, 1 rerender left --- src/pages/home/report/ReportActionCompose.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 7a14fd124208a..0eaccb05813fc 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -509,6 +509,7 @@ class ReportActionCompose extends React.Component { const inputPlaceholder = this.getInputPlaceholder(); const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH; + console.log("i rerender!"); return ( this.updateComment(comment, true)} + // onChangeText={comment => this.updateComment(comment, true)} onKeyPress={this.triggerHotkeyActions} onDragEnter={(e, isOriginComposer) => { if (!isOriginComposer) { @@ -655,7 +656,7 @@ class ReportActionCompose extends React.Component { isFullComposerAvailable={this.state.isFullComposerAvailable} setIsFullComposerAvailable={this.setIsFullComposerAvailable} isComposerFullSize={this.props.isComposerFullSize} - value={this.state.value} + // value={this.state.value} /> From 49acc1a00db28127fc23cd4362aa24fe9dca2a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 8 Oct 2022 13:35:36 +0200 Subject: [PATCH 002/110] wip: prevent other rerender --- src/pages/home/report/ReportActionCompose.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 0eaccb05813fc..d2ceaee9ac5e4 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -651,8 +651,8 @@ class ReportActionCompose extends React.Component { shouldClear={this.state.textInputShouldClear} onClear={() => this.setTextInputShouldClear(false)} isDisabled={isComposeDisabled || isBlockedFromConcierge} - selection={this.state.selection} - onSelectionChange={this.onSelectionChange} + // selection={this.state.selection} + // onSelectionChange={this.onSelectionChange} isFullComposerAvailable={this.state.isFullComposerAvailable} setIsFullComposerAvailable={this.setIsFullComposerAvailable} isComposerFullSize={this.props.isComposerFullSize} From 02e027b264a9dff3cf3fca3aa70f61be967a2f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 8 Oct 2022 13:48:10 +0200 Subject: [PATCH 003/110] reduce rerenders by moving comment input out of state --- src/pages/home/report/ReportActionCompose.js | 46 ++++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index d2ceaee9ac5e4..aa2dd25743577 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -142,7 +142,6 @@ class ReportActionCompose extends React.Component { end: props.comment.length, }, maxLines: props.isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES, - value: props.comment, conciergePlaceholderRandomIndex: _.random(this.props.translate('reportActionCompose.conciergePlaceholderOptions').length - 1), }; } @@ -379,30 +378,30 @@ class ReportActionCompose extends React.Component { * @param {Boolean} shouldDebounceSaveComment */ updateComment(newComment, shouldDebounceSaveComment) { - this.setState({ - isCommentEmpty: !!newComment.match(/^(\s|`)*$/), - value: newComment, - }); - - // Indicate that draft has been created. - if (this.comment.length === 0 && newComment.length !== 0) { - Report.setReportWithDraft(this.props.reportID, true); - } - - // The draft has been deleted. - if (newComment.length === 0) { - Report.setReportWithDraft(this.props.reportID, false); + const isCommentEmpty = newComment.length === 0; + if (this.state.isCommentEmpty !== isCommentEmpty) { + this.setState({isCommentEmpty}); } + // // Indicate that draft has been created. + // if (this.comment.length === 0 && newComment.length !== 0) { + // Report.setReportWithDraft(this.props.reportID, true); + // } + // + // // The draft has been deleted. + // if (newComment.length === 0) { + // Report.setReportWithDraft(this.props.reportID, false); + // } + // this.comment = newComment; - if (shouldDebounceSaveComment) { - this.debouncedSaveReportComment(newComment); - } else { - Report.saveReportComment(this.props.reportID, newComment || ''); - } - if (newComment) { - this.debouncedBroadcastUserIsTyping(); - } + // if (shouldDebounceSaveComment) { + // this.debouncedSaveReportComment(newComment); + // } else { + // Report.saveReportComment(this.props.reportID, newComment || ''); + // } + // if (newComment) { + // this.debouncedBroadcastUserIsTyping(); + // } } /** @@ -615,7 +614,7 @@ class ReportActionCompose extends React.Component { textAlignVertical="top" placeholder={inputPlaceholder} placeholderTextColor={themeColors.placeholderText} - // onChangeText={comment => this.updateComment(comment, true)} + onChangeText={comment => this.updateComment(comment, true)} onKeyPress={this.triggerHotkeyActions} onDragEnter={(e, isOriginComposer) => { if (!isOriginComposer) { @@ -656,7 +655,6 @@ class ReportActionCompose extends React.Component { isFullComposerAvailable={this.state.isFullComposerAvailable} setIsFullComposerAvailable={this.setIsFullComposerAvailable} isComposerFullSize={this.props.isComposerFullSize} - // value={this.state.value} /> From 58a1330e56693bdeb4fc96f36248e24ee23fe7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 8 Oct 2022 14:08:07 +0200 Subject: [PATCH 004/110] add some old code back thats unproblematic --- src/pages/home/report/ReportActionCompose.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index aa2dd25743577..d5e58eb35d003 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -383,16 +383,16 @@ class ReportActionCompose extends React.Component { this.setState({isCommentEmpty}); } - // // Indicate that draft has been created. - // if (this.comment.length === 0 && newComment.length !== 0) { - // Report.setReportWithDraft(this.props.reportID, true); - // } - // - // // The draft has been deleted. - // if (newComment.length === 0) { - // Report.setReportWithDraft(this.props.reportID, false); - // } - // + // Indicate that draft has been created. + if (this.comment.length === 0 && newComment.length !== 0) { + Report.setReportWithDraft(this.props.reportID, true); + } + + // The draft has been deleted. + if (newComment.length === 0) { + Report.setReportWithDraft(this.props.reportID, false); + } + this.comment = newComment; // if (shouldDebounceSaveComment) { // this.debouncedSaveReportComment(newComment); From d83423f6cbfaf923e4ea575077fe558d1138ef03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 8 Oct 2022 14:10:07 +0200 Subject: [PATCH 005/110] add some old code back thats unproblematic --- src/pages/home/report/ReportActionCompose.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index d5e58eb35d003..7e4867efdcf5a 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -394,14 +394,14 @@ class ReportActionCompose extends React.Component { } this.comment = newComment; - // if (shouldDebounceSaveComment) { - // this.debouncedSaveReportComment(newComment); - // } else { - // Report.saveReportComment(this.props.reportID, newComment || ''); - // } - // if (newComment) { - // this.debouncedBroadcastUserIsTyping(); - // } + if (shouldDebounceSaveComment) { + this.debouncedSaveReportComment(newComment); + } else { + Report.saveReportComment(this.props.reportID, newComment || ''); + } + if (newComment) { + this.debouncedBroadcastUserIsTyping(); + } } /** From 460b554e00685c35454c8059cbd89f760adc5c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 8 Oct 2022 14:21:29 +0200 Subject: [PATCH 006/110] fix: use regex to check for empty text but memoize it --- src/pages/home/report/ReportActionCompose.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 7e4867efdcf5a..5df71677bbf9b 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -109,6 +109,8 @@ const defaultProps = { ...withCurrentUserPersonalDetailsDefaultProps, }; +const IS_EMPTY_PATTERN = /^(\s|`)*$/; + class ReportActionCompose extends React.Component { constructor(props) { super(props); @@ -378,7 +380,7 @@ class ReportActionCompose extends React.Component { * @param {Boolean} shouldDebounceSaveComment */ updateComment(newComment, shouldDebounceSaveComment) { - const isCommentEmpty = newComment.length === 0; + const isCommentEmpty = !!newComment.match(IS_EMPTY_PATTERN); if (this.state.isCommentEmpty !== isCommentEmpty) { this.setState({isCommentEmpty}); } @@ -508,7 +510,7 @@ class ReportActionCompose extends React.Component { const inputPlaceholder = this.getInputPlaceholder(); const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH; - console.log("i rerender!"); + console.log('i rerender!'); return ( this.setTextInputShouldClear(false)} isDisabled={isComposeDisabled || isBlockedFromConcierge} + // selection={this.state.selection} // onSelectionChange={this.onSelectionChange} isFullComposerAvailable={this.state.isFullComposerAvailable} From d94cdd0522958a76e9bd53d2a371eabafbc1becb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 8 Oct 2022 14:31:47 +0200 Subject: [PATCH 007/110] add todo --- src/pages/home/report/ReportActionCompose.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 5df71677bbf9b..b2fb4627fa1b6 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -385,6 +385,7 @@ class ReportActionCompose extends React.Component { this.setState({isCommentEmpty}); } + // TODO: when further deferring this code we can make the rendering more instantaneous // Indicate that draft has been created. if (this.comment.length === 0 && newComment.length !== 0) { Report.setReportWithDraft(this.props.reportID, true); From 71022564bb8fcdbfbff50ff0d49c802faadc2716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 8 Oct 2022 17:05:19 +0200 Subject: [PATCH 008/110] temp: update empji text using setNativeProps --- src/pages/home/report/ReportActionCompose.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index b2fb4627fa1b6..b3c0953bbaa28 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -328,6 +328,9 @@ class ReportActionCompose extends React.Component { }, })); this.updateComment(newComment); + + // TODO: issue: we use setNativeProps which isn't fabric supported! + this.textInput.setNativeProps({text: newComment}); } /** @@ -653,9 +656,8 @@ class ReportActionCompose extends React.Component { shouldClear={this.state.textInputShouldClear} onClear={() => this.setTextInputShouldClear(false)} isDisabled={isComposeDisabled || isBlockedFromConcierge} - - // selection={this.state.selection} - // onSelectionChange={this.onSelectionChange} + selection={this.state.selection} + onSelectionChange={this.onSelectionChange} isFullComposerAvailable={this.state.isFullComposerAvailable} setIsFullComposerAvailable={this.setIsFullComposerAvailable} isComposerFullSize={this.props.isComposerFullSize} From 5616ae59dd3389f646f6ca35aefd749be4928ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 8 Oct 2022 17:07:40 +0200 Subject: [PATCH 009/110] remove 'selection' from state to prevent permanent rerender --- src/pages/home/report/ReportActionCompose.js | 25 +++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index b3c0953bbaa28..1ff02b6b9ae08 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -132,6 +132,10 @@ class ReportActionCompose extends React.Component { this.comment = props.comment; this.shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); + this.selection = { + start: props.comment.length, + end: props.comment.length, + }; this.state = { isFocused: this.shouldFocusInputOnScreenFocus, @@ -139,10 +143,6 @@ class ReportActionCompose extends React.Component { textInputShouldClear: false, isCommentEmpty: props.comment.length === 0, isMenuVisible: false, - selection: { - start: props.comment.length, - end: props.comment.length, - }, maxLines: props.isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES, conciergePlaceholderRandomIndex: _.random(this.props.translate('reportActionCompose.conciergePlaceholderOptions').length - 1), }; @@ -192,7 +192,7 @@ class ReportActionCompose extends React.Component { } onSelectionChange(e) { - this.setState({selection: e.nativeEvent.selection}); + this.selection = e.nativeEvent.selection; } /** @@ -319,14 +319,12 @@ class ReportActionCompose extends React.Component { */ addEmojiToTextBox(emoji) { const emojiWithSpace = `${emoji} `; - const newComment = this.comment.slice(0, this.state.selection.start) - + emojiWithSpace + this.comment.slice(this.state.selection.end, this.comment.length); - this.setState(prevState => ({ - selection: { - start: prevState.selection.start + emojiWithSpace.length, - end: prevState.selection.start + emojiWithSpace.length, - }, - })); + const newComment = this.comment.slice(0, this.selection.start) + + emojiWithSpace + this.comment.slice(this.selection.end, this.comment.length); + this.selection = { + start: this.selection.start + emojiWithSpace.length, + end: this.selection.start + emojiWithSpace.length, + }; this.updateComment(newComment); // TODO: issue: we use setNativeProps which isn't fabric supported! @@ -656,7 +654,6 @@ class ReportActionCompose extends React.Component { shouldClear={this.state.textInputShouldClear} onClear={() => this.setTextInputShouldClear(false)} isDisabled={isComposeDisabled || isBlockedFromConcierge} - selection={this.state.selection} onSelectionChange={this.onSelectionChange} isFullComposerAvailable={this.state.isFullComposerAvailable} setIsFullComposerAvailable={this.setIsFullComposerAvailable} From b261d77b57fe9f8fb4cd58912fcfcdd1a2fd6bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 8 Oct 2022 17:38:30 +0200 Subject: [PATCH 010/110] refactor: one component for android and ios (99% the same code) --- src/components/Composer/index.android.js | 118 ------------------ .../{index.ios.js => index.native.js} | 7 +- 2 files changed, 5 insertions(+), 120 deletions(-) delete mode 100644 src/components/Composer/index.android.js rename src/components/Composer/{index.ios.js => index.native.js} (95%) diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js deleted file mode 100644 index 76b63eb7f972a..0000000000000 --- a/src/components/Composer/index.android.js +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react'; -import {StyleSheet} from 'react-native'; -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import RNTextInput from '../RNTextInput'; -import themeColors from '../../styles/themes/default'; -import CONST from '../../CONST'; -import * as ComposerUtils from '../../libs/ComposerUtils'; - -const propTypes = { - /** If the input should clear, it actually gets intercepted instead of .clear() */ - shouldClear: PropTypes.bool, - - /** A ref to forward to the text input */ - forwardedRef: PropTypes.func, - - /** When the input has cleared whoever owns this input should know about it */ - onClear: PropTypes.func, - - /** Set focus to this component the first time it renders. - * Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ - autoFocus: PropTypes.bool, - - /** Prevent edits and interactions like focus for this input. */ - isDisabled: PropTypes.bool, - - /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), - - /** Whether the full composer can be opened */ - isFullComposerAvailable: PropTypes.bool, - - /** Allow the full composer to be opened */ - setIsFullComposerAvailable: PropTypes.func, - - /** Whether the composer is full size */ - isComposerFullSize: PropTypes.bool.isRequired, - - /** General styles to apply to the text input */ - // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.any, - -}; - -const defaultProps = { - shouldClear: false, - onClear: () => {}, - autoFocus: false, - isDisabled: false, - forwardedRef: null, - selection: { - start: 0, - end: 0, - }, - isFullComposerAvailable: false, - setIsFullComposerAvailable: () => {}, - style: null, -}; - -class Composer extends React.Component { - constructor(props) { - super(props); - - this.state = { - propStyles: StyleSheet.flatten(this.props.style), - }; - } - - componentDidMount() { - // 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)) { - return; - } - - this.props.forwardedRef(this.textInput); - } - - componentDidUpdate(prevProps) { - if (prevProps.shouldClear || !this.props.shouldClear) { - return; - } - - this.textInput.clear(); - this.props.onClear(); - } - - render() { - return ( - this.textInput = el} - maxHeight={this.props.isComposerFullSize ? '100%' : CONST.COMPOSER_MAX_HEIGHT} - onContentSizeChange={e => ComposerUtils.updateNumberOfLines(this.props, e)} - rejectResponderTermination={false} - textAlignVertical="center" - style={this.state.propStyles} - /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...this.props} - editable={!this.props.isDisabled} - /> - ); - } -} - -Composer.propTypes = propTypes; -Composer.defaultProps = defaultProps; - -export default React.forwardRef((props, ref) => ( - /* eslint-disable-next-line react/jsx-props-no-spreading */ - -)); diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.native.js similarity index 95% rename from src/components/Composer/index.ios.js rename to src/components/Composer/index.native.js index 9b1e6680dea3f..d855a1739a751 100644 --- a/src/components/Composer/index.ios.js +++ b/src/components/Composer/index.native.js @@ -1,5 +1,5 @@ import React from 'react'; -import {StyleSheet} from 'react-native'; +import {Platform, StyleSheet} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import RNTextInput from '../RNTextInput'; @@ -95,7 +95,10 @@ class Composer extends React.Component { // user can read new chats without the keyboard in the way of the view. // On Android, the selection prop is required on the TextInput but this prop has issues on IOS // https://github.com/facebook/react-native/issues/29063 - const propsToPass = _.omit(this.props, 'selection'); + const propsToPass = Platform.select({ + android: this.props, + ios: _.omit(this.props, 'selection'), + }); return ( Date: Sat, 8 Oct 2022 17:53:59 +0200 Subject: [PATCH 011/110] avoid using setNativeProps --- src/components/Composer/index.native.js | 30 ++++++++++++++++++++ src/pages/home/report/ReportActionCompose.js | 8 +++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index d855a1739a751..ec0a83f8a059e 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -43,6 +43,12 @@ const propTypes = { // eslint-disable-next-line react/forbid-prop-types style: PropTypes.any, + /** The text to display in the input */ + value: PropTypes.string, + + /** Called when the text gets changed by user input */ + onChangeText: PropTypes.func, + }; const defaultProps = { @@ -58,18 +64,27 @@ const defaultProps = { isFullComposerAvailable: false, setIsFullComposerAvailable: () => {}, style: null, + value: '', + onChangeText: null, }; class Composer extends React.Component { constructor(props) { super(props); + this.onChangeText = this.onChangeText.bind(this); this.state = { propStyles: StyleSheet.flatten(this.props.style), + value: props.value, }; } componentDidMount() { + // we pass the ref to the native view instance, + // however, we want this method to be + // available to be called from the outside as well. + this.textInput.onChangeText = this.onChangeText; + // 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 @@ -90,6 +105,16 @@ class Composer extends React.Component { this.props.onClear(); } + onChangeText(text) { + // updates the text input to reflect the current value + this.setState({value: text}); + + // calls the onChangeText callback prop + if (this.props.onChangeText != null) { + this.props.onChangeText(text); + } + } + render() { // On native layers we like to have the Text Input not focused so the // user can read new chats without the keyboard in the way of the view. @@ -112,6 +137,11 @@ class Composer extends React.Component { /* eslint-disable-next-line react/jsx-props-no-spreading */ {...propsToPass} editable={!this.props.isDisabled} + onChangeText={this.onChangeText} + + // we have a value explicitly set so the value can be changed imperatively + // (needed e.g. when we are injecting emojis into the text view) + value={this.state.value} /> ); } diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 1ff02b6b9ae08..731ce64b52f93 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -325,10 +325,11 @@ class ReportActionCompose extends React.Component { start: this.selection.start + emojiWithSpace.length, end: this.selection.start + emojiWithSpace.length, }; - this.updateComment(newComment); - // TODO: issue: we use setNativeProps which isn't fabric supported! - this.textInput.setNativeProps({text: newComment}); + // this will call the function we passed + // to the TextInput's onChangeText, + // so updateComment gets called after this. + this.textInput.onChangeText(newComment); } /** @@ -512,7 +513,6 @@ class ReportActionCompose extends React.Component { const inputPlaceholder = this.getInputPlaceholder(); const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH; - console.log('i rerender!'); return ( Date: Sun, 9 Oct 2022 09:47:00 +0200 Subject: [PATCH 012/110] fix: persisted comments are not shown when reloading app --- src/components/Composer/index.native.js | 5 ++++- src/pages/home/report/ReportActionCompose.js | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index ec0a83f8a059e..c66239b908ec7 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -49,6 +49,8 @@ const propTypes = { /** Called when the text gets changed by user input */ onChangeText: PropTypes.func, + /** A value the input should have when it first mounts. Default is empty. */ + defaultValue: PropTypes.string, }; const defaultProps = { @@ -66,6 +68,7 @@ const defaultProps = { style: null, value: '', onChangeText: null, + defaultValue: '', }; class Composer extends React.Component { @@ -75,7 +78,7 @@ class Composer extends React.Component { this.onChangeText = this.onChangeText.bind(this); this.state = { propStyles: StyleSheet.flatten(this.props.style), - value: props.value, + value: props.defaultValue || props.value, }; } diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 731ce64b52f93..deb8ff72639b6 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -658,6 +658,7 @@ class ReportActionCompose extends React.Component { isFullComposerAvailable={this.state.isFullComposerAvailable} setIsFullComposerAvailable={this.setIsFullComposerAvailable} isComposerFullSize={this.props.isComposerFullSize} + defaultValue={this.props.comment} /> From d647146430c4872236dbfe8266c9edf1115b3e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 9 Oct 2022 09:50:26 +0200 Subject: [PATCH 013/110] web: avoid function recreation --- src/components/Composer/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 37c951ca84ddb..00688eb0bac6b 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -135,6 +135,7 @@ class Composer extends React.Component { this.handlePaste = this.handlePaste.bind(this); this.handlePastedHTML = this.handlePastedHTML.bind(this); this.handleWheel = this.handleWheel.bind(this); + this.updateNumberOfLines = this.updateNumberOfLines.bind(this); } componentDidMount() { @@ -373,9 +374,7 @@ class Composer extends React.Component { placeholderTextColor={themeColors.placeholderText} ref={el => this.textInput = el} selection={this.state.selection} - onChange={() => { - this.updateNumberOfLines(); - }} + onChange={this.updateNumberOfLines} onSelectionChange={this.onSelectionChange} numberOfLines={this.state.numberOfLines} style={propStyles} From 5ce050255fc0b7246b66fec9f92eb2dd0a90493f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 9 Oct 2022 10:01:31 +0200 Subject: [PATCH 014/110] fix(android): remove selection prop all together which fixes input issue on android --- src/components/Composer/index.native.js | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index c66239b908ec7..b4051641455b4 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -1,5 +1,5 @@ import React from 'react'; -import {Platform, StyleSheet} from 'react-native'; +import {StyleSheet} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import RNTextInput from '../RNTextInput'; @@ -24,12 +24,6 @@ const propTypes = { /** Prevent edits and interactions like focus for this input. */ isDisabled: PropTypes.bool, - /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), - /** Whether the full composer can be opened */ isFullComposerAvailable: PropTypes.bool, @@ -59,10 +53,6 @@ const defaultProps = { autoFocus: false, isDisabled: false, forwardedRef: null, - selection: { - start: 0, - end: 0, - }, isFullComposerAvailable: false, setIsFullComposerAvailable: () => {}, style: null, @@ -119,14 +109,6 @@ class Composer extends React.Component { } render() { - // On native layers we like to have the Text Input not focused so the - // user can read new chats without the keyboard in the way of the view. - // On Android, the selection prop is required on the TextInput but this prop has issues on IOS - // https://github.com/facebook/react-native/issues/29063 - const propsToPass = Platform.select({ - android: this.props, - ios: _.omit(this.props, 'selection'), - }); return ( Date: Sun, 9 Oct 2022 12:19:05 +0200 Subject: [PATCH 015/110] fix(web): inserting emojis was broken --- src/components/Composer/index.js | 73 ++++++++++++++------ src/pages/home/report/ReportActionCompose.js | 3 + 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 00688eb0bac6b..b5662ab217654 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -60,12 +60,6 @@ const propTypes = { /** Update selection position on change */ onSelectionChange: PropTypes.func, - /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), - /** Whether the full composer can be opened */ isFullComposerAvailable: PropTypes.bool, @@ -91,10 +85,6 @@ const defaultProps = { autoFocus: false, forwardedRef: null, onSelectionChange: () => {}, - selection: { - start: 0, - end: 0, - }, isFullComposerAvailable: false, setIsFullComposerAvailable: () => {}, }; @@ -123,12 +113,14 @@ class Composer extends React.Component { ? `${props.defaultValue}` : `${props.value || ''}`; + this.selection = { + start: initialValue.length, + end: initialValue.length, + }; + this.state = { numberOfLines: 1, - selection: { - start: initialValue.length, - end: initialValue.length, - }, + value: initialValue, }; this.dragNDropListener = this.dragNDropListener.bind(this); this.paste = this.paste.bind(this); @@ -136,9 +128,21 @@ class Composer extends React.Component { this.handlePastedHTML = this.handlePastedHTML.bind(this); this.handleWheel = this.handleWheel.bind(this); this.updateNumberOfLines = this.updateNumberOfLines.bind(this); + this.onChangeText = this.onChangeText.bind(this); + this.focus = this.focus.bind(this); + this.onSelectionChange = this.onSelectionChange.bind(this); } componentDidMount() { + // we pass the ref to the native view instance, + // however, we want this method to be + // available to be called from the outside as well. + this.textInput.onChangeText = this.onChangeText; + + // overwrite focus with this components implementation + this.textInput.nativeFocus = this.textInput.focus; + this.textInput.focus = this.focus; + this.updateNumberOfLines(); // This callback prop is used by the parent component using the constructor to @@ -160,6 +164,8 @@ class Composer extends React.Component { document.addEventListener('drop', this.dragNDropListener); this.textInput.addEventListener('paste', this.handlePaste); this.textInput.addEventListener('wheel', this.handleWheel); + + this.textInput.setSelectionRange(this.selection.start, this.selection.end); } } @@ -174,11 +180,6 @@ class Composer extends React.Component { || prevProps.isComposerFullSize !== this.props.isComposerFullSize) { this.updateNumberOfLines(); } - - if (prevProps.selection !== this.props.selection) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({selection: this.props.selection}); - } } componentWillUnmount() { @@ -194,6 +195,31 @@ class Composer extends React.Component { this.textInput.removeEventListener('wheel', this.handleWheel); } + onChangeText(text) { + // updates the text input to reflect the current value + this.setState({value: text}); + + // calls the onChangeText callback prop + if (this.props.onChangeText != null) { + this.props.onChangeText(text); + } + } + + onSelectionChange(event) { + this.selection = event.nativeEvent.selection; + this.props.onSelectionChange(event); + } + + focus() { + this.textInput.nativeFocus(); + requestAnimationFrame(() => { + this.textInput.setSelectionRange( + this.selection.start, + this.selection.end, + ); + }); + } + /** * Handles all types of drag-N-drop events on the composer * @@ -367,20 +393,21 @@ class Composer extends React.Component { render() { const propStyles = StyleSheet.flatten(this.props.style); propStyles.outline = 'none'; - const propsWithoutStyles = _.omit(this.props, 'style'); + const propsToPass = _.omit(this.props, 'style', 'defaultValue'); return ( this.textInput = el} - selection={this.state.selection} onChange={this.updateNumberOfLines} - onSelectionChange={this.onSelectionChange} numberOfLines={this.state.numberOfLines} style={propStyles} /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...propsWithoutStyles} + {...propsToPass} disabled={this.props.isDisabled} + onChangeText={this.onChangeText} + value={this.state.value} + onSelectionChange={this.onSelectionChange} /> ); } diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index deb8ff72639b6..411cd1f73fecc 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -513,6 +513,9 @@ class ReportActionCompose extends React.Component { const inputPlaceholder = this.getInputPlaceholder(); const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH; + // eslint-disable-next-line no-console + // console.log(this.textInput); + return ( Date: Sun, 9 Oct 2022 12:30:41 +0200 Subject: [PATCH 016/110] fix(web): after inserting emojis cursor was at wrong position --- src/components/Composer/index.js | 6 ++++++ src/components/Composer/index.native.js | 1 + src/pages/home/report/ReportActionCompose.js | 2 ++ 3 files changed, 9 insertions(+) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index b5662ab217654..7f53e4003cac7 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -131,6 +131,7 @@ class Composer extends React.Component { this.onChangeText = this.onChangeText.bind(this); this.focus = this.focus.bind(this); this.onSelectionChange = this.onSelectionChange.bind(this); + this.updateSelection = this.updateSelection.bind(this); } componentDidMount() { @@ -138,6 +139,7 @@ class Composer extends React.Component { // however, we want this method to be // available to be called from the outside as well. this.textInput.onChangeText = this.onChangeText; + this.textInput.updateSelection = this.updateSelection; // overwrite focus with this components implementation this.textInput.nativeFocus = this.textInput.focus; @@ -220,6 +222,10 @@ class Composer extends React.Component { }); } + updateSelection(selection) { + this.selection = selection; + } + /** * Handles all types of drag-N-drop events on the composer * diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index b4051641455b4..5f651cd72b129 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -77,6 +77,7 @@ class Composer extends React.Component { // however, we want this method to be // available to be called from the outside as well. this.textInput.onChangeText = this.onChangeText; + this.textInput.updateSelection = () => {}; // noop // 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 diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 411cd1f73fecc..cab0e7c2c0f32 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -326,6 +326,8 @@ class ReportActionCompose extends React.Component { end: this.selection.start + emojiWithSpace.length, }; + this.textInput.updateSelection(this.selection); + // this will call the function we passed // to the TextInput's onChangeText, // so updateComment gets called after this. From bab754048416dc4ebf973ab438ab77ac23a1830e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 10 Oct 2022 09:46:41 +0200 Subject: [PATCH 017/110] fix: ReportActionItemMessageEdit --- .../report/ReportActionItemMessageEdit.js | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 49a442e9b0318..a471dadab1663 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -72,12 +72,12 @@ class ReportActionItemMessageEdit extends React.Component { const parser = new ExpensiMark(); const draftMessage = parser.htmlToMarkdown(this.props.draftMessage); + this.selection = { + start: draftMessage.length, + end: draftMessage.length, + }; this.state = { draft: draftMessage, - selection: { - start: draftMessage.length, - end: draftMessage.length, - }, }; } @@ -87,7 +87,7 @@ class ReportActionItemMessageEdit extends React.Component { * @param {Event} e */ onSelectionChange(e) { - this.setState({selection: e.nativeEvent.selection}); + this.selection = e.nativeEvent.selection; } /** @@ -156,15 +156,14 @@ class ReportActionItemMessageEdit extends React.Component { * @param {String} emoji */ addEmojiToTextBox(emoji) { - const newComment = this.state.draft.slice(0, this.state.selection.start) - + emoji + this.state.draft.slice(this.state.selection.end, this.state.draft.length); - this.setState(prevState => ({ - selection: { - start: prevState.selection.start + emoji.length, - end: prevState.selection.start + emoji.length, - }, - })); - this.updateDraft(newComment); + const newComment = this.state.draft.slice(0, this.selection.start) + + emoji + this.state.draft.slice(this.selection.end, this.state.draft.length); + this.selection = { + start: this.selection.start + emoji.length, + end: this.selection.start + emoji.length, + }; + this.textInput.updateSelection(this.selection); + this.textInput.onChangeText(newComment); } /** @@ -212,7 +211,6 @@ class ReportActionItemMessageEdit extends React.Component { toggleReportActionComposeView(true, VirtualKeyboard.shouldAssumeIsOpen()); }} - selection={this.state.selection} onSelectionChange={this.onSelectionChange} /> From 3220710e8aca90a2ff73a68858e6967cdae0adc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 10 Oct 2022 10:04:29 +0200 Subject: [PATCH 018/110] fix(web): cursor might be at wrong place when inserting emoji --- src/components/Composer/index.js | 8 ++++++-- .../home/report/ReportActionItemMessageEdit.js | 15 +++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 7f53e4003cac7..eba2872edd78f 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -213,11 +213,15 @@ class Composer extends React.Component { } focus() { + // capture the selection, as the "native focus" call will + // call onSelectionChange to the end of the text + const selection = this.selection; + this.textInput.nativeFocus(); requestAnimationFrame(() => { this.textInput.setSelectionRange( - this.selection.start, - this.selection.end, + selection.start, + selection.end, ); }); } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index a471dadab1663..50ed69f577be0 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -76,9 +76,7 @@ class ReportActionItemMessageEdit extends React.Component { start: draftMessage.length, end: draftMessage.length, }; - this.state = { - draft: draftMessage, - }; + this.draft = draftMessage; } /** @@ -96,7 +94,7 @@ class ReportActionItemMessageEdit extends React.Component { * @param {String} newDraft */ updateDraft(newDraft) { - this.setState({draft: newDraft}); + this.draft = newDraft; // This component is rendered only when draft is set to a non-empty string. In order to prevent component // unmount when user deletes content of textarea, we set previous message instead of empty string. @@ -135,7 +133,7 @@ class ReportActionItemMessageEdit extends React.Component { // debounce here. this.debouncedSaveDraft.cancel(); - const trimmedNewDraft = this.state.draft.trim(); + const trimmedNewDraft = this.draft.trim(); // When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting. if (!trimmedNewDraft) { @@ -156,12 +154,13 @@ class ReportActionItemMessageEdit extends React.Component { * @param {String} emoji */ addEmojiToTextBox(emoji) { - const newComment = this.state.draft.slice(0, this.selection.start) - + emoji + this.state.draft.slice(this.selection.end, this.state.draft.length); + const newComment = this.draft.slice(0, this.selection.start) + + emoji + this.draft.slice(this.selection.end, this.draft.length); this.selection = { start: this.selection.start + emoji.length, end: this.selection.start + emoji.length, }; + this.textInput.updateSelection(this.selection); this.textInput.onChangeText(newComment); } @@ -196,7 +195,6 @@ class ReportActionItemMessageEdit extends React.Component { }} onChangeText={this.updateDraft} // Debounced saveDraftComment onKeyPress={this.triggerSaveOrCancel} - value={this.state.draft} maxLines={16} // This is the same that slack has style={[styles.textInputCompose, styles.flex4, styles.editInputComposeSpacing]} onFocus={() => { @@ -212,6 +210,7 @@ class ReportActionItemMessageEdit extends React.Component { toggleReportActionComposeView(true, VirtualKeyboard.shouldAssumeIsOpen()); }} onSelectionChange={this.onSelectionChange} + defaultValue={this.props.draftMessage} /> Date: Mon, 10 Oct 2022 15:13:06 +0200 Subject: [PATCH 019/110] remove unused comments --- src/pages/home/report/ReportActionCompose.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index cab0e7c2c0f32..a4e624fa6bc41 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -515,9 +515,6 @@ class ReportActionCompose extends React.Component { const inputPlaceholder = this.getInputPlaceholder(); const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH; - // eslint-disable-next-line no-console - // console.log(this.textInput); - return ( Date: Mon, 10 Oct 2022 15:25:14 +0200 Subject: [PATCH 020/110] remove todo, see issue: https://github.com/Expensify/App/issues/11697 --- src/pages/home/report/ReportActionCompose.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index a4e624fa6bc41..ee62db8817053 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -389,7 +389,6 @@ class ReportActionCompose extends React.Component { this.setState({isCommentEmpty}); } - // TODO: when further deferring this code we can make the rendering more instantaneous // Indicate that draft has been created. if (this.comment.length === 0 && newComment.length !== 0) { Report.setReportWithDraft(this.props.reportID, true); From 762e6ac36d816c05b1d6f1e097255e60d5480c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 12 Oct 2022 08:40:23 +0200 Subject: [PATCH 021/110] Apply suggestions from code review Co-authored-by: Rajat Parashar --- src/components/Composer/index.js | 10 +++++----- src/components/Composer/index.native.js | 6 +++--- src/pages/home/report/ReportActionCompose.js | 5 ++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index eba2872edd78f..2b94469ac1f2c 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -135,13 +135,13 @@ class Composer extends React.Component { } componentDidMount() { - // we pass the ref to the native view instance, + // We pass the ref to the native view instance // however, we want this method to be // available to be called from the outside as well. this.textInput.onChangeText = this.onChangeText; this.textInput.updateSelection = this.updateSelection; - // overwrite focus with this components implementation + // Overwrite focus with this component's implementation this.textInput.nativeFocus = this.textInput.focus; this.textInput.focus = this.focus; @@ -198,10 +198,10 @@ class Composer extends React.Component { } onChangeText(text) { - // updates the text input to reflect the current value + // Updates the text input to reflect the current value this.setState({value: text}); - // calls the onChangeText callback prop + // Calls the onChangeText callback prop if (this.props.onChangeText != null) { this.props.onChangeText(text); } @@ -213,7 +213,7 @@ class Composer extends React.Component { } focus() { - // capture the selection, as the "native focus" call will + // Capture the selection, as the "native focus" call will // call onSelectionChange to the end of the text const selection = this.selection; diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index 5f651cd72b129..7160d85376462 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -73,7 +73,7 @@ class Composer extends React.Component { } componentDidMount() { - // we pass the ref to the native view instance, + // We pass the ref to the native view instance, // however, we want this method to be // available to be called from the outside as well. this.textInput.onChangeText = this.onChangeText; @@ -100,7 +100,7 @@ class Composer extends React.Component { } onChangeText(text) { - // updates the text input to reflect the current value + // Updates the text input to reflect the current value this.setState({value: text}); // calls the onChangeText callback prop @@ -125,7 +125,7 @@ class Composer extends React.Component { editable={!this.props.isDisabled} onChangeText={this.onChangeText} - // we have a value explicitly set so the value can be changed imperatively + // We have a value explicitly set so the value can be changed imperatively // (needed e.g. when we are injecting emojis into the text view) value={this.state.value} /> diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index ee62db8817053..b83fb409762d3 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -328,9 +328,8 @@ class ReportActionCompose extends React.Component { this.textInput.updateSelection(this.selection); - // this will call the function we passed - // to the TextInput's onChangeText, - // so updateComment gets called after this. + // This will call the function we passed to the TextInput's onChangeText. + // So updateComment gets called after this. this.textInput.onChangeText(newComment); } From 1a446b1bf8c65c8ab29b59348cc1d8da3588bf9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 12 Oct 2022 08:44:54 +0200 Subject: [PATCH 022/110] move under if condition --- src/components/Composer/index.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 2b94469ac1f2c..e75ebf5da36b3 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -135,16 +135,6 @@ class Composer extends React.Component { } componentDidMount() { - // We pass the ref to the native view instance - // however, we want this method to be - // available to be called from the outside as well. - this.textInput.onChangeText = this.onChangeText; - this.textInput.updateSelection = this.updateSelection; - - // Overwrite focus with this component's implementation - this.textInput.nativeFocus = this.textInput.focus; - this.textInput.focus = this.focus; - this.updateNumberOfLines(); // This callback prop is used by the parent component using the constructor to @@ -155,9 +145,20 @@ class Composer extends React.Component { 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) { + // We pass the ref to the native view instance + // however, we want this method to be + // available to be called from the outside as well. + this.textInput.onChangeText = this.onChangeText; + this.textInput.updateSelection = this.updateSelection; + + // Overwrite focus with this component's implementation + this.textInput.nativeFocus = this.textInput.focus; + this.textInput.focus = this.focus; + + // There is no onPaste or onDrag for TextInput in react-native so we will add event + // listeners here and unbind when the component unmounts + // Firefox will not allow dropping unless we call preventDefault on the dragover event // We listen on document to extend the Drop area beyond Composer document.addEventListener('dragover', this.dragNDropListener); From 6e987a8544f462cd2e2c21eff0a8ee8100361202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 12 Oct 2022 08:46:23 +0200 Subject: [PATCH 023/110] Apply suggestions from code review Co-authored-by: Rajat Parashar --- src/components/Composer/index.native.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index 7160d85376462..c582aad3e1340 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -103,10 +103,11 @@ class Composer extends React.Component { // Updates the text input to reflect the current value this.setState({value: text}); - // calls the onChangeText callback prop - if (this.props.onChangeText != null) { - this.props.onChangeText(text); + // Calls the onChangeText callback prop + if (!this.props.onChangeText) { + return; } + this.props.onChangeText(text); } render() { From 68cda491cc48a1ec77e59db7acf793c436e74864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 12 Oct 2022 08:50:19 +0200 Subject: [PATCH 024/110] add onChangeText to props --- src/components/Composer/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index e75ebf5da36b3..9437540993cde 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -66,6 +66,9 @@ const propTypes = { /** Allow the full composer to be opened */ setIsFullComposerAvailable: PropTypes.func, + /** Called when the user changes the text in the input */ + onChangeText: PropTypes.func, + ...withLocalizePropTypes, }; @@ -87,6 +90,7 @@ const defaultProps = { onSelectionChange: () => {}, isFullComposerAvailable: false, setIsFullComposerAvailable: () => {}, + onChangeText: () => {}, }; const IMAGE_EXTENSIONS = { @@ -201,11 +205,7 @@ class Composer extends React.Component { onChangeText(text) { // Updates the text input to reflect the current value this.setState({value: text}); - - // Calls the onChangeText callback prop - if (this.props.onChangeText != null) { - this.props.onChangeText(text); - } + this.props.onChangeText(text); } onSelectionChange(event) { From 29aca94398742188e70b1601aa13d39c87c50c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 12 Oct 2022 08:55:09 +0200 Subject: [PATCH 025/110] move to CONST.js --- src/CONST.js | 2 ++ src/pages/home/report/ReportActionCompose.js | 12 +++--------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 59403775effa1..a30d1c7cfd912 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -737,6 +737,8 @@ const CONST = { EMOJIS: /(?:\uD83D(?:\uDC41\u200D\uD83D\uDDE8|\uDC68\u200D\uD83D[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uDC69\u200D\uD83D\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g, TAX_ID: /^\d{9}$/, NON_NUMERIC: /\D/g, + + IS_COMMENT_EMPTY: /^(\s|`)*$/, }, PRONOUNS: { diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index b83fb409762d3..62edb13a233f0 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -1,10 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - View, - TouchableOpacity, - InteractionManager, -} from 'react-native'; +import {InteractionManager, TouchableOpacity, View} from 'react-native'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import {withOnyx} from 'react-native-onyx'; @@ -34,7 +30,7 @@ import * as ReportUtils from '../../../libs/ReportUtils'; import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager'; import participantPropTypes from '../../../components/participantPropTypes'; import ParticipantLocalTime from './ParticipantLocalTime'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../../components/withCurrentUserPersonalDetails'; import {withNetwork, withPersonalDetails} from '../../../components/OnyxProvider'; import * as User from '../../../libs/actions/User'; import Tooltip from '../../../components/Tooltip'; @@ -109,8 +105,6 @@ const defaultProps = { ...withCurrentUserPersonalDetailsDefaultProps, }; -const IS_EMPTY_PATTERN = /^(\s|`)*$/; - class ReportActionCompose extends React.Component { constructor(props) { super(props); @@ -383,7 +377,7 @@ class ReportActionCompose extends React.Component { * @param {Boolean} shouldDebounceSaveComment */ updateComment(newComment, shouldDebounceSaveComment) { - const isCommentEmpty = !!newComment.match(IS_EMPTY_PATTERN); + const isCommentEmpty = !!newComment.match(CONST.REGEX.IS_COMMENT_EMPTY); if (this.state.isCommentEmpty !== isCommentEmpty) { this.setState({isCommentEmpty}); } From 37d11ba0a7fa1b351c7aedf3314ae6adb20d5490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 12 Oct 2022 09:37:05 +0200 Subject: [PATCH 026/110] fix: web pasting text broken cursor Addresses: - https://github.com/Expensify/App/pull/11684#pullrequestreview-1138108653 - https://github.com/Expensify/App/pull/11684#pullrequestreview-1138103867 --- 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 9437540993cde..d231e54d1921e 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -283,7 +283,7 @@ class Composer extends React.Component { // 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(); + this.textInput.nativeFocus(); // eslint-disable-next-line no-empty } catch (e) {} } From aa20edc6ce4c94a357dfd66efa4fa28fd5cd1f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 12 Oct 2022 15:24:02 +0200 Subject: [PATCH 027/110] fix: selection issues --- src/components/Composer/index.js | 21 ++++++++++----- src/components/Composer/index.native.js | 22 ++++++++++----- src/pages/home/report/ReportActionCompose.js | 27 ++++++++++++++----- .../report/ReportActionItemMessageEdit.js | 4 ++- 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index d231e54d1921e..91e1fd0ea930f 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -136,6 +136,7 @@ class Composer extends React.Component { this.focus = this.focus.bind(this); this.onSelectionChange = this.onSelectionChange.bind(this); this.updateSelection = this.updateSelection.bind(this); + this.setText = this.setText.bind(this); } componentDidMount() { @@ -153,7 +154,7 @@ class Composer extends React.Component { // We pass the ref to the native view instance // however, we want this method to be // available to be called from the outside as well. - this.textInput.onChangeText = this.onChangeText; + this.textInput.setText = this.setText; this.textInput.updateSelection = this.updateSelection; // Overwrite focus with this component's implementation @@ -204,7 +205,7 @@ class Composer extends React.Component { onChangeText(text) { // Updates the text input to reflect the current value - this.setState({value: text}); + this.setText(text); this.props.onChangeText(text); } @@ -213,6 +214,18 @@ class Composer extends React.Component { this.props.onSelectionChange(event); } + setText(text) { + this.setState({value: text}); + } + + updateSelection(selection) { + this.selection = selection; + this.textInput.setSelectionRange( + selection.start, + selection.end, + ); + } + focus() { // Capture the selection, as the "native focus" call will // call onSelectionChange to the end of the text @@ -227,10 +240,6 @@ class Composer extends React.Component { }); } - updateSelection(selection) { - this.selection = selection; - } - /** * Handles all types of drag-N-drop events on the composer * diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index c582aad3e1340..28a36d2a3e65a 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -57,7 +57,7 @@ const defaultProps = { setIsFullComposerAvailable: () => {}, style: null, value: '', - onChangeText: null, + onChangeText: () => {}, defaultValue: '', }; @@ -66,6 +66,8 @@ class Composer extends React.Component { super(props); this.onChangeText = this.onChangeText.bind(this); + this.setText = this.setText.bind(this); + this.updateSelection = this.updateSelection.bind(this); this.state = { propStyles: StyleSheet.flatten(this.props.style), value: props.defaultValue || props.value, @@ -76,8 +78,8 @@ class Composer extends React.Component { // We pass the ref to the native view instance, // however, we want this method to be // available to be called from the outside as well. - this.textInput.onChangeText = this.onChangeText; - this.textInput.updateSelection = () => {}; // noop + this.textInput.setText = this.setText; + this.textInput.updateSelection = this.updateSelection; // 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 @@ -100,14 +102,19 @@ class Composer extends React.Component { } onChangeText(text) { - // Updates the text input to reflect the current value + this.setText(text); + this.props.onChangeText(text); + } + + setText(text) { this.setState({value: text}); + } - // Calls the onChangeText callback prop - if (!this.props.onChangeText) { + updateSelection(selection) { + if (this.textInput == null || selection == null) { return; } - this.props.onChangeText(text); + this.textInput.setSelection(selection.start, selection.end); } render() { @@ -128,6 +135,7 @@ class Composer extends React.Component { // We have a value explicitly set so the value can be changed imperatively // (needed e.g. when we are injecting emojis into the text view) + // Can be avoided once: https://github.com/facebook/react-native/pull/34955 gets merged value={this.state.value} /> ); diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 62edb13a233f0..54e6ea1265859 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -186,7 +186,12 @@ class ReportActionCompose extends React.Component { } onSelectionChange(e) { - this.selection = e.nativeEvent.selection; + if (this.onNextSelectionChange == null) { + this.selection = e.nativeEvent.selection; + } else { + this.onNextSelectionChange(e.nativeEvent.selection); + this.onNextSelectionChange = null; + } } /** @@ -315,16 +320,26 @@ class ReportActionCompose extends React.Component { const emojiWithSpace = `${emoji} `; const newComment = this.comment.slice(0, this.selection.start) + emojiWithSpace + this.comment.slice(this.selection.end, this.comment.length); - this.selection = { + const selection = { start: this.selection.start + emojiWithSpace.length, end: this.selection.start + emojiWithSpace.length, }; - this.textInput.updateSelection(this.selection); + // Update selection before updating the text + // is intentional and makes the behaviour equally across all platforms (needed for web) + this.textInput.updateSelection(selection); + + // When the text input gets assigned a new value we will + // receive a new selection event, which will be the end of + // the text input. So we need to wait for that event and then + // set the selection to the place where we want it to be. + this.onNextSelectionChange = () => { + this.selection = selection; + this.textInput.updateSelection(selection); + }; - // This will call the function we passed to the TextInput's onChangeText. - // So updateComment gets called after this. - this.textInput.onChangeText(newComment); + this.updateComment(newComment, true); + this.textInput.setText(newComment); } /** diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 50ed69f577be0..684b38578826d 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -161,8 +161,10 @@ class ReportActionItemMessageEdit extends React.Component { end: this.selection.start + emoji.length, }; + this.updateDraft(newComment); + this.textInput.updateSelection(this.selection); - this.textInput.onChangeText(newComment); + this.textInput.setText(newComment); } /** From f26aca89c964c213d84e726e63135246f06a293f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 12 Oct 2022 15:26:38 +0200 Subject: [PATCH 028/110] change(web): make composer component uncontrolled (perf gain) --- src/components/Composer/index.js | 8 +------- src/components/Composer/index.native.js | 6 +----- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 91e1fd0ea930f..3d8dadf507d17 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -19,9 +19,6 @@ const propTypes = { /** The default value of the comment box */ defaultValue: PropTypes.string, - /** The value of the comment box */ - value: PropTypes.string, - /** Callback method to handle pasting a file */ onPasteFile: PropTypes.func, @@ -74,7 +71,6 @@ const propTypes = { const defaultProps = { defaultValue: undefined, - value: undefined, maxLines: -1, onPasteFile: () => {}, shouldClear: false, @@ -124,7 +120,6 @@ class Composer extends React.Component { this.state = { numberOfLines: 1, - value: initialValue, }; this.dragNDropListener = this.dragNDropListener.bind(this); this.paste = this.paste.bind(this); @@ -215,7 +210,7 @@ class Composer extends React.Component { } setText(text) { - this.setState({value: text}); + this.textInput.value = text; } updateSelection(selection) { @@ -426,7 +421,6 @@ class Composer extends React.Component { {...propsToPass} disabled={this.props.isDisabled} onChangeText={this.onChangeText} - value={this.state.value} onSelectionChange={this.onSelectionChange} /> ); diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index 28a36d2a3e65a..a699c6b5800f4 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -37,9 +37,6 @@ const propTypes = { // eslint-disable-next-line react/forbid-prop-types style: PropTypes.any, - /** The text to display in the input */ - value: PropTypes.string, - /** Called when the text gets changed by user input */ onChangeText: PropTypes.func, @@ -56,7 +53,6 @@ const defaultProps = { isFullComposerAvailable: false, setIsFullComposerAvailable: () => {}, style: null, - value: '', onChangeText: () => {}, defaultValue: '', }; @@ -70,7 +66,7 @@ class Composer extends React.Component { this.updateSelection = this.updateSelection.bind(this); this.state = { propStyles: StyleSheet.flatten(this.props.style), - value: props.defaultValue || props.value, + value: props.defaultValue, }; } From 2e7dee92cd165f35477cde973f22c0df6e4b2821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 12 Oct 2022 15:47:25 +0200 Subject: [PATCH 029/110] fix(web): edit composer selection --- src/components/Composer/index.js | 7 ++++--- src/pages/home/report/ReportActionItemMessageEdit.js | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 3d8dadf507d17..8927177fc5c81 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -109,13 +109,13 @@ class Composer extends React.Component { constructor(props) { super(props); - const initialValue = props.defaultValue + this.initialValue = props.defaultValue ? `${props.defaultValue}` : `${props.value || ''}`; this.selection = { - start: initialValue.length, - end: initialValue.length, + start: this.initialValue.length, + end: this.initialValue.length, }; this.state = { @@ -168,6 +168,7 @@ class Composer extends React.Component { this.textInput.addEventListener('paste', this.handlePaste); this.textInput.addEventListener('wheel', this.handleWheel); + this.setText(this.initialValue); this.textInput.setSelectionRange(this.selection.start, this.selection.end); } } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 684b38578826d..7ea9d9e5903a6 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -163,8 +163,8 @@ class ReportActionItemMessageEdit extends React.Component { this.updateDraft(newComment); - this.textInput.updateSelection(this.selection); this.textInput.setText(newComment); + this.textInput.updateSelection(this.selection); } /** @@ -212,7 +212,7 @@ class ReportActionItemMessageEdit extends React.Component { toggleReportActionComposeView(true, VirtualKeyboard.shouldAssumeIsOpen()); }} onSelectionChange={this.onSelectionChange} - defaultValue={this.props.draftMessage} + defaultValue={this.draft} /> Date: Wed, 12 Oct 2022 18:33:29 +0200 Subject: [PATCH 030/110] fix(native): set text imperatively --- src/components/Composer/index.native.js | 32 ------- src/components/RNTextInput.js | 94 +++++++++++++++---- src/pages/home/report/ReportActionCompose.js | 41 ++++---- .../report/ReportActionItemMessageEdit.js | 4 +- 4 files changed, 93 insertions(+), 78 deletions(-) diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index a699c6b5800f4..e6ebcdc14e16e 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -61,22 +61,12 @@ class Composer extends React.Component { constructor(props) { super(props); - this.onChangeText = this.onChangeText.bind(this); - this.setText = this.setText.bind(this); - this.updateSelection = this.updateSelection.bind(this); this.state = { propStyles: StyleSheet.flatten(this.props.style), - value: props.defaultValue, }; } componentDidMount() { - // We pass the ref to the native view instance, - // however, we want this method to be - // available to be called from the outside as well. - this.textInput.setText = this.setText; - this.textInput.updateSelection = this.updateSelection; - // 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 @@ -97,22 +87,6 @@ class Composer extends React.Component { this.props.onClear(); } - onChangeText(text) { - this.setText(text); - this.props.onChangeText(text); - } - - setText(text) { - this.setState({value: text}); - } - - updateSelection(selection) { - if (this.textInput == null || selection == null) { - return; - } - this.textInput.setSelection(selection.start, selection.end); - } - render() { return ( ); } diff --git a/src/components/RNTextInput.js b/src/components/RNTextInput.js index 75e14b0966e5e..afee64cc0564e 100644 --- a/src/components/RNTextInput.js +++ b/src/components/RNTextInput.js @@ -1,35 +1,91 @@ -import React from 'react'; +import React, {useCallback, useMemo, useRef} from 'react'; import _ from 'underscore'; // eslint-disable-next-line no-restricted-imports -import {TextInput} from 'react-native'; +import {Platform, TextInput} from 'react-native'; import PropTypes from 'prop-types'; const propTypes = { /** A ref to forward to the text input */ forwardedRef: PropTypes.func, + + /** If true, the text input can be multiple lines. The default value is false. */ + multiline: PropTypes.bool, + + /** Callback that is called when the text input's text changes. */ + onChange: PropTypes.func, }; const defaultProps = { forwardedRef: () => {}, + multiline: false, + onChange: () => {}, }; -const RNTextInput = props => ( - { - if (!_.isFunction(props.forwardedRef)) { - return; - } - props.forwardedRef(ref); - }} - - // By default, align input to the left to override right alignment in RTL mode which is not yet supported in the App. - // eslint-disable-next-line react/jsx-props-no-multi-spaces - textAlign="left" - - // eslint-disable-next-line - {...props} - /> -); +// Getting the commands module of the TextInput native component +// See: https://github.com/facebook/react-native/blob/4a786d6b0d7a3420afdfb6b136d2ee3fa3b53145/Libraries/Components/TextInput/TextInput.js#L40 +let AndroidTextInputCommands; +let RCTSinglelineTextInputNativeCommands; +let RCTMultilineTextInputNativeCommands; +if (Platform.OS === 'android') { + AndroidTextInputCommands = require('react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent').Commands; +} else if (Platform.OS === 'ios') { + RCTSinglelineTextInputNativeCommands = require('react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent').Commands; + RCTMultilineTextInputNativeCommands = require('react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent').Commands; +} + +const getViewCommands = (multiline) => { + let viewCommands; + if (AndroidTextInputCommands) { + viewCommands = AndroidTextInputCommands; + } else { + viewCommands = multiline === true + ? RCTMultilineTextInputNativeCommands + : RCTSinglelineTextInputNativeCommands; + } + return viewCommands; +}; + +const RNTextInput = (props) => { + const mostRecentEventCount = useRef(0); + const viewCommands = useMemo(() => getViewCommands(props.multiline), [props.multiline]); + + const onChange = useCallback((event) => { + mostRecentEventCount.current = event.nativeEvent.eventCount; + props.onChange(event); + }, [props.onChange]); + + const forwardRef = useCallback((ref) => { + if (!_.isFunction(props.forwardedRef)) { + return; + } + + // Add the setTextAndSelection method to the ref + if (ref != null) { + // eslint-disable-next-line no-param-reassign + ref.setTextAndSelection = (text, start, end) => { + console.debug('[HGD] RNTextInput.setTextAndSelection', text, start, end); + viewCommands.setTextAndSelection(ref, mostRecentEventCount.current, text, start, end); + }; + } + + props.forwardedRef(ref); + }, [props.forwardedRef, viewCommands]); + + return ( + + ); +}; RNTextInput.propTypes = propTypes; RNTextInput.defaultProps = defaultProps; diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 54e6ea1265859..e2dad3ab531a6 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -1,6 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {InteractionManager, TouchableOpacity, View} from 'react-native'; +import { + InteractionManager, Platform, TouchableOpacity, View, +} from 'react-native'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import {withOnyx} from 'react-native-onyx'; @@ -186,12 +188,7 @@ class ReportActionCompose extends React.Component { } onSelectionChange(e) { - if (this.onNextSelectionChange == null) { - this.selection = e.nativeEvent.selection; - } else { - this.onNextSelectionChange(e.nativeEvent.selection); - this.onNextSelectionChange = null; - } + this.selection = e.nativeEvent.selection; } /** @@ -317,29 +314,25 @@ class ReportActionCompose extends React.Component { * @param {String} emoji */ addEmojiToTextBox(emoji) { + const hasRangeSelected = this.selection.start !== this.selection.end; + if (Platform.OS === 'android' && hasRangeSelected) { + // Android: when we have a range selected setTextAndSelection + // won't remove the highlight, so we manually set the cursor + // to a selection range of 0 (so there won't be any selection highlight). + this.textInput.setSelection(this.selection.start, this.selection.start); + } + const emojiWithSpace = `${emoji} `; const newComment = this.comment.slice(0, this.selection.start) + emojiWithSpace + this.comment.slice(this.selection.end, this.comment.length); - const selection = { - start: this.selection.start + emojiWithSpace.length, - end: this.selection.start + emojiWithSpace.length, - }; - - // Update selection before updating the text - // is intentional and makes the behaviour equally across all platforms (needed for web) - this.textInput.updateSelection(selection); - - // When the text input gets assigned a new value we will - // receive a new selection event, which will be the end of - // the text input. So we need to wait for that event and then - // set the selection to the place where we want it to be. - this.onNextSelectionChange = () => { - this.selection = selection; - this.textInput.updateSelection(selection); + const newSelection = this.selection.start + emojiWithSpace.length; + this.selection = { + start: newSelection, + end: newSelection, }; + this.textInput.setTextAndSelection(newComment, this.selection.start, this.selection.end); this.updateComment(newComment, true); - this.textInput.setText(newComment); } /** diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 7ea9d9e5903a6..fe0e7ddc3283f 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -161,10 +161,8 @@ class ReportActionItemMessageEdit extends React.Component { end: this.selection.start + emoji.length, }; + this.textInput.setTextAndSelection(newComment, this.selection.start, this.selection.end); this.updateDraft(newComment); - - this.textInput.setText(newComment); - this.textInput.updateSelection(this.selection); } /** From 15c5ff5b48d6e77fb6c716d4cfdf5bef9099204f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 12 Oct 2022 19:10:05 +0200 Subject: [PATCH 031/110] clean: separated platform dependent code nicer --- src/components/Composer/index.js | 62 +++---------------- src/components/Composer/index.native.js | 57 ++++++++++++++++- src/components/RNTextInput.js | 81 ++++--------------------- 3 files changed, 76 insertions(+), 124 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 8927177fc5c81..c8873bd458502 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -127,11 +127,7 @@ class Composer extends React.Component { this.handlePastedHTML = this.handlePastedHTML.bind(this); this.handleWheel = this.handleWheel.bind(this); this.updateNumberOfLines = this.updateNumberOfLines.bind(this); - this.onChangeText = this.onChangeText.bind(this); - this.focus = this.focus.bind(this); - this.onSelectionChange = this.onSelectionChange.bind(this); - this.updateSelection = this.updateSelection.bind(this); - this.setText = this.setText.bind(this); + this.setTextAndSelection = this.setTextAndSelection.bind(this); } componentDidMount() { @@ -146,15 +142,7 @@ class Composer extends React.Component { } if (this.textInput) { - // We pass the ref to the native view instance - // however, we want this method to be - // available to be called from the outside as well. - this.textInput.setText = this.setText; - this.textInput.updateSelection = this.updateSelection; - - // Overwrite focus with this component's implementation - this.textInput.nativeFocus = this.textInput.focus; - this.textInput.focus = this.focus; + this.textInput.setTextAndSelection = this.setTextAndSelection; // There is no onPaste or onDrag for TextInput in react-native so we will add event // listeners here and unbind when the component unmounts @@ -168,7 +156,7 @@ class Composer extends React.Component { this.textInput.addEventListener('paste', this.handlePaste); this.textInput.addEventListener('wheel', this.handleWheel); - this.setText(this.initialValue); + this.textInput.value = this.initialValue; this.textInput.setSelectionRange(this.selection.start, this.selection.end); } } @@ -199,41 +187,9 @@ class Composer extends React.Component { this.textInput.removeEventListener('wheel', this.handleWheel); } - onChangeText(text) { - // Updates the text input to reflect the current value - this.setText(text); - this.props.onChangeText(text); - } - - onSelectionChange(event) { - this.selection = event.nativeEvent.selection; - this.props.onSelectionChange(event); - } - - setText(text) { + setTextAndSelection(text, start, end) { this.textInput.value = text; - } - - updateSelection(selection) { - this.selection = selection; - this.textInput.setSelectionRange( - selection.start, - selection.end, - ); - } - - focus() { - // Capture the selection, as the "native focus" call will - // call onSelectionChange to the end of the text - const selection = this.selection; - - this.textInput.nativeFocus(); - requestAnimationFrame(() => { - this.textInput.setSelectionRange( - selection.start, - selection.end, - ); - }); + this.textInput.setSelectionRange(start, end); } /** @@ -288,7 +244,7 @@ class Composer extends React.Component { // 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.nativeFocus(); + this.textInput.focus(); // eslint-disable-next-line no-empty } catch (e) {} } @@ -400,9 +356,7 @@ class Composer extends React.Component { + parseInt(computedStyle.paddingTop, 10); const numberOfLines = getNumberOfLines(this.props.maxLines, lineHeight, paddingTopAndBottom, this.textInput.scrollHeight); updateIsFullComposerAvailable(this.props, numberOfLines); - this.setState({ - numberOfLines, - }); + this.setState({numberOfLines}); }); } @@ -421,8 +375,6 @@ class Composer extends React.Component { /* eslint-disable-next-line react/jsx-props-no-spreading */ {...propsToPass} disabled={this.props.isDisabled} - onChangeText={this.onChangeText} - onSelectionChange={this.onSelectionChange} /> ); } diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index e6ebcdc14e16e..d1202dd1216de 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -1,5 +1,5 @@ import React from 'react'; -import {StyleSheet} from 'react-native'; +import {Platform, StyleSheet} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import RNTextInput from '../RNTextInput'; @@ -7,6 +7,30 @@ import themeColors from '../../styles/themes/default'; import CONST from '../../CONST'; import * as ComposerUtils from '../../libs/ComposerUtils'; +// Getting the commands module of the TextInput native component +// See: https://github.com/facebook/react-native/blob/4a786d6b0d7a3420afdfb6b136d2ee3fa3b53145/Libraries/Components/TextInput/TextInput.js#L40 +let AndroidTextInputCommands; +let RCTSinglelineTextInputNativeCommands; +let RCTMultilineTextInputNativeCommands; +if (Platform.OS === 'android') { + AndroidTextInputCommands = require('react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent').Commands; +} else if (Platform.OS === 'ios') { + RCTSinglelineTextInputNativeCommands = require('react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent').Commands; + RCTMultilineTextInputNativeCommands = require('react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent').Commands; +} + +const getViewCommands = (multiline) => { + let viewCommands; + if (AndroidTextInputCommands) { + viewCommands = AndroidTextInputCommands; + } else { + viewCommands = multiline === true + ? RCTMultilineTextInputNativeCommands + : RCTSinglelineTextInputNativeCommands; + } + return viewCommands; +}; + const propTypes = { /** If the input should clear, it actually gets intercepted instead of .clear() */ shouldClear: PropTypes.bool, @@ -42,6 +66,12 @@ const propTypes = { /** A value the input should have when it first mounts. Default is empty. */ defaultValue: PropTypes.string, + + /** If true, the text input can be multiple lines. The default value is false. */ + multiline: PropTypes.bool, + + /** Callback that is called when the text input's text changes. */ + onChange: PropTypes.func, }; const defaultProps = { @@ -55,12 +85,20 @@ const defaultProps = { style: null, onChangeText: () => {}, defaultValue: '', + multiline: false, + onChange: () => {}, }; class Composer extends React.Component { constructor(props) { super(props); + this.mostRecentEventCount = 0; + this.viewCommands = getViewCommands(props.multiline); + + this.onChange = this.onChange.bind(this); + this.setTextAndSelection = this.setTextAndSelection.bind(this); + this.state = { propStyles: StyleSheet.flatten(this.props.style), }; @@ -74,6 +112,7 @@ class Composer extends React.Component { if (!this.props.forwardedRef || !_.isFunction(this.props.forwardedRef)) { return; } + this.textInput.setTextAndSelection = this.setTextAndSelection; this.props.forwardedRef(this.textInput); } @@ -87,6 +126,21 @@ class Composer extends React.Component { this.props.onClear(); } + onChange(event) { + this.mostRecentEventCount = event.nativeEvent.eventCount; + this.props.onChange(event); + } + + setTextAndSelection(text, start, end) { + this.viewCommands.setTextAndSelection( + this.textInput, + this.mostRecentEventCount, + text, + start, + end, + ); + } + render() { return ( ); } diff --git a/src/components/RNTextInput.js b/src/components/RNTextInput.js index afee64cc0564e..af14b54ddedc8 100644 --- a/src/components/RNTextInput.js +++ b/src/components/RNTextInput.js @@ -1,91 +1,36 @@ -import React, {useCallback, useMemo, useRef} from 'react'; +import React from 'react'; import _ from 'underscore'; // eslint-disable-next-line no-restricted-imports -import {Platform, TextInput} from 'react-native'; +import {TextInput} from 'react-native'; import PropTypes from 'prop-types'; const propTypes = { /** A ref to forward to the text input */ forwardedRef: PropTypes.func, - - /** If true, the text input can be multiple lines. The default value is false. */ - multiline: PropTypes.bool, - - /** Callback that is called when the text input's text changes. */ - onChange: PropTypes.func, }; const defaultProps = { forwardedRef: () => {}, - multiline: false, - onChange: () => {}, -}; - -// Getting the commands module of the TextInput native component -// See: https://github.com/facebook/react-native/blob/4a786d6b0d7a3420afdfb6b136d2ee3fa3b53145/Libraries/Components/TextInput/TextInput.js#L40 -let AndroidTextInputCommands; -let RCTSinglelineTextInputNativeCommands; -let RCTMultilineTextInputNativeCommands; -if (Platform.OS === 'android') { - AndroidTextInputCommands = require('react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent').Commands; -} else if (Platform.OS === 'ios') { - RCTSinglelineTextInputNativeCommands = require('react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent').Commands; - RCTMultilineTextInputNativeCommands = require('react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent').Commands; -} - -const getViewCommands = (multiline) => { - let viewCommands; - if (AndroidTextInputCommands) { - viewCommands = AndroidTextInputCommands; - } else { - viewCommands = multiline === true - ? RCTMultilineTextInputNativeCommands - : RCTSinglelineTextInputNativeCommands; - } - return viewCommands; }; -const RNTextInput = (props) => { - const mostRecentEventCount = useRef(0); - const viewCommands = useMemo(() => getViewCommands(props.multiline), [props.multiline]); - - const onChange = useCallback((event) => { - mostRecentEventCount.current = event.nativeEvent.eventCount; - props.onChange(event); - }, [props.onChange]); +const RNTextInput = props => ( + { + if (!_.isFunction(props.forwardedRef) || !ref) { + return; + } - const forwardRef = useCallback((ref) => { - if (!_.isFunction(props.forwardedRef)) { - return; - } - - // Add the setTextAndSelection method to the ref - if (ref != null) { - // eslint-disable-next-line no-param-reassign - ref.setTextAndSelection = (text, start, end) => { - console.debug('[HGD] RNTextInput.setTextAndSelection', text, start, end); - viewCommands.setTextAndSelection(ref, mostRecentEventCount.current, text, start, end); - }; - } - - props.forwardedRef(ref); - }, [props.forwardedRef, viewCommands]); - - return ( - - ); -}; + /> +); RNTextInput.propTypes = propTypes; RNTextInput.defaultProps = defaultProps; From 17e9de097cc8869d172dd73b6318c74ee6e8584d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 12 Oct 2022 19:11:58 +0200 Subject: [PATCH 032/110] change: don't insert emoji with whitespace https://expensify.slack.com/archives/C035J5C9FAP/p1665560521827199 --- src/pages/home/report/ReportActionCompose.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index e2dad3ab531a6..e798477296d85 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -322,10 +322,9 @@ class ReportActionCompose extends React.Component { this.textInput.setSelection(this.selection.start, this.selection.start); } - const emojiWithSpace = `${emoji} `; const newComment = this.comment.slice(0, this.selection.start) - + emojiWithSpace + this.comment.slice(this.selection.end, this.comment.length); - const newSelection = this.selection.start + emojiWithSpace.length; + + emoji + this.comment.slice(this.selection.end, this.comment.length); + const newSelection = this.selection.start + emoji.length; this.selection = { start: newSelection, end: newSelection, From d8d1cb8984a39f5b829d17f6fb5aeeda092003b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 14 Oct 2022 07:56:39 +0200 Subject: [PATCH 033/110] wip --- src/components/Composer/index.native.js | 59 +++++-------------------- 1 file changed, 11 insertions(+), 48 deletions(-) diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index d1202dd1216de..756db57db023c 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -1,5 +1,5 @@ import React from 'react'; -import {Platform, StyleSheet} from 'react-native'; +import {StyleSheet} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import RNTextInput from '../RNTextInput'; @@ -7,30 +7,6 @@ import themeColors from '../../styles/themes/default'; import CONST from '../../CONST'; import * as ComposerUtils from '../../libs/ComposerUtils'; -// Getting the commands module of the TextInput native component -// See: https://github.com/facebook/react-native/blob/4a786d6b0d7a3420afdfb6b136d2ee3fa3b53145/Libraries/Components/TextInput/TextInput.js#L40 -let AndroidTextInputCommands; -let RCTSinglelineTextInputNativeCommands; -let RCTMultilineTextInputNativeCommands; -if (Platform.OS === 'android') { - AndroidTextInputCommands = require('react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent').Commands; -} else if (Platform.OS === 'ios') { - RCTSinglelineTextInputNativeCommands = require('react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent').Commands; - RCTMultilineTextInputNativeCommands = require('react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent').Commands; -} - -const getViewCommands = (multiline) => { - let viewCommands; - if (AndroidTextInputCommands) { - viewCommands = AndroidTextInputCommands; - } else { - viewCommands = multiline === true - ? RCTMultilineTextInputNativeCommands - : RCTSinglelineTextInputNativeCommands; - } - return viewCommands; -}; - const propTypes = { /** If the input should clear, it actually gets intercepted instead of .clear() */ shouldClear: PropTypes.bool, @@ -66,12 +42,6 @@ const propTypes = { /** A value the input should have when it first mounts. Default is empty. */ defaultValue: PropTypes.string, - - /** If true, the text input can be multiple lines. The default value is false. */ - multiline: PropTypes.bool, - - /** Callback that is called when the text input's text changes. */ - onChange: PropTypes.func, }; const defaultProps = { @@ -85,22 +55,18 @@ const defaultProps = { style: null, onChangeText: () => {}, defaultValue: '', - multiline: false, - onChange: () => {}, }; class Composer extends React.Component { constructor(props) { super(props); - this.mostRecentEventCount = 0; - this.viewCommands = getViewCommands(props.multiline); - - this.onChange = this.onChange.bind(this); + this.onChangeText = this.onChangeText.bind(this); this.setTextAndSelection = this.setTextAndSelection.bind(this); this.state = { propStyles: StyleSheet.flatten(this.props.style), + value: this.props.defaultValue, }; } @@ -126,19 +92,15 @@ class Composer extends React.Component { this.props.onClear(); } - onChange(event) { - this.mostRecentEventCount = event.nativeEvent.eventCount; - this.props.onChange(event); + onChangeText(text) { + this.setState({value: text}); + this.props.onChangeText(text); } setTextAndSelection(text, start, end) { - this.viewCommands.setTextAndSelection( - this.textInput, - this.mostRecentEventCount, - text, - start, - end, - ); + this.setState({value: text}, () => { + this.textInput.setSelection(start, end); + }); } render() { @@ -155,7 +117,8 @@ class Composer extends React.Component { /* eslint-disable-next-line react/jsx-props-no-spreading */ {...this.props} editable={!this.props.isDisabled} - onChange={this.onChange} + onChangeText={this.onChangeText} + value={this.state.value} /> ); } From d16ad45ca13370dd903a98f6f2f0e6eb0c7838f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 14 Oct 2022 14:48:44 +0200 Subject: [PATCH 034/110] add comment --- src/components/Composer/index.native.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index 756db57db023c..cfe870586f951 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -78,6 +78,7 @@ class Composer extends React.Component { if (!this.props.forwardedRef || !_.isFunction(this.props.forwardedRef)) { return; } + // We want this to be an available method on the ref for parent components this.textInput.setTextAndSelection = this.setTextAndSelection; this.props.forwardedRef(this.textInput); From adf1062f848bbb7b73c90b074742e06dec5418cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 14 Oct 2022 14:54:56 +0200 Subject: [PATCH 035/110] eslint --- src/components/Composer/index.native.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index cfe870586f951..c4018f6ec1e16 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -78,6 +78,7 @@ class Composer extends React.Component { if (!this.props.forwardedRef || !_.isFunction(this.props.forwardedRef)) { return; } + // We want this to be an available method on the ref for parent components this.textInput.setTextAndSelection = this.setTextAndSelection; From 7ecf0f3f6c819b2ea182e67ac71678c2b6fb10c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 14 Oct 2022 18:56:44 +0200 Subject: [PATCH 036/110] Apply suggestions from code review Co-authored-by: Rory Abraham <47436092+roryabraham@users.noreply.github.com> --- src/components/RNTextInput.js | 8 ++++---- src/pages/home/report/ReportActionCompose.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/RNTextInput.js b/src/components/RNTextInput.js index af14b54ddedc8..8a16d7adf0362 100644 --- a/src/components/RNTextInput.js +++ b/src/components/RNTextInput.js @@ -23,12 +23,12 @@ const RNTextInput = props => ( props.forwardedRef(ref); }} - // By default, align input to the left to override right alignment in RTL mode which is not yet supported in the App. - // eslint-disable-next-line react/jsx-props-no-multi-spaces + // By default, align input to the left to override right alignment in RTL mode which is not yet supported in the App. + // eslint-disable-next-line react/jsx-props-no-multi-spaces textAlign="left" - // eslint-disable-next-line - {...props} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} /> ); diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index e798477296d85..8da8234eceb83 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -384,7 +384,7 @@ class ReportActionCompose extends React.Component { * @param {Boolean} shouldDebounceSaveComment */ updateComment(newComment, shouldDebounceSaveComment) { - const isCommentEmpty = !!newComment.match(CONST.REGEX.IS_COMMENT_EMPTY); + const isCommentEmpty = Boolean(newComment.match(CONST.REGEX.IS_COMMENT_EMPTY)); if (this.state.isCommentEmpty !== isCommentEmpty) { this.setState({isCommentEmpty}); } From 7465a77d988b294dc3d916bbd0af51484fd98018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 14 Oct 2022 18:57:33 +0200 Subject: [PATCH 037/110] clean import --- src/pages/home/report/ReportActionCompose.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 8da8234eceb83..351b15a64abb6 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -32,7 +32,7 @@ import * as ReportUtils from '../../../libs/ReportUtils'; import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager'; import participantPropTypes from '../../../components/participantPropTypes'; import ParticipantLocalTime from './ParticipantLocalTime'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../../components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; import {withNetwork, withPersonalDetails} from '../../../components/OnyxProvider'; import * as User from '../../../libs/actions/User'; import Tooltip from '../../../components/Tooltip'; From d7b60759ed93f4d41f5a73788ac6664732bdb714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 14 Oct 2022 20:00:56 +0200 Subject: [PATCH 038/110] move platform code to specific functions --- .../addEmojiToComposerTextInput.js | 46 +++++++++++++++++++ .../index.android.js | 22 +++++++++ src/libs/addEmojiToComposerTextInput/index.js | 3 ++ src/pages/home/report/ReportActionCompose.js | 31 +++++-------- .../report/ReportActionItemMessageEdit.js | 19 ++++---- 5 files changed, 93 insertions(+), 28 deletions(-) create mode 100644 src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js create mode 100644 src/libs/addEmojiToComposerTextInput/index.android.js create mode 100644 src/libs/addEmojiToComposerTextInput/index.js diff --git a/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js b/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js new file mode 100644 index 0000000000000..fe670197a3e16 --- /dev/null +++ b/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js @@ -0,0 +1,46 @@ +/** + * @typedef AddEmojiToComposerTextInputReturnType + * @property {String} newText The new text with the emoji added + * @property {{start: Number, end: Number}} newSelection The new selection after the emoji was added + */ + +/** + * @typedef AddEmojiToComposerTextInputParams + * @property {String} text The text where the emoji should be added + * @property {String} emoji The emoji to add + * @property {Object} textInput + * @property {{start: Number, end: Number}} selection + */ + +/** + * Takes a text and adds an emoji at the place of selection. + * It will then update the text of the given TextInput using its `setTextAndSelection` method. + * `setTextAndSelection` method is usually available on TextInput refs from the composer component. + * + * Note: This is a separate method as for some platforms the update of the TextInput has to be + * handled differently, and the method is used in several places. + * + * @param {AddEmojiToComposerTextInputParams} params + * @return {AddEmojiToComposerTextInputReturnType} + */ +const addEmojiToComposerTextInput = ({ + text, + emoji, + textInput, + selection, +}) => { + const newText = text.slice(0, selection.start) + emoji + text.slice(selection.end, text.length); + const newSelectionStart = selection.start + emoji.length; + const newSelection = { + start: newSelectionStart, + end: newSelectionStart, + }; + + textInput.setTextAndSelection(newText, newSelection.start, newSelection.end); + + return { + newText, + newSelection, + }; +}; +export default addEmojiToComposerTextInput; diff --git a/src/libs/addEmojiToComposerTextInput/index.android.js b/src/libs/addEmojiToComposerTextInput/index.android.js new file mode 100644 index 0000000000000..1e834746a543f --- /dev/null +++ b/src/libs/addEmojiToComposerTextInput/index.android.js @@ -0,0 +1,22 @@ +import addEmojiToComposerTextInput from './addEmojiToComposerTextInput'; + +/** + * Takes a text and adds an emoji at the place of selection. + * It will then update the text of the given TextInput using its `setTextAndSelection` method. + * `setTextAndSelection` method is usually available on TextInput refs from the composer component. + * + * @param {AddEmojiToComposerTextInputParams} params + * @return {AddEmojiToComposerTextInputReturnType} + */ +export default (params) => { + const {prevSelection, textInput} = params; + const hasRangeSelected = prevSelection.start !== prevSelection.end; + if (hasRangeSelected) { + // Android: when we have a range selected setSelection + // won't remove the highlight, so we manually set the cursor + // to a selection range of 0 (so there won't be any selection highlight). + textInput.setSelection(prevSelection.start, prevSelection.start); + } + + return addEmojiToComposerTextInput(params); +}; diff --git a/src/libs/addEmojiToComposerTextInput/index.js b/src/libs/addEmojiToComposerTextInput/index.js new file mode 100644 index 0000000000000..573dbb9433c38 --- /dev/null +++ b/src/libs/addEmojiToComposerTextInput/index.js @@ -0,0 +1,3 @@ +import addEmojiToComposerTextInput from './addEmojiToComposerTextInput'; + +export default addEmojiToComposerTextInput; diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 351b15a64abb6..f7bebc4b05002 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -1,7 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - InteractionManager, Platform, TouchableOpacity, View, + View, + TouchableOpacity, + InteractionManager, } from 'react-native'; import _ from 'underscore'; import lodashGet from 'lodash/get'; @@ -44,6 +46,7 @@ import OfflineIndicator from '../../../components/OfflineIndicator'; import ExceededCommentLength from '../../../components/ExceededCommentLength'; import withNavigationFocus from '../../../components/withNavigationFocus'; import reportPropTypes from '../../reportPropTypes'; +import addEmojiToComposerTextInput from '../../../libs/addEmojiToComposerTextInput'; const propTypes = { /** Beta features list */ @@ -314,24 +317,14 @@ class ReportActionCompose extends React.Component { * @param {String} emoji */ addEmojiToTextBox(emoji) { - const hasRangeSelected = this.selection.start !== this.selection.end; - if (Platform.OS === 'android' && hasRangeSelected) { - // Android: when we have a range selected setTextAndSelection - // won't remove the highlight, so we manually set the cursor - // to a selection range of 0 (so there won't be any selection highlight). - this.textInput.setSelection(this.selection.start, this.selection.start); - } - - const newComment = this.comment.slice(0, this.selection.start) - + emoji + this.comment.slice(this.selection.end, this.comment.length); - const newSelection = this.selection.start + emoji.length; - this.selection = { - start: newSelection, - end: newSelection, - }; - - this.textInput.setTextAndSelection(newComment, this.selection.start, this.selection.end); - this.updateComment(newComment, true); + const {newText, newSelection} = addEmojiToComposerTextInput({ + emoji, + text: this.comment, + textInput: this.textInput, + selection: this.selection, + }); + this.selection = newSelection; + this.updateComment(newText, true); } /** diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index fe0e7ddc3283f..f4bf3ba264a6d 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -19,6 +19,7 @@ import EmojiPickerButton from '../../../components/EmojiPicker/EmojiPickerButton import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; import VirtualKeyboard from '../../../libs/VirtualKeyboard'; import reportPropTypes from '../../reportPropTypes'; +import addEmojiToComposerTextInput from '../../../libs/addEmojiToComposerTextInput'; const propTypes = { /** All the data of the action */ @@ -154,15 +155,15 @@ class ReportActionItemMessageEdit extends React.Component { * @param {String} emoji */ addEmojiToTextBox(emoji) { - const newComment = this.draft.slice(0, this.selection.start) - + emoji + this.draft.slice(this.selection.end, this.draft.length); - this.selection = { - start: this.selection.start + emoji.length, - end: this.selection.start + emoji.length, - }; - - this.textInput.setTextAndSelection(newComment, this.selection.start, this.selection.end); - this.updateDraft(newComment); + const {newText, newSelection} = addEmojiToComposerTextInput({ + emoji, + text: this.draft, + textInput: this.textInput, + selection: this.selection, + }); + + this.selection = newSelection; + this.updateDraft(newText); } /** From c05328861213571ad482bb6cf18123052c89fab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 19 Oct 2022 13:18:38 +0200 Subject: [PATCH 039/110] web: immediately update number of lines when changing text/selection imperatively --- src/components/Composer/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 8c1b6f9142ce1..af147de842114 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -192,6 +192,10 @@ class Composer extends React.Component { setTextAndSelection(text, start, end) { this.textInput.value = text; this.textInput.setSelectionRange(start, end); + + // immediately update number of lines (otherwise we'd wait + // for "onChange" callback which gets called "too late"): + this.updateNumberOfLines(); } /** From 3f8036fe1dc2ebc0c7269c3495e1b191bce73f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 19 Oct 2022 13:24:44 +0200 Subject: [PATCH 040/110] remove `this.initialText` as we have `defaultValue` --- src/components/Composer/index.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index af147de842114..f56e7eff2242c 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -70,7 +70,7 @@ const propTypes = { }; const defaultProps = { - defaultValue: undefined, + defaultValue: '', maxLines: -1, onPasteFile: () => {}, shouldClear: false, @@ -109,13 +109,9 @@ class Composer extends React.Component { constructor(props) { super(props); - this.initialValue = props.defaultValue - ? `${props.defaultValue}` - : `${props.value || ''}`; - this.selection = { - start: this.initialValue.length, - end: this.initialValue.length, + start: props.defaultValue.length, + end: props.defaultValue.length, }; this.state = { @@ -156,7 +152,7 @@ class Composer extends React.Component { this.textInput.addEventListener('paste', this.handlePaste); this.textInput.addEventListener('wheel', this.handleWheel); - this.textInput.value = this.initialValue; + // selection will always be at 0,0 - so we need to update it to be at the end of the defaultValue text this.textInput.setSelectionRange(this.selection.start, this.selection.end); } } @@ -381,7 +377,7 @@ class Composer extends React.Component { render() { const propStyles = StyleSheet.flatten(this.props.style); propStyles.outline = 'none'; - const propsToPass = _.omit(this.props, 'style', 'defaultValue'); + const propsWithoutStyles = _.omit(this.props, 'style'); return ( ); From bc0cd1890edffb8aa91bf982ce842c1f5c1c962d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 19 Oct 2022 13:40:54 +0200 Subject: [PATCH 041/110] only set selection when mounting composer --- src/components/Composer/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index f56e7eff2242c..633997e690c04 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -152,8 +152,10 @@ class Composer extends React.Component { this.textInput.addEventListener('paste', this.handlePaste); this.textInput.addEventListener('wheel', this.handleWheel); - // selection will always be at 0,0 - so we need to update it to be at the end of the defaultValue text - this.textInput.setSelectionRange(this.selection.start, this.selection.end); + // selection will be at start (0,0) - so we need to update it to be at the + // end of the text + const defaultValue = this.props.defaultValue; + this.textInput.setSelectionRange(defaultValue.length, defaultValue.length); } } From 72f6a17fe269cecf11cb28d31acec05d9246269e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 19 Oct 2022 13:43:56 +0200 Subject: [PATCH 042/110] Update src/components/Composer/index.js Co-authored-by: Rajat Parashar --- src/components/Composer/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 633997e690c04..03b98e2252b2b 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -187,6 +187,10 @@ class Composer extends React.Component { this.textInput.removeEventListener('wheel', this.handleWheel); } + /* + * @param {String} text + * @param {Number} start selection start index + * @param {Number} end selection end index setTextAndSelection(text, start, end) { this.textInput.value = text; this.textInput.setSelectionRange(start, end); From 3e23bebeb84e26d4713541787cad09924c19a6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 19 Oct 2022 13:44:20 +0200 Subject: [PATCH 043/110] Update src/CONST.js Co-authored-by: Rajat Parashar --- src/CONST.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CONST.js b/src/CONST.js index 4bd9b73af4069..55f3769c40959 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -739,7 +739,6 @@ const CONST = { EMOJIS: /(?:\uD83D(?:\uDC41\u200D\uD83D\uDDE8|\uDC68\u200D\uD83D[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uDC69\u200D\uD83D\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g, TAX_ID: /^\d{9}$/, NON_NUMERIC: /\D/g, - IS_COMMENT_EMPTY: /^(\s|`)*$/, }, From e2ba4daebbeb080e195a28688181eb331782f608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 19 Oct 2022 13:46:33 +0200 Subject: [PATCH 044/110] fix jsdoc comment --- src/components/Composer/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 03b98e2252b2b..e959cdfa2704a 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -187,10 +187,13 @@ class Composer extends React.Component { this.textInput.removeEventListener('wheel', this.handleWheel); } - /* + /** + * Imperatively set the text and the selection of the text input. + * * @param {String} text * @param {Number} start selection start index * @param {Number} end selection end index + */ setTextAndSelection(text, start, end) { this.textInput.value = text; this.textInput.setSelectionRange(start, end); From 993c32646a73566581cd9bfd41457feb1c344311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 19 Oct 2022 13:48:01 +0200 Subject: [PATCH 045/110] add jsdoc comments --- src/components/Composer/index.native.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index c4018f6ec1e16..1122f00abc060 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -94,11 +94,25 @@ class Composer extends React.Component { this.props.onClear(); } + /** + * Handler for when the text of the text input changes. + * Will also propagate change to parent component, via + * onChangeText prop. + * + * @param {String} text + */ onChangeText(text) { this.setState({value: text}); this.props.onChangeText(text); } + /** + * Imperatively set the text and the selection of the text input. + * + * @param {String} text + * @param {Number} start selection start index + * @param {Number} end selection end index + */ setTextAndSelection(text, start, end) { this.setState({value: text}, () => { this.textInput.setSelection(start, end); From e21808e919e46e2442da719c1b4e87480bfb6700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 19 Oct 2022 13:48:56 +0200 Subject: [PATCH 046/110] removed obsolete check --- src/components/RNTextInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RNTextInput.js b/src/components/RNTextInput.js index 8a16d7adf0362..355ab24844e65 100644 --- a/src/components/RNTextInput.js +++ b/src/components/RNTextInput.js @@ -16,7 +16,7 @@ const defaultProps = { const RNTextInput = props => ( { - if (!_.isFunction(props.forwardedRef) || !ref) { + if (!_.isFunction(props.forwardedRef)) { return; } From e34cc0d9d777a1ea66b51189f765cc420d703ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 19 Oct 2022 13:57:19 +0200 Subject: [PATCH 047/110] code style: use function --- .../addEmojiToComposerTextInput.js | 7 ++++--- src/libs/addEmojiToComposerTextInput/index.android.js | 10 ++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js b/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js index fe670197a3e16..01e329652b81f 100644 --- a/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js +++ b/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js @@ -23,12 +23,12 @@ * @param {AddEmojiToComposerTextInputParams} params * @return {AddEmojiToComposerTextInputReturnType} */ -const addEmojiToComposerTextInput = ({ +function addEmojiToComposerTextInput({ text, emoji, textInput, selection, -}) => { +}) { const newText = text.slice(0, selection.start) + emoji + text.slice(selection.end, text.length); const newSelectionStart = selection.start + emoji.length; const newSelection = { @@ -42,5 +42,6 @@ const addEmojiToComposerTextInput = ({ newText, newSelection, }; -}; +} + export default addEmojiToComposerTextInput; diff --git a/src/libs/addEmojiToComposerTextInput/index.android.js b/src/libs/addEmojiToComposerTextInput/index.android.js index 1e834746a543f..3d5f4a0f9c2ab 100644 --- a/src/libs/addEmojiToComposerTextInput/index.android.js +++ b/src/libs/addEmojiToComposerTextInput/index.android.js @@ -1,4 +1,4 @@ -import addEmojiToComposerTextInput from './addEmojiToComposerTextInput'; +import addEmojiToComposerTextInputImpl from './addEmojiToComposerTextInput'; /** * Takes a text and adds an emoji at the place of selection. @@ -8,7 +8,7 @@ import addEmojiToComposerTextInput from './addEmojiToComposerTextInput'; * @param {AddEmojiToComposerTextInputParams} params * @return {AddEmojiToComposerTextInputReturnType} */ -export default (params) => { +function addEmojiToComposerTextInput(params) { const {prevSelection, textInput} = params; const hasRangeSelected = prevSelection.start !== prevSelection.end; if (hasRangeSelected) { @@ -18,5 +18,7 @@ export default (params) => { textInput.setSelection(prevSelection.start, prevSelection.start); } - return addEmojiToComposerTextInput(params); -}; + return addEmojiToComposerTextInputImpl(params); +} + +export default addEmojiToComposerTextInput; From 265b547e8afdd052688c1641a1f149d322f4a44f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 19 Oct 2022 14:03:29 +0200 Subject: [PATCH 048/110] jsdoc comments --- .../addEmojiToComposerTextInput.js | 23 ++++++------------- .../index.android.js | 9 ++++++-- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js b/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js index 01e329652b81f..b2a2727a87e30 100644 --- a/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js +++ b/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js @@ -1,17 +1,3 @@ -/** - * @typedef AddEmojiToComposerTextInputReturnType - * @property {String} newText The new text with the emoji added - * @property {{start: Number, end: Number}} newSelection The new selection after the emoji was added - */ - -/** - * @typedef AddEmojiToComposerTextInputParams - * @property {String} text The text where the emoji should be added - * @property {String} emoji The emoji to add - * @property {Object} textInput - * @property {{start: Number, end: Number}} selection - */ - /** * Takes a text and adds an emoji at the place of selection. * It will then update the text of the given TextInput using its `setTextAndSelection` method. @@ -20,8 +6,13 @@ * Note: This is a separate method as for some platforms the update of the TextInput has to be * handled differently, and the method is used in several places. * - * @param {AddEmojiToComposerTextInputParams} params - * @return {AddEmojiToComposerTextInputReturnType} + * @param {Object} params + * @param {String} params.text The text where the emoji should be added + * @param {String} params.emoji The emoji to add + * @param {Object} params.textInput + * @param {{start: Number, end: Number}} params.selection + * + * @return {{ newSelection: {start: Number, end: Number}, newText: String }} results */ function addEmojiToComposerTextInput({ text, diff --git a/src/libs/addEmojiToComposerTextInput/index.android.js b/src/libs/addEmojiToComposerTextInput/index.android.js index 3d5f4a0f9c2ab..326c1e517d080 100644 --- a/src/libs/addEmojiToComposerTextInput/index.android.js +++ b/src/libs/addEmojiToComposerTextInput/index.android.js @@ -5,8 +5,13 @@ import addEmojiToComposerTextInputImpl from './addEmojiToComposerTextInput'; * It will then update the text of the given TextInput using its `setTextAndSelection` method. * `setTextAndSelection` method is usually available on TextInput refs from the composer component. * - * @param {AddEmojiToComposerTextInputParams} params - * @return {AddEmojiToComposerTextInputReturnType} + * @param {Object} params + * @param {String} params.text The text where the emoji should be added + * @param {String} params.emoji The emoji to add + * @param {Object} params.textInput + * @param {{start: Number, end: Number}} params.selection + * + * @return {{ newSelection: {start: Number, end: Number}, newText: String }} results */ function addEmojiToComposerTextInput(params) { const {prevSelection, textInput} = params; From 9441915446b9bb2591aa10ad9cf01069293eee5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 20 Oct 2022 12:48:23 +0200 Subject: [PATCH 049/110] fix: using incorrect param name --- src/libs/addEmojiToComposerTextInput/index.android.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/addEmojiToComposerTextInput/index.android.js b/src/libs/addEmojiToComposerTextInput/index.android.js index 326c1e517d080..e20c185b85f7f 100644 --- a/src/libs/addEmojiToComposerTextInput/index.android.js +++ b/src/libs/addEmojiToComposerTextInput/index.android.js @@ -14,13 +14,13 @@ import addEmojiToComposerTextInputImpl from './addEmojiToComposerTextInput'; * @return {{ newSelection: {start: Number, end: Number}, newText: String }} results */ function addEmojiToComposerTextInput(params) { - const {prevSelection, textInput} = params; - const hasRangeSelected = prevSelection.start !== prevSelection.end; + const {selection, textInput} = params; + const hasRangeSelected = selection.start !== selection.end; if (hasRangeSelected) { // Android: when we have a range selected setSelection // won't remove the highlight, so we manually set the cursor // to a selection range of 0 (so there won't be any selection highlight). - textInput.setSelection(prevSelection.start, prevSelection.start); + textInput.setSelection(selection.start, selection.start); } return addEmojiToComposerTextInputImpl(params); From 65f4ed899e4db4b1834a400457f22808628fcf9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 21 Oct 2022 08:13:12 +0200 Subject: [PATCH 050/110] Update src/components/Composer/index.js Co-authored-by: Rajat Parashar --- 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 e959cdfa2704a..f418ab5292d71 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -152,7 +152,7 @@ class Composer extends React.Component { this.textInput.addEventListener('paste', this.handlePaste); this.textInput.addEventListener('wheel', this.handleWheel); - // selection will be at start (0,0) - so we need to update it to be at the + // Selection will be at start (0,0) - so we need to update it to be at the // end of the text const defaultValue = this.props.defaultValue; this.textInput.setSelectionRange(defaultValue.length, defaultValue.length); From 5e63e66c1324f3af117c0ff1cb9039fcc54dca3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 21 Oct 2022 08:26:47 +0200 Subject: [PATCH 051/110] add "replace emoji while typing" functionality back --- src/pages/home/report/ReportActionCompose.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index e2965db602ed7..37fdc08ae4f63 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -383,6 +383,9 @@ class ReportActionCompose extends React.Component { if (this.state.isCommentEmpty !== isCommentEmpty) { this.setState({isCommentEmpty}); } + if (newComment !== comment) { + this.textInput.setTextAndSelection(newComment, this.selection.start, this.selection.end); + } // Indicate that draft has been created. if (this.comment.length === 0 && newComment.length !== 0) { From 3347f39494b58e16df060dac001d24f94d23ffba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 21 Oct 2022 11:46:06 +0200 Subject: [PATCH 052/110] add "replace emoji while typing" functionality back --- src/pages/home/report/ReportActionCompose.js | 7 ++++++- src/pages/home/report/ReportActionItemMessageEdit.js | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 37fdc08ae4f63..311059da3bfe4 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -383,8 +383,13 @@ class ReportActionCompose extends React.Component { if (this.state.isCommentEmpty !== isCommentEmpty) { this.setState({isCommentEmpty}); } + + // When the comment has changed after replacing emojis we need to update the text in the input if (newComment !== comment) { - this.textInput.setTextAndSelection(newComment, this.selection.start, this.selection.end); + const lengthDiff = newComment.length - comment.length; + + // we assume that at the last position of our text a emoji has been added, thus we have to add a offset of 1 + this.textInput.setTextAndSelection(newComment, this.selection.start + lengthDiff + 1, this.selection.end + lengthDiff + 1); } // Indicate that draft has been created. diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 467346e672aad..4e6e0565b72a9 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -97,6 +97,15 @@ class ReportActionItemMessageEdit extends React.Component { */ updateDraft(draft) { const newDraft = EmojiUtils.replaceEmojis(draft); + + // When the draft has changed after replacing emojis we need to update the text in the input + if (newDraft !== draft) { + const lengthDiff = newDraft.length - draft.length; + + // we assume that at the last position of our text a emoji has been added, thus we have to add a offset of 1 + this.textInput.setTextAndSelection(newDraft, this.selection.start + lengthDiff + 1, this.selection.end + lengthDiff + 1); + } + this.draft = newDraft; // This component is rendered only when draft is set to a non-empty string. In order to prevent component From 1539e65f515003faef86b83c88d673a9c6149173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 21 Oct 2022 12:46:06 +0200 Subject: [PATCH 053/110] fix cursor position after adding emoji using :emojiCodeWord: across platforms --- src/libs/EmojiUtils.js | 26 ++++++++++++++++---- src/pages/home/report/ReportActionCompose.js | 10 ++++---- tests/unit/EmojiCodeTest.js | 8 +++++- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/libs/EmojiUtils.js b/src/libs/EmojiUtils.js index 3c2fe3fb8f2c0..98dfa15a17685 100644 --- a/src/libs/EmojiUtils.js +++ b/src/libs/EmojiUtils.js @@ -203,21 +203,37 @@ function addToFrequentlyUsedEmojis(frequentlyUsedEmojis, newEmoji) { /** * Replace any emoji name in a text with the emoji icon * @param {String} text - * @returns {String} + * @returns {{ newText: String, lastReplacedSelection: { start: Number, end: Number, newSelectionEnd: Number } }} */ function replaceEmojis(text) { let newText = text; const emojiData = text.match(CONST.REGEX.EMOJI_NAME); + + const lastReplacedSelection = { + start: 0, + end: 0, + newSelectionEnd: 0, + }; + if (!emojiData || emojiData.length === 0) { - return text; + return {newText, lastReplacedSelection}; } for (let i = 0; i < emojiData.length; i++) { - const checkEmoji = emojisTrie.search(emojiData[i].slice(1, -1)); + const match = emojiData[i]; + const checkEmoji = emojisTrie.search(match.slice(1, -1)); if (checkEmoji && checkEmoji.metaData.code) { - newText = newText.replace(emojiData[i], checkEmoji.metaData.code); + const emojiCode = checkEmoji.metaData.code; + + lastReplacedSelection.start = newText.indexOf(match); + lastReplacedSelection.end = lastReplacedSelection.start + match.length; + lastReplacedSelection.newSelectionEnd = lastReplacedSelection.start + emojiCode.length; + + newText = newText.substr(0, lastReplacedSelection.start) + + emojiCode + + newText.substr(lastReplacedSelection.end); } } - return newText; + return {newText, lastReplacedSelection}; } /** diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 311059da3bfe4..e77c036fdeaf8 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -378,7 +378,9 @@ class ReportActionCompose extends React.Component { * @param {Boolean} shouldDebounceSaveComment */ updateComment(comment, shouldDebounceSaveComment) { - const newComment = EmojiUtils.replaceEmojis(comment); + const emojiReplaceResults = EmojiUtils.replaceEmojis(comment); + const newComment = emojiReplaceResults.newText; + const isCommentEmpty = Boolean(newComment.match(CONST.REGEX.IS_COMMENT_EMPTY)); if (this.state.isCommentEmpty !== isCommentEmpty) { this.setState({isCommentEmpty}); @@ -386,10 +388,8 @@ class ReportActionCompose extends React.Component { // When the comment has changed after replacing emojis we need to update the text in the input if (newComment !== comment) { - const lengthDiff = newComment.length - comment.length; - - // we assume that at the last position of our text a emoji has been added, thus we have to add a offset of 1 - this.textInput.setTextAndSelection(newComment, this.selection.start + lengthDiff + 1, this.selection.end + lengthDiff + 1); + const cursorPosition = emojiReplaceResults.lastReplacedSelection.newSelectionEnd; + this.textInput.setTextAndSelection(newComment, cursorPosition, cursorPosition); } // Indicate that draft has been created. diff --git a/tests/unit/EmojiCodeTest.js b/tests/unit/EmojiCodeTest.js index 6513e68f046fc..6dda646c1efea 100644 --- a/tests/unit/EmojiCodeTest.js +++ b/tests/unit/EmojiCodeTest.js @@ -3,7 +3,13 @@ import * as EmojiUtils from '../../src/libs/EmojiUtils'; describe('EmojiCode', () => { it('Test replacing emoji codes with emojis inside a text', () => { const text = 'Hi :smile:'; - expect(EmojiUtils.replaceEmojis(text)).toBe('Hi 😄'); + const {newText, lastReplacedSelection} = EmojiUtils.replaceEmojis(text); + expect(newText).toBe('Hi 😄'); + expect(lastReplacedSelection).toEqual({ + start: 3, + end: 10, + newEmojiEnd: 5, + }); }); it('Test suggesting emojis when typing emojis prefix after colon', () => { From dfd3011ef93057573ac785a02d40087a9dde6d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 21 Oct 2022 14:08:44 +0200 Subject: [PATCH 054/110] fix(Edit Action Message): cursor position after adding emoji using :emojiCodeWord: across platforms --- src/pages/home/report/ReportActionItemMessageEdit.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 4e6e0565b72a9..b9803d8b92504 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -96,14 +96,13 @@ class ReportActionItemMessageEdit extends React.Component { * @param {String} draft */ updateDraft(draft) { - const newDraft = EmojiUtils.replaceEmojis(draft); + const emojiReplaceResults = EmojiUtils.replaceEmojis(draft); + const newDraft = emojiReplaceResults.newText; // When the draft has changed after replacing emojis we need to update the text in the input if (newDraft !== draft) { - const lengthDiff = newDraft.length - draft.length; - - // we assume that at the last position of our text a emoji has been added, thus we have to add a offset of 1 - this.textInput.setTextAndSelection(newDraft, this.selection.start + lengthDiff + 1, this.selection.end + lengthDiff + 1); + const cursorPosition = emojiReplaceResults.lastReplacedSelection.newSelectionEnd; + this.textInput.setTextAndSelection(newDraft, cursorPosition, cursorPosition); } this.draft = newDraft; From c8def279cadb03efb9de3a241f52f2e8d5fa1946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 21 Oct 2022 14:17:00 +0200 Subject: [PATCH 055/110] rename to `addEmojiToComposer` --- .../baseAddEmojiToComposer.js} | 4 ++-- .../index.android.js | 4 ++-- src/libs/addEmojiToComposer/index.js | 3 +++ src/libs/addEmojiToComposerTextInput/index.js | 3 --- src/pages/home/report/ReportActionCompose.js | 4 ++-- src/pages/home/report/ReportActionItemMessageEdit.js | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) rename src/libs/{addEmojiToComposerTextInput/addEmojiToComposerTextInput.js => addEmojiToComposer/baseAddEmojiToComposer.js} (93%) rename src/libs/{addEmojiToComposerTextInput => addEmojiToComposer}/index.android.js (89%) create mode 100644 src/libs/addEmojiToComposer/index.js delete mode 100644 src/libs/addEmojiToComposerTextInput/index.js diff --git a/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js b/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js similarity index 93% rename from src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js rename to src/libs/addEmojiToComposer/baseAddEmojiToComposer.js index b2a2727a87e30..2ce3ed9a97f6b 100644 --- a/src/libs/addEmojiToComposerTextInput/addEmojiToComposerTextInput.js +++ b/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js @@ -14,7 +14,7 @@ * * @return {{ newSelection: {start: Number, end: Number}, newText: String }} results */ -function addEmojiToComposerTextInput({ +function baseAddEmojiToComposer({ text, emoji, textInput, @@ -35,4 +35,4 @@ function addEmojiToComposerTextInput({ }; } -export default addEmojiToComposerTextInput; +export default baseAddEmojiToComposer; diff --git a/src/libs/addEmojiToComposerTextInput/index.android.js b/src/libs/addEmojiToComposer/index.android.js similarity index 89% rename from src/libs/addEmojiToComposerTextInput/index.android.js rename to src/libs/addEmojiToComposer/index.android.js index e20c185b85f7f..6a1836bcbe612 100644 --- a/src/libs/addEmojiToComposerTextInput/index.android.js +++ b/src/libs/addEmojiToComposer/index.android.js @@ -1,4 +1,4 @@ -import addEmojiToComposerTextInputImpl from './addEmojiToComposerTextInput'; +import baseAddEmojiToComposer from './baseAddEmojiToComposer'; /** * Takes a text and adds an emoji at the place of selection. @@ -23,7 +23,7 @@ function addEmojiToComposerTextInput(params) { textInput.setSelection(selection.start, selection.start); } - return addEmojiToComposerTextInputImpl(params); + return baseAddEmojiToComposer(params); } export default addEmojiToComposerTextInput; diff --git a/src/libs/addEmojiToComposer/index.js b/src/libs/addEmojiToComposer/index.js new file mode 100644 index 0000000000000..9b93411217a86 --- /dev/null +++ b/src/libs/addEmojiToComposer/index.js @@ -0,0 +1,3 @@ +import baseAddEmojiToComposer from './baseAddEmojiToComposer'; + +export default baseAddEmojiToComposer; diff --git a/src/libs/addEmojiToComposerTextInput/index.js b/src/libs/addEmojiToComposerTextInput/index.js deleted file mode 100644 index 573dbb9433c38..0000000000000 --- a/src/libs/addEmojiToComposerTextInput/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import addEmojiToComposerTextInput from './addEmojiToComposerTextInput'; - -export default addEmojiToComposerTextInput; diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index e77c036fdeaf8..801db3cf7d0b9 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -47,7 +47,7 @@ import ExceededCommentLength from '../../../components/ExceededCommentLength'; import withNavigationFocus from '../../../components/withNavigationFocus'; import * as EmojiUtils from '../../../libs/EmojiUtils'; import reportPropTypes from '../../reportPropTypes'; -import addEmojiToComposerTextInput from '../../../libs/addEmojiToComposerTextInput'; +import addEmojiToComposer from '../../../libs/addEmojiToComposer'; const propTypes = { /** Beta features list */ @@ -318,7 +318,7 @@ class ReportActionCompose extends React.Component { * @param {String} emoji */ addEmojiToTextBox(emoji) { - const {newText, newSelection} = addEmojiToComposerTextInput({ + const {newText, newSelection} = addEmojiToComposer({ emoji, text: this.comment, textInput: this.textInput, diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index b9803d8b92504..4ee6bdaf727c2 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -20,7 +20,7 @@ import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu' import VirtualKeyboard from '../../../libs/VirtualKeyboard'; import * as EmojiUtils from '../../../libs/EmojiUtils'; import reportPropTypes from '../../reportPropTypes'; -import addEmojiToComposerTextInput from '../../../libs/addEmojiToComposerTextInput'; +import addEmojiToComposer from '../../../libs/addEmojiToComposer'; const propTypes = { /** All the data of the action */ @@ -165,7 +165,7 @@ class ReportActionItemMessageEdit extends React.Component { * @param {String} emoji */ addEmojiToTextBox(emoji) { - const {newText, newSelection} = addEmojiToComposerTextInput({ + const {newText, newSelection} = addEmojiToComposer({ emoji, text: this.draft, textInput: this.textInput, From bdd0be7af9ca2ca2bb81eeaa65f0362974bac276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 21 Oct 2022 14:21:46 +0200 Subject: [PATCH 056/110] remove unused code --- src/components/Composer/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index f418ab5292d71..a4be21fd06a76 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -109,11 +109,6 @@ class Composer extends React.Component { constructor(props) { super(props); - this.selection = { - start: props.defaultValue.length, - end: props.defaultValue.length, - }; - this.state = { numberOfLines: 1, }; From c9d602687e2ea4fc627da752c24688c375b92f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 25 Oct 2022 09:49:48 +0200 Subject: [PATCH 057/110] Update src/components/Composer/index.js Co-authored-by: Rajat Parashar --- 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 a4be21fd06a76..8222a3cac4108 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -193,7 +193,7 @@ class Composer extends React.Component { this.textInput.value = text; this.textInput.setSelectionRange(start, end); - // immediately update number of lines (otherwise we'd wait + // Immediately update number of lines (otherwise we'd wait // for "onChange" callback which gets called "too late"): this.updateNumberOfLines(); } From adc1161649e08525c4bd7d45078cea4cbb69093d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 28 Oct 2022 11:58:10 +0200 Subject: [PATCH 058/110] fix prop types after merge --- 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 3870d142adb0f..5ae660fdf2c22 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -64,7 +64,7 @@ const propTypes = { setIsFullComposerAvailable: PropTypes.func, /** Whether the composer is full size */ - isComposerFullSize: PropTypes.bool.isRequired, + isComposerFullSize: PropTypes.bool, /** Called when the user changes the text in the input */ onChangeText: PropTypes.func, From 1f8ca7399ef1642ee0b2386ff5a6b22ae6a73e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 28 Oct 2022 12:04:20 +0200 Subject: [PATCH 059/110] fix: clear text --- src/pages/home/report/ReportActionCompose.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 801db3cf7d0b9..ebbb17487bd58 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -456,6 +456,7 @@ class ReportActionCompose extends React.Component { return ''; } + this.textInput.clear(); this.updateComment(''); this.setTextInputShouldClear(true); if (this.props.isComposerFullSize) { From 9a3777272ca9eb61b0ae4d8829f96b28b54bb746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 29 Oct 2022 22:35:16 +0200 Subject: [PATCH 060/110] fix comment exceed --- .../report/ReportActionItemMessageEdit.js | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index df1b1e689f928..ba76d2167d9ce 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -80,9 +80,16 @@ class ReportActionItemMessageEdit extends React.Component { this.selection = { start: draftMessage.length, end: draftMessage.length, - isFocused: false, }; this.draft = draftMessage; + + this.state = { + isFocused: false, + + // if this is undefined it means we haven't exceeded the max comment length + // if it is a number it means we have exceeded the max comment length and the number is the total length + exceededContentLength: this.draft.length > CONST.MAX_COMMENT_LENGTH ? this.draft.length : undefined, + }; } /** @@ -118,6 +125,14 @@ class ReportActionItemMessageEdit extends React.Component { } else { this.debouncedSaveDraft(this.props.action.message[0].html); } + + const hasExceededMaxCommentLength = this.draft.length > CONST.MAX_COMMENT_LENGTH; + const exceededContentLength = hasExceededMaxCommentLength ? this.draft.length : undefined; + if (this.state.exceededContentLength !== exceededContentLength) { + this.setState({ + exceededContentLength, + }); + } } /** @@ -145,7 +160,7 @@ class ReportActionItemMessageEdit extends React.Component { */ publishDraft() { // Do nothing if draft exceed the character limit - if (this.state.draft.length > CONST.MAX_COMMENT_LENGTH) { + if (this.draft.length > CONST.MAX_COMMENT_LENGTH) { return; } @@ -204,7 +219,7 @@ class ReportActionItemMessageEdit extends React.Component { } render() { - const hasExceededMaxCommentLength = this.state.draft.length > CONST.MAX_COMMENT_LENGTH; + const hasExceededMaxCommentLength = this.state.exceededContentLength != null; return ( - + ); From 110797094b1d19f5766625af46c1c656f6a76d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 30 Oct 2022 08:25:16 +0100 Subject: [PATCH 061/110] prevent unnecessary re-renders --- src/pages/home/report/ReportActionCompose.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index ebbb17487bd58..8f012c4e67a4c 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -205,6 +205,9 @@ class ReportActionCompose extends React.Component { } setIsFullComposerAvailable(isFullComposerAvailable) { + if (this.state.isFullComposerAvailable === isFullComposerAvailable) { + return; + } this.setState({isFullComposerAvailable}); } @@ -305,6 +308,9 @@ class ReportActionCompose extends React.Component { if (this.props.isComposerFullSize) { maxLines = CONST.COMPOSER.MAX_LINES_FULL; } + if (this.state.maxLines === maxLines) { + return; + } this.setState({maxLines}); } From 7b05d90bf636c3034b11c6c6eb6b1674d8dc6589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 30 Oct 2022 09:39:19 +0100 Subject: [PATCH 062/110] fix ExceededCommentLength in ReportActionCompose --- src/pages/home/report/ReportActionCompose.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 5b06abc4b3b04..85b809c5a1c87 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -145,6 +145,11 @@ class ReportActionCompose extends React.Component { isMenuVisible: false, maxLines: props.isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES, conciergePlaceholderRandomIndex: _.random(this.props.translate('reportActionCompose.conciergePlaceholderOptions').length - 1), + + // if this is undefined it means we haven't exceeded the max comment length + // if it is a number it means we have exceeded the max comment length and the number is the total length + // we only want to set this value when necessary to avoid re-renders + exceededCommentLength: this.comment.length > CONST.MAX_COMMENT_LENGTH ? this.comment.length : undefined, }; } @@ -417,6 +422,12 @@ class ReportActionCompose extends React.Component { if (newComment) { this.debouncedBroadcastUserIsTyping(); } + + const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH; + const exceededCommentLength = hasExceededMaxCommentLength ? this.comment.length : undefined; + if (this.state.exceededCommentLength !== exceededCommentLength) { + this.setState({exceededCommentLength}); + } } /** @@ -522,7 +533,7 @@ class ReportActionCompose extends React.Component { const isComposeDisabled = this.props.isDrawerOpen && this.props.isSmallScreenWidth; const isBlockedFromConcierge = ReportUtils.chatIncludesConcierge(this.props.report) && User.isBlockedFromConcierge(this.props.blockedFromConcierge); const inputPlaceholder = this.getInputPlaceholder(); - const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH; + const hasExceededMaxCommentLength = this.state.exceededCommentLength != null; return ( {!this.props.isSmallScreenWidth && } - + ); From f403467657513bed5d32ef4a2c7950b4c4acd9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sun, 30 Oct 2022 09:39:41 +0100 Subject: [PATCH 063/110] rename var --- src/pages/home/report/ReportActionItemMessageEdit.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index ba76d2167d9ce..5b21f2e5f2e11 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -88,7 +88,7 @@ class ReportActionItemMessageEdit extends React.Component { // if this is undefined it means we haven't exceeded the max comment length // if it is a number it means we have exceeded the max comment length and the number is the total length - exceededContentLength: this.draft.length > CONST.MAX_COMMENT_LENGTH ? this.draft.length : undefined, + exceededCommentLength: this.draft.length > CONST.MAX_COMMENT_LENGTH ? this.draft.length : undefined, }; } @@ -127,10 +127,10 @@ class ReportActionItemMessageEdit extends React.Component { } const hasExceededMaxCommentLength = this.draft.length > CONST.MAX_COMMENT_LENGTH; - const exceededContentLength = hasExceededMaxCommentLength ? this.draft.length : undefined; - if (this.state.exceededContentLength !== exceededContentLength) { + const exceededCommentLength = hasExceededMaxCommentLength ? this.draft.length : undefined; + if (this.state.exceededCommentLength !== exceededCommentLength) { this.setState({ - exceededContentLength, + exceededCommentLength, }); } } @@ -219,7 +219,7 @@ class ReportActionItemMessageEdit extends React.Component { } render() { - const hasExceededMaxCommentLength = this.state.exceededContentLength != null; + const hasExceededMaxCommentLength = this.state.exceededCommentLength != null; return ( - + ); From bbdecac188e1bd9c7c1424ec56569c5969a02fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 31 Oct 2022 17:58:30 +0100 Subject: [PATCH 064/110] Update src/pages/home/report/ReportActionItemMessageEdit.js Co-authored-by: Rajat Parashar --- src/pages/home/report/ReportActionItemMessageEdit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 5b21f2e5f2e11..549629e6fdc4b 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -86,8 +86,8 @@ class ReportActionItemMessageEdit extends React.Component { this.state = { isFocused: false, - // if this is undefined it means we haven't exceeded the max comment length - // if it is a number it means we have exceeded the max comment length and the number is the total length + // If this is undefined it means we haven't exceeded the max comment length. + // If it is a number it means we have exceeded the max comment length and the number is the total length. exceededCommentLength: this.draft.length > CONST.MAX_COMMENT_LENGTH ? this.draft.length : undefined, }; } From 08eb167b5324dac0ab452ffd2638e1b79d9279c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 31 Oct 2022 17:58:39 +0100 Subject: [PATCH 065/110] Update src/pages/home/report/ReportActionCompose.js Co-authored-by: Rajat Parashar --- src/pages/home/report/ReportActionCompose.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 85b809c5a1c87..097936ea34838 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -146,9 +146,9 @@ class ReportActionCompose extends React.Component { maxLines: props.isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES, conciergePlaceholderRandomIndex: _.random(this.props.translate('reportActionCompose.conciergePlaceholderOptions').length - 1), - // if this is undefined it means we haven't exceeded the max comment length - // if it is a number it means we have exceeded the max comment length and the number is the total length - // we only want to set this value when necessary to avoid re-renders + // If this is undefined it means we haven't exceeded the max comment length. + // If it is a number it means we have exceeded the max comment length and the number is the total length. + // We only want to set this value when necessary to avoid re-renders. exceededCommentLength: this.comment.length > CONST.MAX_COMMENT_LENGTH ? this.comment.length : undefined, }; } From b5237a1ce716fe83a47c6a36dfa095e211f5dc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 2 Nov 2022 22:30:20 +0100 Subject: [PATCH 066/110] fix test --- tests/unit/EmojiTest.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit/EmojiTest.js b/tests/unit/EmojiTest.js index 5a66a57e970b6..f813aeb9f1e05 100644 --- a/tests/unit/EmojiTest.js +++ b/tests/unit/EmojiTest.js @@ -80,7 +80,10 @@ describe('EmojiTest', () => { it('replaces emoji codes with emojis inside a text', () => { const text = 'Hi :smile::wave:'; - expect(EmojiUtils.replaceEmojis(text)).toBe('Hi 😄👋'); + const replacedResults = EmojiUtils.replaceEmojis(text); + expect(replacedResults.newText).toBe('Hi 😄👋'); + expect(replacedResults.lastReplacedSelection.start).toEqual(5); + expect(replacedResults.lastReplacedSelection.end).toEqual(11); }); it('suggests emojis when typing emojis prefix after colon', () => { From 7ac9b0a3537fe2108b202d0b1c92b2ad5580d22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 3 Nov 2022 19:38:40 +0100 Subject: [PATCH 067/110] add emoji with whitespace --- .../baseAddEmojiToComposer.js | 5 +++-- tests/unit/EmojiTest.js | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js b/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js index 2ce3ed9a97f6b..93ee79904baff 100644 --- a/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js +++ b/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js @@ -20,8 +20,9 @@ function baseAddEmojiToComposer({ textInput, selection, }) { - const newText = text.slice(0, selection.start) + emoji + text.slice(selection.end, text.length); - const newSelectionStart = selection.start + emoji.length; + const emojiWithSpace = `${emoji} `; + const newText = text.slice(0, selection.start) + emojiWithSpace + text.slice(selection.end, text.length); + const newSelectionStart = selection.start + emojiWithSpace.length; const newSelection = { start: newSelectionStart, end: newSelectionStart, diff --git a/tests/unit/EmojiTest.js b/tests/unit/EmojiTest.js index f813aeb9f1e05..a922ab7f96149 100644 --- a/tests/unit/EmojiTest.js +++ b/tests/unit/EmojiTest.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import Emoji from '../../assets/emojis'; import * as EmojiUtils from '../../src/libs/EmojiUtils'; +import baseAddEmojiToComposer from '../../src/libs/addEmojiToComposer'; describe('EmojiTest', () => { it('matches all the emojis in the list', () => { @@ -101,4 +102,21 @@ describe('EmojiTest', () => { const text = ':thumb'; expect(EmojiUtils.suggestEmojis(text)).toEqual([{code: '👍', name: '+1'}, {code: '👎', name: '-1'}]); }); + + it('should insert emoji correctly with a whitespace within a text given a selection', () => { + const res = baseAddEmojiToComposer({ + emoji: '😄', + text: 'add emoji here', + textInput: { + setTextAndSelection: jest.fn(), + }, + selection: { + start: 4, + end: 4, + }, + }); + + expect(res.newText).toEqual('add 😄 emoji here'); + expect(res.newSelection).toEqual({start: 7, end: 7}); + }); }); From 239e23f103ac4cec38de43e1fc985ec9cb7ca2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 9 Nov 2022 10:14:24 +0100 Subject: [PATCH 068/110] simplify --- src/components/Composer/index.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 5ae660fdf2c22..693f1d5ad455f 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -151,10 +151,8 @@ class Composer extends React.Component { this.textInput.addEventListener('paste', this.handlePaste); this.textInput.addEventListener('wheel', this.handleWheel); - // Selection will be at start (0,0) - so we need to update it to be at the - // end of the text - const defaultValue = this.props.defaultValue; - this.textInput.setSelectionRange(defaultValue.length, defaultValue.length); + // Selection will be at start (0,0) - so we need to update it to be at the end + this.textInput.setSelectionRange(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); } } From 910ac7b7e2ebf3e7e1eadd1575fcfbbb022c7d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 9 Nov 2022 10:18:58 +0100 Subject: [PATCH 069/110] remove value prop usage --- src/components/Composer/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 693f1d5ad455f..cc0fe217bc978 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -164,8 +164,7 @@ class Composer extends React.Component { this.props.onClear(); } - if (prevProps.value !== this.props.value - || prevProps.defaultValue !== this.props.defaultValue + if (prevProps.defaultValue !== this.props.defaultValue || prevProps.isComposerFullSize !== this.props.isComposerFullSize) { this.updateNumberOfLines(); } @@ -351,7 +350,7 @@ class Composer extends React.Component { * as updateNumberOfLines is already being called when value changes in componentDidUpdate */ shouldCallUpdateNumberOfLines() { - if (!_.isEmpty(this.props.value)) { + if (!_.isEmpty(this.props.defaultValue)) { return; } From ae25dddc7cc9c8efcead853584184f4cc3f76b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 9 Nov 2022 10:27:46 +0100 Subject: [PATCH 070/110] explicit null/undefined check --- src/pages/home/report/ReportActionCompose.js | 2 +- src/pages/home/report/ReportActionItemMessageEdit.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 097936ea34838..d6fdcb1dc9402 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -533,7 +533,7 @@ class ReportActionCompose extends React.Component { const isComposeDisabled = this.props.isDrawerOpen && this.props.isSmallScreenWidth; const isBlockedFromConcierge = ReportUtils.chatIncludesConcierge(this.props.report) && User.isBlockedFromConcierge(this.props.blockedFromConcierge); const inputPlaceholder = this.getInputPlaceholder(); - const hasExceededMaxCommentLength = this.state.exceededCommentLength != null; + const hasExceededMaxCommentLength = this.state.exceededCommentLength !== undefined; return ( Date: Wed, 9 Nov 2022 10:36:40 +0100 Subject: [PATCH 071/110] turn into pure component, remove obsolete checks --- src/pages/home/report/ReportActionCompose.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index d6fdcb1dc9402..041d664efe05c 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -111,7 +111,7 @@ const defaultProps = { ...withCurrentUserPersonalDetailsDefaultProps, }; -class ReportActionCompose extends React.Component { +class ReportActionCompose extends React.PureComponent { constructor(props) { super(props); @@ -210,9 +210,6 @@ class ReportActionCompose extends React.Component { } setIsFullComposerAvailable(isFullComposerAvailable) { - if (this.state.isFullComposerAvailable === isFullComposerAvailable) { - return; - } this.setState({isFullComposerAvailable}); } @@ -313,9 +310,6 @@ class ReportActionCompose extends React.Component { if (this.props.isComposerFullSize) { maxLines = CONST.COMPOSER.MAX_LINES_FULL; } - if (this.state.maxLines === maxLines) { - return; - } this.setState({maxLines}); } From 68af0d32d4cda2833e3bda22c6ed88945cbb5b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 9 Nov 2022 10:47:56 +0100 Subject: [PATCH 072/110] Update src/libs/addEmojiToComposer/baseAddEmojiToComposer.js Co-authored-by: Rory Abraham <47436092+roryabraham@users.noreply.github.com> --- src/libs/addEmojiToComposer/baseAddEmojiToComposer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js b/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js index 93ee79904baff..5a63b59cdc973 100644 --- a/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js +++ b/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js @@ -10,7 +10,9 @@ * @param {String} params.text The text where the emoji should be added * @param {String} params.emoji The emoji to add * @param {Object} params.textInput - * @param {{start: Number, end: Number}} params.selection + * @param {Object} params.selection + * @param {Number} params.selection.start + * @param {Number} params.selection.end * * @return {{ newSelection: {start: Number, end: Number}, newText: String }} results */ From 1e8799d286dbe17dfaed760a014f64a102fe2410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 9 Nov 2022 10:50:55 +0100 Subject: [PATCH 073/110] fix jsdoc --- src/libs/addEmojiToComposer/baseAddEmojiToComposer.js | 2 +- src/libs/addEmojiToComposer/index.android.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js b/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js index 5a63b59cdc973..0e0cae3e3a0a8 100644 --- a/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js +++ b/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js @@ -14,7 +14,7 @@ * @param {Number} params.selection.start * @param {Number} params.selection.end * - * @return {{ newSelection: {start: Number, end: Number}, newText: String }} results + * @return {Object} results */ function baseAddEmojiToComposer({ text, diff --git a/src/libs/addEmojiToComposer/index.android.js b/src/libs/addEmojiToComposer/index.android.js index 6a1836bcbe612..4b80b128d9cf4 100644 --- a/src/libs/addEmojiToComposer/index.android.js +++ b/src/libs/addEmojiToComposer/index.android.js @@ -9,9 +9,11 @@ import baseAddEmojiToComposer from './baseAddEmojiToComposer'; * @param {String} params.text The text where the emoji should be added * @param {String} params.emoji The emoji to add * @param {Object} params.textInput - * @param {{start: Number, end: Number}} params.selection + * @param {Object} params.selection + * @param {Number} params.selection.start + * @param {Number} params.selection.end * - * @return {{ newSelection: {start: Number, end: Number}, newText: String }} results + * @return {Object} results */ function addEmojiToComposerTextInput(params) { const {selection, textInput} = params; From 93a7d644647ec679b7188ad255f1e7ea9db9d4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 16 Nov 2022 13:50:40 +0100 Subject: [PATCH 074/110] remove checks as pure components handles it --- src/pages/home/report/ReportActionCompose.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 76a571b1b90fc..ea45246494f51 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -388,11 +388,6 @@ class ReportActionCompose extends React.PureComponent { const emojiReplaceResults = EmojiUtils.replaceEmojis(comment); const newComment = emojiReplaceResults.newText; - const isCommentEmpty = Boolean(newComment.match(CONST.REGEX.IS_COMMENT_EMPTY)); - if (this.state.isCommentEmpty !== isCommentEmpty) { - this.setState({isCommentEmpty}); - } - // When the comment has changed after replacing emojis we need to update the text in the input if (newComment !== comment) { const cursorPosition = emojiReplaceResults.lastReplacedSelection.newSelectionEnd; @@ -419,11 +414,10 @@ class ReportActionCompose extends React.PureComponent { this.debouncedBroadcastUserIsTyping(); } + const isCommentEmpty = Boolean(newComment.match(CONST.REGEX.IS_COMMENT_EMPTY)); const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH; const exceededCommentLength = hasExceededMaxCommentLength ? this.comment.length : undefined; - if (this.state.exceededCommentLength !== exceededCommentLength) { - this.setState({exceededCommentLength}); - } + this.setState({exceededCommentLength, isCommentEmpty}); } /** From 81a3e8979d69221ac1de7888d3193c6fffaf54e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 16 Nov 2022 22:03:47 +0100 Subject: [PATCH 075/110] Revert "remove checks as pure components handles it" This reverts commit 93a7d644647ec679b7188ad255f1e7ea9db9d4eb. --- src/pages/home/report/ReportActionCompose.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 2e7867a9afe6c..7c0200af38cbd 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -388,6 +388,11 @@ class ReportActionCompose extends React.PureComponent { const emojiReplaceResults = EmojiUtils.replaceEmojis(comment); const newComment = emojiReplaceResults.newText; + const isCommentEmpty = Boolean(newComment.match(CONST.REGEX.IS_COMMENT_EMPTY)); + if (this.state.isCommentEmpty !== isCommentEmpty) { + this.setState({isCommentEmpty}); + } + // When the comment has changed after replacing emojis we need to update the text in the input if (newComment !== comment) { const cursorPosition = emojiReplaceResults.lastReplacedSelection.newSelectionEnd; @@ -414,10 +419,11 @@ class ReportActionCompose extends React.PureComponent { this.debouncedBroadcastUserIsTyping(); } - const isCommentEmpty = Boolean(newComment.match(CONST.REGEX.IS_COMMENT_EMPTY)); const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH; const exceededCommentLength = hasExceededMaxCommentLength ? this.comment.length : undefined; - this.setState({exceededCommentLength, isCommentEmpty}); + if (this.state.exceededCommentLength !== exceededCommentLength) { + this.setState({exceededCommentLength}); + } } /** From 634a38e62562d76b38b26ba20e46ea17e630679d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 16 Nov 2022 22:10:24 +0100 Subject: [PATCH 076/110] fix: web composer height updating too janky --- src/components/Composer/index.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index cc0fe217bc978..925f3a539e6f3 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -122,7 +122,7 @@ class Composer extends React.Component { this.handlePastedHTML = this.handlePastedHTML.bind(this); this.handleWheel = this.handleWheel.bind(this); this.setTextAndSelection = this.setTextAndSelection.bind(this); - this.shouldCallUpdateNumberOfLines = this.shouldCallUpdateNumberOfLines.bind(this); + this.updateNumberOfLines = this.updateNumberOfLines.bind(this); } componentDidMount() { @@ -345,18 +345,6 @@ class Composer extends React.Component { 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.defaultValue)) { - return; - } - - 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. @@ -388,7 +376,7 @@ class Composer extends React.Component { autoComplete="off" placeholderTextColor={themeColors.placeholderText} ref={el => this.textInput = el} - onChange={this.shouldCallUpdateNumberOfLines} + onChange={this.updateNumberOfLines} numberOfLines={this.state.numberOfLines} style={propStyles} /* eslint-disable-next-line react/jsx-props-no-spreading */ From 8f0e0f383c160dcd6095574debd8d75bb89d48fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 16 Nov 2022 22:43:58 +0100 Subject: [PATCH 077/110] Revert "Revert "remove checks as pure components handles it"" This reverts commit 81a3e8979d69221ac1de7888d3193c6fffaf54e5. --- src/pages/home/report/ReportActionCompose.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 7c0200af38cbd..2e7867a9afe6c 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -388,11 +388,6 @@ class ReportActionCompose extends React.PureComponent { const emojiReplaceResults = EmojiUtils.replaceEmojis(comment); const newComment = emojiReplaceResults.newText; - const isCommentEmpty = Boolean(newComment.match(CONST.REGEX.IS_COMMENT_EMPTY)); - if (this.state.isCommentEmpty !== isCommentEmpty) { - this.setState({isCommentEmpty}); - } - // When the comment has changed after replacing emojis we need to update the text in the input if (newComment !== comment) { const cursorPosition = emojiReplaceResults.lastReplacedSelection.newSelectionEnd; @@ -419,11 +414,10 @@ class ReportActionCompose extends React.PureComponent { this.debouncedBroadcastUserIsTyping(); } + const isCommentEmpty = Boolean(newComment.match(CONST.REGEX.IS_COMMENT_EMPTY)); const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH; const exceededCommentLength = hasExceededMaxCommentLength ? this.comment.length : undefined; - if (this.state.exceededCommentLength !== exceededCommentLength) { - this.setState({exceededCommentLength}); - } + this.setState({exceededCommentLength, isCommentEmpty}); } /** From c084b4cdd138f686d158c4875e819209c6542d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 25 Nov 2022 18:21:31 +0100 Subject: [PATCH 078/110] fix: iOS can't insert emojis in succession when selecting a text range fix: iOS can't insert emojis in succession when selecting a text range wip: working approach on native wip: add autofocus back wip: clean CLEAN --- src/components/Composer/index.js | 19 +++++------ src/components/Composer/index.native.js | 17 +++------- .../baseAddEmojiToComposer.js | 2 +- src/pages/home/report/ReportActionCompose.js | 32 +++++++++++++++++-- 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 925f3a539e6f3..3e20a3bb98f38 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -121,7 +121,7 @@ class Composer extends React.Component { this.handlePaste = this.handlePaste.bind(this); this.handlePastedHTML = this.handlePastedHTML.bind(this); this.handleWheel = this.handleWheel.bind(this); - this.setTextAndSelection = this.setTextAndSelection.bind(this); + this.setText = this.setText.bind(this); this.updateNumberOfLines = this.updateNumberOfLines.bind(this); } @@ -137,7 +137,8 @@ class Composer extends React.Component { } if (this.textInput) { - this.textInput.setTextAndSelection = this.setTextAndSelection; + this.textInput.setText = this.setText; + this.textInput.setSelection = this.setSelection; // There is no onPaste or onDrag for TextInput in react-native so we will add event // listeners here and unbind when the component unmounts @@ -183,22 +184,18 @@ class Composer extends React.Component { this.textInput.removeEventListener('wheel', this.handleWheel); } - /** - * Imperatively set the text and the selection of the text input. - * - * @param {String} text - * @param {Number} start selection start index - * @param {Number} end selection end index - */ - setTextAndSelection(text, start, end) { + setText(text, start, end) { this.textInput.value = text; - this.textInput.setSelectionRange(start, end); // Immediately update number of lines (otherwise we'd wait // for "onChange" callback which gets called "too late"): this.updateNumberOfLines(); } + setSelection(start, end) { + this.textInput.setSelectionRange(start, end); + } + /** * Handles all types of drag-N-drop events on the composer * diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index 7d27deb69aced..9ea93ac567709 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -63,7 +63,7 @@ class Composer extends React.Component { super(props); this.onChangeText = this.onChangeText.bind(this); - this.setTextAndSelection = this.setTextAndSelection.bind(this); + this.setText = this.setText.bind(this); this.state = { propStyles: StyleSheet.flatten(this.props.style), @@ -81,7 +81,7 @@ class Composer extends React.Component { } // We want this to be an available method on the ref for parent components - this.textInput.setTextAndSelection = this.setTextAndSelection; + this.textInput.setText = this.setText; this.props.forwardedRef(this.textInput); } @@ -107,17 +107,8 @@ class Composer extends React.Component { this.props.onChangeText(text); } - /** - * Imperatively set the text and the selection of the text input. - * - * @param {String} text - * @param {Number} start selection start index - * @param {Number} end selection end index - */ - setTextAndSelection(text, start, end) { - this.setState({value: text}, () => { - this.textInput.setSelection(start, end); - }); + setText(text) { + this.setState({value: text}); } render() { diff --git a/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js b/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js index 0e0cae3e3a0a8..a90e3b8abdb51 100644 --- a/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js +++ b/src/libs/addEmojiToComposer/baseAddEmojiToComposer.js @@ -30,7 +30,7 @@ function baseAddEmojiToComposer({ end: newSelectionStart, }; - textInput.setTextAndSelection(newText, newSelection.start, newSelection.end); + textInput.setText(newText); return { newText, diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 12c20a7926466..b46f0c3d4ddb2 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -129,14 +129,21 @@ class ReportActionCompose extends React.PureComponent { this.getInputPlaceholder = this.getInputPlaceholder.bind(this); this.getIOUOptions = this.getIOUOptions.bind(this); this.addAttachment = this.addAttachment.bind(this); + this.focusInputAndSetSelection = this.focusInputAndSetSelection.bind(this); this.comment = props.comment; this.shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); + + // This variable will be kept up to date by the text input's onSelectionChange callback this.selection = { start: props.comment.length, end: props.comment.length, }; + // This variable will be set when we insert an emoji using the picker. It will be used + // to set the selection caret behing the inserted emoji. + this.nextSelectionAfterEmojiInsertion = null; + this.state = { isFocused: this.shouldFocusInputOnScreenFocus, isFullComposerAvailable: props.isComposerFullSize, @@ -331,10 +338,31 @@ class ReportActionCompose extends React.PureComponent { textInput: this.textInput, selection: this.selection, }); - this.selection = newSelection; + this.nextSelectionAfterEmojiInsertion = newSelection; this.updateComment(newText, true); } + /** + * This will be called when the emoji picker modal closes. + * Once thats closed we want to focus the text input again and + * set the selection to the new position if an emoji was added. + */ + focusInputAndSetSelection() { + // We first need to focus the input, and then set the selection, as otherwise + // the focus might causes the selection to be set to the end of the text input + this.textInput.focus(); + + if (!this.nextSelectionAfterEmojiInsertion) { + return; + } + + requestAnimationFrame(() => { + this.selection = this.nextSelectionAfterEmojiInsertion; + this.textInput.setSelection(this.selection.start, this.selection.end); + this.nextSelectionAfterEmojiInsertion = null; + }); + } + /** * Focus the composer text input * @param {Boolean} [shouldelay=false] Impose delay before focusing the composer @@ -684,7 +712,7 @@ class ReportActionCompose extends React.PureComponent { {canUseTouchScreen() && this.props.isMediumScreenWidth ? null : ( this.focus(true)} + onModalHide={this.focusInputAndSetSelection} onEmojiSelected={this.addEmojiToTextBox} /> )} From 42e0725e7b66041247bcb7e512a039736a2977dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 25 Nov 2022 19:20:54 +0100 Subject: [PATCH 079/110] fix: android keyboard not opening correctly anymore --- src/pages/home/report/ReportActionCompose.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index b46f0c3d4ddb2..68e196602c5b8 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -350,7 +350,7 @@ class ReportActionCompose extends React.PureComponent { focusInputAndSetSelection() { // We first need to focus the input, and then set the selection, as otherwise // the focus might causes the selection to be set to the end of the text input - this.textInput.focus(); + this.focus(true); if (!this.nextSelectionAfterEmojiInsertion) { return; From 22408b18a7986e2122e7563268166bc790e41833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 25 Nov 2022 19:37:36 +0100 Subject: [PATCH 080/110] fix: only delay on android focus call to avoid UI gliches --- src/pages/home/report/ReportActionCompose.js | 34 +++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 68e196602c5b8..5e4788a008034 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -48,6 +48,7 @@ import withNavigationFocus from '../../../components/withNavigationFocus'; import * as EmojiUtils from '../../../libs/EmojiUtils'; import reportPropTypes from '../../reportPropTypes'; import addEmojiToComposer from '../../../libs/addEmojiToComposer'; +import getOperatingSystem from '../../../libs/getOperatingSystem'; const propTypes = { /** Beta features list */ @@ -348,27 +349,31 @@ class ReportActionCompose extends React.PureComponent { * set the selection to the new position if an emoji was added. */ focusInputAndSetSelection() { + // Only on android we need to delay the focus call to make sure the gets keyboard open + const isAndroid = getOperatingSystem() === CONST.OS.ANDROID; + // We first need to focus the input, and then set the selection, as otherwise // the focus might causes the selection to be set to the end of the text input - this.focus(true); - - if (!this.nextSelectionAfterEmojiInsertion) { - return; - } + this.focus(isAndroid, () => { + if (!this.nextSelectionAfterEmojiInsertion) { + return; + } - requestAnimationFrame(() => { - this.selection = this.nextSelectionAfterEmojiInsertion; - this.textInput.setSelection(this.selection.start, this.selection.end); - this.nextSelectionAfterEmojiInsertion = null; + requestAnimationFrame(() => { + this.selection = this.nextSelectionAfterEmojiInsertion; + this.textInput.setSelection(this.selection.start, this.selection.end); + this.nextSelectionAfterEmojiInsertion = null; + }); }); } /** * Focus the composer text input * @param {Boolean} [shouldelay=false] Impose delay before focusing the composer + * @param {Function} [onDone] Callback to be called after the composer is focused * @memberof ReportActionCompose */ - focus(shouldelay = false) { + focus(shouldelay = false, onDone = () => {}) { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. InteractionManager.runAfterInteractions(() => { @@ -376,14 +381,19 @@ class ReportActionCompose extends React.PureComponent { return; } - if (!shouldelay) { + const focusAndCallback = () => { this.textInput.focus(); + onDone(); + }; + + if (!shouldelay) { + focusAndCallback(); } else { // Keyboard is not opened after Emoji Picker is closed // SetTimeout is used as a workaround // https://github.com/react-native-modal/react-native-modal/issues/114 // We carefully choose a delay. 100ms is found enough for keyboard to open. - setTimeout(() => this.textInput.focus(), 100); + setTimeout(focusAndCallback, 100); } }); } From 0def00f03771e7f7317e5eaf2afa7bc0a309f186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 25 Nov 2022 19:47:34 +0100 Subject: [PATCH 081/110] revert --- src/pages/home/report/ReportActionCompose.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 5e4788a008034..352d6f565633b 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -48,7 +48,6 @@ import withNavigationFocus from '../../../components/withNavigationFocus'; import * as EmojiUtils from '../../../libs/EmojiUtils'; import reportPropTypes from '../../reportPropTypes'; import addEmojiToComposer from '../../../libs/addEmojiToComposer'; -import getOperatingSystem from '../../../libs/getOperatingSystem'; const propTypes = { /** Beta features list */ @@ -339,6 +338,7 @@ class ReportActionCompose extends React.PureComponent { textInput: this.textInput, selection: this.selection, }); + this.selection = newSelection; this.nextSelectionAfterEmojiInsertion = newSelection; this.updateComment(newText, true); } @@ -349,12 +349,9 @@ class ReportActionCompose extends React.PureComponent { * set the selection to the new position if an emoji was added. */ focusInputAndSetSelection() { - // Only on android we need to delay the focus call to make sure the gets keyboard open - const isAndroid = getOperatingSystem() === CONST.OS.ANDROID; - // We first need to focus the input, and then set the selection, as otherwise // the focus might causes the selection to be set to the end of the text input - this.focus(isAndroid, () => { + this.focus(false, () => { if (!this.nextSelectionAfterEmojiInsertion) { return; } From 6305a518ecb90c2e15502ae6749a9af1c5a4cb9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 25 Nov 2022 19:51:59 +0100 Subject: [PATCH 082/110] remove unused params --- 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 3e20a3bb98f38..212b6c2e63b7d 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -184,7 +184,7 @@ class Composer extends React.Component { this.textInput.removeEventListener('wheel', this.handleWheel); } - setText(text, start, end) { + setText(text) { this.textInput.value = text; // Immediately update number of lines (otherwise we'd wait From a971dff77a755e1cead70ff30d32a4ad58250f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 28 Nov 2022 18:20:47 +0100 Subject: [PATCH 083/110] fix platform dependent delay --- src/components/Composer/index.android.js | 35 ++++++++++ src/components/Composer/index.js | 11 ++++ src/components/Composer/index.native.js | 16 ++++- src/pages/home/report/ReportActionCompose.js | 69 ++++++-------------- 4 files changed, 80 insertions(+), 51 deletions(-) create mode 100644 src/components/Composer/index.android.js diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js new file mode 100644 index 0000000000000..ebb915bfe007e --- /dev/null +++ b/src/components/Composer/index.android.js @@ -0,0 +1,35 @@ +import React, {useEffect} from 'react'; +import ComposerNative from './index.native'; + +// Wraps the native composer implementation to add the possibility +// to delay the ref.focus call on android, which is needed for the +// keyboard to open correctly. +export default React.forwardRef((props, forwardedRef) => { + const ref = React.useRef(null); + + // Overwrite the focus function of the native component + // and add the possibility to pass a delay flag. + useEffect(() => { + const originalFocus = ref.current.focus; + ref.current.focus = (onDone, delay) => { + // Keyboard is not opened after Emoji Picker is closed + // SetTimeout is used as a workaround + // https://github.com/react-native-modal/react-native-modal/issues/114 + // We carefully choose a delay. 100ms is found enough for keyboard to open. + setTimeout(() => originalFocus(onDone), delay ? 100 : 0); + }; + if (forwardedRef) { + forwardedRef(ref.current); + } + }, [ref.current]); + + return ( + { + ref.current = refObj; + }} + /> + ); +}); diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 212b6c2e63b7d..4354c32ca6c06 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -123,6 +123,7 @@ class Composer extends React.Component { this.handleWheel = this.handleWheel.bind(this); this.setText = this.setText.bind(this); this.updateNumberOfLines = this.updateNumberOfLines.bind(this); + this.focus = this.focus.bind(this); } componentDidMount() { @@ -139,6 +140,8 @@ class Composer extends React.Component { if (this.textInput) { this.textInput.setText = this.setText; this.textInput.setSelection = this.setSelection; + this.textInput.focusInput = this.textInput.focus; + this.textInput.focus = this.focus; // There is no onPaste or onDrag for TextInput in react-native so we will add event // listeners here and unbind when the component unmounts @@ -185,6 +188,7 @@ class Composer extends React.Component { } setText(text) { + this.textInput.preventDefault(); this.textInput.value = text; // Immediately update number of lines (otherwise we'd wait @@ -364,6 +368,13 @@ class Composer extends React.Component { }); } + focus(onDone) { + setTimeout(() => { + this.textInput.focusInput(); + onDone(); + }, 500); + } + render() { const propStyles = StyleSheet.flatten(this.props.style); propStyles.outline = 'none'; diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index 9ea93ac567709..fc37840cd43c1 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -1,5 +1,5 @@ import React from 'react'; -import {StyleSheet} from 'react-native'; +import {InteractionManager, StyleSheet} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import RNTextInput from '../RNTextInput'; @@ -64,6 +64,7 @@ class Composer extends React.Component { this.onChangeText = this.onChangeText.bind(this); this.setText = this.setText.bind(this); + this.focus = this.focus.bind(this); this.state = { propStyles: StyleSheet.flatten(this.props.style), @@ -82,6 +83,8 @@ class Composer extends React.Component { // We want this to be an available method on the ref for parent components this.textInput.setText = this.setText; + this.textInput.focusInput = this.textInput.focus; + this.textInput.focus = this.focus; this.props.forwardedRef(this.textInput); } @@ -111,6 +114,17 @@ class Composer extends React.Component { this.setState({value: text}); } + focus(onDone) { + // There could be other animations running while we trigger manual focus. + // This prevents focus from making those animations janky. + InteractionManager.runAfterInteractions(() => { + this.textInput.focusInput(); + if (onDone) { + onDone(); + } + }); + } + render() { return ( { - if (!this.nextSelectionAfterEmojiInsertion) { - return; - } - - requestAnimationFrame(() => { - this.selection = this.nextSelectionAfterEmojiInsertion; - this.textInput.setSelection(this.selection.start, this.selection.end); - this.nextSelectionAfterEmojiInsertion = null; - }); - }); - } - - /** - * Focus the composer text input - * @param {Boolean} [shouldelay=false] Impose delay before focusing the composer - * @param {Function} [onDone] Callback to be called after the composer is focused - * @memberof ReportActionCompose - */ - focus(shouldelay = false, onDone = () => {}) { - // There could be other animations running while we trigger manual focus. - // This prevents focus from making those animations janky. - InteractionManager.runAfterInteractions(() => { - if (!this.textInput) { - return; - } - - const focusAndCallback = () => { - this.textInput.focus(); - onDone(); - }; - - if (!shouldelay) { - focusAndCallback(); - } else { - // Keyboard is not opened after Emoji Picker is closed - // SetTimeout is used as a workaround - // https://github.com/react-native-modal/react-native-modal/issues/114 - // We carefully choose a delay. 100ms is found enough for keyboard to open. - setTimeout(focusAndCallback, 100); - } - }); + // the focus might cause the selection to be set to the end of the text input + this.textInput.focus( + () => { + if (!this.nextSelectionAfterEmojiInsertion) { + return; + } + + requestAnimationFrame(() => { + this.selection = this.nextSelectionAfterEmojiInsertion; + this.textInput.setSelection(this.selection.start, this.selection.end); + this.nextSelectionAfterEmojiInsertion = null; + }); + }, + + // run the focus with a delay. Note: Its platform dependent whether there should be a delay or not. + true, + ); } /** From 418ce328e8dd544daa193f9f63142893df917265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 28 Nov 2022 19:08:55 +0100 Subject: [PATCH 084/110] fix focus for any other platform --- src/components/Composer/index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 4354c32ca6c06..b3b6517b3d988 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -124,6 +124,7 @@ class Composer extends React.Component { this.setText = this.setText.bind(this); this.updateNumberOfLines = this.updateNumberOfLines.bind(this); this.focus = this.focus.bind(this); + this.setSelection = this.setSelection.bind(this); } componentDidMount() { @@ -188,7 +189,6 @@ class Composer extends React.Component { } setText(text) { - this.textInput.preventDefault(); this.textInput.value = text; // Immediately update number of lines (otherwise we'd wait @@ -368,11 +368,13 @@ class Composer extends React.Component { }); } - focus(onDone) { + focus(onDone, delay) { setTimeout(() => { this.textInput.focusInput(); - onDone(); - }, 500); + if (onDone) { + onDone(); + } + }, delay ? 100 : 0); } render() { From 1719feeebb688746bb732c45fb84b66e47569cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 28 Nov 2022 19:37:24 +0100 Subject: [PATCH 085/110] simplify --- src/components/Composer/index.android.js | 35 ------------------------ src/components/Composer/index.native.js | 20 ++++++++------ 2 files changed, 11 insertions(+), 44 deletions(-) delete mode 100644 src/components/Composer/index.android.js diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js deleted file mode 100644 index ebb915bfe007e..0000000000000 --- a/src/components/Composer/index.android.js +++ /dev/null @@ -1,35 +0,0 @@ -import React, {useEffect} from 'react'; -import ComposerNative from './index.native'; - -// Wraps the native composer implementation to add the possibility -// to delay the ref.focus call on android, which is needed for the -// keyboard to open correctly. -export default React.forwardRef((props, forwardedRef) => { - const ref = React.useRef(null); - - // Overwrite the focus function of the native component - // and add the possibility to pass a delay flag. - useEffect(() => { - const originalFocus = ref.current.focus; - ref.current.focus = (onDone, delay) => { - // Keyboard is not opened after Emoji Picker is closed - // SetTimeout is used as a workaround - // https://github.com/react-native-modal/react-native-modal/issues/114 - // We carefully choose a delay. 100ms is found enough for keyboard to open. - setTimeout(() => originalFocus(onDone), delay ? 100 : 0); - }; - if (forwardedRef) { - forwardedRef(ref.current); - } - }, [ref.current]); - - return ( - { - ref.current = refObj; - }} - /> - ); -}); diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index fc37840cd43c1..7f4ca4c6c89cd 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -114,15 +114,17 @@ class Composer extends React.Component { this.setState({value: text}); } - focus(onDone) { - // There could be other animations running while we trigger manual focus. - // This prevents focus from making those animations janky. - InteractionManager.runAfterInteractions(() => { - this.textInput.focusInput(); - if (onDone) { - onDone(); - } - }); + focus(onDone, delay) { + setTimeout(() => { + // There could be other animations running while we trigger manual focus. + // This prevents focus from making those animations janky. + InteractionManager.runAfterInteractions(() => { + this.textInput.focusInput(); + if (onDone) { + onDone(); + } + }); + }, delay ? 100 : 0); } render() { From d6752f604b3621be8eb1823b307ae458bbaba03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 28 Nov 2022 19:37:39 +0100 Subject: [PATCH 086/110] fix broken method calls --- src/pages/home/report/ReportActionCompose.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 956d8bce9ccf6..d3632c0919bf6 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -163,7 +163,7 @@ class ReportActionCompose extends React.PureComponent { return; } - this.focus(false); + this.textInput.focus(); }); this.setMaxLines(); this.updateComment(this.comment); @@ -180,7 +180,7 @@ class ReportActionCompose extends React.PureComponent { // open creates a jarring and broken UX. if (this.shouldFocusInputOnScreenFocus && this.props.isFocused && prevProps.modal.isVisible && !this.props.modal.isVisible) { - this.focus(); + this.textInput.focus(); } if (this.props.isComposerFullSize !== prevProps.isComposerFullSize) { From b53f730bf3140432993f6131ae4e27c79513a995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 28 Nov 2022 20:21:57 +0100 Subject: [PATCH 087/110] fix test --- tests/unit/EmojiTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/EmojiTest.js b/tests/unit/EmojiTest.js index 5ea78febafded..52dca77f3043f 100644 --- a/tests/unit/EmojiTest.js +++ b/tests/unit/EmojiTest.js @@ -113,7 +113,7 @@ describe('EmojiTest', () => { emoji: '😄', text: 'add emoji here', textInput: { - setTextAndSelection: jest.fn(), + setText: jest.fn(), }, selection: { start: 4, From 3951535df9af8f96697600ed62f78480614ab273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 30 Nov 2022 11:36:54 +0100 Subject: [PATCH 088/110] JSDoc --- src/components/Composer/index.js | 17 +++++++++++++++++ src/components/Composer/index.native.js | 13 +++++++++++++ src/libs/EmojiUtils.js | 7 ++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 7fbd6a75c4ba9..ea1436fe2a2e3 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -188,6 +188,11 @@ class Composer extends React.Component { this.textInput.removeEventListener('wheel', this.handleWheel); } + /** + * Updates the text of the input. + * Useful when e.g. adding emojis using an emoji picker. + * @param {String} text + */ setText(text) { this.textInput.value = text; @@ -196,6 +201,11 @@ class Composer extends React.Component { this.updateNumberOfLines(); } + /** + * Sets the selection of the input. + * @param {Number} start + * @param {Number} end + */ setSelection(start, end) { this.textInput.setSelectionRange(start, end); } @@ -374,6 +384,13 @@ class Composer extends React.Component { }); } + /** + * Wrapper around the text input's focus method + * with the possibility to add a delay and a callback + * that gets called once the input is focused. + * @param {Function} [onDone] Called once the input is focused + * @param {Boolean} [delay=false] Whether to delay the focus + */ focus(onDone, delay) { setTimeout(() => { this.textInput.focusInput(); diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index 7f4ca4c6c89cd..2ca7ebcf497d3 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -110,10 +110,23 @@ class Composer extends React.Component { this.props.onChangeText(text); } + /** + * Sets the text of the input. + * @param {String} text + */ setText(text) { this.setState({value: text}); } + /** + * Wrapper around the text input's focus method + * with the possibility to add a delay and a callback. + * Note: it always uses the interaction manager to focus + * once any other interactions are done. + * + * @param {Function} [onDone] Called once the input is focused + * @param {Boolean} [delay=false] Whether to delay the focus + */ focus(onDone, delay) { setTimeout(() => { // There could be other animations running while we trigger manual focus. diff --git a/src/libs/EmojiUtils.js b/src/libs/EmojiUtils.js index ff048f651b19b..630cb0661f51f 100644 --- a/src/libs/EmojiUtils.js +++ b/src/libs/EmojiUtils.js @@ -186,7 +186,12 @@ function addToFrequentlyUsedEmojis(frequentlyUsedEmojis, newEmoji) { /** * Replace any emoji name in a text with the emoji icon * @param {String} text - * @returns {{ newText: String, lastReplacedSelection: { start: Number, end: Number, newSelectionEnd: Number } }} + * @returns {Object} results + * @returns {String} results.newText + * @returns {Object} results.lastReplacedSelection + * @returns {Number} results.lastReplacedSelection.start + * @returns {Number} results.lastReplacedSelection.end + * @returns {Number} results.lastReplacedSelection.newSelectionEnd */ function replaceEmojis(text) { let newText = text; From 4b66abf7c91445060323c0a5691672484ef86622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 5 Dec 2022 21:44:08 +0100 Subject: [PATCH 089/110] Update src/pages/home/report/ReportActionCompose.js Co-authored-by: Rajat Parashar --- src/pages/home/report/ReportActionCompose.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index d3632c0919bf6..b25a5dc02b8d3 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -355,7 +355,7 @@ class ReportActionCompose extends React.PureComponent { }); }, - // run the focus with a delay. Note: Its platform dependent whether there should be a delay or not. + // Run the focus with a delay. Note: Its platform dependent whether there should be a delay or not. true, ); } From 249882e61b0fbf8916ca9033edcc766dabdb2889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 6 Dec 2022 18:14:29 +0100 Subject: [PATCH 090/110] fix: ReportActionItemMessageEdit --- src/pages/home/report/ReportActionCompose.js | 4 +-- .../report/ReportActionItemMessageEdit.js | 34 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 082f22d0d58b8..3468a88e7a44d 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -136,7 +136,7 @@ class ReportActionCompose extends React.PureComponent { }; // This variable will be set when we insert an emoji using the picker. It will be used - // to set the selection caret behing the inserted emoji. + // to set the selection caret being the inserted emoji. this.nextSelectionAfterEmojiInsertion = null; this.state = { @@ -362,7 +362,7 @@ class ReportActionCompose extends React.PureComponent { }); }, - // Run the focus with a delay. Note: Its platform dependent whether there should be a delay or not. + // Run the focus with a delay. Note: Its platform dependent whether the delay will be respected or not. true, ); } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 67e832300afbb..ea416a3084b99 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -70,6 +70,7 @@ class ReportActionItemMessageEdit extends React.Component { this.triggerSaveOrCancel = this.triggerSaveOrCancel.bind(this); this.onSelectionChange = this.onSelectionChange.bind(this); this.addEmojiToTextBox = this.addEmojiToTextBox.bind(this); + this.focusInputAndSetSelection = this.focusInputAndSetSelection.bind(this); this.saveButtonID = 'saveButton'; this.cancelButtonID = 'cancelButton'; this.emojiButtonID = 'emojiButton'; @@ -83,6 +84,10 @@ class ReportActionItemMessageEdit extends React.Component { }; this.draft = draftMessage; + // This variable will be set when we insert an emoji using the picker. It will be used + // to set the selection caret being the inserted emoji. + this.nextSelectionAfterEmojiInsertion = null; + this.state = { isFocused: false, @@ -197,6 +202,7 @@ class ReportActionItemMessageEdit extends React.Component { }); this.selection = newSelection; + this.nextSelectionAfterEmojiInsertion = newSelection; this.updateDraft(newText); } @@ -218,6 +224,32 @@ class ReportActionItemMessageEdit extends React.Component { } } + /** + * This will be called when the emoji picker modal closes. + * Once that's closed we want to focus the text input again and + * set the selection to the new position if an emoji was added. + */ + focusInputAndSetSelection() { + // We first need to focus the input, and then set the selection, as otherwise + // the focus might cause the selection to be set to the end of the text input + this.textInput.focus( + () => { + if (!this.nextSelectionAfterEmojiInsertion) { + return; + } + + requestAnimationFrame(() => { + this.selection = this.nextSelectionAfterEmojiInsertion; + this.textInput.setSelection(this.selection.start, this.selection.end); + this.nextSelectionAfterEmojiInsertion = null; + }); + }, + + // Run the focus with a delay. Note: Its platform dependent whether the delay will be respected or not. + true, + ); + } + render() { const hasExceededMaxCommentLength = this.state.exceededCommentLength !== undefined; return ( @@ -259,7 +291,7 @@ class ReportActionItemMessageEdit extends React.Component { InteractionManager.runAfterInteractions(() => this.textInput.focus())} + onModalHide={this.focusInputAndSetSelection} onEmojiSelected={this.addEmojiToTextBox} nativeID={this.emojiButtonID} /> From 13c66d70d3802ace2ed6583ccc39b1f95ecbd983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 9 Dec 2022 11:27:32 +0100 Subject: [PATCH 091/110] lint after merge --- 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 597c170c16fa9..1842f955869de 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -180,6 +180,7 @@ class Composer extends React.Component { setSelection(start, end) { this.textInput.setSelectionRange(start, end); } + /** * Set pasted text to clipboard * @param {String} text From b19f3ea90df70834d5e76b03edc174bd5e7be1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 3 Jan 2023 15:43:12 -0800 Subject: [PATCH 092/110] fix tests --- tests/unit/EmojiTest.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/EmojiTest.js b/tests/unit/EmojiTest.js index 957f81636688c..dee03e772cf5e 100644 --- a/tests/unit/EmojiTest.js +++ b/tests/unit/EmojiTest.js @@ -107,14 +107,14 @@ describe('EmojiTest', () => { expect(EmojiUtils.replaceEmojis(text).newText).toBe('Hi 😄👋no space after last emoji'); }); - it('will not add a space after the last emoji when there is text after it on mobile', () => { + it('will not add a space after the last emoji when there is text after it', () => { const text = 'Hi :smile::wave:no space after last emoji'; - expect(EmojiUtils.replaceEmojis(text, true)).toBe('Hi 😄👋no space after last emoji'); + expect(EmojiUtils.replaceEmojis(text).newText).toBe('Hi 😄👋no space after last emoji'); }); it('will not add a space after the last emoji if we\'re not on mobile', () => { const text = 'Hi :smile:'; - expect(EmojiUtils.replaceEmojis(text)).toBe('Hi 😄'); + expect(EmojiUtils.replaceEmojis(text).newText).toBe('Hi 😄'); }); it('suggests emojis when typing emojis prefix after colon', () => { From fce09dc5d7096cf6f172d75e9e8ee4a2deb8d42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 3 Jan 2023 16:06:45 -0800 Subject: [PATCH 093/110] add correct emoji replacement behaviour back --- src/libs/EmojiUtils.js | 9 ++++++--- src/pages/home/report/ReportActionCompose.js | 2 +- src/pages/home/report/ReportActionItemMessageEdit.js | 2 +- tests/unit/EmojiTest.js | 7 ++++++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/libs/EmojiUtils.js b/src/libs/EmojiUtils.js index ac7fd3cd77c22..421628dc5b600 100644 --- a/src/libs/EmojiUtils.js +++ b/src/libs/EmojiUtils.js @@ -187,7 +187,7 @@ function addToFrequentlyUsedEmojis(frequentlyUsedEmojis, newEmoji) { * Replace any emoji name in a text with the emoji icon. * If we're on mobile, we also add a space after the emoji granted there's no text after it. * @param {String} text - * @param {Boolean} isSmallScreenWidth + * @param {Boolean} addSpaceAfterEmoji * @returns {Object} results * @returns {String} results.newText * @returns {Object} results.lastReplacedSelection @@ -195,7 +195,7 @@ function addToFrequentlyUsedEmojis(frequentlyUsedEmojis, newEmoji) { * @returns {Number} results.lastReplacedSelection.end * @returns {Number} results.lastReplacedSelection.newSelectionEnd */ -function replaceEmojis(text, isSmallScreenWidth = false) { +function replaceEmojis(text, addSpaceAfterEmoji = false) { let newText = text; const emojiData = text.match(CONST.REGEX.EMOJI_NAME); @@ -212,7 +212,10 @@ function replaceEmojis(text, isSmallScreenWidth = false) { const match = emojiData[i]; const checkEmoji = emojisTrie.search(match.slice(1, -1)); if (checkEmoji && checkEmoji.metaData.code) { - const emojiCode = checkEmoji.metaData.code; + let emojiCode = checkEmoji.metaData.code; + if (addSpaceAfterEmoji) { + emojiCode += ' '; + } lastReplacedSelection.start = newText.indexOf(match); lastReplacedSelection.end = lastReplacedSelection.start + match.length; diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 63339ddbb2c4f..1607d8a19f1f3 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -398,7 +398,7 @@ class ReportActionCompose extends React.PureComponent { * @param {Boolean} shouldDebounceSaveComment */ updateComment(comment, shouldDebounceSaveComment) { - const emojiReplaceResults = EmojiUtils.replaceEmojis(comment); + const emojiReplaceResults = EmojiUtils.replaceEmojis(comment, this.props.isSmallScreenWidth); const newComment = emojiReplaceResults.newText; // When the comment has changed after replacing emojis we need to update the text in the input diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index b8553047ea0a0..4143e83171618 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -110,7 +110,7 @@ class ReportActionItemMessageEdit extends React.Component { * @param {String} draft */ updateDraft(draft) { - const emojiReplaceResults = EmojiUtils.replaceEmojis(draft); + const emojiReplaceResults = EmojiUtils.replaceEmojis(draft, this.props.isSmallScreenWidth); const newDraft = emojiReplaceResults.newText; // When the draft has changed after replacing emojis we need to update the text in the input diff --git a/tests/unit/EmojiTest.js b/tests/unit/EmojiTest.js index dee03e772cf5e..400f4f572f7ca 100644 --- a/tests/unit/EmojiTest.js +++ b/tests/unit/EmojiTest.js @@ -114,7 +114,12 @@ describe('EmojiTest', () => { it('will not add a space after the last emoji if we\'re not on mobile', () => { const text = 'Hi :smile:'; - expect(EmojiUtils.replaceEmojis(text).newText).toBe('Hi 😄'); + expect(EmojiUtils.replaceEmojis(text, false).newText).toBe('Hi 😄'); + }); + + it('will add a space after the last emoji if we\'re on mobile', () => { + const text = 'Hi :smile:'; + expect(EmojiUtils.replaceEmojis(text, true).newText).toBe('Hi 😄 '); }); it('suggests emojis when typing emojis prefix after colon', () => { From 4369799ac3dad534e2b8af56e0a9857b2b9249e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 3 Jan 2023 17:28:57 -0800 Subject: [PATCH 094/110] fix inserting emoji by code --- src/pages/home/report/ReportActionCompose.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 1607d8a19f1f3..e6c4af7eafa5b 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -403,8 +403,11 @@ class ReportActionCompose extends React.PureComponent { // When the comment has changed after replacing emojis we need to update the text in the input if (newComment !== comment) { - const cursorPosition = emojiReplaceResults.lastReplacedSelection.newSelectionEnd; - this.textInput.setTextAndSelection(newComment, cursorPosition, cursorPosition); + const newSelection = emojiReplaceResults.lastReplacedSelection; + this.textInput.setText(newComment); + this.selection = newSelection; + this.nextSelectionAfterEmojiInsertion = newSelection; + this.textInput.setSelection(newSelection.newSelectionEnd, newSelection.newSelectionEnd); } // Indicate that draft has been created. From 2e02f07c302c966c2c159b7079ec88fcf167c72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 4 Jan 2023 11:46:54 -0800 Subject: [PATCH 095/110] fix web input inserting emoji doesn't go to cursor --- src/components/Composer/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 725a55adabd72..4f0665ad3410c 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -179,6 +179,8 @@ class Composer extends React.Component { */ setSelection(start, end) { this.textInput.setSelectionRange(start, end); + this.textInput.blur(); + this.textInput.focus(); } /** From a12fde07c49cdb5f56d0a21ab6bed26d0106b792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 4 Jan 2023 12:18:24 -0800 Subject: [PATCH 096/110] prevent flashes --- src/components/Composer/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 4f0665ad3410c..944c731514f81 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -192,6 +192,9 @@ class Composer extends React.Component { document.execCommand('insertText', false, text); this.updateNumberOfLines(); + // Keep the textinput scrolled to the bottom (prevent flashes) + this.textInput.scrollTop = this.textInput.scrollHeight; + // 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(); @@ -312,7 +315,9 @@ class Composer extends React.Component { + parseInt(computedStyle.paddingTop, 10); const numberOfLines = getNumberOfLines(this.props.maxLines, lineHeight, paddingTopAndBottom, this.textInput.scrollHeight); updateIsFullComposerAvailable(this.props, numberOfLines); - this.setState({numberOfLines}); + this.setState({ + numberOfLines, + }); }); } From e83e17d700aac9c3dbb4f845c2b112292b7bad9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 16 Jan 2023 15:09:10 +0100 Subject: [PATCH 097/110] fix web issues by using `value` as state --- src/components/Composer/index.js | 48 ++++++++++++++++---- src/components/Composer/index.native.js | 32 +++++++++++++ src/pages/home/report/ReportActionCompose.js | 25 +++------- 3 files changed, 79 insertions(+), 26 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 059cdec190e84..34eea1d6ab08c 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -98,6 +98,7 @@ class Composer extends React.Component { this.state = { numberOfLines: 1, + value: this.props.defaultValue, }; this.paste = this.paste.bind(this); @@ -107,6 +108,8 @@ class Composer extends React.Component { this.setText = this.setText.bind(this); this.updateNumberOfLines = this.updateNumberOfLines.bind(this); this.focus = this.focus.bind(this); + this.focusAndSetSelection = this.focusAndSetSelection.bind(this); + this.onChangeText = this.onChangeText.bind(this); this.setSelection = this.setSelection.bind(this); this.putSelectionInClipboard = this.putSelectionInClipboard.bind(this); } @@ -129,6 +132,7 @@ class Composer extends React.Component { this.textInput.setSelection = this.setSelection; this.textInput.focusInput = this.textInput.focus; this.textInput.focus = this.focus; + this.textInput.focusAndSetSelection = this.focusAndSetSelection; this.textInput.addEventListener('paste', this.handlePaste); this.textInput.addEventListener('wheel', this.handleWheel); @@ -162,13 +166,25 @@ class Composer extends React.Component { this.textInput.removeEventListener('wheel', this.handleWheel); } + /** + * Handler for when the text of the text input changes. + * Will also propagate change to parent component, via + * onChangeText prop. + * + * @param {String} text + */ + onChangeText(text) { + this.setState({value: text}); + this.props.onChangeText(text); + } + /** * Updates the text of the input. * Useful when e.g. adding emojis using an emoji picker. * @param {String} text */ setText(text) { - this.textInput.value = text; + this.setState({value: text}); // Immediately update number of lines (otherwise we'd wait // for "onChange" callback which gets called "too late"): @@ -182,8 +198,6 @@ class Composer extends React.Component { */ setSelection(start, end) { this.textInput.setSelectionRange(start, end); - this.textInput.blur(); - this.textInput.focus(); } /** @@ -195,9 +209,6 @@ class Composer extends React.Component { document.execCommand('insertText', false, text); this.updateNumberOfLines(); - // Keep the textinput scrolled to the bottom (prevent flashes) - this.textInput.scrollTop = this.textInput.scrollHeight; - // 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(); @@ -356,10 +367,29 @@ class Composer extends React.Component { }, delay ? 100 : 0); } + /** + * Call this when you have lost focus on the text input + * and want to re-focus it, but with a specific selection. + * Usually focus will set the selection to the end of the text. + * @param {Object} selection + * @param {Number} selection.start + * @param {Number} selection.end + * @param {Function} [onDone] Called once the input is focused + * @param {Boolean} [delay=false] Whether to delay the focus + */ + focusAndSetSelection(selection, onDone, delay) { + // On web, we need to set the selection first + // and then immediately focus the input. + if (selection != null) { + this.setSelection(selection.start, selection.end); + } + this.focus(onDone, delay); + } + render() { const propStyles = StyleSheet.flatten(this.props.style); propStyles.outline = 'none'; - const propsWithoutStyles = _.omit(this.props, 'style'); + const propsWithoutStylesAndDefault = _.omit(this.props, ['style', 'defaultValue']); // We're disabling autoCorrect for iOS Safari until Safari fixes this issue. See https://github.com/Expensify/App/issues/8592 return ( @@ -372,8 +402,10 @@ class Composer extends React.Component { numberOfLines={this.state.numberOfLines} style={propStyles} /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...propsWithoutStyles} + {...propsWithoutStylesAndDefault} disabled={this.props.isDisabled} + value={this.state.value} + onChangeText={this.onChangeText} /> ); } diff --git a/src/components/Composer/index.native.js b/src/components/Composer/index.native.js index 2ca7ebcf497d3..4f5530b04f918 100644 --- a/src/components/Composer/index.native.js +++ b/src/components/Composer/index.native.js @@ -65,6 +65,7 @@ class Composer extends React.Component { this.onChangeText = this.onChangeText.bind(this); this.setText = this.setText.bind(this); this.focus = this.focus.bind(this); + this.focusAndSetSelection = this.focusAndSetSelection.bind(this); this.state = { propStyles: StyleSheet.flatten(this.props.style), @@ -85,6 +86,7 @@ class Composer extends React.Component { this.textInput.setText = this.setText; this.textInput.focusInput = this.textInput.focus; this.textInput.focus = this.focus; + this.textInput.focusAndSetSelection = this.focusAndSetSelection; this.props.forwardedRef(this.textInput); } @@ -140,6 +142,36 @@ class Composer extends React.Component { }, delay ? 100 : 0); } + /** + * Call this when you have lost focus on the text input + * and want to re-focus it, but with a specific selection. + * Usually focus will set the selection to the end of the text. + * @param {Object} selection + * @param {Number} selection.start + * @param {Number} selection.end + * @param {Function} [onDone] Called once the input is focused + * @param {Boolean} [delay=false] Whether to delay the focus + */ + focusAndSetSelection(selection, onDone, delay) { + // We first need to focus the input, and then set the selection, as otherwise + // the focus might cause the selection to be set to the end of the text input + this.focus( + () => { + requestAnimationFrame(() => { + if (selection != null) { + this.textInput.setSelection(selection.start, selection.end); + } + if (onDone) { + onDone(); + } + }); + }, + + // Run the focus with a delay. Note: Its platform dependent whether the delay will be respected or not. + delay, + ); + } + render() { return ( { - if (!this.nextSelectionAfterEmojiInsertion) { - return; - } - - requestAnimationFrame(() => { - this.selection = this.nextSelectionAfterEmojiInsertion; - this.textInput.setSelection(this.selection.start, this.selection.end); - this.nextSelectionAfterEmojiInsertion = null; - }); - }, - - // Run the focus with a delay. Note: Its platform dependent whether the delay will be respected or not. - true, - ); + this.textInput.focusAndSetSelection(this.nextSelectionAfterEmojiInsertion, () => { + if (!this.nextSelectionAfterEmojiInsertion) { + return; + } + this.selection = this.nextSelectionAfterEmojiInsertion; + this.nextSelectionAfterEmojiInsertion = null; + }, true); } /** From bd1fdd1a9a7f4510efda098440efce26dd18bc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 16 Jan 2023 15:52:49 +0100 Subject: [PATCH 098/110] fix flashing content when pasting --- src/components/Composer/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 34eea1d6ab08c..88ff20ebed8da 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -209,6 +209,9 @@ class Composer extends React.Component { document.execCommand('insertText', false, text); this.updateNumberOfLines(); + // Keep the textinput scrolled to the bottom (prevent flashes) + this.textInput.scrollTop = this.textInput.scrollHeight; + // 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(); From e8879e79cf2115f9222cb30d7e9e3dc52beeff55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 16 Jan 2023 18:06:40 +0100 Subject: [PATCH 099/110] fix issue where cursor would jump --- src/components/Composer/index.js | 6 ++++-- src/pages/home/report/ReportActionCompose.js | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 88ff20ebed8da..2e88e70aad89d 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -182,9 +182,10 @@ class Composer extends React.Component { * Updates the text of the input. * Useful when e.g. adding emojis using an emoji picker. * @param {String} text + * @param {Function} onDone */ - setText(text) { - this.setState({value: text}); + setText(text, onDone) { + this.setState({value: text}, onDone); // Immediately update number of lines (otherwise we'd wait // for "onChange" callback which gets called "too late"): @@ -211,6 +212,7 @@ class Composer extends React.Component { // Keep the textinput scrolled to the bottom (prevent flashes) this.textInput.scrollTop = this.textInput.scrollHeight; + alert('Pasted text'); // 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(); diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 29ba7617a7bb9..457a0745c74aa 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -393,10 +393,11 @@ class ReportActionCompose extends React.PureComponent { // When the comment has changed after replacing emojis we need to update the text in the input if (newComment !== comment) { const newSelection = emojiReplaceResults.lastReplacedSelection; - this.textInput.setText(newComment); + this.textInput.setText(newComment, () => { + this.textInput.setSelection(newSelection.newSelectionEnd, newSelection.newSelectionEnd); + }); this.selection = newSelection; this.nextSelectionAfterEmojiInsertion = newSelection; - this.textInput.setSelection(newSelection.newSelectionEnd, newSelection.newSelectionEnd); } // Indicate that draft has been created. From 2d0ca3715fe6acb8f3e38663df21014ca15b8057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 16 Jan 2023 18:12:26 +0100 Subject: [PATCH 100/110] remove debug code --- src/components/Composer/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 2e88e70aad89d..095dadc44f103 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -212,7 +212,6 @@ class Composer extends React.Component { // Keep the textinput scrolled to the bottom (prevent flashes) this.textInput.scrollTop = this.textInput.scrollHeight; - alert('Pasted text'); // 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(); From 05fc8812e50119d56e9c21b472d9834be28be91f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 30 Jan 2023 12:35:26 +0100 Subject: [PATCH 101/110] fix(web): doesn't clear input after sending message --- 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 095dadc44f103..bd1b034f5223c 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -147,7 +147,7 @@ class Composer extends React.Component { if (!prevProps.shouldClear && this.props.shouldClear) { this.textInput.clear(); // eslint-disable-next-line react/no-did-update-set-state - this.setState({numberOfLines: 1}); + this.setState({numberOfLines: 1, value: ''}); this.props.onClear(); } From 9746bd1daf56653acbb5c9b7398ba23e9468c51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 30 Jan 2023 16:42:13 +0100 Subject: [PATCH 102/110] fix: pasting image doesn't reset composer --- src/pages/home/report/ReportActionCompose.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 95bf363d81361..a59255b11981f 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -460,9 +460,10 @@ class ReportActionCompose extends React.PureComponent { } /** + * @param {Function} onDone called when all state updates completed * @returns {String} */ - prepareCommentAndResetComposer() { + prepareCommentAndResetComposer(onDone) { const trimmedComment = this.comment.trim(); // Don't submit empty comments or comments that exceed the character limit @@ -476,7 +477,7 @@ class ReportActionCompose extends React.PureComponent { if (this.props.isComposerFullSize) { Report.setIsComposerFullSize(this.props.reportID, false); } - this.setState({isFullComposerAvailable: false}); + this.setState({isFullComposerAvailable: false}, onDone); return trimmedComment; } @@ -486,8 +487,9 @@ class ReportActionCompose extends React.PureComponent { */ addAttachment(file) { const comment = this.prepareCommentAndResetComposer(); - Report.addAttachment(this.props.reportID, file, comment); - this.setTextInputShouldClear(false); + Report.addAttachment(this.props.reportID, file, comment, () => { + this.setTextInputShouldClear(false); + }); } /** From c5e72c0122f248d15c434d2c5fdadfe81b044558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 10 Feb 2023 12:36:23 +0100 Subject: [PATCH 103/110] fix issue after merge --- src/pages/home/report/ReportActionCompose.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index ab0e8807c0657..c117f541a2202 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -733,7 +733,6 @@ class ReportActionCompose extends React.PureComponent { {!this.props.isSmallScreenWidth && } - {this.state.isDraggingOver && } From 124d32f4f08fba4546781b826ac17d439c21ff1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 10 Feb 2023 13:54:51 +0100 Subject: [PATCH 104/110] fix edit draft --- src/pages/home/report/ReportActionItemMessageEdit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 4b8843d146970..ce10dc4ee385a 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -173,7 +173,7 @@ class ReportActionItemMessageEdit extends React.Component { */ publishDraft() { // Do nothing if draft exceed the character limit - if (ReportUtils.getCommentLength(this.state.draft) > CONST.MAX_COMMENT_LENGTH) { + if (ReportUtils.getCommentLength(this.draft) > CONST.MAX_COMMENT_LENGTH) { return; } From 13c6a8f859f53abb945568a5f34688c3431efaa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 5 May 2023 15:15:39 +0200 Subject: [PATCH 105/110] fix issue after merge --- src/pages/home/report/ReportActionCompose.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index a32d9264eed18..813e8e439c607 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -45,11 +45,9 @@ import ExceededCommentLength from '../../../components/ExceededCommentLength'; import withNavigationFocus from '../../../components/withNavigationFocus'; import withNavigation from '../../../components/withNavigation'; import * as EmojiUtils from '../../../libs/EmojiUtils'; -import reportPropTypes from '../../reportPropTypes'; import addEmojiToComposer from '../../../libs/addEmojiToComposer'; import ReportDropUI from './ReportDropUI'; import DragAndDrop from '../../../components/DragAndDrop'; -import reportPropTypes from '../../reportPropTypes'; import EmojiSuggestions from '../../../components/EmojiSuggestions'; import withKeyboardState, {keyboardStatePropTypes} from '../../../components/withKeyboardState'; import ArrowKeyFocusManager from '../../../components/ArrowKeyFocusManager'; From b514461ca2828f453d0cd93599b881479196ae6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 5 May 2023 15:20:45 +0200 Subject: [PATCH 106/110] fix issue after merge --- src/components/Composer/index.js | 2 +- src/pages/home/report/ReportActionCompose.js | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index dc1039602b1a9..df23fd9c7340d 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -437,7 +437,7 @@ class Composer extends React.Component { this.state.numberOfLines < this.props.maxLines ? styles.overflowHidden : {}, ]} /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...propsWithoutStyles} + {...propsWithoutStylesAndDefault} numberOfLines={this.state.numberOfLines} disabled={this.props.isDisabled} onKeyPress={this.handleKeyPress} diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 813e8e439c607..846a85cecdea0 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { View, TouchableOpacity, - InteractionManager, LayoutAnimation, } from 'react-native'; import _ from 'underscore'; @@ -56,6 +55,7 @@ import KeyboardShortcut from '../../../libs/KeyboardShortcut'; import * as ComposerUtils from '../../../libs/ComposerUtils'; import * as Welcome from '../../../libs/actions/Welcome'; import Permissions from '../../../libs/Permissions'; +import reportPropTypes from '../../reportPropTypes'; const propTypes = { /** Beta features list */ @@ -179,7 +179,6 @@ class ReportActionCompose extends React.PureComponent { this.getTaskOption = this.getTaskOption.bind(this); this.addAttachment = this.addAttachment.bind(this); this.insertSelectedEmoji = this.insertSelectedEmoji.bind(this); - this.setExceededMaxCommentLength = this.setExceededMaxCommentLength.bind(this); this.updateNumberOfLines = this.updateNumberOfLines.bind(this); this.showPopoverMenu = this.showPopoverMenu.bind(this); this.focusInputAndSetSelection = this.focusInputAndSetSelection.bind(this); @@ -218,7 +217,6 @@ class ReportActionCompose extends React.PureComponent { shouldShowSuggestionMenu: false, isEmojiPickerLarge: false, composerHeight: 0, - hasExceededMaxCommentLength: false, // If this is undefined it means we haven't exceeded the max comment length. // If it is a number it means we have exceeded the max comment length and the number is the total length. @@ -389,16 +387,6 @@ class ReportActionCompose extends React.PureComponent { return _.map(ReportUtils.getMoneyRequestOptions(this.props.report, reportParticipants, this.props.betas), option => options[option]); } - /** - * Updates the composer when the comment length is exceeded - * Shows red borders and prevents the comment from being sent - * - * @param {Boolean} hasExceededMaxCommentLength - */ - setExceededMaxCommentLength(hasExceededMaxCommentLength) { - this.setState({hasExceededMaxCommentLength}); - } - /** * Set the maximum number of lines for the composer */ @@ -552,6 +540,7 @@ class ReportActionCompose extends React.PureComponent { this.selection = newSelection; this.nextSelectionAfterEmojiInsertion = newSelection; this.updateComment(newText, true); + // TODO: this.updateComment(ComposerUtils.insertText(this.comment, this.state.selection, emoji)); } From a0fd2e89fd20c410708c436ff0a70986b0058de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 5 May 2023 15:42:21 +0200 Subject: [PATCH 107/110] temp: fix warnings --- src/pages/home/report/ReportActionCompose.js | 2 +- .../home/report/ReportActionItemMessageEdit.js | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 846a85cecdea0..caf341515f1c1 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -978,7 +978,7 @@ class ReportActionCompose extends React.PureComponent { > {!this.props.isSmallScreenWidth && } - + {this.state.isDraggingOver && } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index c89b6ab29428f..bcbab27adbc52 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -139,16 +139,6 @@ class ReportActionItemMessageEdit extends React.Component { this.selection = e.nativeEvent.selection; } - /** - * Updates the composer when the comment length is exceeded - * Shows red borders and prevents the comment from being sent - * - * @param {Boolean} hasExceededMaxCommentLength - */ - setExceededMaxCommentLength(hasExceededMaxCommentLength) { - this.setState({hasExceededMaxCommentLength}); - } - /** * Update the value of the draft in Onyx * @@ -257,6 +247,7 @@ class ReportActionItemMessageEdit extends React.Component { this.selection = newSelection; this.nextSelectionAfterEmojiInsertion = newSelection; this.updateDraft(newText); + // TODO: this.updateDraft(ComposerUtils.insertText(this.state.draft, this.state.selection, emoji)); } @@ -401,7 +392,7 @@ class ReportActionItemMessageEdit extends React.Component { - + ); } From d629dcb703a5ed0dc091f52b9f518e7078c37bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 5 May 2023 16:21:32 +0200 Subject: [PATCH 108/110] fix web composer height not updating immediately --- src/components/Composer/index.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index df23fd9c7340d..782cd8d489ded 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -183,14 +183,6 @@ class Composer extends React.Component { this.textInput.removeEventListener('wheel', this.handleWheel); } - // 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); - } - /** * Handler for when the text of the text input changes. * Will also propagate change to parent component, via @@ -201,6 +193,7 @@ class Composer extends React.Component { onChangeText(text) { this.setState({value: text}); this.props.onChangeText(text); + this.updateNumberOfLines(); } /** @@ -226,6 +219,14 @@ class Composer extends React.Component { this.textInput.setSelectionRange(start, end); } + // 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); + } + /** * Set pasted text to clipboard * @param {String} text From b61ad7b852f5b743bf66f6a8eb865682ba1a4fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 5 May 2023 16:57:46 +0200 Subject: [PATCH 109/110] fix web update of input --- src/components/Composer/index.js | 7 ++++--- src/pages/home/report/ReportActionItemMessageEdit.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 782cd8d489ded..57fb4676d84e5 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -389,11 +389,12 @@ class Composer extends React.Component { * @param {Boolean} [delay=false] Whether to delay the focus */ focus(onDone, delay) { + // On web we need to run any effects before the focus + if (onDone) { + onDone(); + } setTimeout(() => { this.textInput.focusInput(); - if (onDone) { - onDone(); - } }, delay ? 100 : 0); } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index bcbab27adbc52..89a024eb7159c 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -365,7 +365,7 @@ class ReportActionItemMessageEdit extends React.Component { InteractionManager.runAfterInteractions(() => this.textInput.focus())} + onModalHide={this.focusInputAndSetSelection} onEmojiSelected={this.addEmojiToTextBox} nativeID={this.emojiButtonID} /> From b195f5bc7568127fb37169a6c620e22bda9d6d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 8 May 2023 10:41:53 +0200 Subject: [PATCH 110/110] fix emoji suggestion --- src/pages/home/report/ReportActionCompose.js | 37 ++++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index caf341515f1c1..4f4549c94c374 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -277,7 +277,7 @@ class ReportActionCompose extends React.PureComponent { // Value state does not have the same value as comment props when the comment gets changed from another tab. // In this case, we should synchronize the value between tabs. - const shouldSyncComment = prevProps.comment !== this.props.comment && this.state.value !== this.props.comment; + const shouldSyncComment = prevProps.comment !== this.props.comment && this.comment !== this.props.comment; // As the report IDs change, make sure to update the composer comment as we need to make sure // we do not show incorrect data in there (ie. draft of message from other report). @@ -452,7 +452,7 @@ class ReportActionCompose extends React.PureComponent { * Calculates and cares about the content of an Emoji Suggester */ calculateEmojiSuggestion() { - if (!this.state.value) { + if (!this.comment) { this.resetSuggestedEmojis(); return; } @@ -460,9 +460,9 @@ class ReportActionCompose extends React.PureComponent { this.setState({shouldBlockEmojiCalc: false}); return; } - const leftString = this.state.value.substring(0, this.state.selection.end); + const leftString = this.comment.substring(0, this.selection.end); const colonIndex = leftString.lastIndexOf(':'); - const isCurrentlyShowingEmojiSuggestion = this.isEmojiCode(this.state.value, this.state.selection.end); + const isCurrentlyShowingEmojiSuggestion = this.isEmojiCode(this.comment, this.selection.end); // the larger composerHeight the less space for EmojiPicker, Pixel 2 has pretty small screen and this value equal 5.3 const hasEnoughSpaceForLargeSuggestion = this.props.windowHeight / this.state.composerHeight >= 6.8; @@ -505,19 +505,27 @@ class ReportActionCompose extends React.PureComponent { * @param {Number} highlightedEmojiIndex */ insertSelectedEmoji(highlightedEmojiIndex) { - const commentBeforeColon = this.state.value.slice(0, this.state.colonIndex); + const commentBeforeColon = this.comment.slice(0, this.state.colonIndex); const emojiObject = this.state.suggestedEmojis[highlightedEmojiIndex]; const emojiCode = emojiObject.types && emojiObject.types[this.props.preferredSkinTone] ? emojiObject.types[this.props.preferredSkinTone] : emojiObject.code; - const commentAfterColonWithEmojiNameRemoved = this.state.value.slice(this.state.selection.end).replace(CONST.REGEX.EMOJI_REPLACER, CONST.SPACE); + const commentAfterColonWithEmojiNameRemoved = this.comment.slice(this.selection.end).replace(CONST.REGEX.EMOJI_REPLACER, CONST.SPACE); - this.updateComment(`${commentBeforeColon}${emojiCode} ${commentAfterColonWithEmojiNameRemoved}`, true); - this.setState(prevState => ({ - selection: { + this.setState((prevState) => { + this.selection = { start: prevState.colonIndex + emojiCode.length + CONST.SPACE_LENGTH, end: prevState.colonIndex + emojiCode.length + CONST.SPACE_LENGTH, - }, - suggestedEmojis: [], - })); + }; + + return { + suggestedEmojis: [], + }; + }, () => { + const newComment = `${commentBeforeColon}${emojiCode} ${commentAfterColonWithEmojiNameRemoved}`; + this.textInput.setText(newComment, () => { + this.textInput.setSelection(this.selection.end, this.selection.end); + }); + this.updateComment(newComment, true); + }); EmojiUtils.addToFrequentlyUsedEmojis(this.props.frequentlyUsedEmojis, emojiObject); } @@ -993,10 +1001,9 @@ class ReportActionCompose extends React.PureComponent { onClose={() => this.setState({suggestedEmojis: []})} highlightedEmojiIndex={this.state.highlightedEmojiIndex} emojis={this.state.suggestedEmojis} - comment={this.state.value} - updateComment={newComment => this.setState({value: newComment})} + comment={this.comment} colonIndex={this.state.colonIndex} - prefix={this.state.value.slice(this.state.colonIndex + 1, this.state.selection.start)} + prefix={this.comment.slice(this.state.colonIndex + 1, this.selection.start)} onSelect={this.insertSelectedEmoji} isComposerFullSize={this.props.isComposerFullSize} preferredSkinToneIndex={this.props.preferredSkinTone}