diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index ad7a84cc182..f845134acc5 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -445,6 +445,7 @@ function Composer({ StyleSheet.flatten([style, {outline: 'none'}]), StyleUtils.getComposeTextAreaPadding(numberOfLines, isComposerFullSize), + Browser.isMobileSafari() || Browser.isSafari() ? styles.rtlTextRenderForSafari : {}, ], [style, maxLines, numberOfLines, isComposerFullSize], ); diff --git a/src/libs/convertToLTRForComposer/index.android.ts b/src/libs/convertToLTRForComposer/index.android.ts new file mode 100644 index 00000000000..09e7f2e5cd8 --- /dev/null +++ b/src/libs/convertToLTRForComposer/index.android.ts @@ -0,0 +1,8 @@ +import ConvertToLTRForComposer from './types'; + +/** + * 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; +export default convertToLTRForComposer; diff --git a/src/libs/convertToLTRForComposer/index.ts b/src/libs/convertToLTRForComposer/index.ts new file mode 100644 index 00000000000..eb14bfa8c11 --- /dev/null +++ b/src/libs/convertToLTRForComposer/index.ts @@ -0,0 +1,34 @@ +import CONST from '../../CONST'; +import ConvertToLTRForComposer from './types'; + +function hasLTRorRTLCharacters(text: string): boolean { + // Regular expressions to match LTR and RTL character ranges. + // eslint-disable-next-line no-control-regex + const ltrPattern = /[\u0001-\u05FF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/; + const rtlPattern = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/; + + return ltrPattern.test(text) || rtlPattern.test(text); +} + +// Converts a given text to ensure it starts with the LTR (Left-to-Right) marker. +const convertToLTRForComposer: ConvertToLTRForComposer = (text) => { + // Ensure the text contains LTR or RTL characters to avoid an unwanted special character at the beginning, even after a backspace deletion. + if (!hasLTRorRTLCharacters(text)) { + return ''; + } + + // Check if the text contains only spaces. If it does, we do not concatenate it with CONST.UNICODE.LTR, + // as doing so would alter the normal behavior of the input box. + if (/^\s*$/.test(text)) { + return text; + } + + // Check if the text already starts with the LTR marker (if so, return as is). + if (text.startsWith(CONST.UNICODE.LTR)) { + return text; + } + + // Add the LTR marker to the beginning of the text. + return `${CONST.UNICODE.LTR}${text}`; +}; +export default convertToLTRForComposer; diff --git a/src/libs/convertToLTRForComposer/types.ts b/src/libs/convertToLTRForComposer/types.ts new file mode 100644 index 00000000000..c6edeaaba44 --- /dev/null +++ b/src/libs/convertToLTRForComposer/types.ts @@ -0,0 +1,3 @@ +type ConvertToLTRForComposer = (text: string) => string; + +export default ConvertToLTRForComposer; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index e194d087088..a1872bf0b71 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -36,6 +36,7 @@ import focusWithDelay from '../../../../libs/focusWithDelay'; import useDebounce from '../../../../hooks/useDebounce'; import updateMultilineInputRange from '../../../../libs/UpdateMultilineInputRange'; import * as InputFocus from '../../../../libs/actions/InputFocus'; +import convertToLTRForComposer from '../../../../libs/convertToLTRForComposer'; const {RNTextInputReset} = NativeModules; @@ -212,7 +213,6 @@ function ComposerWithSuggestions({ (commentValue, shouldDebounceSaveComment) => { raiseIsScrollLikelyLayoutTriggered(); const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis(commentValue, preferredSkinTone, preferredLocale); - if (!_.isEmpty(emojis)) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (!_.isEmpty(newEmojis)) { @@ -224,9 +224,10 @@ function ComposerWithSuggestions({ debouncedUpdateFrequentlyUsedEmojis(); } } + const newCommentConverted = convertToLTRForComposer(newComment); emojisPresentBefore.current = emojis; - setIsCommentEmpty(!!newComment.match(/^(\s)*$/)); - setValue(newComment); + setIsCommentEmpty(!!newCommentConverted.match(/^(\s)*$/)); + setValue(newCommentConverted); if (commentValue !== newComment) { const remainder = ComposerUtils.getCommonSuffixLength(commentValue, newComment); setSelection({ @@ -236,22 +237,22 @@ function ComposerWithSuggestions({ } // Indicate that draft has been created. - if (commentRef.current.length === 0 && newComment.length !== 0) { + if (commentRef.current.length === 0 && newCommentConverted.length !== 0) { Report.setReportWithDraft(reportID, true); } // The draft has been deleted. - if (newComment.length === 0) { + if (newCommentConverted.length === 0) { Report.setReportWithDraft(reportID, false); } - commentRef.current = newComment; + commentRef.current = newCommentConverted; if (shouldDebounceSaveComment) { - debouncedSaveReportComment(reportID, newComment); + debouncedSaveReportComment(reportID, newCommentConverted); } else { - Report.saveReportComment(reportID, newComment || ''); + Report.saveReportComment(reportID, newCommentConverted || ''); } - if (newComment) { + if (newCommentConverted) { debouncedBroadcastUserIsTyping(reportID); } }, diff --git a/src/styles/styles.ts b/src/styles/styles.ts index 86810a33733..01cf92e5f92 100644 --- a/src/styles/styles.ts +++ b/src/styles/styles.ts @@ -246,6 +246,11 @@ const styles = (theme: ThemeDefault) => alignItems: 'center', }, + rtlTextRenderForSafari: { + textAlign: 'left', + ...writingDirection.ltr, + }, + emojiSuggestionsEmoji: { fontSize: variables.fontSizeMedium, width: 51,