From f59155c99138f142aecb8e13116856bcdc8ec669 Mon Sep 17 00:00:00 2001 From: Samil Abud Date: Thu, 28 Dec 2023 13:55:56 -0400 Subject: [PATCH 1/4] Fixed regression Android - Chat - Message gets displayed from right to left --- .../convertToLTRForComposer/index.android.ts | 64 ++++++++++++++++++- src/libs/convertToLTRForComposer/index.ts | 4 ++ .../ComposerWithSuggestions.js | 36 +++++++---- 3 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/libs/convertToLTRForComposer/index.android.ts b/src/libs/convertToLTRForComposer/index.android.ts index 09e7f2e5cd87..f060bd8a9192 100644 --- a/src/libs/convertToLTRForComposer/index.android.ts +++ b/src/libs/convertToLTRForComposer/index.android.ts @@ -1,8 +1,70 @@ +import CONST from '@src/CONST'; import ConvertToLTRForComposer from './types'; +/** + * Android only - The composer can be converted to LTR if its content is the LTR character followed by an @ or space + * because to mention sugggestion works the @ character must not have any character at the beginning e.g.: \u2066@ doesn't work + * also to avoid sending empty messages the unicode character with space could enable the send button. + */ +function canComposerBeConvertedToLTR(text: string): boolean { + // This regex handles the case when a user only types spaces into the composer. + const containOnlySpaces = /^\s*$/; + // This regex handles the case where someone has RTL enabled and they began typing an @mention for someone. + const startsWithLTRAndAt = new RegExp(`^${CONST.UNICODE.LTR}@$`); + const startsWithAt = new RegExp(`^@$`); + // This regex handles the case where the composer can contain multiple lines of whitespace + const startsWithLTRAndSpace = new RegExp(`${CONST.UNICODE.LTR}\\s*$`); + const emptyExpressions = [containOnlySpaces, startsWithLTRAndAt, startsWithAt, startsWithLTRAndSpace]; + return emptyExpressions.some((exp) => exp.test(text)); +} + +/** + * Android only - We should remove the LTR unicode when the input is empty to prevent: + * Sending an empty message; + * Mention suggestions not works if @ or \s (at or space) is the first character; + * Placeholder is not displayed if the unicode character is the only character remaining; + * + * @param {String} newComment - the comment written by the user + * @param {Boolean} force - always remove the LTR unicode, going to be used when composer is consider as empty + * @return {String} + */ + +const resetLTRWhenEmpty = (newComment: string, force?: boolean) => { + const result = newComment.length <= 1 || force ? newComment.replaceAll(CONST.UNICODE.LTR, '') : newComment; + return result; +}; + /** * Android only - Do not convert RTL text to a LTR text for input box using Unicode controls. * Android does not properly support bidirectional text for mixed content for input box */ -const convertToLTRForComposer: ConvertToLTRForComposer = (text) => text; +const convertToLTRForComposer: ConvertToLTRForComposer = (text) => { + const shouldComposerMaintainAsLTR = canComposerBeConvertedToLTR(text); + const newText = resetLTRWhenEmpty(text, shouldComposerMaintainAsLTR); + if (shouldComposerMaintainAsLTR) { + return newText; + } + return `${CONST.UNICODE.LTR}${newText}`; +}; + +/** + * This is necessary to convert the input to LTR, there is a delay that causes the cursor not to go to the end of the input line when pasting text or typing fast. The delay is caused for the time that takes the input to convert from RTL to LTR and viceversa. + */ +const moveCursorToEndOfLine = ( + commentLength: number, + setSelection: ( + value: React.SetStateAction<{ + start: number; + end: number; + }>, + ) => void, +) => { + setSelection({ + start: commentLength + 1, + end: commentLength + 1, + }); +}; + +export {moveCursorToEndOfLine}; + export default convertToLTRForComposer; diff --git a/src/libs/convertToLTRForComposer/index.ts b/src/libs/convertToLTRForComposer/index.ts index dd6ee50d862e..771e303cdc38 100644 --- a/src/libs/convertToLTRForComposer/index.ts +++ b/src/libs/convertToLTRForComposer/index.ts @@ -7,6 +7,10 @@ function hasRTLCharacters(text: string): boolean { return rtlPattern.test(text); } +const moveCursorToEndOfLine = (commentLength: number) => commentLength; + +export {moveCursorToEndOfLine}; + // Converts a given text to ensure it starts with the LTR (Left-to-Right) marker. const convertToLTRForComposer: ConvertToLTRForComposer = (text) => { // Ensure that the text starts with RTL characters if not we return the same text to avoid concatination with special diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 0edee4b48241..3d1af292a6df 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -18,7 +18,7 @@ import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import compose from '@libs/compose'; import * as ComposerUtils from '@libs/ComposerUtils'; import getDraftComment from '@libs/ComposerUtils/getDraftComment'; -import convertToLTRForComposer from '@libs/convertToLTRForComposer'; +import convertToLTRForComposer, {moveCursorToEndOfLine} from '@libs/convertToLTRForComposer'; import * as EmojiUtils from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import getPlatform from '@libs/getPlatform'; @@ -111,6 +111,7 @@ function ComposerWithSuggestions({ const StyleUtils = useStyleUtils(); const {preferredLocale} = useLocalize(); const isFocused = useIsFocused(); + const composerIsEmpty = useRef(true); const navigation = useNavigation(); const emojisPresentBefore = useRef([]); const [value, setValue] = useState(() => { @@ -228,16 +229,29 @@ function ComposerWithSuggestions({ debouncedUpdateFrequentlyUsedEmojis(); } } - const newCommentConverted = convertToLTRForComposer(newComment); - const isNewCommentEmpty = !!newCommentConverted.match(/^(\s)*$/); - const isPrevCommentEmpty = !!commentRef.current.match(/^(\s)*$/); + let newCommentConvertedToLTR = newComment; + const prevComment = commentRef.current; + + // This prevent the double execution of setting input value that could affect the place holder and could send an empty message or draft messages in android + if (prevComment !== newComment) { + newCommentConvertedToLTR = convertToLTRForComposer(newCommentConvertedToLTR); + setValue(newCommentConvertedToLTR); + moveCursorToEndOfLine(newComment.length, setSelection); + composerIsEmpty.current = false; + } + + const isNewCommentEmpty = !!newCommentConvertedToLTR.match(/^(\s)*$/); + const isPrevCommentEmpty = !!prevComment.match(/^(\s)*$/); /** Only update isCommentEmpty state if it's different from previous one */ if (isNewCommentEmpty !== isPrevCommentEmpty) { setIsCommentEmpty(isNewCommentEmpty); + if (isNewCommentEmpty) { + composerIsEmpty.current = true; + } } emojisPresentBefore.current = emojis; - setValue(newCommentConverted); + if (commentValue !== newComment) { const position = Math.max(selection.end + (newComment.length - commentRef.current.length), cursorPosition || 0); @@ -252,22 +266,22 @@ function ComposerWithSuggestions({ } // Indicate that draft has been created. - if (commentRef.current.length === 0 && newCommentConverted.length !== 0) { + if (commentRef.current.length === 0 && newCommentConvertedToLTR.length !== 0) { Report.setReportWithDraft(reportID, true); } // The draft has been deleted. - if (newCommentConverted.length === 0) { + if (newCommentConvertedToLTR.length === 0) { Report.setReportWithDraft(reportID, false); } - commentRef.current = newCommentConverted; + commentRef.current = newCommentConvertedToLTR; if (shouldDebounceSaveComment) { - debouncedSaveReportComment(reportID, newCommentConverted); + debouncedSaveReportComment(reportID, newCommentConvertedToLTR); } else { - Report.saveReportComment(reportID, newCommentConverted || ''); + Report.saveReportComment(reportID, newCommentConvertedToLTR || ''); } - if (newCommentConverted) { + if (newCommentConvertedToLTR) { debouncedBroadcastUserIsTyping(reportID); } }, From 7180981bf99f6db81f6e63aeadca2d7cb3169aab Mon Sep 17 00:00:00 2001 From: Samil Abud Date: Sat, 13 Jan 2024 20:38:07 -0400 Subject: [PATCH 2/4] Fixed the regex so that the composer works with searches when mentioning(@) --- src/libs/convertToLTRForComposer/index.android.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/convertToLTRForComposer/index.android.ts b/src/libs/convertToLTRForComposer/index.android.ts index f1dd1251f8fd..316229ffdded 100644 --- a/src/libs/convertToLTRForComposer/index.android.ts +++ b/src/libs/convertToLTRForComposer/index.android.ts @@ -11,7 +11,7 @@ function canComposerBeConvertedToLTR(text: string): boolean { const containOnlySpaces = /^\s*$/; // This regex handles the case where someone has RTL enabled and they began typing an @mention for someone. const startsWithLTRAndAt = new RegExp(`^${CONST.UNICODE.LTR}@$`); - const startsWithAt = new RegExp(`^@$`); + const startsWithAt = new RegExp(`^@`); // This regex handles the case where the composer can contain multiple lines of whitespace const startsWithLTRAndSpace = new RegExp(`${CONST.UNICODE.LTR}\\s*$`); const emptyExpressions = [containOnlySpaces, startsWithLTRAndAt, startsWithAt, startsWithLTRAndSpace]; From cb5ef976e786e0eaa86f3ba7c1e08ea1218fa1f2 Mon Sep 17 00:00:00 2001 From: Samil Abud Date: Wed, 24 Jan 2024 21:57:08 -0400 Subject: [PATCH 3/4] Added colon to the list of allowed characters --- src/libs/convertToLTRForComposer/index.android.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/convertToLTRForComposer/index.android.ts b/src/libs/convertToLTRForComposer/index.android.ts index 316229ffdded..5b7f7e74803c 100644 --- a/src/libs/convertToLTRForComposer/index.android.ts +++ b/src/libs/convertToLTRForComposer/index.android.ts @@ -12,9 +12,10 @@ function canComposerBeConvertedToLTR(text: string): boolean { // This regex handles the case where someone has RTL enabled and they began typing an @mention for someone. const startsWithLTRAndAt = new RegExp(`^${CONST.UNICODE.LTR}@$`); const startsWithAt = new RegExp(`^@`); + const startWithSmile = new RegExp('^:'); // This regex handles the case where the composer can contain multiple lines of whitespace const startsWithLTRAndSpace = new RegExp(`${CONST.UNICODE.LTR}\\s*$`); - const emptyExpressions = [containOnlySpaces, startsWithLTRAndAt, startsWithAt, startsWithLTRAndSpace]; + const emptyExpressions = [containOnlySpaces, startsWithLTRAndAt, startsWithAt, startsWithLTRAndSpace, startWithSmile]; return emptyExpressions.some((exp) => exp.test(text)); } From 0bc9ce27064748a0ed6ac056f489f7c995f61edc Mon Sep 17 00:00:00 2001 From: Samil Abud Date: Wed, 31 Jan 2024 23:12:16 -0400 Subject: [PATCH 4/4] Fixed regex to tag and add smiles --- src/libs/convertToLTRForComposer/index.android.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/convertToLTRForComposer/index.android.ts b/src/libs/convertToLTRForComposer/index.android.ts index 5b7f7e74803c..b6cb6d40377a 100644 --- a/src/libs/convertToLTRForComposer/index.android.ts +++ b/src/libs/convertToLTRForComposer/index.android.ts @@ -11,11 +11,11 @@ function canComposerBeConvertedToLTR(text: string): boolean { const containOnlySpaces = /^\s*$/; // This regex handles the case where someone has RTL enabled and they began typing an @mention for someone. const startsWithLTRAndAt = new RegExp(`^${CONST.UNICODE.LTR}@$`); - const startsWithAt = new RegExp(`^@`); - const startWithSmile = new RegExp('^:'); + const containsAt = new RegExp(`@`); + const containSmile = new RegExp(':'); // This regex handles the case where the composer can contain multiple lines of whitespace const startsWithLTRAndSpace = new RegExp(`${CONST.UNICODE.LTR}\\s*$`); - const emptyExpressions = [containOnlySpaces, startsWithLTRAndAt, startsWithAt, startsWithLTRAndSpace, startWithSmile]; + const emptyExpressions = [containOnlySpaces, startsWithLTRAndAt, containsAt, startsWithLTRAndSpace, containSmile]; return emptyExpressions.some((exp) => exp.test(text)); }