diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5a5c2bb90562..68266651661a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2360,7 +2360,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.143): + - RNLiveMarkdown (0.1.164): - DoubleConversion - glog - hermes-engine @@ -2380,9 +2380,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.143) + - RNLiveMarkdown/newarch (= 0.1.164) - Yoga - - RNLiveMarkdown/common (0.1.143): + - RNLiveMarkdown/newarch (0.1.164): - DoubleConversion - glog - hermes-engine @@ -3229,7 +3229,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: e44918843c2638692348f39eafc275698baf0444 + RNLiveMarkdown: b2bd97a6f1206be16cf6536c092fe39f986aca34 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 diff --git a/package-lock.json b/package-lock.json index ae7504954072..e08766b5e490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.143", + "@expensify/react-native-live-markdown": "0.1.164", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -3635,9 +3635,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.143", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.143.tgz", - "integrity": "sha512-hZXYjKyTl/b2p7Ig9qhoB7cfVtTTcoE2cWvea8NJT3f5ZYckdyHDAgHI4pg0S0N68jP205Sk5pzqlltZUpZk5w==", + "version": "0.1.164", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.164.tgz", + "integrity": "sha512-x1/Oa+I1AI82xWEFYd2kSkSj4rZ1q2JG4aEDomUHSqcNjuQetQPw9kVFN5DaLHt0Iu0iKEUrXIhy5LpMSHJQLg==", "workspaces": [ "parser", "example", diff --git a/package.json b/package.json index 233d2904032a..b944cd941493 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.143", + "@expensify/react-native-live-markdown": "0.1.164", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 1f63d8a15217..26eb0f960c61 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -2,19 +2,18 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import lodashDebounce from 'lodash/debounce'; import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {flushSync} from 'react-dom'; // eslint-disable-next-line no-restricted-imports -import type {NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData, ViewStyle} from 'react-native'; -import {DeviceEventEmitter, StyleSheet, View} from 'react-native'; +import type {NativeSyntheticEvent, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native'; +import {DeviceEventEmitter, StyleSheet} from 'react-native'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; -import Text from '@components/Text'; import useHtmlPaste from '@hooks/useHtmlPaste'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as Browser from '@libs/Browser'; import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import * as EmojiUtils from '@libs/EmojiUtils'; @@ -23,31 +22,9 @@ import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposit import CONST from '@src/CONST'; import type {ComposerProps} from './types'; -/** - * Retrieves the characters from the specified cursor position up to the next space or new line. - * - * @param inputString - The input string. - * @param cursorPosition - The position of the cursor within the input string. - * @returns - The substring from the cursor position up to the next space or new line. - * If no space or new line is found, returns the substring from the cursor position to the end of the input string. - */ -const getNextChars = (inputString: string, cursorPosition: number): string => { - // Get the substring starting from the cursor position - const subString = inputString.substring(cursorPosition); - - // Find the index of the next space or new line character - const spaceIndex = subString.search(/[ \n]/); - - if (spaceIndex === -1) { - return subString; - } - - // If there is a space or new line, return the substring up to the space or new line - return subString.substring(0, spaceIndex); -}; - const excludeNoStyles: Array = []; const excludeReportMentionStyle: Array = ['mentionReport']; +const imagePreviewAuthRequiredURLs = [CONST.EXPENSIFY_URL, CONST.STAGING_EXPENSIFY_URL]; // Enable Markdown parsing. // On web we like to have the Text Input field always focused so the user can easily type a new chat @@ -83,7 +60,6 @@ function Composer( const styles = useThemeStyles(); const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles); const StyleUtils = useStyleUtils(); - const textRef = useRef(null); const textInput = useRef(null); const [selection, setSelection] = useState< | { @@ -97,9 +73,6 @@ function Composer( start: selectionProp.start, end: selectionProp.end, }); - const [caretContent, setCaretContent] = useState(''); - const [valueBeforeCaret, setValueBeforeCaret] = useState(''); - const [textInputWidth, setTextInputWidth] = useState(''); const [isRendered, setIsRendered] = useState(false); const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); const [prevScroll, setPrevScroll] = useState(); @@ -118,17 +91,25 @@ function Composer( */ const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent) => { const webEvent = event as BaseSyntheticEvent; - if (shouldCalculateCaretPosition && isRendered) { - // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state - flushSync(() => { - setValueBeforeCaret((webEvent.target as HTMLInputElement).value.slice(0, webEvent.nativeEvent.selection.start)); - setCaretContent(getNextChars(value ?? '', webEvent.nativeEvent.selection.start)); - }); + const sel = window.getSelection(); + if (shouldCalculateCaretPosition && isRendered && sel) { + const range = sel.getRangeAt(0).cloneRange(); + range.collapse(true); + const rect = range.getClientRects()[0] || range.startContainer.parentElement?.getClientRects()[0]; + const containerRect = textInput.current?.getBoundingClientRect(); + + let x = 0; + let y = 0; + if (rect && containerRect) { + x = rect.left - containerRect.left; + y = rect.top - containerRect.top + (textInput?.current?.scrollTop ?? 0) - rect.height / 2; + } + const selectionValue = { start: webEvent.nativeEvent.selection.start, end: webEvent.nativeEvent.selection.end, - positionX: (textRef.current?.offsetLeft ?? 0) - CONST.SPACE_CHARACTER_WIDTH, - positionY: textRef.current?.offsetTop, + positionX: x - CONST.SPACE_CHARACTER_WIDTH, + positionY: y, }; onSelectionChange({ @@ -335,26 +316,6 @@ function Composer( [onKeyPress], ); - const renderElementForCaretPosition = ( - - - {`${valueBeforeCaret} `} - - {`${caretContent}`} - - - - ); - const scrollStyleMemo = useMemo(() => { if (shouldContainScroll) { return isScrollBarVisible ? [styles.overflowScroll, styles.overscrollBehaviorContain] : styles.overflowHidden; @@ -377,32 +338,30 @@ function Composer( ); return ( - <> - (textInput.current = el)} - selection={selection} - style={[inputStyleMemo]} - markdownStyle={markdownStyle} - value={value} - defaultValue={defaultValue} - autoFocus={autoFocus} - inputMode={showSoftInputOnFocus ? 'text' : 'none'} - /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...props} - onSelectionChange={addCursorPositionToSelectionChange} - onContentSizeChange={(e) => { - setTextInputWidth(`${e.nativeEvent.contentSize.width}px`); - updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles); - }} - disabled={isDisabled} - onKeyPress={handleKeyPress} - /> - {shouldCalculateCaretPosition && renderElementForCaretPosition} - + (textInput.current = el)} + selection={selection} + style={[inputStyleMemo]} + markdownStyle={markdownStyle} + value={value} + defaultValue={defaultValue} + autoFocus={autoFocus} + inputMode={showSoftInputOnFocus ? 'text' : 'none'} + /* eslint-disable-next-line react/jsx-props-no-spreading */ + {...props} + onSelectionChange={addCursorPositionToSelectionChange} + onContentSizeChange={(e) => { + updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles); + }} + disabled={isDisabled} + onKeyPress={handleKeyPress} + addAuthTokenToImageURLCallback={addEncryptedAuthTokenToURL} + imagePreviewAuthRequiredURLs={imagePreviewAuthRequiredURLs} + /> ); } diff --git a/src/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts index c7e9bf2c0218..2006ca85dd13 100644 --- a/src/hooks/useMarkdownStyle.ts +++ b/src/hooks/useMarkdownStyle.ts @@ -75,6 +75,19 @@ function useMarkdownStyle(message: string | null = null, excludeStyles: Array