Skip to content

Commit b85d761

Browse files
committed
Show current user mention in MarkdownTextInput with green outline
1 parent 653af04 commit b85d761

File tree

2 files changed

+117
-7
lines changed

2 files changed

+117
-7
lines changed
Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import type {MarkdownTextInputProps} from '@expensify/react-native-live-markdown';
1+
import type {MarkdownRange, MarkdownTextInputProps} from '@expensify/react-native-live-markdown';
22
import {MarkdownTextInput, parseExpensiMark} from '@expensify/react-native-live-markdown';
33
import type {ForwardedRef} from 'react';
4-
import React from 'react';
4+
import React, {forwardRef, useCallback} from 'react';
55
import Animated from 'react-native-reanimated';
6+
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
67
import useTheme from '@hooks/useTheme';
78
import CONST from '@src/CONST';
89

@@ -11,25 +12,70 @@ const AnimatedMarkdownTextInput = Animated.createAnimatedComponent(MarkdownTextI
1112

1213
type AnimatedMarkdownTextInputRef = typeof AnimatedMarkdownTextInput & MarkdownTextInput & HTMLInputElement;
1314

14-
type RNMarkdownTextInputProps = Omit<MarkdownTextInputProps, 'parser'>;
15+
// Make the parser prop optional for this component because we are always defaulting to `parseExpensiMark`
16+
type RNMarkdownTextInputWithRefProps = Omit<MarkdownTextInputProps, 'parser'> & {
17+
parser?: MarkdownTextInputProps['parser'];
18+
};
1519

16-
function RNMarkdownTextInputWithRef({maxLength, ...props}: RNMarkdownTextInputProps, ref: ForwardedRef<AnimatedMarkdownTextInputRef>) {
20+
function decorateRangesWithCurrentUser(ranges: MarkdownRange[], text: string, currentUser: string): MarkdownRange[] {
21+
'worklet';
22+
23+
return ranges.map((range) => {
24+
if (range.type === 'mention-user') {
25+
const mentionText = text.slice(range.start, range.start + range.length);
26+
const isCurrentUser = mentionText === `@${currentUser}`;
27+
if (isCurrentUser) {
28+
return {
29+
...range,
30+
type: 'mention-here',
31+
};
32+
}
33+
}
34+
35+
return range;
36+
});
37+
}
38+
39+
function RNMarkdownTextInputWithRef({maxLength, ...props}: RNMarkdownTextInputWithRefProps, ref: ForwardedRef<AnimatedMarkdownTextInputRef>) {
1740
const theme = useTheme();
41+
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
42+
43+
const {parser, ...restProps} = props;
44+
const currentUserLogin = currentUserPersonalDetails.login;
45+
46+
// We accept parser passed down as an argument or use expensiMark
47+
const parserFunction = useCallback(
48+
(text: string) => {
49+
'worklet';
50+
51+
if (parser) {
52+
return parser(text);
53+
}
54+
55+
const parsedMentions = parseExpensiMark(text);
56+
if (!currentUserLogin) {
57+
return parsedMentions;
58+
}
59+
60+
return decorateRangesWithCurrentUser(parsedMentions, text, currentUserLogin);
61+
},
62+
[currentUserLogin, parser],
63+
);
1864

1965
return (
2066
<AnimatedMarkdownTextInput
2167
allowFontScaling={false}
2268
textBreakStrategy="simple"
2369
keyboardAppearance={theme.colorScheme}
24-
parser={parseExpensiMark}
70+
parser={parserFunction}
2571
ref={(refHandle) => {
2672
if (typeof ref !== 'function') {
2773
return;
2874
}
2975
ref(refHandle as AnimatedMarkdownTextInputRef);
3076
}}
3177
// eslint-disable-next-line
32-
{...props}
78+
{...restProps}
3379
/**
3480
* If maxLength is not set, we should set the it to CONST.MAX_COMMENT_LENGTH + 1, to avoid parsing markdown for large text
3581
*/
@@ -40,5 +86,6 @@ function RNMarkdownTextInputWithRef({maxLength, ...props}: RNMarkdownTextInputPr
4086

4187
RNMarkdownTextInputWithRef.displayName = 'RNTextInputWithRef';
4288

43-
export default React.forwardRef(RNMarkdownTextInputWithRef);
89+
export default forwardRef(RNMarkdownTextInputWithRef);
90+
export {decorateRangesWithCurrentUser};
4491
export type {AnimatedMarkdownTextInputRef};
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type {MarkdownRange} from '@expensify/react-native-live-markdown';
2+
import {decorateRangesWithCurrentUser} from '@components/RNMarkdownTextInput';
3+
4+
describe('decorateRangesWithCurrentUser', () => {
5+
test('returns empty list for empty text', () => {
6+
const result = decorateRangesWithCurrentUser([], '', '');
7+
expect(result).toEqual([]);
8+
});
9+
10+
test('returns empty list when there are no mentions', () => {
11+
const text = 'Lorem ipsum';
12+
const result = decorateRangesWithCurrentUser([], text, '');
13+
expect(result).toEqual([]);
14+
});
15+
16+
test('returns unchanged ranges when there are other markups than user-mentions', () => {
17+
const text = 'Lorem ipsum';
18+
const ranges: MarkdownRange[] = [
19+
{
20+
type: 'bold',
21+
start: 5,
22+
length: 3,
23+
},
24+
];
25+
const result = decorateRangesWithCurrentUser(ranges, text, '');
26+
expect(result).toEqual([
27+
{
28+
type: 'bold',
29+
start: 5,
30+
length: 3,
31+
},
32+
]);
33+
});
34+
35+
test('returns ranges with current user type changed to "mention-here"', () => {
36+
const text = 'Lorem ipsum @myUser';
37+
const ranges: MarkdownRange[] = [
38+
{
39+
type: 'bold',
40+
start: 5,
41+
length: 3,
42+
},
43+
{
44+
type: 'mention-user',
45+
start: 12,
46+
length: 8,
47+
},
48+
];
49+
const result = decorateRangesWithCurrentUser(ranges, text, 'myUser');
50+
expect(result).toEqual([
51+
{
52+
type: 'bold',
53+
start: 5,
54+
length: 3,
55+
},
56+
{
57+
type: 'mention-here',
58+
start: 12,
59+
length: 8,
60+
},
61+
]);
62+
});
63+
});

0 commit comments

Comments
 (0)