diff --git a/src/components/RNMarkdownTextInput.tsx b/src/components/RNMarkdownTextInput.tsx index e8ed0256bf0a..faed9b462682 100644 --- a/src/components/RNMarkdownTextInput.tsx +++ b/src/components/RNMarkdownTextInput.tsx @@ -1,8 +1,9 @@ -import type {MarkdownTextInputProps} from '@expensify/react-native-live-markdown'; +import type {MarkdownRange, MarkdownTextInputProps} from '@expensify/react-native-live-markdown'; import {MarkdownTextInput, parseExpensiMark} from '@expensify/react-native-live-markdown'; import type {ForwardedRef} from 'react'; -import React from 'react'; +import React, {forwardRef, useCallback} from 'react'; import Animated from 'react-native-reanimated'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useTheme from '@hooks/useTheme'; import CONST from '@src/CONST'; @@ -11,17 +12,62 @@ const AnimatedMarkdownTextInput = Animated.createAnimatedComponent(MarkdownTextI type AnimatedMarkdownTextInputRef = typeof AnimatedMarkdownTextInput & MarkdownTextInput & HTMLInputElement; -type RNMarkdownTextInputProps = Omit; +// Make the parser prop optional for this component because we are always defaulting to `parseExpensiMark` +type RNMarkdownTextInputWithRefProps = Omit & { + parser?: MarkdownTextInputProps['parser']; +}; -function RNMarkdownTextInputWithRef({maxLength, ...props}: RNMarkdownTextInputProps, ref: ForwardedRef) { +function decorateRangesWithCurrentUser(ranges: MarkdownRange[], text: string, currentUser: string): MarkdownRange[] { + 'worklet'; + + return ranges.map((range) => { + if (range.type === 'mention-user') { + const mentionText = text.slice(range.start, range.start + range.length); + const isCurrentUser = mentionText === `@${currentUser}`; + if (isCurrentUser) { + return { + ...range, + type: 'mention-here', + }; + } + } + + return range; + }); +} + +function RNMarkdownTextInputWithRef({maxLength, ...props}: RNMarkdownTextInputWithRefProps, ref: ForwardedRef) { const theme = useTheme(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + + const {parser, ...restProps} = props; + const currentUserLogin = currentUserPersonalDetails.login; + + // We accept parser passed down as an argument or use expensiMark + const parserFunction = useCallback( + (text: string) => { + 'worklet'; + + if (parser) { + return parser(text); + } + + const parsedMentions = parseExpensiMark(text); + if (!currentUserLogin) { + return parsedMentions; + } + + return decorateRangesWithCurrentUser(parsedMentions, text, currentUserLogin); + }, + [currentUserLogin, parser], + ); return ( { if (typeof ref !== 'function') { return; @@ -29,7 +75,7 @@ function RNMarkdownTextInputWithRef({maxLength, ...props}: RNMarkdownTextInputPr ref(refHandle as AnimatedMarkdownTextInputRef); }} // eslint-disable-next-line - {...props} + {...restProps} /** * If maxLength is not set, we should set the it to CONST.MAX_COMMENT_LENGTH + 1, to avoid parsing markdown for large text */ @@ -40,5 +86,6 @@ function RNMarkdownTextInputWithRef({maxLength, ...props}: RNMarkdownTextInputPr RNMarkdownTextInputWithRef.displayName = 'RNTextInputWithRef'; -export default React.forwardRef(RNMarkdownTextInputWithRef); +export default forwardRef(RNMarkdownTextInputWithRef); +export {decorateRangesWithCurrentUser}; export type {AnimatedMarkdownTextInputRef}; diff --git a/tests/unit/decorateRangesWithCurrentUserTest.ts b/tests/unit/decorateRangesWithCurrentUserTest.ts new file mode 100644 index 000000000000..0d03cf1eb3f3 --- /dev/null +++ b/tests/unit/decorateRangesWithCurrentUserTest.ts @@ -0,0 +1,63 @@ +import type {MarkdownRange} from '@expensify/react-native-live-markdown'; +import {decorateRangesWithCurrentUser} from '@components/RNMarkdownTextInput'; + +describe('decorateRangesWithCurrentUser', () => { + test('returns empty list for empty text', () => { + const result = decorateRangesWithCurrentUser([], '', ''); + expect(result).toEqual([]); + }); + + test('returns empty list when there are no mentions', () => { + const text = 'Lorem ipsum'; + const result = decorateRangesWithCurrentUser([], text, ''); + expect(result).toEqual([]); + }); + + test('returns unchanged ranges when there are other markups than user-mentions', () => { + const text = 'Lorem ipsum'; + const ranges: MarkdownRange[] = [ + { + type: 'bold', + start: 5, + length: 3, + }, + ]; + const result = decorateRangesWithCurrentUser(ranges, text, ''); + expect(result).toEqual([ + { + type: 'bold', + start: 5, + length: 3, + }, + ]); + }); + + test('returns ranges with current user type changed to "mention-here"', () => { + const text = 'Lorem ipsum @myUser'; + const ranges: MarkdownRange[] = [ + { + type: 'bold', + start: 5, + length: 3, + }, + { + type: 'mention-user', + start: 12, + length: 8, + }, + ]; + const result = decorateRangesWithCurrentUser(ranges, text, 'myUser'); + expect(result).toEqual([ + { + type: 'bold', + start: 5, + length: 3, + }, + { + type: 'mention-here', + start: 12, + length: 8, + }, + ]); + }); +});